From 304f447d484efa3aa86bc5dbf8fcfe0914ed76a4 Mon Sep 17 00:00:00 2001 From: zoroyihan7 Date: Thu, 17 Sep 2026 04:34:04 +0000 Subject: [PATCH 1/2] fix(hands): let a command see the session it is running for CLAW_SESSION_ID was set on the sandbox and then filtered out before any model-issued command could read it, so a run could not name the session it belonged to. Two mechanisms removed it, and neither was aimed at it. The child environment is an allowlist -- WORKSPACE_FACING, built up rather than filtered down so that no future secret arrives by default -- and the session id was simply never added to it. The other route in, the per-request env file, refuses every BOOTSTRAP_OWNED name so that a request cannot overrule what this process was launched with; CLAW_SESSION_ID is on that list for the good reason that a request must not rewrite its own session id, and the side effect was that the file could not carry it either. What made this invisible is that the same value already reached the child under a second name. HYPERLOOM_SESSION_ID is set on the adjacent line and is not bootstrap-owned, so it passes -- and the name every consumer actually reads did not. Hyperloom's manifest and its LLM attribution both read CLAW_SESSION_ID; on a real run against this cluster, manifest.json recorded `claw_session_id: null`, so Claw-session correlation has never worked. Adding the name to WORKSPACE_FACING costs no secrecy: the identical value is already exposed under the other name, it is an identifier rather than a credential, and the credential this list exists to withhold -- AUTH_CLAW_TOKEN -- stays omitted. Nor does it become user-writable: isClawInternalEnv already denies the whole CLAW_* prefix in user env. The alias stays for now, and a scan of the sibling repositories is what stopped it being deleted: the Hyperloom TUI resolves its session from HYPERLOOM_SESSION_ID and from no other name. A comment records that, and records what it looks like -- every other field in that file falls back to a CLAW_* name and this one does not, which is the shape of a consumer that found CLAW_SESSION_ID empty. The alias is a workaround for this bug, not a second contract, and it can go once that consumer reads the platform's own name. Test added for the visibility, alongside the existing one that scans the whole child environment for the token's value. Co-Authored-By: Claude Opus 5 (1M context) --- claw/packages/brain/src/sandbox/ensure-hands.ts | 9 +++++++++ .../hands/src/runtime/child-privilege.ts | 7 +++++++ .../packages/hands/test/child-privilege.test.ts | 17 +++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/claw/packages/brain/src/sandbox/ensure-hands.ts b/claw/packages/brain/src/sandbox/ensure-hands.ts index a2e56d9e..f3225b8a 100644 --- a/claw/packages/brain/src/sandbox/ensure-hands.ts +++ b/claw/packages/brain/src/sandbox/ensure-hands.ts @@ -944,6 +944,15 @@ async function provisionHands( ...(action.params.env ?? {}), AUTH_CLAW_TOKEN: handsToken, CLAW_SESSION_ID: sessionId, + // The same id under a second name, kept because something still reads it: + // the Hyperloom TUI resolves the session it is running inside from + // HYPERLOOM_SESSION_ID and from no other name (Hyperloom-Web + // packages/hyperloom-tui/src/cli.ts). That is worth reading as a symptom + // rather than a design: every other field there falls back to a CLAW_* name + // and this one does not, which is what you would expect from a consumer + // that found CLAW_SESSION_ID empty -- as, until the line above reached the + // child, it always was. This line has no reason to outlive that consumer + // reading the platform's own name for the session. HYPERLOOM_SESSION_ID: sessionId, INFERENCE_OPTIMIZER_SESSION_LAYOUT: "per_model_ts", MCP_PORT: mcpPort, 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); From c16e0fede2a66c9eea3bb611cebf1930754df35d Mon Sep 17 00:00:00 2001 From: zoroyihan7 Date: Thu, 17 Sep 2026 04:53:07 +0000 Subject: [PATCH 2/2] chore(brain): retire the HYPERLOOM_SESSION_ID alias It existed because the platform's own name for the session could not be read from inside a sandbox. The parent commit fixes that, so the alias is carrying nothing the line above it does not. Both halves go: the injection in ensure-hands.ts, and the user-env deny entry that reserved the name. Nothing in this repository reads it, and the one consumer outside it -- the Hyperloom TUI, which resolved its session from this name and no other -- reads CLAW_SESSION_ID as of AMD-AGI/Hyperloom-Web's fix/tui-session-id-from-claw. Merge order matters, which is why this is a commit of its own rather than part of the fix. Landing it before that TUI change ships leaves an older TUI unable to find the session it is running inside; it degrades rather than crashes -- initializeSession falls through to createSession() and the run gets a fresh session instead of the one it is sitting in -- but it is a regression, and it is avoidable by waiting. Co-Authored-By: Claude Opus 5 (1M context) --- claw/packages/brain/src/sandbox/ensure-hands.ts | 10 ---------- claw/packages/protocol/src/user-env.ts | 1 - 2 files changed, 11 deletions(-) diff --git a/claw/packages/brain/src/sandbox/ensure-hands.ts b/claw/packages/brain/src/sandbox/ensure-hands.ts index f3225b8a..283714c9 100644 --- a/claw/packages/brain/src/sandbox/ensure-hands.ts +++ b/claw/packages/brain/src/sandbox/ensure-hands.ts @@ -944,16 +944,6 @@ async function provisionHands( ...(action.params.env ?? {}), AUTH_CLAW_TOKEN: handsToken, CLAW_SESSION_ID: sessionId, - // The same id under a second name, kept because something still reads it: - // the Hyperloom TUI resolves the session it is running inside from - // HYPERLOOM_SESSION_ID and from no other name (Hyperloom-Web - // packages/hyperloom-tui/src/cli.ts). That is worth reading as a symptom - // rather than a design: every other field there falls back to a CLAW_* name - // and this one does not, which is what you would expect from a consumer - // that found CLAW_SESSION_ID empty -- as, until the line above reached the - // child, it always was. This line has no reason to outlive that consumer - // reading the platform's own name for the session. - HYPERLOOM_SESSION_ID: sessionId, INFERENCE_OPTIMIZER_SESSION_LAYOUT: "per_model_ts", MCP_PORT: mcpPort, WORKSPACE_PATH: "/workspace", 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",