diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 561b59e10e..6b69cae609 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -108,6 +108,9 @@ flowchart LR pkg_subagent_acp["subagent-acp"] pkg_bash["bash"] svc_bash["ctx.bash
Bash executor seam"] + pkg_pwsh_local["pwsh-local"] + pkg_tool_pwsh["tool-pwsh"] + pkg_bash_env["bash-env"] svc_bashEnv["ctx.bashEnv
Managed bash environment registry"] pkg_pty["pty"] svc_pty["ctx.pty
Persistent PTY session registry"] @@ -170,6 +173,7 @@ flowchart LR pkg_agent_loop --> svc_agentLoop pkg_approval --> svc_approval pkg_bash --> svc_bash + pkg_bash_env --> svc_bashEnv pkg_bash_local --> svc_bash pkg_bash_sandbox --> svc_bash pkg_code_runtime --> svc_codeRuntime @@ -197,6 +201,7 @@ flowchart LR pkg_plan_mode --> svc_planMode pkg_pty --> svc_pty pkg_pty_local --> svc_pty + pkg_pwsh_local --> svc_bash pkg_sandbox --> svc_sandbox pkg_sandbox_local --> svc_sandbox pkg_sandbox_policy --> svc_sandboxPolicy @@ -234,7 +239,6 @@ flowchart LR pkg_tasks --> svc_tasks pkg_tasks_local --> svc_tasks pkg_token_meter --> svc_tokenMeter - pkg_tool_bash --> svc_bashEnv pkg_tools --> svc_tools pkg_tui --> svc_tui pkg_tui --> svc_userInteraction @@ -260,6 +264,9 @@ flowchart LR svc_bash --> pkg_hooks_claude svc_bash --> pkg_hooks_codex svc_bash --> pkg_tool_bash + svc_bash --> pkg_tool_pwsh + svc_bashEnv --> pkg_tool_bash + svc_bashEnv --> pkg_tool_pwsh svc_clientModuleHost --> pkg_hmr svc_codeRuntime --> pkg_tools svc_commands --> pkg_tui @@ -381,8 +388,8 @@ flowchart LR | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. | | `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp) | - | The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. | -| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. | -| `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. | +| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pwsh-local`](../packages/bash/pwsh-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. | +| `ctx.bashEnv` | `core` | [`bash-env`](../packages/bash/bash-env) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. | | `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface. | | `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. | | `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/bash/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox), [`pty-local`](../packages/pty/pty-local) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. | diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index 824780ae0f..c107a1e2b0 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -6,6 +6,7 @@ import { dirname, join } from 'node:path' import { homedir } from 'node:os' import { expect, it } from 'vitest' import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from '@deepseek-ai/dsh-acp-snapshot' +import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local' import { decodeStorageRecord } from '@deepseek-ai/dsh-session' /** @@ -170,7 +171,9 @@ const SCENARIOS: Scenario[] = [ headerClass: 'pwsh', configPath: PWSH_CONFIG, // The composition boots the real pwsh executor; hosts without a `pwsh` - // binary skip the run (fixtures stay guarded). + // binary skip the run (fixtures stay guarded). The recorded turn writes + // PWSH_OK via [Console]::Out.Write so the fixture carries no platform + // newline and one recording replays on every host. pwshOnly: true, }, { name: 'todo-write', hasModelTurn: true, recorded: true }, @@ -435,9 +438,10 @@ const SCENARIOS: Scenario[] = [ }, ] -// Hosts without a `pwsh` binary skip the pwsh-tool-turn run (its fixtures -// stay guarded); a bare `pwsh` probe keeps this suite dependency-light. -const hasPwsh = spawnSync('pwsh', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0 +// Hosts without a usable PowerShell skip the pwsh-tool-turn run (its fixtures +// stay guarded); the probe follows the executor's own resolution so a Windows +// host with only an install-location pwsh still runs the scenario. +const hasPwsh = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0 defineAcpSnapshotSuite({ agent: AGENT, diff --git a/examples/acp-agent/tests/snapshots/pwsh-tool-turn/input.json b/examples/acp-agent/tests/snapshots/pwsh-tool-turn/input.json index 4101a2c1f3..653e9a346c 100644 --- a/examples/acp-agent/tests/snapshots/pwsh-tool-turn/input.json +++ b/examples/acp-agent/tests/snapshots/pwsh-tool-turn/input.json @@ -2,6 +2,6 @@ "steps": [ { "op": "initialize" }, { "op": "newSession" }, - { "op": "prompt", "text": "Use the pwsh tool to run exactly: Write-Output PWSH_OK. Then reply with the single word DONE and stop." } + { "op": "prompt", "text": "Use the pwsh tool to run exactly: [Console]::Out.Write('PWSH_OK'). Then reply with the single word DONE and stop." } ] } diff --git a/examples/acp-agent/tests/snapshots/pwsh-tool-turn/session.jsonl b/examples/acp-agent/tests/snapshots/pwsh-tool-turn/session.jsonl index 333948541c..9dc17ef799 100644 --- a/examples/acp-agent/tests/snapshots/pwsh-tool-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/pwsh-tool-turn/session.jsonl @@ -1,32 +1,32 @@ -{"type":"session","version":0,"id":"1ec0d099-552b-44e1-8fb8-fd9742fbdc1c","createdAt":1785655505943,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"turn/start","seq":0,"time":1785655505948,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":1785655505948,"data":{"content":[{"type":"text","text":"Use the pwsh tool to run exactly: Write-Output PWSH_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"e3bca81a-d4e4-46fb-aa30-916f785c27a0"},"surfaceOp":"append"} -{"type":"session/title","seq":2,"time":1785655505949,"data":{"title":"Use the pwsh tool to","messageSeqs":[1],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":3,"time":1785655505971,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":1785655505972,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","seq":5,"time":1785655505972,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}} -{"type":"assistant/chunk","seq":6,"time":1785655506674,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"reasoning-chunks","seq0":7,"time0":1785655506674,"data":{"turn":1,"step":1,"index":0,"dt":[157,27,1,0,0,0,38,0,0,43,0,0,0,50,0,1,0,0,0,36,0,1,0,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," PowerShell"," command"," that"," outputs"," \"","P","WS","H","_OK","\""," and"," then"," reply"," with"," \"","D","ONE","\"."]}} -{"type":"assistant/chunk","seq":32,"time":1785655507160,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} -{"type":"tool-call-chunks","seq0":33,"time0":1785655507161,"data":{"turn":1,"step":1,"index":1,"dt":[51,0,0,0,0,32,0,0,0,55,1,0,0,0,74,0,0,0,1,45,0,42,0,0,0,0,54,0],"id":"call_00_oEhmLGLNsvlumiE0WkXD0511","name":"pwsh","args":["","{","\"","command","\"",": ","\"","Write","-","Output"," P","WS","H","_OK","\"",", ","\"","description","\"",": ","\"","Output"," P","WS","H","_OK"," string","\"","}"]}} -{"type":"assistant/chunk","seq":62,"time":1785655507601,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a PowerShell command that outputs \"PWSH_OK\" and then reply with \"DONE\"."}}}} -{"type":"assistant/chunk","seq":63,"time":1785655507602,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_oEhmLGLNsvlumiE0WkXD0511","name":"pwsh","arguments":"{\"command\": \"Write-Output PWSH_OK\", \"description\": \"Output PWSH_OK string\"}"}}}} -{"type":"assistant/chunk","seq":64,"time":1785655507602,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":816,"outputTokens":96,"cacheReadTokens":0,"reasoningTokens":25}}}} -{"type":"assistant/chunk","seq":65,"time":1785655507602,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":66,"time":1785655507604,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to run a PowerShell command that outputs \"PWSH_OK\" and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_oEhmLGLNsvlumiE0WkXD0511","name":"pwsh","arguments":"{\"command\": \"Write-Output PWSH_OK\", \"description\": \"Output PWSH_OK string\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"4d81cc10-507e-49a8-96f4-aa3ae5ad2eb5"},"usage":{"inputTokens":816,"outputTokens":96,"cacheReadTokens":0,"reasoningTokens":25}},"sourceEventSeqs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65],"surfaceOp":"append"} -{"type":"tool/call","seq":67,"time":1785655507605,"data":{"turn":1,"step":1,"callId":"call_00_oEhmLGLNsvlumiE0WkXD0511","name":"pwsh","arguments":"{\"command\": \"Write-Output PWSH_OK\", \"description\": \"Output PWSH_OK string\"}"}} -{"type":"tool/result","seq":68,"time":1785655507994,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_oEhmLGLNsvlumiE0WkXD0511"},"content":[{"type":"tool-result","toolCallId":"call_00_oEhmLGLNsvlumiE0WkXD0511","content":[{"type":"text","text":"PWSH_OK\n"}],"isError":false}],"role":"user","id":"84a96d55-5bd5-46fc-ab0d-918882c504e2"}},"sourceEventSeqs":[67],"surfaceOp":"append"} -{"type":"step/end","seq":69,"time":1785655507994,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":70,"time":1785655508000,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":71,"time":1785655508608,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"reasoning-chunks","seq0":72,"time0":1785655508608,"data":{"turn":1,"step":2,"index":0,"dt":[168,38,0,60,1,0,0,0,0,27,1,0,0,0,46,0,0,0,0,0,41],"texts":["The"," command"," successfully"," output"," \"","P","WS","H","_OK","\"."," Now"," I"," should"," reply"," with"," \"","D","ONE","\""," as"," instructed","."]}} -{"type":"assistant/chunk","seq":94,"time":1785655508990,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":95,"time":1785655508991,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} -{"type":"assistant/chunk","seq":96,"time":1785655508991,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} -{"type":"assistant/chunk","seq":97,"time":1785655508991,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command successfully output \"PWSH_OK\". Now I should reply with \"DONE\" as instructed."}}}} -{"type":"assistant/chunk","seq":98,"time":1785655508991,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":99,"time":1785655508991,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":34,"outputTokens":25,"cacheReadTokens":896,"reasoningTokens":22}}}} -{"type":"assistant/chunk","seq":100,"time":1785655508991,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":101,"time":1785655508991,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The command successfully output \"PWSH_OK\". Now I should reply with \"DONE\" as instructed."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"d8905016-347c-477a-a8f7-2b31839a8357"},"usage":{"inputTokens":34,"outputTokens":25,"cacheReadTokens":896,"reasoningTokens":22}},"sourceEventSeqs":[71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100],"surfaceOp":"append"} -{"type":"step/end","seq":102,"time":1785655508992,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":103,"time":1785655508992,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"session","version":0,"id":"0b7ff6ab-2486-4b2f-a43e-0fa29a1a46ed","createdAt":1785678162241,"cwd":"{{cwd}}","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1785678162244,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1785678162245,"data":{"content":[{"type":"text","text":"Use the pwsh tool to run exactly: [Console]::Out.Write('PWSH_OK'). Then reply with the single word DONE and stop."}],"source":{"kind":"user"},"role":"user","id":"6efbdc24-7abe-4f34-ac6d-15f93d49ad9a"},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1785678162246,"data":{"title":"Use the pwsh tool to","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"step/start","seq":3,"time":1785678162261,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":4,"time":1785678162261,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","seq":5,"time":1785678162262,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}} +{"type":"assistant/chunk","seq":6,"time":1785678162968,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"reasoning-chunks","seq0":7,"time0":1785678162968,"data":{"turn":1,"step":1,"index":0,"dt":[393,0,0,0,1,0,0,0,0,0,17,0,0,0,0,0,0,0,1,290,0],"texts":["The"," user"," wants"," me"," to"," run"," a"," PowerShell"," command"," and"," then"," reply"," with"," \"","D","ONE","\"."," Let"," me"," execute"," it","."]}} +{"type":"assistant/chunk","seq":29,"time":1785678163671,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"tool-call-chunks","seq0":30,"time0":1785678163671,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,0,0,0,0,17,0,0,0,0,0,109,0,0,0,0,0,0,22,0,0,0,0,275,0,1,0,0,0,0,0,0,0],"id":"call_00_e0MSVSocL0o4UWjOdG4c2072","name":"pwsh","args":["","{","\"","command","\"",": ","\"","[","Console","]","::","Out",".Write","('","P","WS","H","_OK","')","\"",", ","\"","description","\"",": ","\"","Write"," P","WS","H","_OK"," to"," console","\"","}"]}} +{"type":"assistant/chunk","seq":65,"time":1785678164124,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a PowerShell command and then reply with \"DONE\". Let me execute it."}}}} +{"type":"assistant/chunk","seq":66,"time":1785678164124,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_e0MSVSocL0o4UWjOdG4c2072","name":"pwsh","arguments":"{\"command\": \"[Console]::Out.Write('PWSH_OK')\", \"description\": \"Write PWSH_OK to console\"}"}}}} +{"type":"assistant/chunk","seq":67,"time":1785678164124,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1255,"outputTokens":99,"cacheReadTokens":0,"reasoningTokens":22}}}} +{"type":"assistant/chunk","seq":68,"time":1785678164124,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":69,"time":1785678164126,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to run a PowerShell command and then reply with \"DONE\". Let me execute it."},{"type":"tool-call","id":"call_00_e0MSVSocL0o4UWjOdG4c2072","name":"pwsh","arguments":"{\"command\": \"[Console]::Out.Write('PWSH_OK')\", \"description\": \"Write PWSH_OK to console\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"6e968eea-46b8-4489-8005-5e898d53c1a9"},"usage":{"inputTokens":1255,"outputTokens":99,"cacheReadTokens":0,"reasoningTokens":22}},"sourceEventSeqs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68],"surfaceOp":"append"} +{"type":"tool/call","seq":70,"time":1785678164127,"data":{"turn":1,"step":1,"callId":"call_00_e0MSVSocL0o4UWjOdG4c2072","name":"pwsh","arguments":"{\"command\": \"[Console]::Out.Write('PWSH_OK')\", \"description\": \"Write PWSH_OK to console\"}"}} +{"type":"tool/result","seq":71,"time":1785678164405,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_e0MSVSocL0o4UWjOdG4c2072"},"content":[{"type":"tool-result","toolCallId":"call_00_e0MSVSocL0o4UWjOdG4c2072","content":[{"type":"text","text":"PWSH_OK"}],"isError":false}],"role":"user","id":"964dfad5-651e-47f0-90a5-fe5bc711a3ff"}},"sourceEventSeqs":[70],"surfaceOp":"append"} +{"type":"step/end","seq":72,"time":1785678164405,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":73,"time":1785678164410,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":74,"time":1785678165135,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"reasoning-chunks","seq0":75,"time0":1785678165136,"data":{"turn":1,"step":2,"index":0,"dt":[176,44,56,0,0,42,0,0,0,48,0,0,0,48,0,0,60,0,0,0,0,39,0,0],"texts":["The"," command"," executed"," successfully"," and"," printed"," \"","P","WS","H","_OK","\"."," Now"," I"," need"," to"," reply"," with"," \"","D","ONE","\""," and"," stop","."]}} +{"type":"assistant/chunk","seq":100,"time":1785678165649,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":101,"time":1785678165649,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":102,"time":1785678165649,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":103,"time":1785678165693,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command executed successfully and printed \"PWSH_OK\". Now I need to reply with \"DONE\" and stop."}}}} +{"type":"assistant/chunk","seq":104,"time":1785678165693,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":105,"time":1785678165693,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":90,"outputTokens":28,"cacheReadTokens":1280,"reasoningTokens":25}}}} +{"type":"assistant/chunk","seq":106,"time":1785678165693,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":107,"time":1785678165693,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The command executed successfully and printed \"PWSH_OK\". Now I need to reply with \"DONE\" and stop."},{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"91f35706-53e9-4fcd-891e-2c9eafccde98"},"usage":{"inputTokens":90,"outputTokens":28,"cacheReadTokens":1280,"reasoningTokens":25}},"sourceEventSeqs":[74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106],"surfaceOp":"append"} +{"type":"step/end","seq":108,"time":1785678165694,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":109,"time":1785678165694,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/packages/bash/pwsh-local/README.i18n.yaml b/packages/bash/pwsh-local/README.i18n.yaml index 49d38f2b25..0d11b89d6b 100644 --- a/packages/bash/pwsh-local/README.i18n.yaml +++ b/packages/bash/pwsh-local/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bash/pwsh-local/README.md -README.md: c5e0cc524e32d2218e734539c74adcddb663d4f3 -README.zh.md: 7906d8a511ee72ecdffaddaf88ddebe195a20857 +README.md: e061d1bd614fa4aa13d3ae9418c2e4af47d72518 +README.zh.md: 3a8d2b7ea41c02dbaf72273b0e9ef0f2c023068f diff --git a/packages/bash/pwsh-local/README.md b/packages/bash/pwsh-local/README.md index c5e0cc524e..e061d1bd61 100644 --- a/packages/bash/pwsh-local/README.md +++ b/packages/bash/pwsh-local/README.md @@ -50,6 +50,6 @@ No direct invalidation; the named consumer owns any request-prefix changes. - **The command string is PowerShell text** — the `-Command` domain has no shell-quoting layer, but a model-facing command is parsed by PowerShell itself, so PowerShell syntax errors are command failures, not launch failures. - **A background spawn-failure note is single-delivery** — the subprocess service buffers no output for a process that never ran, so the executor injects `spawn failed: …` into exactly one `readOutput()` delta; a reader that discards that delta cannot recover it. - **Windows termination reports no signal** — a force-killed process settles as exit 1 with `signal: null`, so signal-based status classification (POSIX `killed`) does not apply on Windows; `kill()`-initiated stops still stamp `killed` directly. -- **The encoding preamble precedes the command** — PowerShell requires `param(...)`, `#requires`, and `using namespace`/`using assembly` statements at the very top of a script, so a command whose first statement is one of those cannot run under the UTF-8 output preamble; prefix such scripts with a no-op statement (or `& { … }`) first. +- **The encoding preamble precedes the command** — PowerShell requires `param(...)`, `#requires`, and `using namespace`/`using assembly` statements at the very top of a script, so a command whose first statement is one of those cannot run under the UTF-8 output preamble. Wrap a `param(...)` script in `& { … }` (a param block legally heads a script block); `using` statements and `#requires` have no in-command workaround (`#requires` is inert inside `-Command` regardless of position) — run such scripts from a file instead. Scrub-heuristic and spill-retention caveats live with [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md), which owns those mechanics. diff --git a/packages/bash/pwsh-local/README.zh.md b/packages/bash/pwsh-local/README.zh.md index 7906d8a511..3a8d2b7ea4 100644 --- a/packages/bash/pwsh-local/README.zh.md +++ b/packages/bash/pwsh-local/README.zh.md @@ -50,6 +50,6 @@ - **命令字符串是 PowerShell 文本**——`-Command` 域没有 shell 引号层,但面向模型的命令由 PowerShell 自己解析,因此 PowerShell 语法错误是命令失败,而非启动失败。 - **后台 spawn 失败提示只投递一次**——subprocess 服务不会为从未运行的进程缓冲输出,因此执行器只把 `spawn failed: …` 注入一次 `readOutput()` 增量;丢弃该增量的读取方无法恢复它。 - **Windows 终止不报告信号**——被强制终止的进程以退出码 1、`signal: null` 结束,因此基于信号的状态分类(POSIX `killed`)在 Windows 上不适用;`kill()` 发起的停止仍会直接盖上 `killed`。 -- **编码 preamble 位于命令之前**——PowerShell 要求 `param(...)`、`#requires` 与 `using namespace`/`using assembly` 语句位于脚本最顶部,因此以其中一种开头的命令无法在 UTF-8 输出 preamble 下运行;请先用无操作语句(或 `& { … }`)开头。 +- **编码 preamble 位于命令之前**——PowerShell 要求 `param(...)`、`#requires` 与 `using namespace`/`using assembly` 语句位于脚本最顶部,因此以其中一种开头的命令无法在 UTF-8 输出 preamble 下运行。`param(...)` 脚本可包进 `& { … }`(param 块可以合法地位于脚本块开头);`using` 语句与 `#requires` 在命令内没有变通办法(`#requires` 在 `-Command` 中无论位置如何都不生效)——此类脚本请改从文件运行。 清理启发式与 spill 保留的注意事项由 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 持有,它拥有这些机制。 diff --git a/packages/bash/tool-bash/tests/tools.spec.ts b/packages/bash/tool-bash/tests/tools.spec.ts index e6228ceca8..4f913d78ee 100644 --- a/packages/bash/tool-bash/tests/tools.spec.ts +++ b/packages/bash/tool-bash/tests/tools.spec.ts @@ -1017,9 +1017,9 @@ describe('tool-owned UI presentation (presentCall / presentResult)', () => { // not renderResult output, so a generic fenced card, no terminal output/exit. const out = ctx.tools.get('bash')!.presentResult!( { command: 'x', description: 'x' }, - { content: [{ type: 'text', text: 'command aborted' }], isError: true }, + { content: [{ type: 'text', text: 'tool call aborted' }], isError: true }, ) - expect(out).toEqual({ card: 'generic', content: [{ type: 'text', text: '```console\ncommand aborted\n```' }] }) + expect(out).toEqual({ card: 'generic', content: [{ type: 'text', text: '```console\ntool call aborted\n```' }] }) }) it('bash presentResult: leaves a non-text (unexpected) result untouched → undefined (UI keeps raw content)', async () => { diff --git a/packages/subprocess/subprocess/src/index.ts b/packages/subprocess/subprocess/src/index.ts index 5d0bbe2c76..d484faa24d 100644 --- a/packages/subprocess/subprocess/src/index.ts +++ b/packages/subprocess/subprocess/src/index.ts @@ -45,7 +45,10 @@ export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i * `HOME`, locale, and proxy variables survive, so child CLIs run normally; * 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 + * which merges after this scrub). Both scrubs match case-insensitively: + * Windows environment names are case-insensitive, so a parent `dsh_*` entry + * would otherwise survive and read back as `$env:DSH_*` in the child; + * deliberate lowercase `dsh_*` names on POSIX are implausible. 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. @@ -53,7 +56,7 @@ export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i export function scrubbedParentEnv(): Record { const env: Record = {} for (const [key, value] of Object.entries(process.env)) { - if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.startsWith(DSH_ENV_PREFIX)) env[key] = value + if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX)) env[key] = value } return env } diff --git a/packages/subprocess/subprocess/tests/service.spec.ts b/packages/subprocess/subprocess/tests/service.spec.ts index 28a1dce299..d0ef5c9fd6 100644 --- a/packages/subprocess/subprocess/tests/service.spec.ts +++ b/packages/subprocess/subprocess/tests/service.spec.ts @@ -52,20 +52,23 @@ describe('SubprocessService seam', () => { await expect(ctx.plugin(SecondService)).rejects.toThrow(/service "subprocess" has been registered/) }) - it('scrubbedParentEnv drops credential-shaped and DSH_ names but keeps PATH', () => { + it('scrubbedParentEnv drops credential-shaped and DSH_ names (case-insensitively) but keeps PATH', () => { process.env.DSH_SCRUB_PROBE = 'stale' + process.env.dsh_scrub_probe_lower = 'stale' process.env.SCRUB_PROBE_TOKEN = 'secret' process.env.SCRUB_PROBE_PASSWORD = 'secret' process.env.SCRUB_PROBE_PLAIN = 'visible' try { const env = scrubbedParentEnv() expect(env.DSH_SCRUB_PROBE).toBeUndefined() + expect(env.dsh_scrub_probe_lower).toBeUndefined() expect(env.SCRUB_PROBE_TOKEN).toBeUndefined() expect(env.SCRUB_PROBE_PASSWORD).toBeUndefined() expect(env.SCRUB_PROBE_PLAIN).toBe('visible') expect(env.PATH).toBeDefined() } finally { delete process.env.DSH_SCRUB_PROBE + delete process.env.dsh_scrub_probe_lower delete process.env.SCRUB_PROBE_TOKEN delete process.env.SCRUB_PROBE_PASSWORD delete process.env.SCRUB_PROBE_PLAIN diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 2bbfe56b2c..842467c764 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -338,16 +338,17 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'bash', title: 'Bash executor seam', mode: 'seam', - implementations: ['bash-local', 'bash-sandbox'], - consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'], - note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.', + implementations: ['bash-local', 'bash-sandbox', 'pwsh-local'], + consumers: ['tool-bash', 'tool-pwsh', 'hooks-claude', 'hooks-codex'], + note: 'The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them.', }, { key: 'bashEnv', - pkg: 'tool-bash', + pkg: 'bash-env', title: 'Managed bash environment registry', mode: 'core', - note: 'Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace.', + consumers: ['tool-bash', 'tool-pwsh'], + note: 'Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace.', }, { key: 'pty',