diff --git a/claw/packages/brain/src/sandbox/ensure-hands.ts b/claw/packages/brain/src/sandbox/ensure-hands.ts index a2e56d9e..283714c9 100644 --- a/claw/packages/brain/src/sandbox/ensure-hands.ts +++ b/claw/packages/brain/src/sandbox/ensure-hands.ts @@ -944,7 +944,6 @@ async function provisionHands( ...(action.params.env ?? {}), AUTH_CLAW_TOKEN: handsToken, CLAW_SESSION_ID: sessionId, - HYPERLOOM_SESSION_ID: sessionId, INFERENCE_OPTIMIZER_SESSION_LAYOUT: "per_model_ts", MCP_PORT: mcpPort, WORKSPACE_PATH: "/workspace", diff --git a/claw/packages/hands/src/runtime/child-privilege.ts b/claw/packages/hands/src/runtime/child-privilege.ts index 1a0bfbbb..62d329ce 100644 --- a/claw/packages/hands/src/runtime/child-privilege.ts +++ b/claw/packages/hands/src/runtime/child-privilege.ts @@ -191,6 +191,13 @@ function persistAllocations(table: Map): void { */ const WORKSPACE_FACING = [ "PATH", "HOME", "SHELL", "TERM", "TZ", "LANG", "LC_ALL", "PWD", "USER", "LOGNAME", + // The session a command is running for. Named here because a command needs to + // be able to say which session it belongs to -- Hyperloom stamps it into its + // run manifest and its LLM attribution -- and because the alternative is what + // was there before: the same value reaching the child anyway under a + // second, application-specific name. An identifier, not a credential; the + // credential is AUTH_CLAW_TOKEN, which this list deliberately omits. + "CLAW_SESSION_ID", ]; /** diff --git a/claw/packages/hands/test/child-privilege.test.ts b/claw/packages/hands/test/child-privilege.test.ts index 3fcebfb9..9c1f23a6 100644 --- a/claw/packages/hands/test/child-privilege.test.ts +++ b/claw/packages/hands/test/child-privilege.test.ts @@ -100,6 +100,23 @@ test("the child environment carries no Brain-facing credential, over the whole e assert.equal(env.PATH, process.env.PATH); }); +test("the child environment carries the session id", () => { + // Regression guard. This value used to reach a command only under a second, + // application-specific name, while the name every consumer actually reads was + // filtered out -- so a run could not say which session it belonged to, and the + // one document that told agents to branch on it was branching on a variable + // that is never set. It is an identifier and the same value is already on the + // sandbox, so naming it here costs no secrecy. + const previous = process.env.CLAW_SESSION_ID; + process.env.CLAW_SESSION_ID = "sess-visible-to-the-child"; + try { + assert.equal(privilege.childEnvironment().CLAW_SESSION_ID, "sess-visible-to-the-child"); + } finally { + if (previous === undefined) delete process.env.CLAW_SESSION_ID; + else process.env.CLAW_SESSION_ID = previous; + } +}); + test("a spawned command cannot read the token out of its own environment", async () => { bg.spawnBackground("sess-env", "run-env", "env; cat /proc/self/environ | tr '\\0' '\\n'", "envdump"); await settle(300); diff --git a/claw/packages/protocol/src/user-env.ts b/claw/packages/protocol/src/user-env.ts index af7abffc..f1048cce 100644 --- a/claw/packages/protocol/src/user-env.ts +++ b/claw/packages/protocol/src/user-env.ts @@ -46,7 +46,6 @@ export const USER_ENV_DENY_LIST: ReadonlySet = new Set([ "WORKSPACE_PATH", "USER_ID_HEX", "USER_DATA_PATH", - "HYPERLOOM_SESSION_ID", "INFERENCE_OPTIMIZER_SESSION_LAYOUT", "SAFE_API_URL", "SAFE_API_KEY",