diff --git a/packages/cli/tests/launcher.test.mjs b/packages/cli/tests/launcher.test.mjs index 766bbb35a..df6dea07d 100644 --- a/packages/cli/tests/launcher.test.mjs +++ b/packages/cli/tests/launcher.test.mjs @@ -82,6 +82,8 @@ function prepare(t, more = (doc) => doc) { const f = fixture(root, "acme", (doc) => more(edit(doc))); for (const k of SESSION_CREDENTIALS.flat()) delete f.env[k]; f.env.CLAUDE_CODE_OAUTH_TOKEN = "stand-in"; + if (f.doc.roles.chief.vars.harness === "pi" || f.doc.roles.chief.vars["fallback.harness"] === "pi") + f.env.ZAI_API_KEY = "fx332-pi"; // The project root is a repository on main, so each launch can clone it. const project = f.doc.projects.stack.root; gitIn(project, "init", "-q", "-b", "main"); @@ -145,6 +147,29 @@ function ask(path, line) { const log = (f) => readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l)); +// Named mutant: proxy key serialized into session.json. Keep the real launcher, +// harness env generation, session writer and runner; only the model adapter is fake. +test('P3 proxy key reaches the spawned runner but never session.json or the bundle', { skip, timeout: 30000 }, async t => { + const proxy = { baseUrl: 'http://127.0.0.1:12345', key: 'fx332-agent-a' }; + const prepared = prepare(t, doc => { + doc.launch.models['claude-opus-5-5'] = { provider: 'proxy', model: 'claude-fixture', route: 'anthropic' }; + return doc; + }); + const { launcher, f, close } = await setup(t, doc => doc, prepared, { proxy }); + const launched = await launcher.launchPm(); + const session = readSessions(f.dataRoot).find(s => s.run === launched.run); + assert.ok(session); + const environ = readFileSync(`/proc/${session.runnerPid}/environ`, 'utf8').split('\0'); + assert.ok(environ.includes(`ANTHROPIC_API_KEY=${proxy.key}`)); + assert.ok(!environ.some(value => value.startsWith('CLAUDE_CODE_OAUTH_TOKEN='))); + const stored = readFileSync(join(session.runDir, 'session.json'), 'utf8'); + assert.ok(!stored.includes(proxy.key), 'session.json must never persist the proxy key'); + const bundleFiles = readdirSync(join(session.runDir, 'bundle'), { recursive: true, withFileTypes: true }) + .filter(entry => entry.isFile()).map(entry => join(entry.parentPath, entry.name)); + for (const file of bundleFiles) assert.ok(!readFileSync(file, 'utf8').includes(proxy.key), file); + assert.equal(await close(), 0); +}); + test("bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest", { skip, timeout: 30000 }, async (t) => { // Row 122: uncertain write receipts cannot prevent the window's first chief launch. const prepared = prepare(t); @@ -672,14 +697,15 @@ test("a Claude Code launch with no session credential refuses before it starts", assert.deepEqual(readdirSync(join(f.dataRoot, "launches")), ["acme.jsonl"], "no run was made"); }); -test("a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables", { skip, timeout: 30000 }, async (t) => { +test("a pi launch with no own credential refuses no-session-credential", { skip, timeout: 30000 }, async (t) => { const given = prepare(t); delete given.f.env.CLAUDE_CODE_OAUTH_TOKEN; + delete given.f.env.ZAI_API_KEY; assert.deepEqual(SESSION_CREDENTIALS.flat().filter((k) => k in given.f.env), []); const { f, launcher } = await setup(t, undefined, given); - const chief = await launcher.launchPm(); - assert.equal(chief.harness, "pi"); - assert.deepEqual(log(f).map((e) => [e.event, e.instance]), [["launch", "chief"]]); + await assert.rejects(launcher.launchPm(), /PM launch refused: no-session-credential/); + assert.deepEqual(log(f).map((e) => [e.event, e.code]), [["refused", "no-session-credential"]]); + assert.deepEqual(readSessions(f.dataRoot), []); }); // Row 65: a harness module no core file names, switched on in the organization file, runs a real @@ -696,6 +722,7 @@ test("row 65 a failed primary preflight launches the fallback on a fixture harne write("harness", "down", `export * from ${pi};\nexport const preflight = () => ({ code: "fixture-down", message: "fixture preflight fails" });\n`); write("harness", "fixture", `export * from ${pi};\nexport const adapter = () => ${JSON.stringify(given.adapter)};\n`); }); + given.f.env.ZAI_API_KEY = "fx332-pi"; const { f, launcher } = await setup(t, undefined, given, { makeLoader }); const chief = await launcher.launchPm(); assert.deepEqual({ ...chief, run: undefined }, { run: undefined, instance: "chief", harness: "fixture", model: "claude-sonnet-5-5" }); @@ -825,7 +852,7 @@ test("row 64 on Claude Code a role skill that isn't enabled refuses the same way // API, runs one Bash call that tries the founder's stand-ins, another run // and the checkout. No real credential and no real model. const which = spawnSync("sh", ["-c", "command -v claude"], { encoding: "utf8" }); -const CLAUDE = which.status === 0 ? which.stdout.trim() : null; +const CLAUDE = process.env.MOSAIC_CLAUDE_BIN ?? (which.status === 0 ? which.stdout.trim() : null); test("a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold", { skip: skip || (CLAUDE ? false : "claude is not on PATH"), timeout: 180000 }, async (t) => { const script = []; diff --git a/packages/cli/tests/model-managed-launch-fixture.mjs b/packages/cli/tests/model-managed-launch-fixture.mjs index 1a63106ba..3a3bcd42b 100644 --- a/packages/cli/tests/model-managed-launch-fixture.mjs +++ b/packages/cli/tests/model-managed-launch-fixture.mjs @@ -15,13 +15,16 @@ import { readSessions, runsDir, sessionsFile, SESSIONS_VERSION, launchLogFile } export const waitFor = async (read, message, ms=15000) => { const end=Date.now()+ms;while(Date.now()setTimeout(r,20));}throw Error(message); }; export const pause = ms => new Promise(r=>setTimeout(r,ms)); const id = value => createHash('sha256').update(value).digest('hex').slice(0,12); -export async function managedLaunchFixture(t,{proxy=true,launches=true,refuseProbe=false}={}){ +export async function managedLaunchFixture(t,{proxy=true,launches=true,refuseProbe=false,harness='pi'}={}){ const root=fs.mkdtempSync(join(tmpdir(),'ml653-')),closers=[],history=new Set(['fx653-manage']),proxyReceipts=[],logs=[]; let server,bus,launcher,stderr;const trace={preflightAttempts:0,successfulPreflights:0,enableBeforeSpawn:[],bindings:[],stalls:[]}; const checkHistory=entries=>{const text=JSON.stringify(entries);for(const value of history)if(text.includes(value))throw Error('fixture credential leaked');}; t.after(async()=>{const failures=[];for(const close of [...closers].reverse()){try{await close();}catch{failures.push('owned-close');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));}catch{failures.push('owned-server-close');}try{checkHistory({logs,proxyReceipts,trace});}catch{failures.push('value-free-receipts');}if(stderr)process.stderr.write=stderr;try{fs.rmSync(root,{recursive:true,force:true});}catch{failures.push('owned-root-close');}if(failures.length)throw new AggregateError(failures.map(code=>Error(code)),'owned managed fixture cleanup failed');}); - const f=fixture(root,'acme',doc=>{doc.plugins={...doc.plugins,harness:['pi']};for(const role of Object.values(doc.roles))role.vars={...role.vars,harness:'pi'};doc.roles.chief.vars.model='claude-opus-5-5';return doc;}); + const f=fixture(root,'acme',doc=>{doc.plugins={...doc.plugins,harness:[harness]};for(const role of Object.values(doc.roles))role.vars={...role.vars,harness};doc.roles.chief.vars.model='claude-opus-5-5';return doc;}); const env={PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:f.config,MOSAIC_ROLES_DIR:f.roles}; + // The nonproxy positive fixture supplies its own fake pi login. Managed + // proxy fixtures deliberately hold no upstream credential at all. + if(!proxy)env.ZAI_API_KEY='fx653-pi'; const configDir=join(root,'config'),prefix=`msk-${id(configDir)}-`,sentinel=prefix+'sentinel-'+randomBytes(32).toString('base64url');history.add(sentinel); const state={keys:[sentinel],calls:[],refuseProbe}; server=http.createServer(async(req,res)=>{try{ @@ -78,7 +81,7 @@ export async function managedLaunchFixture(t,{proxy=true,launches=true,refusePro return actualOp(m); }, bindLaunch:async record=>{const result=bus.bindLaunch(record);trace.bindings.push({run:record.run,pid:record.pid,startTime:record.startTime,modelKeyId:record.modelKeyId});return result;}}; - const makeLoader=await fakeHarnesses({after:close=>closers.push(close)},{pi:adapter}); + const makeLoader=await fakeHarnesses({after:close=>closers.push(close)},{[harness]:adapter}); launcher=createLauncher({host,system,env,launches,makeLoader,sessionDefaults:{pollInterval:50},harnessVersion:'0.85.1',log:line=>logs.push(line)}); // The existing trusted host hook owns all launcher-created children. No registry // PID is signalled by fixture teardown; normal bounded launcher close is used. @@ -87,6 +90,7 @@ export async function managedLaunchFixture(t,{proxy=true,launches=true,refusePro const launchLog=()=>fs.existsSync(launchLogFile(f.dataRoot,'acme'))?fs.readFileSync(launchLogFile(f.dataRoot,'acme'),'utf8').trim().split('\n').filter(Boolean).map(l=>JSON.parse(l)):[]; const valueFree=()=>checkHistory({logs,proxyReceipts,launch:launchLog(),trace}); return {root,f,state,trace,proxyReceipts,logs,launcher,bus,launchLog,expectedKeyId:()=>expectedKeyId,valueFree, + upstreamCredentialNames:Object.keys(env).filter(name=>['ANTHROPIC_API_KEY','ANTHROPIC_AUTH_TOKEN','CLAUDE_CODE_OAUTH_TOKEN','OPENAI_API_KEY','OPENROUTER_API_KEY','ZAI_API_KEY'].includes(name)), runDirectories:()=>fs.existsSync(runsDir(f.dataRoot,'acme'))?fs.readdirSync(runsDir(f.dataRoot,'acme')).map(name=>join(runsDir(f.dataRoot,'acme'),name)):[], sessions:()=>readSessions(f.dataRoot),send:body=>bus.broker.request(human,{verb:'message.send',args:{to:'chief',body,class:'REQUEST'}}), reply:sent=>waitFor(()=>bus.broker.request(human,{verb:'message.receive'}).find(m=>m.in_reply_to===sent.id),'real managed reply observed'),