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:
@@ -12,7 +12,6 @@
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import { DSH_ENV_PREFIX } from './types.ts'
|
||||
import type { DshEnvironment, DshEnvironmentKey } from './types.ts'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts'
|
||||
|
||||
export { DSH_ENV_PREFIX } from './types.ts'
|
||||
@@ -46,11 +45,10 @@ export const SENSITIVE_ENV_PATTERN = /KEY|SECRET|TOKEN/i
|
||||
* The ambient parent environment minus credential-shaped names and minus all
|
||||
* `DSH_*` names — the canonical base every harness child starts from. `PATH`,
|
||||
* `HOME`, locale, and proxy variables survive, so child CLIs run normally;
|
||||
* harness identity never leaks implicitly (a child that needs current `DSH_*`
|
||||
* facts receives them through {@link SubprocessSpawnSpec.dshEnv}, and a
|
||||
* deliberately forwarded credential goes through an explicit env layer, which
|
||||
* merges after this scrub). Exported as a plain function so spawners that
|
||||
* cannot route through the service (node-pty backends, SDK-managed
|
||||
* harness identity never leaks implicitly (a deliberately forwarded
|
||||
* credential or current `DSH_*` fact goes through the spec's explicit `env`,
|
||||
* which merges after this scrub). Exported as a plain function so spawners
|
||||
* that cannot route through the service (node-pty backends, SDK-managed
|
||||
* transports) share the one scrub definition.
|
||||
* @returns a fresh environment object safe to hand to a child spawn.
|
||||
*/
|
||||
@@ -62,27 +60,6 @@ export function scrubbedParentEnv(): Record<string, string> {
|
||||
return env
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition one mixed explicit-env map onto the spec's two channels: `DSH_*`
|
||||
* names are deployment-owned facts for the child and take the managed
|
||||
* {@link SubprocessSpawnSpec.dshEnv} channel (the ordinary channel rejects the
|
||||
* reserved namespace), everything else stays ordinary `env`. For consumers
|
||||
* whose configs expose a single env map (lsp-local servers, the ACP backend)
|
||||
* rather than two channel-shaped fields.
|
||||
* @param env - explicit entries from a consumer's config, both namespaces mixed.
|
||||
* @returns the two spec channels, each safe for its validator.
|
||||
*/
|
||||
export function splitEnvChannels(env: Readonly<Record<string, string>>): { env: Record<string, string>; dshEnv: DshEnvironment } {
|
||||
const ordinary: Record<string, string> = {}
|
||||
const managed: Record<DshEnvironmentKey, string> = {}
|
||||
const isDshKey = (key: string): key is DshEnvironmentKey => key.startsWith(DSH_ENV_PREFIX)
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (isDshKey(key)) managed[key] = value
|
||||
else ordinary[key] = value
|
||||
}
|
||||
return { env: ordinary, dshEnv: managed }
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
subprocess: SubprocessService
|
||||
|
||||
Reference in New Issue
Block a user