Files
stack/packages/cli/tests/launch-account.test.mjs
T
jason.woltje ca15d73e9f limits: a daily token quota for the account the stack uses, and bus schema 3h to 3i (row 126, #1620)
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.
2026-10-18 20:35:02 -05:00

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);
});