subprocess: one explicit env channel on the spawn spec

Drop SubprocessSpawnSpec.dshEnv and splitEnvChannels(); childEnv() is now
scrubbed-base + explicit entries with no namespace validation. The invariant
dropped is the reserved-namespace check on explicit entries (DSH_* rejected
from env, non-DSH_* rejected from dshEnv). Explicit-entry trust already
covers it: an explicit credential-shaped entry has always merged after the
scrub as a deliberate caller opt-in, and an explicit DSH_* entry is the same
deliberate act — the staleness invariant lives entirely in scrubbedParentEnv
dropping AMBIENT credential-shaped and DSH_* names, which stays. The
validation's only observed effect was rejecting legitimate explicit entries:
both recent CI breakages (DSH_GATE_CONCURRENCY exported into every job
crashing lsp specs, DSH_PERMISSION_MODE in acp config.env crashing the
child spawn) were this check firing on values a caller meant to pass, each
fixed by routing around the bureaucracy the seam itself imposed.

The bash seam keeps its own request/spec dshEnv field: that is bash-owned
trusted-plugin vocabulary (the ctx.bashEnv collected overlay) whose merge-last
position guarantees a caller env entry cannot displace a managed fact;
bash-local now flattens ENV_OVERRIDES -> spec.env -> spec.dshEnv into the
seam's one env map. subagent-acp and lsp-local pass their single config env
map straight through. DshEnvironment/DshEnvironmentKey/DSH_ENV_PREFIX stay on
the subprocess seam as the namespace vocabulary (bash re-exports them;
scrubbedParentEnv filters on the prefix).

Tests: the two channel-rejection specs and the splitEnvChannels partition
spec are deleted; one spawn spec now proves an explicit DSH_* env entry
reaches the child while an ambient one is scrubbed; the acp/lsp forwarding
specs keep their MOCK_ECHO_ENV / LSP_FAKE_ECHO_ENV assertions with the split
comments rewritten to merge-after-scrub. Docs (en+zh, re-recorded) and the
owning Agent Notes updated; cordis api/services catalogs regenerated.
This commit is contained in:
Tianyi Cui
2026-07-27 04:14:51 +08:00
parent cb1864795e
commit 5717726835
43 changed files with 134 additions and 201 deletions
@@ -16,10 +16,9 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { setTimeout as sleepMs } from 'node:timers/promises'
import { deadline } from '@deepseek-ai/dsh-timeout'
import { DSH_ENV_PREFIX, scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
import type {
CollectedOutput,
DshEnvironment,
SubprocessCollect,
SubprocessDisposeGraces,
SubprocessHandle,
@@ -29,28 +28,14 @@ import type {
} from '@deepseek-ai/dsh-subprocess'
/**
* Build a child environment from the scrubbed parent base, ordinary caller
* entries, and a managed `DSH_*` snapshot. Ordinary and managed entries
* reject the other channel's namespace before `dshEnv` merges last.
* @param extra - caller entries; `DSH_*` names are rejected.
* @param dshEnv - managed entries; non-`DSH_*` names are rejected.
* Build a child environment: explicit caller entries merge after the scrubbed
* parent base, so a deliberately supplied credential or current `DSH_*` fact
* wins over the scrub that dropped its ambient namesake.
* @param extra - explicit caller entries, merged verbatim after the scrub.
* @returns the environment to hand to `spawn` for the child process.
*/
export function childEnv(
extra?: Readonly<Record<string, string>>,
dshEnv?: DshEnvironment,
): NodeJS.ProcessEnv {
for (const key of Object.keys(extra ?? {})) {
if (key.startsWith(DSH_ENV_PREFIX)) {
throw new Error(`ordinary child env cannot set reserved variable "${key}"; use dshEnv`)
}
}
for (const key of Object.keys(dshEnv ?? {})) {
if (!key.startsWith(DSH_ENV_PREFIX)) {
throw new Error(`managed child env cannot set ordinary variable "${key}"; use env`)
}
}
return { ...scrubbedParentEnv(), ...extra, ...dshEnv }
export function childEnv(extra?: Readonly<Record<string, string>>): NodeJS.ProcessEnv {
return { ...scrubbedParentEnv(), ...extra }
}
/** Injectable knobs so tests can exercise spill and platform behavior deterministically. */
@@ -339,7 +324,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
const errMode = spec.stdio.stderr
const stdinMode = spec.stdio.stdin
const env = childEnv(spec.env, spec.dshEnv)
const env = childEnv(spec.env)
const child = spawn(program, args, {
cwd: spec.cwd,
env,