Files
stack/packages/bus/src/autonomy-host.mjs
jason.woltje cfe7535533 fix(autonomy): account quota coverage reads the harness module's countsAccount, not a name (#1873)
authorityBudgetCovered named claude-code to decide whether an account
quota covers every future launch, which fails the row 65 scan. The bus
host now passes each harness module's own countsAccount answer, read
once with its levels in harnessLevels, through autonomyHost to the
broker. Without the lookup no account quota counts (fail closed); the
provider/model branch is unchanged.
2026-11-11 13:39:44 -06:00

92 lines
5.8 KiB
JavaScript

// AU trusted host seam. No worker can choose a path, snapshot or inverse operation.
import { mkdirSync, symlinkSync } from 'node:fs';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { prepareRevert, finishLand, dropLand } from '../../seat/src/working.mjs';
import { isMosaicStack, runGate, gateEnv } from '../../seat/src/landing.mjs';
const refuse=code=>{throw Object.assign(new Error(code),{code});};
// countsAccount(name) answers what the named harness module declares (runtime harnessLevels);
// the broker's authority budget check reads it here instead of naming a harness.
export function autonomyHost({writer,dataRoot,business,hires,reload,processing,countsAccount=()=>null,env=process.env,gate={classify:isMosaicStack,run:runGate}}) {
return {
countsAccount,
get:id=>{const r=writer.read(id);return {revision:r.revision,autonomy:r.document.autonomy??{}};},
set:(id,args,actor)=>{
const old=writer.read(id);if(old.revision!==args.revision)refuse('revision-conflict');
const result=writer.write(id,{...old.document,autonomy:args.autonomy},{revision:args.revision,actor,detail:{verb:'autonomy.set'}});
const live=reload(id);return {...result,restart:live===false};
},
reversible:cls=>['cross-role','merge'].includes(cls) || cls==='authority' && typeof hires?.reverse==='function',
async reverse({business:id,decision,decisionClass,raised,effects,actor,beforeWrite,beforePublish,fire,stop,complete,relaunch,record}) {
if(decisionClass==='authority') {
if(!hires?.reverse)refuse('reversal-unavailable');
let stopped;
const result=await hires.reverse(id,decision,{actor,beforeWrite,beforePublish,fire,stop:async p=>{const r=await stop(p);stopped=p;return r;},relaunch,record});
// The writer admitted publication within the window. Reload and relaunch
// complete that committed inverse; crossing the deadline here cannot undo it.
const receipt={committed:true,transaction:result.transaction??null,completion:stopped?'recorded':'not-needed',relaunch:result.relaunch?'pending':'not-needed'};
try {if(stopped)complete?.(stopped,result);} catch {receipt.completion='failed';receipt.restart=true;}
try {
if(reload(id)===false)receipt.restart=true;
if(result.relaunch && !receipt.restart){await relaunch(result.relaunch);receipt.relaunch='requested';}
} catch {receipt.restart=true;if(result.relaunch)receipt.relaunch='failed';}
return receipt;
}
const used=effects.filter(e=>e.operation!=='decision.raise' && !String(e.operation??'').startsWith('autonomy.'));
// An unused approval has changed no task or policy. Reversing it revokes that permission.
if(!used.length) {beforeWrite();return {permissionRevoked:true};}
if(decisionClass!=='merge')refuse('reversal-unavailable');
const landing=used.find(e=>e.action==='git.merge.protected' && typeof e.commit==='string');
if(!landing)refuse('reversal-unavailable');
const b=business(id),p=b.projects[landing.project];if(!p)refuse('project-not-found');
const name=`reverse-${randomBytes(6).toString('hex')}`,dir=join(dataRoot,'autonomy-reversals',decision,name);
mkdirSync(dir,{recursive:true,mode:0o700});
const where={dataRoot,business:id,project:landing.project,env};
let prepared;
return processing.work(async scope=>{
try {
if(!await gate.classify(p.root,env))refuse('gate-missing');
prepared=await prepareRevert({...where,commit:landing.commit,dir,name,decision});
symlinkSync(join(p.root,'node_modules'),join(prepared.worktree,'node_modules'));
const result=await gate.run({worktree:prepared.worktree,logs:dir,env:gateEnv(env),signal:scope.signal});
scope.guard();if(!result.ok)refuse('gate-failed');beforeWrite();
const receipt=await finishLand({...where,land:prepared,record:receipt=>{scope.guard();beforeWrite();record({...receipt,reverted:landing.commit});}});
return {...receipt,reverted:landing.commit};
} finally {if(prepared)await dropLand({...where,worktree:prepared.worktree});}
});
},
};
}
// The runtime adapter is shared with integration tests, including key revocation order.
export function authorityHost(service,{modelKeys,reload,proxyOrganization}) {
return {
...service,
reverse:async(id,decision,caller)=>{
const proposal=service.read(decision);
if(!modelKeys || (proposal.level==='org' && proposal.op==='hire'))return service.reverse(id,decision,caller);
const instance=`${id}/${proposal.level==='project'?`${proposal.name}.${proposal.project}`:proposal.name}`;
let prepared=false;
return service.reverse(id,decision,{
...caller,
beforeWrite:()=>{caller.beforeWrite();if(prepared){modelKeys.completeRevoke(instance);prepared=false;}caller.beforeWrite();},
stop:async(p,guard)=>{await modelKeys.prepareRevoke(instance,guard);prepared=true;return caller.stop(p);},
});
},
apply:async(id,decision,caller)=>{
const proposal=service.read(decision);
const publish=async()=>{
let approved=caller;
if(modelKeys && proposal.op==='fire' && proposal.level!=='org') {
const instance=`${id}/${proposal.level==='project'?`${proposal.name}.${proposal.project}`:proposal.name}`;
await modelKeys.prepareRevoke(instance,()=>caller.beforeWrite?.());
approved={...caller,beforeWrite:()=>{caller.beforeWrite?.();modelKeys.completeRevoke(instance);caller.beforeWrite?.();}};
}
const result=await service.apply(id,decision,approved);reload(id);return result;
};
if(proxyOrganization && proposal.level==='org' && proposal.op==='hire')return proxyOrganization.hire(id,proposal.name,()=>service.check(id,decision,caller),publish);
return publish();
},
};
}