Просмотр исходного кода

feat(tool-pwsh): teach the confined named-pipe capture boundary to the model

Under read-only and workspace-write the Windows ACL sandbox leaves
programs unable to open named pipes, so a piped-stdio spawn fails with
EPERM. State that boundary in the pwsh tool description next to the
ConstrainedLanguage contract, pin it in the tool tests, and bring the
package README and both implemented Agent Notes current with it.
Huanqi Cao 2 месяцев назад
Родитель
Сommit
33e4c6580f

+ 2 - 2
.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.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 .agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md
-2026-08-01-pwsh-tool-and-executor.md: a511035d959ed873d75af87e9f56374dd4cd4f27
-2026-08-01-pwsh-tool-and-executor.zh.md: c0e6a73228e08f2b5f2d5a6ccfb9a6eecce3ba7d
+2026-08-01-pwsh-tool-and-executor.md: 855d8e5db8a78e7e798061724d89fde31b28e975
+2026-08-01-pwsh-tool-and-executor.zh.md: a29967d628640eafd2362b74528ee14172f19376

+ 2 - 2
.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md

@@ -13,7 +13,7 @@ The harness spoke one shell dialect on every platform: `bash`. Windows hosts cou
 Two new packages under `packages/bash/`:
 
 - **`@deepseek-ai/dsh-pwsh-local`** — a local implementation of the `ctx.bash` executor seam over `ctx.subprocess`, mirroring `dsh-bash-local` call-for-call: `resolve()` defaults and caps from config, `run()` fuses the config-clamped timeout with the caller's signal through one deadline, `start()` returns a consuming background handle whose processes belong to the subprocess service. The command string rides as ONE argv element to `pwsh -NoLogo -NoProfile -NonInteractive -Command`, so PowerShell parses it and no shell-quoting layer exists. Executable resolution (`resolvePwshPath`) is a pure function of `(configured, env, platform)`: explicit config first, then Windows probes PowerShell 7's install, PATH entries (quotes stripped), and Windows PowerShell 5.1, else a bare `pwsh` via PATH.
-- **`@deepseek-ai/dsh-tool-pwsh`** — the model-facing tool over `ctx.bash`, PowerShell-dialect by contract, mirroring `dsh-tool-bash` call-for-call: foreground and `run_in_background` execution through the generic task runtime, managed `DSH_*` environment through the shared [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) registry, the bash marker/truncation rendering story (a clean exit produces no marker), and — since the Windows ACL sandbox decision — the sandbox denial rendering and `sandbox_permissions` escalation surface, plus a Windows-specific ConstrainedLanguage contract in the tool description. The parity decision supersedes this note's minimal-profile tool description.
+- **`@deepseek-ai/dsh-tool-pwsh`** — the model-facing tool over `ctx.bash`, PowerShell-dialect by contract, mirroring `dsh-tool-bash` call-for-call: foreground and `run_in_background` execution through the generic task runtime, managed `DSH_*` environment through the shared [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) registry, the bash marker/truncation rendering story (a clean exit produces no marker), and — since the Windows ACL sandbox decision — the sandbox denial rendering and `sandbox_permissions` escalation surface, plus the Windows-specific ConstrainedLanguage and named-pipe contracts in the tool description. The parity decision supersedes this note's minimal-profile tool description.
 
 Windows vitest coverage is deliberately NOT part of this change: the repo's Windows CI lane owns build/static gates, and unit coverage runs on Linux, where both packages' suites run against a real `pwsh` (preinstalled on the GitHub-hosted runners) or self-skip when absent. The vitest `windowsUnsupportedPackages` exclusion narrows from `packages/bash/*` to the bash-requiring packages so the pwsh suites can also run natively on Windows dev machines.
 
@@ -30,6 +30,6 @@ The roadmap beyond this decision — defaulting Windows hosts to `pwsh` (bash of
 ## Consequences
 
 - The bash executor seam gains a second, Windows-native implementation with an identical request/spec contract, so model-facing consumers beyond `tool-pwsh` (hooks bridges, in-process plugins) can run PowerShell without dialect shims.
-- `tool-pwsh` is the model-visible Windows-first shell tool: behaviorally interchangeable with the bash tool for foreground, background, and sandboxed work — including the same-turn `sandbox_permissions` escalation through `ctx.approval` — with prompt guidance that states the marker contract, the sandbox denial/escalation vocabulary, and the ConstrainedLanguage boundary precisely.
+- `tool-pwsh` is the model-visible Windows-first shell tool: behaviorally interchangeable with the bash tool for foreground, background, and sandboxed work — including the same-turn `sandbox_permissions` escalation through `ctx.approval` — with prompt guidance that states the marker contract, the sandbox denial/escalation vocabulary, and the ConstrainedLanguage and named-pipe boundaries precisely.
 - Windows semantics differ where the platform differs: forced termination reports exit 1 with no signal (so `signal`/`killed` status facts are POSIX-only), and PowerShell writes CRLF, which tests normalize.
 - The CLI gains two workspace dependencies and two tsconfig projects without mounting either plugin — the composition decision stays with the Windows-default proposal.

+ 2 - 2
.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md

@@ -13,7 +13,7 @@ harness 在每个平台只说一种 shell 方言:`bash`。Windows 主机只能
 在 `packages/bash/` 下新增两个包:
 
 - **`@deepseek-ai/dsh-pwsh-local`** —— `ctx.bash` 执行器 seam 的本地实现,基于 `ctx.subprocess`,逐调用镜像 `dsh-bash-local`:`resolve()` 从配置默认化并设上限,`run()` 通过一个 deadline 融合配置夹取的超时与调用方信号,`start()` 返回消费式后台句柄,其进程归属于 subprocess 服务。命令字符串作为 ONE argv 元素传给 `pwsh -NoLogo -NoProfile -NonInteractive -Command`,由 PowerShell 解析,不存在 shell 引号层。可执行文件解析(`resolvePwshPath`)是 `(configured, env, platform)` 的纯函数:先显式配置,再在 Windows 上探测 PowerShell 7 安装位置、PATH 条目(剥离引号)与 Windows PowerShell 5.1,否则经 PATH 解析裸 `pwsh`。
-- **`@deepseek-ai/dsh-tool-pwsh`** —— 基于 `ctx.bash` 的面向模型工具,契约是 PowerShell 方言,逐调用镜像 `dsh-tool-bash`:经通用任务运行时执行前台与 `run_in_background`,经共享 [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) 注册表管理 `DSH_*` 环境,bash 的 marker/截断渲染故事(干净退出不产生 marker),以及——自 Windows ACL sandbox 决策以来——沙箱拒绝渲染与 `sandbox_permissions` 升级面,外加工具描述中的 Windows 专属 ConstrainedLanguage 契约。parity 决策取代了本 note 的最小画像工具描述。
+- **`@deepseek-ai/dsh-tool-pwsh`** —— 基于 `ctx.bash` 的面向模型工具,契约是 PowerShell 方言,逐调用镜像 `dsh-tool-bash`:经通用任务运行时执行前台与 `run_in_background`,经共享 [`dsh-bash-env`](../feature/2026-08-02-pwsh-tool-bash-parity.md) 注册表管理 `DSH_*` 环境,bash 的 marker/截断渲染故事(干净退出不产生 marker),以及——自 Windows ACL sandbox 决策以来——沙箱拒绝渲染与 `sandbox_permissions` 升级面,外加工具描述中的 Windows 专属 ConstrainedLanguage 与 named-pipe 契约。parity 决策取代了本 note 的最小画像工具描述。
 
 Windows vitest 覆盖率刻意不属本次改动:仓库的 Windows CI 通道负责构建/静态门禁,单元覆盖在 Linux 上运行,两个包的套件在那里以真实 `pwsh` 运行(GitHub 托管 runner 预装)或缺失时自行跳过。vitest 的 `windowsUnsupportedPackages` 排除从 `packages/bash/*` 收窄为真正需要 bash 的包,使 pwsh 套件也能在 Windows 开发机上原生运行。
 
@@ -30,6 +30,6 @@ Windows vitest 覆盖率刻意不属本次改动:仓库的 Windows CI 通道
 ## 后果
 
 - bash 执行器 seam 有了第二个、Windows 原生的实现,请求/规范契约一致,因此 `tool-pwsh` 之外的面向模型消费方(hooks 桥、进程内插件)无需方言垫片即可运行 PowerShell。
-- `tool-pwsh` 是模型可见的 Windows 优先 shell 工具:在前台、后台与沙箱化工作上与 bash 工具行为可互换——包括经 `ctx.approval` 的同轮次 `sandbox_permissions` 升级——提示词指导精确陈述 marker 契约、沙箱拒绝/升级词汇与 ConstrainedLanguage 边界。
+- `tool-pwsh` 是模型可见的 Windows 优先 shell 工具:在前台、后台与沙箱化工作上与 bash 工具行为可互换——包括经 `ctx.approval` 的同轮次 `sandbox_permissions` 升级——提示词指导精确陈述 marker 契约、沙箱拒绝/升级词汇,以及 ConstrainedLanguage 与 named-pipe 边界。
 - Windows 语义在平台差异处不同:强制终止报告退出码 1 且无信号(因此 `signal`/`killed` 状态实情仅限 POSIX),PowerShell 输出 CRLF,测试做归一化。
 - CLI 增加两个 workspace 依赖与两个 tsconfig 工程,但不挂载任一插件——组合决策留给 Windows 默认提案。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.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 .agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md
-2026-08-08-windows-acl-restricted-token-sandbox.md: adbec6f733dcf2903e11b70726a07a9aae211fec
-2026-08-08-windows-acl-restricted-token-sandbox.zh.md: e35f4f0a20e7f58e24a8ad47964f43d6c9c43681
+2026-08-08-windows-acl-restricted-token-sandbox.md: d6915cadb5817679749474c1046d1c715adc2b9b
+2026-08-08-windows-acl-restricted-token-sandbox.zh.md: 1533e39bf5c239730b6e027e5f0b345d4c6c42e4

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md


+ 2 - 2
packages/bash/tool-pwsh/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/tool-pwsh/README.md
-README.md: c0c253fe24bd7eaa4f4d814141e17a67f10f4fb8
-README.zh.md: 7e672bfb4fbda8f683cab3dbb018624d43b2a6d2
+README.md: 3fd5a53946e2b101d6ef4457e312e52f4db5f3a8
+README.zh.md: bc0d6ca6099a729db56b7e0de9dfa23a0a6190b8

+ 1 - 1
packages/bash/tool-pwsh/README.md

@@ -120,7 +120,7 @@ Append-only; newly visible content follows the reusable request prefix and does
 
 ## Known Limitations and Deferred Work
 
-- **ConstrainedLanguage under the Windows sandbox** — when the [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md) confines a call (read-only or workspace-write), the restricted token puts pwsh into ConstrainedLanguage mode: `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors, and the mode cannot be lifted from inside. The tool description teaches this contract to the model; the backend README owns the full limitation.
+- **ConstrainedLanguage and named-pipe capture under the Windows sandbox** — when the [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md) confines a call (read-only or workspace-write), the restricted token puts pwsh into ConstrainedLanguage mode: `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors, and the mode cannot be lifted from inside. The same modes deny named-pipe opens, so a piped-stdio spawn inside a confined command fails with EPERM. The tool description teaches both contracts to the model; the backend README owns the full limitations.
 - **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`; the PTY backends are Linux/macOS-only today, and a Windows ConPTY persistent shell is roadmap work.
 - **PowerShell-dialect contract** — the model must write PowerShell (native paths, `$env:` variables), not bash; there is no dialect translation.
 - **Session-cwd identity is not canonicalized** — the workdir base is the session header cwd as-is, unlike the bash tool's sandbox-root-canonicalized identity. Under a confining executor the policy's workspace root IS canonicalized (by the shared policy service), so the workdir and the confinement root can diverge when the raw session cwd differs from its canonical form — a parity gap deferred to the shared shell-tool base extraction.

+ 1 - 1
packages/bash/tool-pwsh/README.zh.md

@@ -120,7 +120,7 @@ ack 是固定短行;任务输出按读取有界。
 
 ## Known Limitations and Deferred Work
 
-- **Windows sandbox 下的 ConstrainedLanguage** — 当 [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md) 隔离某次调用(read-only 或 workspace-write)时,受限令牌使 pwsh 进入 ConstrainedLanguage 模式:`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射都会以“only core types”错误失败,且该模式无法从内部解除。工具描述把这一契约教给模型;后端 README 负责完整的限制说明。
+- **Windows sandbox 下的 ConstrainedLanguage 与 named-pipe 捕获** — 当 [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md) 隔离某次调用(read-only 或 workspace-write)时,受限令牌使 pwsh 进入 ConstrainedLanguage 模式:`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射都会以“only core types”错误失败,且该模式无法从内部解除。这两种模式同样会拒绝 named-pipe 打开,因此受限命令内的管道 stdio spawn 以 EPERM 失败。工具描述把这两个契约教给模型;后端 README 负责完整的限制说明。
 - **无持久 shell 或 PTY** — 每次调用都启动全新的 `pwsh -Command`;PTY 后端目前仅限 Linux/macOS,Windows ConPTY 持久 shell 属于路线图工作。
 - **PowerShell 方言契约** — 模型必须写 PowerShell(原生路径、`$env:` 变量),而不是 bash;没有方言翻译。
 - **会话 cwd 身份不做规范化** — workdir 基座直接取会话头 cwd 原值,不同于 bash 工具经 sandbox-root 规范化的身份。在隔离执行器下,策略的工作区根**会**被规范化(由共享的策略服务完成),因此当原始会话 cwd 与其规范化形态不同时,workdir 与隔离根可能不一致——这一 parity 差距留待共享 shell 工具基座提取时解决。

+ 13 - 6
packages/bash/tool-pwsh/src/index.ts

@@ -114,16 +114,23 @@ function pwshDescription(backgroundEnabled: boolean, escalationModes: readonly S
     + 'On Windows a force-killed command settles as `[exit code: 1]` without a signal marker — treat it as an interruption, not a command failure. '
     + background
   if (escalationModes.length === 0) return base
-  // The CLM contract below is Windows-restricted-token behavior, but the gate
-  // is 'any confining executor is mounted' (escalationModes non-empty). The
-  // conflation is safe today because every shipped composition pairing
-  // tool-pwsh with a confining executor is win32-only; a future POSIX
-  // pwsh-sandbox composition must gate the CLM sentence on the platform
-  // instead (tracked in the pwsh-tool-and-executor Agent Note).
+  // The CLM and named-pipe contracts below are Windows-restricted-token
+  // behavior, but the gate is 'any confining executor is mounted'
+  // (escalationModes non-empty). The conflation is safe today because every
+  // shipped composition pairing tool-pwsh with a confining executor is
+  // win32-only; a future POSIX pwsh-sandbox composition must gate both
+  // sentences on the platform instead (tracked in the pwsh-tool-and-executor
+  // Agent Note).
   return base + ' Under the Windows sandbox, pwsh runs in PowerShell ConstrainedLanguage mode (read-only and '
     + 'workspace-write): prefer cmdlets and core types (`[string]`, `[datetime]`, `[regex]`, `[guid]`); '
     + '.NET static calls (`[System.IO.*]::`, `[math]::`), `Add-Type`, COM objects, and reflection fail '
     + 'with "only core types" errors. `-f` formatting, property access, and core cmdlets work. '
+    + 'In the same modes, programs cannot open named pipes, so a command that captures another '
+    + 'program\'s output through piped stdio (Node.js `child_process.spawn`/`exec` with the default '
+    + '`stdio: \'pipe\'`) fails with EPERM, while `stdio: \'inherit\'` and `stdio: \'ignore\'` spawns '
+    + 'work and PowerShell\'s own pipelines are unaffected. That EPERM is the documented boundary: '
+    + 'do not retry the command another way — escalate the exact command once or restructure it to '
+    + 'avoid capturing output. '
     + 'Attempting a command the sandbox may deny is safe and expected: run it and read the '
     + 'marker rather than assuming the denial. When a command is denied and a wider mode would let it '
     + 'succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry '

+ 5 - 2
packages/bash/tool-pwsh/tests/tools.spec.ts

@@ -552,13 +552,15 @@ describe('sandbox escalation through ctx.approval', () => {
     justification: 'the command needs workspace writes',
   }
 
-  it('advertises the sandbox fields, the escalation clause, and the ConstrainedLanguage contract', async () => {
+  it('advertises the sandbox fields, the escalation clause, and the confined-mode contracts', async () => {
     const { ctx } = await setupSandboxed()
     const schema = ctx.tools.schemas().find(item => item.name === 'pwsh')!
     const properties = schema.parameters.properties as Record<string, { enum?: string[] }>
     expect(properties['sandbox_permissions']?.enum).toEqual(['workspace-write', 'danger-full-access'])
     expect(schema.description).toContain('approval prompt')
     expect(schema.description).toContain('ConstrainedLanguage')
+    expect(schema.description).toContain('named pipes')
+    expect(schema.description).toContain('fails with EPERM')
 
     for (const args of [
       { command: 'Write-Output ok', description: 'd', sandbox_permissions: 'workspace-write' },
@@ -569,10 +571,11 @@ describe('sandbox escalation through ctx.approval', () => {
     }
   })
 
-  it('the escalation fields and the ConstrainedLanguage clause stay out of sandbox-less compositions', async () => {
+  it('the escalation fields and the confined-mode clauses stay out of sandbox-less compositions', async () => {
     const { ctx } = await setup()
     const schema = ctx.tools.schemas().find(item => item.name === 'pwsh')!
     expect(schema.description).not.toContain('ConstrainedLanguage')
+    expect(schema.description).not.toContain('named pipes')
     expect(schema.description).not.toContain('sandbox_permissions')
     expect(schema.parameters.properties).not.toHaveProperty('sandbox_permissions')
   })

Некоторые файлы не были показаны из-за большого количества измененных файлов