fix(subprocess): consumers with one mixed env map split DSH_* onto the managed channel

Codex review of the acp-only fix found lsp-local carries the identical
defect: its server config's unrestricted env merges into the connection's
ordinary spawn channel, so a configured DSH_* fact crashed the spawn with
the reserved-namespace rejection. The partition now lives on the seam as
splitEnvChannels() beside the scrub it complements; the ACP run and the
LSP connection both use it, and each proves child delivery end-to-end
(MOCK_ECHO_ENV / LSP_FAKE_ECHO_ENV fixture knobs). Seam + consumer README
rows updated (en+zh, re-recorded). bash-local is already two-channel;
mcp/pty/sdk bypass the seam and only share the scrub.
This commit is contained in:
Tianyi Cui
2026-07-27 01:21:32 +08:00
parent fced51d4eb
commit 43d81b67ce
12 changed files with 62 additions and 25 deletions

View File

@@ -12,6 +12,7 @@
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'
@@ -61,6 +62,27 @@ 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