fix(subagent): route explicit DSH_* config entries onto the managed env channel

The acp run passed config.env wholesale into the spawn spec's ordinary
channel, which rejects the reserved DSH_* namespace — a deployment fact
like DSH_PERMISSION_MODE (the acp-agent example's own knob, used by the
with-key e2e) crashed the spawn. The run now splits DSH_* entries onto
dshEnv, where the scrubbed base expects current facts to arrive. New
layering test drives the split through the real seam via a MOCK_ECHO_ENV
knob on the mock server; README env prose updated (en+zh, re-recorded).
This commit is contained in:
Tianyi Cui
2026-07-27 01:04:37 +08:00
parent 23e82fdfb0
commit fced51d4eb
6 changed files with 45 additions and 8 deletions
+17 -3
View File
@@ -25,7 +25,8 @@ 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 type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-subprocess'
import type { DshEnvironmentKey, SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
/** Fixed response to child permission requests: reject by default, or select the first allow option. */
export type PermissionPolicy = 'allow' | 'reject'
@@ -49,6 +50,9 @@ export interface AcpRunSpec {
* `DEEPSEEK_API_KEY`). Merged on top of the subprocess seam's scrubbed
* 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.
*/
env: Record<string, string>
/**
@@ -166,13 +170,23 @@ export async function startAcpRun(request: SubagentStartRequest, spec: AcpRunSpe
// 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.
// (the child's own key) merges after it. Explicit DSH_* entries are the
// deployment's facts for the child and take the managed channel — the
// ordinary channel rejects that reserved namespace.
const env: Record<string, string> = {}
const dshEnv: Record<DshEnvironmentKey, string> = {}
const isDshKey = (key: string): key is DshEnvironmentKey => key.startsWith(DSH_ENV_PREFIX)
for (const [key, value] of Object.entries(spec.env)) {
if (isDshKey(key)) dshEnv[key] = value
else env[key] = value
}
const child = spec.spawn({
argv: [spec.command, ...spec.args],
cwd: spec.cwd,
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
graceMs: spec.disposeGraceMs,
env: spec.env,
env,
dshEnv,
})
/* v8 ignore start -- 'pipe' dispositions expose both streams by the seam contract; defensive. */
if (child.stdin === undefined || child.stdout === undefined) {