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
+5 -7
View File
@@ -25,7 +25,6 @@ import {
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
import { splitEnvChannels } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
/** Fixed response to child permission requests: reject by default, or select the first allow option. */
@@ -51,8 +50,8 @@ export interface AcpRunSpec {
* parent env. A value here is forwarded even if its name matches the
* credential-scrub pattern (an explicit opt-in for the child's own creds).
* Explicit `DSH_*` entries are deployment-owned facts for the child harness
* (e.g. `DSH_PERMISSION_MODE`) and ride the seam's managed channel, which
* the scrubbed base reserves for current values.
* (e.g. `DSH_PERMISSION_MODE`); they simply merge after the scrub that
* dropped their stale ambient namesakes.
*/
env: Record<string, string>
/**
@@ -169,15 +168,14 @@ export async function startAcpRun(request: SubagentStartRequest, spec: AcpRunSpe
const id = SessionId(randomUUID())
// Keep diagnostics on parent stderr ('inherit'); only ACP output contributes
// to the result. The seam's scrub drops ambient credentials while spec.env
// (the child's own key) merges after it; explicit DSH_* entries ride the
// managed channel via the seam's split.
// to the result. The seam's scrub drops ambient credentials and DSH_* names
// while spec.env (the child's own key, its deployment facts) merges after it.
const child = spec.spawn({
argv: [spec.command, ...spec.args],
cwd: spec.cwd,
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
graceMs: spec.disposeGraceMs,
...splitEnvChannels(spec.env),
env: spec.env,
})
/* v8 ignore start -- 'pipe' dispositions expose both streams by the seam contract; defensive. */
if (child.stdin === undefined || child.stdout === undefined) {