Files
stack/packages/cli/tests/launch-account.test.mjs
jason.woltje 85ecdfbde5 fix(row 332): proxy first check runs every key-independent refusal; testProxy test option (Vikunja #774)
F1: with system.proxy set, the first launcher check no longer returns
before the account, sandbox, capacity and limit checks. It skips only the
harness preflight and the session proxy key match, which need the key P2
admits in startLocked. A launch that can never start now refuses before
authorizeLaunch, the workspace and checkouts, and modelPreflight. The first
proxy pass accepts only credential "proxy" for a declared account, tries
the fallback model when the primary refuses, and lets mayLaunch
key-quota-no-model-key through so P2 can issue the first key. The final
check is unchanged and still runs before newRun and spawn.

F4: the proxy test option is renamed testProxy and createLauncher throws
test-proxy-refused when it is given with system.proxy.
launch-account.test.mjs is a second caller, so the option stays.

Tests: proxy-launch-order.test.mjs (6), launcher.test.mjs F4 throw test;
the managed launch fixture gains bwrap, admit and trace.ops.
2026-11-02 20:51:51 -06:00

157 lines
9.4 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 {initializeControl} from '../../processing-control/src/state.mjs';
import {fixture,tmp,withoutHostCredentials} 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. A pi
// launch gets one made-up pi key, so its own preflight passes on any host. proxy goes to the launcher's
// testProxy option, a static session proxy context (row 332, #774 F4); edit changes the rest of the file.
function setup(t,{harness='claude-code',account=ACCOUNT,set=['CLAUDE_CODE_OAUTH_TOKEN'],proxy=null,edit=doc=>doc}={}){
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 edit(doc);
}),ops=[];
initializeControl(f.dataRoot);
withoutHostCredentials(f.env,{piKey:harness==='pi'});
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:()=>{},...(proxy?{testProxy:proxy}:{})});
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 session login variable at all, and two at once: neither refuses a pi launch. pi itself runs
// on the made-up pi key setup gives it, which is no session login.
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);
});
// Row 332 R2 (#1830, review F3): the account checks on a launch with a session proxy context. A
// proxy model is routed through the launcher's testProxy option; no proxy runs.
const PROXY={baseUrl:'http://127.0.0.1:9',key:'fx332-acct-key'};
const proxyModel=doc=>{
doc.launch.models['proxy-sonnet']={provider:'proxy',model:'claude-sonnet-5-5',route:'anthropic'};
doc.roles.coder.vars.model='proxy-sonnet';
return doc;
};
test('row 332 R2 a proxy session refuses account-credential-missing when launch.account names a host login, not proxy',async t=>{
const {launcher,ops,last}=setup(t,{proxy:PROXY,edit:proxyModel});
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'account-credential-missing'});
const event=last();
assert.equal(event.code,'account-credential-missing');
assert.equal(event.why,'account jason-max needs the declared proxy credential');
for(const value of [VALUE,PROXY.key])assert.ok(!JSON.stringify(event).includes(value),'no value in the log');
assert.equal(ops.find(x=>x.op==='mayLaunch'),undefined);
});
test('row 332 R2 launch.account credential proxy with no proxy session refuses account-credential-missing',async t=>{
const {launcher,ops,last}=setup(t,{account:{name:'jason-max',credential:'proxy'}});
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'account-credential-missing'});
const event=last();
assert.equal(event.code,'account-credential-missing');
assert.equal(event.why,'account jason-max needs the declared proxy credential');
assert.ok(!JSON.stringify(event).includes(VALUE),'no value in the log');
assert.equal(ops.find(x=>x.op==='mayLaunch'),undefined);
});
test('row 332 R2 account-is-credential: launch.account.name is the session proxy key; another name passes the account checks',async t=>{
const {launcher,ops,last}=setup(t,{account:{name:PROXY.key,credential:'proxy'},set:[],proxy:PROXY,edit:proxyModel});
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'account-is-credential'});
const event=last();
assert.equal(event.why,'launch.account.name is the value of session proxy key; it names the account, never its credential');
assert.ok(!JSON.stringify(event).includes(PROXY.key),'no proxy key in the log');
assert.equal(ops.find(x=>x.op==='mayLaunch'),undefined);
// The same proxy session and credential with an account name that is no key value launches.
const named=setup(t,{account:{name:'jason-max',credential:'proxy'},set:[],proxy:PROXY,edit:proxyModel});
assert.equal((await named.launcher.fromSession('fixture-cap','coder')).error,'fixture-policy');
assert.deepEqual(named.may(),{op:'mayLaunch',business:'acme',instance:'coder',provider:'proxy',model:'claude-sonnet-5-5',account:'jason-max'});
});