121 lines
10 KiB
Diff
121 lines
10 KiB
Diff
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()<end){const value=await read();if(value)return value;await new Promise(r=>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'),
|