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.
This commit is contained in:
1 parent
b1fc96f4ae
commit
cfe7535533
7 files changed
+63
-15
No files matched your search
@@ -5,8 +5,11 @@ 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});};
|
||||
export function autonomyHost({writer,dataRoot,business,hires,reload,processing,env=process.env,gate={classify:isMosaicStack,run:runGate}}) {
|
||||
// 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');
|
||||
|
||||
@@ -6415,7 +6415,7 @@ export class Broker {
|
||||
if(!role?.authority?.withinRole.includes(right) || (cls==='authority' && name===b.launch?.by))fail('autonomy-precondition');
|
||||
}
|
||||
if(['merge','policy'].includes(cls) && !Object.values(b.roles).some(r=>r.authority?.withinRole.includes('review.verdict')))fail('autonomy-precondition');
|
||||
if(cls==='authority' && !authorityBudgetCovered(b))fail('autonomy-precondition');
|
||||
if(cls==='authority' && !this.#budgetCovered(b))fail('autonomy-precondition');
|
||||
if(after==='out of the loop') {
|
||||
if(before!=='near the loop')fail('autonomy-precondition');
|
||||
const review=this.#store.get("SELECT seq,body FROM events WHERE business=? AND kind='action.allowed' AND json_extract(body,'$.operation')='autonomy.review' AND json_extract(body,'$.decisionClass')=? ORDER BY seq DESC LIMIT 1",s.business,cls);
|
||||
@@ -6504,7 +6504,7 @@ export class Broker {
|
||||
let route='human';
|
||||
if(!immediate && level!=='in the loop' && p.level!=='root' && !p.flagged.length && !p.credentialLines.length) {
|
||||
if(level==='out of the loop')fail('authority-limits-unavailable');
|
||||
if(!authorityBudgetCovered(this.#business(s.business)))fail('autonomy-precondition');
|
||||
if(!this.#budgetCovered(this.#business(s.business)))fail('autonomy-precondition');
|
||||
route=b.autonomy?.authorityResolver;
|
||||
if(!b.roles[route]?.authority?.withinRole.includes('decision.resolve.authority'))fail('autonomy-precondition');
|
||||
if(route===s.role || resolverSubject(b,route,p.name))route='human';
|
||||
@@ -6518,6 +6518,8 @@ export class Broker {
|
||||
if(immediate) return this.#hireConsole(s,'hire.resolve.console',{id:did,choice:'yes',digest:p.digest});
|
||||
return {...this.#decisionView(this.#decision(s,did)),hire:p};
|
||||
}
|
||||
// The harness fact comes from the host's loaded modules; no host, no account coverage.
|
||||
#budgetCovered(b) { return authorityBudgetCovered(b,{countsAccount:this.#autonomyHost?.countsAccount}); }
|
||||
// Trusted runtime inverse only; never a socket verb.
|
||||
hireReverse(...args) {
|
||||
if(!this.#hires?.reverse)fail('reversal-unavailable');
|
||||
@@ -6534,7 +6536,7 @@ export class Broker {
|
||||
if(this.#expireHire(s,d,p))fail('decision-expired');
|
||||
if(this.#autonomyLevel(d)==='in the loop')fail('hire-resolution-required');
|
||||
if(this.#autonomyLevel(d)==='out of the loop')fail('authority-limits-unavailable');
|
||||
if(!authorityBudgetCovered(this.#business(s.business)))fail('autonomy-precondition');
|
||||
if(!this.#budgetCovered(this.#business(s.business)))fail('autonomy-precondition');
|
||||
if(!this.#business(s.business).roles[s.role]?.authority?.withinRole.includes('decision.resolve.authority'))fail('not-resolver');
|
||||
};
|
||||
validate();
|
||||
@@ -6554,7 +6556,7 @@ export class Broker {
|
||||
const recheck=()=>{
|
||||
this.#agent(s);
|
||||
if(this.#autonomyLevel(d)==='out of the loop')fail('authority-limits-unavailable');
|
||||
if(!authorityBudgetCovered(this.#business(s.business)))fail('autonomy-precondition');
|
||||
if(!this.#budgetCovered(this.#business(s.business)))fail('autonomy-precondition');
|
||||
if(this.#autonomyLevel(d)==='in the loop' || !this.#business(s.business).roles[s.role]?.authority?.withinRole.includes('decision.resolve.authority') || resolverSubject(this.#business(s.business),s.role,p.name))fail('not-resolver');
|
||||
};
|
||||
recheck();
|
||||
|
||||
@@ -111,18 +111,28 @@ export async function harnessLevels({ log = (e) => process.stderr.write(`plugins
|
||||
try { names = [...new Set([...names,...Object.keys(readPluginLock(dataRoot).plugins).filter(name=>hasModule('harness',name,dataRoot))])]; }
|
||||
catch { log({event:'harness-levels-unread',message:'installed harness discovery refused'}); }
|
||||
}
|
||||
const accounts = new Map();
|
||||
for (const name of names) {
|
||||
const loader = createLoader({ log, dataRoot });
|
||||
if ((await loader.load({ harness: [name] })).refused.length) continue;
|
||||
const [module] = loader.modules('harness');
|
||||
try {
|
||||
const list = loader.modules('harness')[0].levels();
|
||||
const list = module.levels();
|
||||
if (Array.isArray(list) && list.every((level) => typeof level === 'string')) levels.set(name, Object.freeze([...list]));
|
||||
} catch (e) {
|
||||
log({ event: 'harness-levels-failed', name, message: e.message });
|
||||
}
|
||||
// AU (#1873): whether this harness's launches spend launch.account, as its module declares.
|
||||
try {
|
||||
const counts = module.countsAccount();
|
||||
if (typeof counts === 'boolean') accounts.set(name, counts);
|
||||
} catch (e) {
|
||||
log({ event: 'harness-account-failed', name, message: e.message });
|
||||
}
|
||||
}
|
||||
const lookup = (name) => levels.get(name) ?? null;
|
||||
lookup.names = [...levels.keys()].sort();
|
||||
lookup.countsAccount = (name) => accounts.get(name) ?? null;
|
||||
return lookup;
|
||||
}
|
||||
// Row 224 C2-05b: a host-only adapter. A failed fetch keeps serving immutable old
|
||||
@@ -891,7 +901,7 @@ export async function startBroker({
|
||||
return INSTANCE_VERBS.has(verb)?{...result,restart:!live}:result;
|
||||
}):null,
|
||||
// Row 194 (D2.3): the role files as they stand, for the hire checks.
|
||||
autonomy: organization ? autonomyHost({writer:organizationWriter({dir:organization.dir,rolesDir:organization.rolesDir,dataRoot:instanceRoot??dataRoot}),dataRoot,business:id=>businesses[id],hires:{reverse:(...args)=>broker.hireReverse(...args)},reload,processing}) : null,
|
||||
autonomy: organization ? autonomyHost({writer:organizationWriter({dir:organization.dir,rolesDir:organization.rolesDir,dataRoot:instanceRoot??dataRoot}),dataRoot,business:id=>businesses[id],countsAccount:levels.countsAccount,hires:{reverse:(...args)=>broker.hireReverse(...args)},reload,processing}) : null,
|
||||
hires: organization ? (()=>{
|
||||
const service=hireDecisions({writer:organizationWriter({dir:organization.dir,rolesDir:organization.rolesDir,dataRoot:instanceRoot??dataRoot}),rolesDir:organization.rolesDir,configDir:organization.dir,dataRoot,
|
||||
system:organization.system,levels,catalogueFor:async(id,doc)=>doc.plugins?.['model-provider']?.length?catalogueFor(id,{fresh:true}):null,assertClean:value=>credentials.assertClean(value),scanDefinition:(name,entry)=>scanDefinitions({[name]:entry},value=>credentials.assertClean(value)),registryFor:doc=>organizationPlugins(instanceRoot??dataRoot,doc.plugins).registry});
|
||||
|
||||
@@ -17,6 +17,8 @@ function world(t,options={}) {
|
||||
const root=mkdtempSync(join(tmpdir(),'au-authority-')),rolesDir=rolesCopy(root),dataRoot=join(root,'data');mkdirSync(dataRoot,{mode:0o700});
|
||||
const doc=businessDoc(root,'demo');doc.plugins={harness:['pi'],'limit-check':['token-limit']};
|
||||
doc.launch.limits.tokens={providers:{zai:cap}};
|
||||
// #1873: an account quota alone, covered only through the host's declared harness fact.
|
||||
if(options.account){doc.launch.account={name:'shared',credential:'FIXTURE_TOKEN'};doc.launch.limits.tokens={keys:{shared:cap}};}
|
||||
doc.definitions={judge:{rev:1,role:{...inline,authority:{withinRole:['decision.resolve.authority'],crossRole:[]}},instructions:'Judge'},scout:{rev:3,role:structuredClone(inline),instructions:'Prior role'}};
|
||||
// Definition instructions live beside the role composition.
|
||||
for(const d of Object.values(doc.definitions))delete d.role.instructions;
|
||||
@@ -31,7 +33,7 @@ function world(t,options={}) {
|
||||
const reload=id=>{if(reversing && options.crossAfterCommit)Date.now=()=>now()+3600001;if(reversing && options.reloadRefused)return false;broker.reloadBusiness(id,loaded());return true;};
|
||||
const service=hireDecisions({writer,rolesDir,configDir:root,dataRoot,assertClean:()=>{}});
|
||||
const hires=authorityHost(service,{reload,modelKeys:{prepareRevoke:async(instance,guard)=>{guard();assert.equal(instance,'demo/scout');order.push('prepare');},completeRevoke:instance=>{assert.equal(instance,'demo/scout');assert.ok(store.get("SELECT 1 FROM events WHERE json_extract(body,'$.action')='fire.stopped'"));assert.equal(writer.read('demo').document.definitions.scout.instructions,'Changed role');order.push('complete');}}});
|
||||
const host=autonomyHost({writer,dataRoot,business:()=>loaded(),hires,reload});
|
||||
const host=autonomyHost({writer,dataRoot,business:()=>loaded(),hires,reload,...(options.countsAccount?{countsAccount:options.countsAccount}:{})});
|
||||
const instances=instanceSettings({dir:root,rolesDir,writer,revoke:()=>{order.push('fire-revoke');}});
|
||||
broker=new Broker({store,businesses:{demo:loaded()},hires,autonomy:host,organization:(id,verb,args,actor,context)=>{if(verb==='instance.fireCheck'){instances.checkFire(id,args.name);return {};}if(verb==='instance.fire')return instances.fire(id,args,{actor,context});throw Error('unexpected organization verb');}});
|
||||
const caps={};const call=(who,verb,args={})=>broker.request(caps[who],{verb,args});
|
||||
@@ -107,3 +109,11 @@ test('AU inverse receipt-write failure returns committed with trailPending and n
|
||||
assert.equal(f.writer.read('demo').document.definitions.scout.instructions,'Prior role');
|
||||
assert.equal(f.store.get("SELECT count(*) n FROM events WHERE json_extract(body,'$.operation')='autonomy.reverse.failed'").n,0);
|
||||
});
|
||||
test('AU an account-quota authority raise routes to the resolver only when the host says every harness spends the account (#1873)',async t=>{
|
||||
const covered=world(t,{account:true,countsAccount:name=>name==='pi'}),d=await covered.raise();assert.equal(d.route_to,'judge');
|
||||
for(const countsAccount of [undefined,()=>false,()=>null]){
|
||||
const f=world(t,{account:true,countsAccount});
|
||||
await assert.rejects(f.raise(),{code:'autonomy-precondition'});
|
||||
assert.equal(f.store.get("SELECT count(*) n FROM decisions").n,0);
|
||||
}
|
||||
});
|
||||
@@ -96,6 +96,9 @@ test('row 65 runtime: harnessLevels answers each module in the tree, and null fo
|
||||
assert.deepEqual(levels('pi'), ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
||||
assert.deepEqual(levels('claude-code'), ['low', 'medium', 'high', 'xhigh', 'max']);
|
||||
for (const name of ['cursor', '', '../pi']) assert.equal(levels(name), null, name);
|
||||
// AU (#1873): each module's own countsAccount answer, read once with its levels.
|
||||
assert.deepEqual([levels.countsAccount('pi'), levels.countsAccount('claude-code')], [false, true]);
|
||||
for (const name of ['cursor', '', '../pi']) assert.equal(levels.countsAccount(name), null, name);
|
||||
assert.deepEqual(logged, []);
|
||||
});
|
||||
|
||||
|
||||
@@ -30,12 +30,15 @@ export function resolverSubject(business, resolver, target) {
|
||||
}
|
||||
|
||||
// Aggregate provider/model limits cover names that do not exist yet. An account
|
||||
// quota covers every future launch only when every enabled harness uses that account.
|
||||
export function authorityBudgetCovered(business) {
|
||||
// quota covers every future launch only when every enabled harness's module declares
|
||||
// that its launches spend launch.account (its countsAccount export). The host passes
|
||||
// that answer as countsAccount(name) -> true|false|null; core names no harness, and
|
||||
// without the lookup no account quota counts.
|
||||
export function authorityBudgetCovered(business, {countsAccount=()=>null}={}) {
|
||||
const launch=business.launch, tokens=launch?.limits?.tokens, checks=business.plugins?.['limit-check']??[];
|
||||
const quota=x=>Number.isSafeInteger(x?.limit) && x.limit>=0 && typeof x.since==='string' && !Number.isNaN(Date.parse(x.since));
|
||||
const account=launch?.account?.name, harnesses=business.plugins?.harness??[];
|
||||
if(account && harnesses.length && harnesses.every(h=>h==='claude-code') &&
|
||||
if(account && harnesses.length && harnesses.every(h=>countsAccount(h)===true) &&
|
||||
(checks.includes('daily-quota') && quota(tokens?.daily?.[account]) ||
|
||||
checks.includes('token-limit') && quota(tokens?.keys?.[account]) && tokens.keys[account].reset===undefined))return true;
|
||||
const models=Object.values(launch?.models??{});
|
||||
|
||||
@@ -38,8 +38,25 @@ test('AU authority budgets cover all selectable models and only enforced account
|
||||
b.launch.limits.tokens.models={two:{n:cap}};assert.equal(authorityBudgetCovered(b),true);
|
||||
b.plugins['limit-check']=[];assert.equal(authorityBudgetCovered(b),false);
|
||||
b.launch.account={name:'shared'};b.launch.limits.tokens={keys:{shared:cap}};b.plugins['limit-check']=['token-limit'];
|
||||
assert.equal(authorityBudgetCovered(b),false);
|
||||
b.plugins.harness=['claude-code'];assert.equal(authorityBudgetCovered(b),true);
|
||||
b.plugins.harness.push('pi');assert.equal(authorityBudgetCovered(b),false);
|
||||
b.plugins.harness=['claude-code'];b.plugins['limit-check']=['daily-quota'];b.launch.limits.tokens={daily:{shared:{...cap,reset:'00:00',timeZone:'UTC'}}};assert.equal(authorityBudgetCovered(b),true);
|
||||
// What today's two harness modules declare (packages/plugins/harness/*/index.mjs countsAccount).
|
||||
const declared={countsAccount:h=>({'claude-code':true,pi:false})[h]??null};
|
||||
assert.equal(authorityBudgetCovered(b,declared),false);
|
||||
b.plugins.harness=['claude-code'];assert.equal(authorityBudgetCovered(b,declared),true);
|
||||
b.plugins.harness.push('pi');assert.equal(authorityBudgetCovered(b,declared),false);
|
||||
b.plugins.harness=['claude-code'];b.plugins['limit-check']=['daily-quota'];b.launch.limits.tokens={daily:{shared:{...cap,reset:'00:00',timeZone:'UTC'}}};assert.equal(authorityBudgetCovered(b,declared),true);
|
||||
});
|
||||
test('AU an account quota covers authority only when every enabled harness declares it spends the launch account (#1873)',()=>{
|
||||
const cap={limit:0,since:'2026-01-01T00:00:00.000Z'};
|
||||
const b={plugins:{harness:['alpha','beta'],'limit-check':['token-limit']},launch:{account:{name:'shared'},models:{one:{provider:'one',model:'m'}},limits:{tokens:{keys:{shared:cap}}}}};
|
||||
const declares=map=>({countsAccount:h=>map[h]??null});
|
||||
// The declared fact decides, whatever the harness is called.
|
||||
assert.equal(authorityBudgetCovered(b,declares({alpha:true,beta:true})),true);
|
||||
assert.equal(authorityBudgetCovered(b,declares({alpha:true,beta:false})),false);
|
||||
assert.equal(authorityBudgetCovered(b,declares({alpha:true})),false,'an undeclared harness is not covered');
|
||||
assert.equal(authorityBudgetCovered(b,declares({alpha:true,beta:'yes'})),false,'only true counts');
|
||||
b.plugins.harness=['claude-code'];
|
||||
assert.equal(authorityBudgetCovered(b,declares({'claude-code':false})),false,'the name alone covers nothing');
|
||||
assert.equal(authorityBudgetCovered(b),false,'no lookup: fail closed');
|
||||
// The provider/model branch needs no harness fact.
|
||||
b.launch.limits.tokens.providers={one:cap};assert.equal(authorityBudgetCovered(b),true);
|
||||
});
|
||||
Reference in new issue
Block a user