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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user