From 0d5dbeea1838de1b0b1ace88467ac63fe9c54a27 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Mon, 12 Oct 2026 18:58:56 +0700 Subject: [PATCH] 0.1.2: hide the provider API key from the agent's shell buildShellEnv copied process.env and the settings env into every bash call, so DEEPCODE_API_KEY (and the prefix-stripped API_KEY) were visible to the model. Anything it printed ended up in tool output, the JSON result and orchestrator logs. Strip them (vendor patch scrub-api-key). Also make the timeout test poll for the killed child instead of racing it. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- test/e2e.test.mjs | 22 +++++++++++++++++-- vendor/deepcode-core/UPSTREAM.md | 1 + .../deepcode-core/src/common/shell-utils.ts | 8 ++++++- vendor/patches/scrub-api-key.patch | 22 +++++++++++++++++++ 7 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 vendor/patches/scrub-api-key.patch diff --git a/README.md b/README.md index 2d0da2c..d47e990 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ No TTY to ask, so nothing ever prompts. `--mode` (same names as zh) picks a pres | `edit` | reads, writes/deletes inside cwd and tmp, network, MCP | writes/deletes outside cwd, git history changes | | `plan` | reads, network, MCP | all writes and deletes, git history changes | +The agent's shell does not inherit the provider credentials (`DEEPCODE_API_KEY`, `DEEPCODE_PLUS_API_KEY`); other environment variables are passed through. + Bash commands declare their own scopes (the model fills `sideEffects`), so a mode is not a boundary for bash. For a read-only agent also pass `--disallowed-tools bash`: the tool leaves the schema and the system prompt, and a call to it anyway is refused. SDK: `createHarness({ mode, permissions, disallowedTools })`. diff --git a/package-lock.json b/package-lock.json index 6fcf34d..e1e0f72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coderbuzz/dsh", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coderbuzz/dsh", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "dependencies": { "chalk": "^5.6.2", diff --git a/package.json b/package.json index d0fb949..1cf7d60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coderbuzz/dsh", - "version": "0.1.1", + "version": "0.1.2", "description": "DeepSeek harness for orchestrators: structured JSON SDK and CLI on top of the deepcode-cli engine", "license": "MIT", "repository": { diff --git a/test/e2e.test.mjs b/test/e2e.test.mjs index d9fe9f9..e6a3e09 100644 --- a/test/e2e.test.mjs +++ b/test/e2e.test.mjs @@ -35,7 +35,11 @@ const server = http.createServer(async (req, res) => { if (last.role === "user" && JSON.stringify(last.content).includes("again")) return sse(res, { role: "assistant", reasoning_content: "thought-3", content: "again-done" }, true); const user = JSON.stringify(json.messages.filter((m) => m.role === "user").at(-1)?.content); if (user.includes("PARALLEL")) return sse(res, { role: "assistant", content: "parallel-done" }, true); - const command = user.includes("SLEEP") ? "sleep 37.25" : "echo hello-dsh"; + const command = user.includes("SLEEP") + ? "sleep 37.25" + : user.includes("ENVCHECK") + ? "env | grep -E '^(DEEPCODE_)?(PLUS_)?API_KEY=' | wc -l; echo MARK=$MARK USERVAR=${OTHER_API_KEY:-unset}" + : "echo hello-dsh"; sse(res, { role: "assistant", reasoning_content: "thought-1", @@ -128,7 +132,12 @@ test("timeout stops the run and kills its child processes", async () => { assert.equal(r.status, "failed"); assert.match(r.failReason, /timeout/); assert.ok(Date.now() - t0 < 15_000); - const left = await run("pgrep", ["-f", "sleep 37.25"]).then((o) => o.stdout, () => ""); + // SIGKILL reaches the child asynchronously: poll briefly instead of asserting at the instant dsh exits. + let left = "x"; + for (let i = 0; i < 20 && left !== ""; i++) { + left = await run("pgrep", ["-f", "sleep 37.25"]).then((o) => o.stdout, () => ""); + if (left !== "") await new Promise((r) => setTimeout(r, 100)); + } assert.equal(left, "", "sleep child was killed"); }); @@ -152,3 +161,12 @@ test("telemetry is off unless DEEPCODE_TELEMETRY_ENABLED=true", async () => { const on = await runWith({ DEEPCODE_TELEMETRY_ENABLED: "true" }); assert.match(on.stderr, /\/api\/plugin\/new/, "probe sees telemetry when enabled"); }); + +test("agent bash does not see the provider API key, but keeps other env", async () => { + const out = await run("node", [cli, "--json", "ENVCHECK"], { cwd, env: { ...env, DEEPCODE_PLUS_API_KEY: "sk-x", MARK: "kept", OTHER_API_KEY: "mine" } }); + const r = JSON.parse(out.stdout); + assert.equal(r.status, "completed", r.failReason); + // 0 = none of DEEPCODE_API_KEY, DEEPCODE_PLUS_API_KEY and the prefix-stripped API_KEY/PLUS_API_KEY reach the shell; + // the user's own variables (even ones ending in API_KEY) are untouched. + assert.equal(r.toolExecutions[0].process_stdout, "0\nMARK=kept USERVAR=mine\n"); +}); diff --git a/vendor/deepcode-core/UPSTREAM.md b/vendor/deepcode-core/UPSTREAM.md index 20d2bfb..72c3408 100644 --- a/vendor/deepcode-core/UPSTREAM.md +++ b/vendor/deepcode-core/UPSTREAM.md @@ -6,6 +6,7 @@ - Copied: `src/` (without `src/tests/`) and `templates/`. - Local patches in `vendor/patches/` (applied by the sync script): - `reasoning-effort-top-level.patch`: `reasoning_effort` sent top level instead of `extra_body` (DeepSeek ignores `extra_body` from the Node SDK; upstream bug). + - `scrub-api-key.patch`: the bash tool's shell no longer inherits `DEEPCODE_API_KEY`, `DEEPCODE_PLUS_API_KEY` or the stripped `API_KEY`/`PLUS_API_KEY` from settings env. - `orchestrator.patch`: `disallowedTools` setting (tool removed from schema and docs, refused if called), and a lock file + atomic write for `sessions-index.json` so parallel runs in one project do not lose sessions. Why vendored: `@vegamo/deepcode-core` is not published on npm. diff --git a/vendor/deepcode-core/src/common/shell-utils.ts b/vendor/deepcode-core/src/common/shell-utils.ts index eb8a2da..af900f2 100644 --- a/vendor/deepcode-core/src/common/shell-utils.ts +++ b/vendor/deepcode-core/src/common/shell-utils.ts @@ -155,12 +155,18 @@ export function toNativeCwd(shellCwd: string): string { } export function buildShellEnv(shellPath: string, extraEnv: Record = {}): NodeJS.ProcessEnv { + // dsh patch: the model's shell must not see the provider credentials. They come in twice: as + // DEEPCODE_* process env vars and, prefix stripped, inside `extraEnv` (settings.env + DEEPCODE_*). + // Anything the shell prints ends up in tool output, the JSON result and orchestrator logs. + const { API_KEY: _apiKey, PLUS_API_KEY: _plusApiKey, ...safeExtraEnv } = extraEnv; const env: NodeJS.ProcessEnv = { ...process.env, - ...extraEnv, + ...safeExtraEnv, SHELL: shellPath, GIT_EDITOR: "true", }; + delete env["DEEPCODE_API_KEY"]; + delete env["DEEPCODE_PLUS_API_KEY"]; if (process.platform === "win32") { const tmpdir = windowsPathToPosixPath(os.tmpdir()); diff --git a/vendor/patches/scrub-api-key.patch b/vendor/patches/scrub-api-key.patch new file mode 100644 index 0000000..211678e --- /dev/null +++ b/vendor/patches/scrub-api-key.patch @@ -0,0 +1,22 @@ +--- a/src/common/shell-utils.ts ++++ b/src/common/shell-utils.ts +@@ -155,12 +155,18 @@ + } + + export function buildShellEnv(shellPath: string, extraEnv: Record = {}): NodeJS.ProcessEnv { ++ // dsh patch: the model's shell must not see the provider credentials. They come in twice: as ++ // DEEPCODE_* process env vars and, prefix stripped, inside `extraEnv` (settings.env + DEEPCODE_*). ++ // Anything the shell prints ends up in tool output, the JSON result and orchestrator logs. ++ const { API_KEY: _apiKey, PLUS_API_KEY: _plusApiKey, ...safeExtraEnv } = extraEnv; + const env: NodeJS.ProcessEnv = { + ...process.env, +- ...extraEnv, ++ ...safeExtraEnv, + SHELL: shellPath, + GIT_EDITOR: "true", + }; ++ delete env["DEEPCODE_API_KEY"]; ++ delete env["DEEPCODE_PLUS_API_KEY"]; + + if (process.platform === "win32") { + const tmpdir = windowsPathToPosixPath(os.tmpdir());