diff --git a/packages/cli/README.md b/packages/cli/README.md index 89a95b32c..3ee324eb1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -432,8 +432,11 @@ A Claude Code session logs in with `CLAUDE_CODE_OAUTH_TOKEN`, on. The bus unit loads it ("Unit" below). Nothing from the host's `~/.claude` reaches a session, whose `HOME` is its own. With none of them set, a Claude Code launch refuses `no-session-credential` before anything -starts. A Pi session gets the same variables through the allowlist. A -shell the harness opens, Claude Code's `Bash` or Pi's `bash`, can read +starts. A Pi session gets the same variables through the allowlist. A Pi +session on provider `zai` logs in with `ZAI_API_KEY`, which the allowlist +passes. Like every allowlist variable, it reaches every session the host +starts, Claude Code ones included, and a shell inside any of them can read +it. A shell the harness opens, Claude Code's `Bash` or Pi's `bash`, can read them. A Pi session logs in only through the variables `ENV_ALLOW` passes. Its @@ -708,8 +711,9 @@ keeps the environment it started with until it restarts. The session credential comes from `EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`, the -one place the path is set. The file holds one line, for example -`CLAUDE_CODE_OAUTH_TOKEN=`, mode 0600. The operator writes it; +one place the path is set. The file holds one line per variable, for +example `CLAUDE_CODE_OAUTH_TOKEN=`, mode 0600. It holds every session +provider key, not only Claude's. The operator writes it; nothing in the repository creates it, and only the Case 2 precheck reads it, printing variable names, never values. No session sees the file itself, only the variable. The leading `-` lets the unit start diff --git a/packages/seat/src/session.mjs b/packages/seat/src/session.mjs index 122fb38d1..f8254d1c6 100644 --- a/packages/seat/src/session.mjs +++ b/packages/seat/src/session.mjs @@ -83,7 +83,7 @@ export function threadStore(dataRoot, business, thread) { export const ENV_ALLOW = Object.freeze([ "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TZ", "TERM", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "CLAUDE_CODE_OAUTH_TOKEN", - "OPENAI_API_KEY", "OPENROUTER_API_KEY", + "OPENAI_API_KEY", "OPENROUTER_API_KEY", "ZAI_API_KEY", "MOSAIC_CLAUDE_BIN", ]); diff --git a/packages/seat/tests/session.test.mjs b/packages/seat/tests/session.test.mjs index 73c3d03fa..08b3d051c 100644 --- a/packages/seat/tests/session.test.mjs +++ b/packages/seat/tests/session.test.mjs @@ -58,6 +58,13 @@ test("sessionEnv passes only the allowlist, the repo's bin on PATH, and the run assert.equal(sessionEnv({ MOSAIC_CLAUDE_BIN: "/opt/claude" }, "r0123456789ab").MOSAIC_CLAUDE_BIN, "/opt/claude"); }); +test("ZAI_API_KEY passes the session allowlist, and ZAI_TOKEN does not", () => { + assert.ok(ENV_ALLOW.includes("ZAI_API_KEY")); + const env = sessionEnv({ ZAI_API_KEY: "fixture-zai", ZAI_TOKEN: "t" }, "r0123456789ab"); + assert.equal(env.ZAI_API_KEY, "fixture-zai"); + assert.equal(Object.hasOwn(env, "ZAI_TOKEN"), false); +}); + test("sessionExit gives a runner's signal death back as the signal; a session credential is one whole set of nonempty variables", () => { assert.deepEqual(sessionExit(0, null), { code: 0, signal: null }); assert.deepEqual(sessionExit(23, null), { code: 23, signal: null });