From 85230cf76c23f08a7d60d4b89b38f0d98d077818 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 07:06:40 -0700 Subject: [PATCH] test(secrets): cover Code Mode exec store env and document the harness boundary (#122405) * test(secrets): cover code-mode nested exec store env; document harness boundary * docs(secrets): warn that store env does not reach external agent harnesses --- docs/cli/secrets.md | 6 ++++- docs/gateway/secrets.md | 4 +++- src/agents/bash-tools.exec.store-env.test.ts | 24 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/cli/secrets.md b/docs/cli/secrets.md index 370364567316..ff00f610d89b 100644 --- a/docs/cli/secrets.md +++ b/docs/cli/secrets.md @@ -92,7 +92,11 @@ openclaw secrets store get LOG_LEVEL Secret values never appear in human, `--json`, or `--plain` output. `store get` refuses a `secret` entry as write-only by design and exits `2`; it exits `3` when the name does not exist. Environment-kind values are readable. -Team-scoped `env` entries also reach agent exec environments. Explicit per-call env wins over store values, and host/sandbox security filters can reject protected or credential-shaped names with a warning. `secret` entries are never exposed as subprocess env; use them through `store` SecretRefs instead. +Team-scoped `env` entries also reach commands run by OpenClaw's own exec tool, including Code Mode, sandboxed exec, and `node`-hosted exec. Explicit per-call env wins over store values, and host/sandbox security filters can reject protected or credential-shaped names with a warning. `secret` entries are never exposed as subprocess env; use them through `store` SecretRefs instead. + + +Store entries do not reach commands run inside an external agent harness. The Codex app-server and its sandbox exec-server, and ACP children such as Claude Code, build their own child environment and never pass through OpenClaw's exec preparation. If an agent run is delegated to one of those harnesses, set the variable in that harness's own configuration instead. + ### Remove values diff --git a/docs/gateway/secrets.md b/docs/gateway/secrets.md index a05de68086c1..0ba637ac0f7d 100644 --- a/docs/gateway/secrets.md +++ b/docs/gateway/secrets.md @@ -278,7 +278,9 @@ The shared secret store is a Gateway-wide, team-scoped place for secrets and env Entries have a `secret` or `env` kind. The kind controls CLI disclosure, not SecretRef resolution: - `secret` values are write-only after saving. Gateway list results, the Control UI, and CLI list/get output never include them; there is no reveal RPC. -- `env` values remain visible to administrators in the Control UI and can be returned by `store list` and `store get`. Team-scoped `env` entries are also added to agent exec environments, after inherited process values and before explicit per-call env. Protected host keys and sandbox-blocked credential names are ignored with a visible warning. +- `env` values remain visible to administrators in the Control UI and can be returned by `store list` and `store get`. Team-scoped `env` entries are also added to the environment of commands run by OpenClaw's own exec tool, after inherited process values and before explicit per-call env. Protected host keys and sandbox-blocked credential names are ignored with a visible warning. This covers direct tool calls, Code Mode (whose guest reaches shell through the same `openclaw:core:exec` tool), sandboxed exec, and `node` -hosted exec. + +It does not cover commands executed inside a provider-native harness — the Codex app-server and its sandbox exec-server, or ACP children such as Claude Code. Those harnesses assemble their own child environment and never pass through OpenClaw's exec preparation, so store entries are absent there. The store snapshot is also read once per agent run, so entries added mid-run apply from the next run onward. `secret` entries are never injected into subprocess environments. They remain available only through `store` SecretRefs because plaintext env injection would bypass the store disclosure boundary; safe secret injection requires a future egress-substitution mechanism. diff --git a/src/agents/bash-tools.exec.store-env.test.ts b/src/agents/bash-tools.exec.store-env.test.ts index 14a835f29101..1c3f24bb299f 100644 --- a/src/agents/bash-tools.exec.store-env.test.ts +++ b/src/agents/bash-tools.exec.store-env.test.ts @@ -120,6 +120,30 @@ describe("exec store environment", () => { ); }); + it("applies store env when code mode invokes exec through the hidden tool catalog", async () => { + // Code mode never runs shell itself: its guest calls `openclaw:core:exec`, which + // re-enters this same tool object. Re-executing one instance is what that nested + // route does, so store env must land on every call, not only the first. + await withTeamStoreEntries( + [ + { name: "AWS_REGION", value: "us-west-2", kind: "env" }, + { name: "INTERNAL_VALUE", value: "not-for-subprocesses", kind: "secret" }, + ], + async () => { + const tool = createLazyExecTool({ host: "gateway", security: "full", ask: "off" }); + + await tool.execute("code-mode-first", { command: "echo one", yieldMs: 120_000 }); + await tool.execute("code-mode-nested", { command: "echo two", yieldMs: 120_000 }); + + expect(mocks.gatewayParams).toHaveLength(2); + for (const params of mocks.gatewayParams) { + expect(params.env.AWS_REGION).toBe("us-west-2"); + expect(params.env).not.toHaveProperty("INTERNAL_VALUE"); + } + }, + ); + }); + it("lets explicitly requested env override a store entry", async () => { await withTeamStoreEntries( [{ name: "AWS_REGION", value: "us-west-2", kind: "env" }],