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

View File

@@ -8,7 +8,7 @@ Source: [`packages/subprocess/subprocess/src/types.ts`](../../packages/subproces
## Managed environment namespace and captured output
`DSH_*` variables are Harness-owned child-process facts; implementations discard ambient `DSH_*` names before merging the caller's snapshot, and each collected stream reports its truncation and spill-recovery state through `CollectedOutput`.
`DSH_*` variables are Harness-owned child-process facts; implementations discard ambient `DSH_*` names before the caller's explicit `env` merges, so a current fact arrives only as a deliberate entry, and each collected stream reports its truncation and spill-recovery state through `CollectedOutput`.
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
@@ -114,19 +114,14 @@ interface SubprocessSpawnSpec {
*/
signal?: AbortSignal | undefined
/**
* Ordinary environment entries merged onto the implementation's scrubbed
* parent base (see `scrubbedParentEnv`). `DSH_*` names are rejected and
* belong in {@link dshEnv}; a deliberately forwarded credential-shaped
* entry survives because this layer merges after the scrub.
* Explicit environment entries merged onto the implementation's scrubbed
* parent base (see `scrubbedParentEnv`), with no namespace validation:
* every entry is a deliberate caller opt-in, so a forwarded
* credential-shaped entry or a current `DSH_*` fact survives precisely
* because this layer merges after the scrub that drops its ambient
* namesake.
*/
env?: Record<string, string> | undefined
/**
* Harness-owned `DSH_*` variables for this execution. The scrubbed base has
* already discarded ambient `DSH_*` entries, so an unavailable current fact
* cannot inherit a stale value from the harness process; non-`DSH_*` names
* on this channel are rejected.
*/
dshEnv?: DshEnvironment | undefined
}
```