Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 })`.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
22 changes: 20 additions & 2 deletions test/e2e.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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");
});

Expand All @@ -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");
});
1 change: 1 addition & 0 deletions vendor/deepcode-core/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 7 additions & 1 deletion vendor/deepcode-core/src/common/shell-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,18 @@ export function toNativeCwd(shellCwd: string): string {
}

export function buildShellEnv(shellPath: string, extraEnv: Record<string, string> = {}): 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());
Expand Down
22 changes: 22 additions & 0 deletions vendor/patches/scrub-api-key.patch
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}): 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());
Loading