The business file can name the account claude-code launches use,
`launch.account` = {name, credential}. `name` is an identifier and
`credential` names one login set's environment variables, never a value.
`launch.limits.tokens.daily.<account>` = {limit, since, reset, timeZone}
sets a daily quota for that account. A daily key that isn't the account's
name refuses the business read.
The new plug-in limit-check/daily-quota (interface 2, pure) starts a day at
the first instant at or after `reset` on that local date in `timeZone`. Across
DST that makes a 23- or 25-hour day, and a reset in a skipped hour starts at
the gap's end. It counts the account's known records of all four kinds since
the later of the day start and `since`, and counts unknown turns apart.
The launcher refuses account-credential-missing, account-credential-ambiguous
and account-is-credential (an account name equal to a session credential's
value). Messages name variables, never values. The account rides mayLaunch,
bindLaunch, the usage records, mustStop and the launch record. A pi launch
has no account, records NULL, and no daily quota covers it. `mosaic limits`
and the Console's Settings show one fixed line when a daily row shows:
"Daily quotas count only this stack's own use of the account, not its use
anywhere else."
Bus schema 3h to 3i: 3h reserved usage_records.account as
CHECK (account IS NULL). The new step 3h-3i.sql rebuilds the table in one
transaction, keeping every row's seq and the sqlite_sequence value, and
recreates the three append-only triggers. All of sqlite_master equals 3h's
apart from the account line. A 3h engine refuses a 3i store.
packages/bus/tests/account-migration.test.mjs covers all three, with a
step that fails partway and leaves 3h intact. 3i goes live only through
`stack-release.sh activate ... --migrate` with Jason present (P19 371); the
activate path takes the pre-migrate backup, and the way back is restoring
it (agents/dewey/work/queue-126/MIGRATION.md).
Mos's go mos-0835, six conditions: (1) one transaction, account-migration
test 2 and mutant D29; (2) sqlite_master against 3h, test 1 and mutant D28;
(3) account is never a credential, the launcher's account-is-credential
refusal (launch-account.test.mjs) and the business name pattern
(token-limit-settings.test.mjs); (4) MIGRATION.md, accepted by Mos in
mos-0837; (5) the packet's DESIGN.md "no schema change" line corrected;
(6) the public/ digest below.
The public/ change in bus.js (the daily line under the token-limit table)
carries Mos's decision 123 go mos-0833 on the +/- digest 03e6d9da...ba78,
unchanged at landing.
Owner Dewey, brief docs/plans/2026-10-14_prd-1.2-build.md, "Daily quota for
the credential the stack uses today". Review round 2 rebases the round 4
build onto 1df04f52f (row 167). Reviewer: Filbert approve 29068 on round 1
(manifest d18314f7...8e65); Filbert approve 29080 on
round 2 (manifest f332db76...6fa6).
Seat run on round 2: touched 158/0, singles 1388/0 with 1 esbuild skip,
test-land-work.sh 86/0, 30/30 mutants killed.
Sage gate on 54319f552: GATE_PASSED, 2474+422/0, all 15 packages, the queue
suite and 10 script selftests.
Nothing live.
110 lines
6.2 KiB
JavaScript
110 lines
6.2 KiB
JavaScript
// Row 126 (#1620, REQ-LIMIT-2): the account a launch spends. Only a claude-code launch carries
|
|
// launch.account.name; the bus environment must hold the named login set and no other credential
|
|
// variable, and the refusals name variables, never values. A pi launch has account null and skips
|
|
// both checks. A stub host answers the ops; no sandbox, Docker or session process.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFileSync} from 'node:fs';
|
|
import {createLauncher} from '../src/launcher.mjs';
|
|
import {fixture,tmp} from './helpers.mjs';
|
|
import {launchLogFile,SESSION_CREDENTIALS} from '../../seat/src/session.mjs';
|
|
|
|
const VALUE='stand-in-value-7f3a';
|
|
const ACCOUNT={name:'jason-max',credential:'CLAUDE_CODE_OAUTH_TOKEN'};
|
|
|
|
// set is the credential variables the bus environment holds; every other one is removed.
|
|
function setup(t,{harness='claude-code',account=ACCOUNT,set=['CLAUDE_CODE_OAUTH_TOKEN']}={}){
|
|
const root=tmp(t),f=fixture(root,'acme',doc=>{
|
|
doc.roles.coder.vars.model='claude-sonnet-5-5';doc.roles.coder.vars.harness=harness;
|
|
if(account)doc.launch.account=account;
|
|
return doc;
|
|
}),ops=[];
|
|
for(const k of SESSION_CREDENTIALS.flat())delete f.env[k];
|
|
for(const k of set)f.env[k]=VALUE;
|
|
const host={business:'acme',beforeClose:()=>{},op:async x=>{
|
|
ops.push(x);
|
|
if(x.op==='identity')return {business:'acme',role:'pm'};
|
|
if(x.op==='mayLaunch')return {allow:true};
|
|
if(x.op==='authorizeLaunch')throw Object.assign(Error('fixture authorization stop'),{code:'fixture-policy'});
|
|
return {};
|
|
}};
|
|
const launcher=createLauncher({host,system:JSON.parse(readFileSync(f.config,'utf8')),env:f.env,bwrap:'/usr/bin/true',log:()=>{}});
|
|
const last=()=>JSON.parse(readFileSync(launchLogFile(f.dataRoot,'acme'),'utf8').trim().split('\n').at(-1));
|
|
const may=()=>ops.find(x=>x.op==='mayLaunch');
|
|
return {launcher,ops,last,may};
|
|
}
|
|
|
|
test('REQ-LIMIT-2 a claude-code launch asks mayLaunch with launch.account.name',async t=>{
|
|
const {launcher,may}=setup(t);
|
|
assert.equal((await launcher.fromSession('fixture-cap','coder')).error,'fixture-policy');
|
|
assert.equal(may().account,'jason-max');
|
|
});
|
|
|
|
test('REQ-LIMIT-2 each login set the account can name is accepted when the bus host holds exactly it',async t=>{
|
|
for(const set of SESSION_CREDENTIALS){
|
|
const {launcher,may}=setup(t,{account:{name:'jason-max',credential:set.join('+')},set});
|
|
assert.equal((await launcher.fromSession('fixture-cap','coder')).error,'fixture-policy',set.join('+'));
|
|
assert.equal(may().account,'jason-max');
|
|
}
|
|
});
|
|
|
|
test('REQ-LIMIT-2 account-credential-missing: the named set is not a login set, or the bus host lacks part of it',async t=>{
|
|
for(const [credential,set] of [
|
|
['ANTHROPIC_API_KEY',['CLAUDE_CODE_OAUTH_TOKEN']],
|
|
// The host holds a login (OAuth), so no-session-credential passes; the named set lacks its URL.
|
|
['ANTHROPIC_AUTH_TOKEN+ANTHROPIC_BASE_URL',['CLAUDE_CODE_OAUTH_TOKEN','ANTHROPIC_AUTH_TOKEN']],
|
|
['ANTHROPIC_AUTH_TOKEN',['ANTHROPIC_AUTH_TOKEN','ANTHROPIC_BASE_URL']],
|
|
['ANTHROPIC_BASE_URL+ANTHROPIC_AUTH_TOKEN',['ANTHROPIC_AUTH_TOKEN','ANTHROPIC_BASE_URL']],
|
|
]){
|
|
const {launcher,ops,last}=setup(t,{account:{name:'jason-max',credential},set});
|
|
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'account-credential-missing'},credential);
|
|
const event=last();
|
|
assert.equal(event.code,'account-credential-missing');
|
|
assert.match(event.why,new RegExp(`^account jason-max names ${credential.split('+').join(' with ')},`));
|
|
assert.ok(!JSON.stringify(event).includes(VALUE),'no value in the log');
|
|
assert.equal(ops.find(x=>x.op==='mayLaunch'),undefined,credential);
|
|
}
|
|
});
|
|
|
|
test('REQ-LIMIT-2 account-credential-ambiguous: the bus host sets a credential variable outside the named set, ANTHROPIC_BASE_URL included',async t=>{
|
|
for(const [extra,named] of [[['ANTHROPIC_API_KEY'],'ANTHROPIC_API_KEY'],[['ANTHROPIC_BASE_URL'],'ANTHROPIC_BASE_URL'],
|
|
[['ANTHROPIC_AUTH_TOKEN','ANTHROPIC_BASE_URL'],'ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL']]){
|
|
const {launcher,ops,last}=setup(t,{set:['CLAUDE_CODE_OAUTH_TOKEN',...extra]});
|
|
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'account-credential-ambiguous'},named);
|
|
const event=last();
|
|
assert.equal(event.why,`account jason-max names CLAUDE_CODE_OAUTH_TOKEN, but the bus host also sets ${named}`);
|
|
assert.ok(!JSON.stringify(event).includes(VALUE),'no value in the log');
|
|
assert.equal(ops.find(x=>x.op==='mayLaunch'),undefined,named);
|
|
}
|
|
});
|
|
|
|
// Usage records keep the account's name, so a name that is a credential variable's value refuses,
|
|
// whichever variable holds it, before any message that names the account.
|
|
test('REQ-LIMIT-2 account-is-credential: launch.account.name is the value of a credential variable the bus host sets',async t=>{
|
|
for(const set of [['CLAUDE_CODE_OAUTH_TOKEN'],['CLAUDE_CODE_OAUTH_TOKEN','ANTHROPIC_API_KEY']]){
|
|
const {launcher,ops,last}=setup(t,{account:{name:VALUE,credential:'CLAUDE_CODE_OAUTH_TOKEN'},set});
|
|
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'account-is-credential'},set.join());
|
|
const event=last();
|
|
assert.equal(event.code,'account-is-credential');
|
|
assert.match(event.why,/^launch\.account\.name is the value of [A-Z_, ]+; it names the account, never its credential$/);
|
|
for(const k of set)assert.ok(event.why.includes(k),k);
|
|
assert.ok(!JSON.stringify(event).includes(VALUE),'no value in the log');
|
|
assert.equal(ops.find(x=>x.op==='mayLaunch'),undefined,set.join());
|
|
}
|
|
});
|
|
|
|
test('REQ-LIMIT-2 a pi launch carries account null and skips both account checks',async t=>{
|
|
// No credential variable at all, and two at once: neither refuses a pi launch.
|
|
for(const set of [[],['CLAUDE_CODE_OAUTH_TOKEN','ANTHROPIC_API_KEY']]){
|
|
const {launcher,may}=setup(t,{harness:'pi',set});
|
|
assert.equal((await launcher.fromSession('fixture-cap','coder')).error,'fixture-policy',set.join());
|
|
assert.equal(may().account,null);
|
|
}
|
|
});
|
|
|
|
test('REQ-LIMIT-2 with no launch.account a claude-code launch carries null and the environment is not checked for ambiguity',async t=>{
|
|
const {launcher,may}=setup(t,{account:null,set:['CLAUDE_CODE_OAUTH_TOKEN','ANTHROPIC_API_KEY']});
|
|
assert.equal((await launcher.fromSession('fixture-cap','coder')).error,'fixture-policy');
|
|
assert.equal(may().account,null);
|
|
});
|