English | 中文
Local PowerShell implementation of the @deepseek-ai/dsh-bash executor seam over the @deepseek-ai/dsh-subprocess service: PwshLocalExecutor spawns pwsh -NoLogo -NoProfile -NonInteractive -Command <command> per call as a managed process through ctx.subprocess, and owns everything PowerShell-shaped — executable resolution, command defaulting and caps, timeout/cancel classification, the model-friendly terminal environment, and the model-facing stdout/stderr merge for background reads. Group mechanics (bounded spill-backed output, credential scrub, kill escalation, disposal) are the subprocess service's.
The command string rides as ONE argv element to -Command: PowerShell itself parses the text, and no intermediate shell exists, so there is no shell-quoting layer to escape (the bash -c string domain has no equivalent here). Native Win32 paths (C:\...) pass through unchanged.
The package root exports the default and named PwshLocalExecutor plugin, its Config, the pure resolvePwshPath/candidatePwshPaths helpers, and the ENV_OVERRIDES/ENCODING_PREAMBLE constants the executor injects into every spawn.
- id: bash
name: '@deepseek-ai/dsh-pwsh-local'
config:
cwd: C:\path\to\workspace # default: process.cwd()
timeoutMs: 120000 # default foreground timeout
maxTimeoutMs: 600000 # cap for per-call overrides
maxOutputBytes: 64000 # per-stream in-memory cap; overflow spills to disk
maxSpillBytes: 67108864 # per-stream full-output spill cap
graceMs: 3000 # kill escalation and post-exit pipe-drain grace
pwshPath: C:\Program Files\PowerShell\7\pwsh.exe # explicit executable; else well-known locations, then PATH
The Windows counterpart of dsh-bash-local, deliberately mirroring its semantics call-for-call:
pwsh -Command (deterministic; no profile files). The -NoLogo -NoProfile -NonInteractive flags disable startup banners, profile loading, and prompts that would garble tool output.[Console]::OutputEncoding and $OutputEncoding set to UTF-8 first, so the Windows PowerShell 5.1 fallback (or any host whose console code page is not UTF-8) cannot garble non-ASCII output: the subprocess collector decodes bytes as UTF-8. Input encoding is left at the host default; pwsh 7 defaults to UTF-8 and is unaffected.resolvePwshPath prefers an explicit pwshPath, then on Windows probes PowerShell 7's install location, every PATH entry (Microsoft Store installs; surrounding quotes stripped), and Windows PowerShell 5.1 as a legacy last resort, checking existsSync on each; elsewhere it falls back to a bare pwsh resolved through PATH. Resolution is a pure function of (configured, env, platform) and happens once at construction.resolve() fills workdir/timeoutMs/stdoutMaxBytes from config, and every spawn hands the service explicit byte caps, spill cap, and graceMs. Tree termination (taskkill on Windows, process-group signals on POSIX), the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files are dsh-subprocess-local mechanics. A foreground BashExecRequest.stdoutMaxBytes can raise stdout's capture budget for one trusted caller; stderr and background runs still use maxOutputBytes.run() fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports timedOut, an upstream cancel reports aborted, and a self-terminated command reports neither (timeout-library Agent Note). Windows reports forced termination as exit 1 without a signal, so signal-stamped facts (signal, killed status) are POSIX-only there; the timeout/abort classification is platform-independent.NO_COLOR=1 PAGER=cat GIT_PAGER=cat (no TERM=dumb: that is a POSIX concept; NO_COLOR is honored by modern PowerShell renderers) merged as ordinary env under the service's credential scrub and DSH_* channel rules; an explicit caller entry still wins.start() returns a live BashProcess handle immediately, no timeout applies, and the handle's readOutput() merges the service's offset-based stdout/stderr reads into one marked-section delta with a consuming cursor. A still-running process belongs to the subprocess service, so it survives executor reloads and dies (killed and joined) with the service's disposal. Everything task-shaped (ids, ownership, polling, notices) lives in the generic ctx.tasks runtime, which the tool layer registers the handle with — this executor never sees a session or a registry.Indirectly, through dsh-tool-pwsh, which renders this executor's bounded stdout/stderr tails, background-process deltas (through the generic task runtime), spill-file paths, and infrastructure failures.
No direct invalidation; the named consumer owns any request-prefix changes.
pwsh -Command.-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.spawn failed: … into exactly one readOutput() delta; a reader that discards that delta cannot recover it.signal: null, so signal-based status classification (POSIX killed) does not apply on Windows; kill()-initiated stops still stamp killed directly.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.[Console]::InputEncoding stays at the host default because setting it under redirected stdin throws. pwsh 7 defaults to UTF-8 and is unaffected.Scrub-heuristic and spill-retention caveats live with dsh-subprocess-local, which owns those mechanics.