From 32744c2b5cb2b3abf27c0b384a7ac3d2bfb204bf Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Tue, 11 Aug 2026 20:21:57 +0800 Subject: [PATCH] refactor(preset): gate tool-pwsh by platform alongside tool-bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web-app overlay now disables the host tool-pwsh row too, and the shipped presets (standard/code/cordis) declare both shell tool rows with inverted platform gates — tool-bash on POSIX, tool-pwsh on win32 — so the preset layer exposes exactly one shell tool per host and a preset can drop or replace the shell tool on either platform. windows-shell.spec pins both preset gates and both host tool rows disabled in the web composition; the loader and Windows pwsh notes are updated in place. --- ...der-entry-disabled-interpolation.i18n.yaml | 4 +- ...-11-loader-entry-disabled-interpolation.md | 4 +- ...-loader-entry-disabled-interpolation.zh.md | 4 +- .../2026-08-01-windows-pwsh-default.i18n.yaml | 4 +- .../2026-08-01-windows-pwsh-default.md | 4 +- .../2026-08-01-windows-pwsh-default.zh.md | 4 +- .../agent-presets/code/agent.cordis.yml | 13 ++++- .../agent-presets/cordis/agent.cordis.yml | 13 ++++- .../agent-presets/standard/agent.cordis.yml | 13 ++++- apps/cli/tests/windows-shell.spec.ts | 56 ++++++++++--------- packages/bundle/web-app/cordis.patch.yml | 8 +++ 11 files changed, 80 insertions(+), 47 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml index 6d1fc5cde6..a3249dcddb 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.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/architecture/2026-08-11-loader-entry-disabled-interpolation.md -2026-08-11-loader-entry-disabled-interpolation.md: 63be14ac5b25a3189f97e0283c36c29bfbcb5cec -2026-08-11-loader-entry-disabled-interpolation.zh.md: 74f14443221bbae5b3d602c3819d611783b59670 +2026-08-11-loader-entry-disabled-interpolation.md: fd760ea0f15f19e5f287aaddc36fb8eeb5f519ba +2026-08-11-loader-entry-disabled-interpolation.zh.md: 15f5a80931c58555dceab359d05e8515334513b2 diff --git a/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md index 63be14ac5b..fd760ea0f1 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md +++ b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.md @@ -10,7 +10,7 @@ The Windows platform layer (then a separate `windows.cordis.patch.yml` beside th ## Decision -The Loader interpolates the entry `disabled` field (`vendor/loader/src/config/entry.ts`): a `!!js` expression evaluates against the loader context at every mount decision. `disabled` is the only interpolated metadata field; `id`, `name`, `group`, and `inject` stay static. The raw node stays in the options, so write-back keeps the `!!js` form. The shipped presets (standard, code, cordis) gate `tool-bash` with `disabled: !!js process.platform === 'win32'`, and `verify-cordis-config` now allows expressions in `disabled` only. +The Loader interpolates the entry `disabled` field (`vendor/loader/src/config/entry.ts`): a `!!js` expression evaluates against the loader context at every mount decision. `disabled` is the only interpolated metadata field; `id`, `name`, `group`, and `inject` stay static. The raw node stays in the options, so write-back keeps the `!!js` form. The shipped presets (standard, code, cordis) declare the shell tool rows themselves and gate them by platform — `tool-bash` with `disabled: !!js process.platform === 'win32'` and its `tool-pwsh` twin with the inverted expression — so the preset layer exposes exactly one shell tool per host; the web-app overlay disables the host rows of both tools, letting each session's preset decide. `verify-cordis-config` now allows expressions in `disabled` only. The mechanism completes the platform-layer fold: the base bundle's `cordis.patch.yml` gates both shell stacks on its own rows — `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'`, and their twins `pwsh-sandbox`/`tool-pwsh` mount only on win32 with the inverted expression. The launcher's separate Windows platform layer (`windows.cordis.patch.yml` plus `apps/cli/src/windows-shell.ts` and its injection into boot, live recomposition, and config dumps) is deleted — the layer existed only because entry metadata was static, and with `disabled` interpolated the condition lives on the row it governs. @@ -22,4 +22,4 @@ The mechanism completes the platform-layer fold: the base bundle's `cordis.patch ## Consequences -A row can gate itself on platform or environment; a bad expression fails loud at boot. Every other metadata field remains literal and the gate keeps rejecting expressions there — the postmortem-0002 hazard is closed for `disabled` by evaluation, not prohibition. The Windows shell swap moved from a launcher-injected patch layer to the base bundle's own rows: win32 mounts the confined pwsh stack, POSIX carries the pwsh rows disabled, and one shared patch file serves both rosters — the [Windows pwsh default](../feature/2026-08-01-windows-pwsh-default.md) note's layer mechanism is superseded. The `minimal` preset's missing win32 PTY stack is a preset-metadata follow-up. +A row can gate itself on platform or environment; a bad expression fails loud at boot. Every other metadata field remains literal and the gate keeps rejecting expressions there — the postmortem-0002 hazard is closed for `disabled` by evaluation, not prohibition. The Windows shell swap moved from a launcher-injected patch layer to the base bundle's own rows: win32 mounts the confined pwsh stack, POSIX carries the pwsh rows disabled, and one shared patch file serves both rosters — the [Windows pwsh default](../feature/2026-08-01-windows-pwsh-default.md) note's layer mechanism is superseded. The shell TOOL rows follow the same one-plane rule as every other preset-declared row: the web-app overlay disables the host `tool-bash`/`tool-pwsh` rows and the presets declare both with inverted platform gates, so a preset can drop or replace the shell tool per session on either host. The `minimal` preset's missing win32 PTY stack is a preset-metadata follow-up. diff --git a/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.zh.md b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.zh.md index 74f1444322..15f5a80931 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-11-loader-entry-disabled-interpolation.zh.md @@ -10,7 +10,7 @@ Windows 平台层(当时是 base patch 旁独立的 `windows.cordis.patch.yml` ## 决策 -Loader 插值条目 `disabled` 字段(`vendor/loader/src/config/entry.ts`):`!!js` 表达式在每次挂载决策时基于 loader 上下文求值。`disabled` 是唯一被插值的元数据字段;`id`、`name`、`group`、`inject` 保持静态。原始节点保留在 options 中,写回保持 `!!js` 形式。shipped 预设(standard、code、cordis)用 `disabled: !!js process.platform === 'win32'` 门控 `tool-bash`,`verify-cordis-config` 现在只允许 `disabled` 中的表达式。 +Loader 插值条目 `disabled` 字段(`vendor/loader/src/config/entry.ts`):`!!js` 表达式在每次挂载决策时基于 loader 上下文求值。`disabled` 是唯一被插值的元数据字段;`id`、`name`、`group`、`inject` 保持静态。原始节点保留在 options 中,写回保持 `!!js` 形式。shipped 预设(standard、code、cordis)自己声明 shell 工具行并按平台门控——`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`,其孪生行 `tool-pwsh` 以取反的表达式——因此预设层每台宿主恰好暴露一个 shell 工具;web-app overlay 禁用两个工具的 host 行,由每个会话的预设决定。`verify-cordis-config` 现在只允许 `disabled` 中的表达式。 该机制补全了平台层折叠:base bundle 的 `cordis.patch.yml` 在自身行上按平台门控两个 shell 栈——`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`,它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载。启动器的独立 Windows 平台层(`windows.cordis.patch.yml` 以及 `apps/cli/src/windows-shell.ts` 及其注入到 boot、live 重组合、config dump 的逻辑)被删除——该层只因条目元数据是静态的而存在,`disabled` 可插值后条件就落在它所治理的行上。 @@ -22,4 +22,4 @@ Loader 插值条目 `disabled` 字段(`vendor/loader/src/config/entry.ts`) ## 后果 -行可以按平台或环境门控自身;错误的表达式在启动时响亮失败。其余元数据字段保持字面值,门禁继续拒绝那里的表达式——`disabled` 上的 postmortem-0002 隐患以「求值」而非「禁止」关闭。Windows shell 栈的切换从启动器注入的 patch 层移到 base bundle 自身的行上:win32 挂载受限 pwsh 栈,POSIX 携带被禁用的 pwsh 行,同一份 patch 文件服务两种阵容——[Windows 默认 pwsh](../feature/2026-08-01-windows-pwsh-default.md) note 的层机制已被取代。`minimal` 预设缺失的 win32 PTY 栈是预设元数据的后续工作。 +行可以按平台或环境门控自身;错误的表达式在启动时响亮失败。其余元数据字段保持字面值,门禁继续拒绝那里的表达式——`disabled` 上的 postmortem-0002 隐患以「求值」而非「禁止」关闭。Windows shell 栈的切换从启动器注入的 patch 层移到 base bundle 自身的行上:win32 挂载受限 pwsh 栈,POSIX 携带被禁用的 pwsh 行,同一份 patch 文件服务两种阵容——[Windows 默认 pwsh](../feature/2026-08-01-windows-pwsh-default.md) note 的层机制已被取代。shell 工具行遵循与其他预设声明行相同的 one-plane 规则:web-app overlay 禁用 host 面的 `tool-bash`/`tool-pwsh` 行,预设以互逆的平台门控声明两者,因此任一宿主的每个会话都可以按预设丢弃或替换 shell 工具。`minimal` 预设缺失的 win32 PTY 栈是预设元数据的后续工作。 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index 2aedf6e59f..88b9f415d4 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.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-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: a749cf4d80f9f52fda23d57fd43433d6c9f4f710 -2026-08-01-windows-pwsh-default.zh.md: f9a1ff2c959268312a3907667ccecd00b32dd380 +2026-08-01-windows-pwsh-default.md: c66e289c24d6024b1df53cd60f25c27d46fafc5a +2026-08-01-windows-pwsh-default.zh.md: b2ad45ec96d546d01436a383ed7d8884b978aa31 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index a749cf4d80..c66e289c24 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -31,13 +31,13 @@ The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches ba ## Consequences -- A Windows host running a shipped `dsh` surface gets the confined `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled). +- A Windows host running a shipped `dsh` surface gets the confined `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there. On the Web surface the shell TOOL rows come from the session's preset (the [loader `disabled` interpolation](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) note owns the one-plane mechanism): each shipped preset declares `tool-pwsh` gated by `process.platform !== 'win32'` and its `tool-bash` twin by the inverted expression, so the preset layer exposes exactly one shell tool per host. - Windows commands and fs operations share the sandbox policy, permission switcher, and approval service. The ACL runner confines writes but reports `enforcement: 'partial'`; explicit `danger-full-access` remains the approved bypass rather than the platform default. - POSIX hosts mount the bash stack as before; the pwsh rows sit disabled in their composition, because the one shared patch file lists both stacks and each row gates itself. - A Windows host that prefers the bash stack (e.g. with WSL/Git-Bash on PATH) overrides the shipped rows through its profile or home `cordis.patch.yml` — disabling `pwsh-sandbox`/`tool-pwsh` and re-enabling `bash-sandbox`/`tool-bash` (both executors register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel. ## Verification -- Unit: `apps/cli/tests/windows-shell.spec.ts` composes the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the app installation) through the boot's patch algorithm and pins the effective per-platform roster — the win32 pwsh roster, the POSIX bash roster, and the base-only profile — plus the preset-level tool-bash gate and the cold-start resolution closure; `packages/bundle/base/tests/base.spec.ts` pins the four shell rows' symmetric `!!js` platform gates and that no separate platform patch ships. +- Unit: `apps/cli/tests/windows-shell.spec.ts` composes the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the app installation) through the boot's patch algorithm and pins the effective per-platform roster — the win32 pwsh roster, the POSIX bash roster, and the base-only profile — plus the preset-level shell-tool gates (`tool-bash`/`tool-pwsh`) and the cold-start resolution closure; `packages/bundle/base/tests/base.spec.ts` pins the four shell rows' symmetric `!!js` platform gates and that no separate platform patch ships. - Keyless: a `dsh --profile --dump-config` shows both stacks in the one shared patch layer, with each row's own `disabled` expression deciding the roster at mount. - The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes). diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index f9a1ff2c95..b2ad45ec96 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -31,13 +31,13 @@ pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-b ## 后果 -- 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得受限 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。 +- 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得受限 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`。在 Web 表面,shell 工具行来自会话的预设([loader `disabled` 插值](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) note 拥有 one-plane 机制):每个 shipped 预设声明 `tool-pwsh`(以 `process.platform !== 'win32'` 门控)及其孪生行 `tool-bash`(取反表达式),因此预设层每台宿主恰好暴露一个 shell 工具。 - Windows 命令与 fs 操作共用沙箱策略、权限切换器和 approval 服务。ACL runner 限制写入,但报告 `enforcement: 'partial'`;显式的 `danger-full-access` 仍是获准的绕过方式,而非平台默认。 - POSIX 主机如常挂载 bash 栈;pwsh 行以其自身的门控表达式处于禁用状态——同一份共享 patch 文件列出两个栈,每个行自己决定挂载。 - 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付行——禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`(两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。 ## 验证 -- 单元:`apps/cli/tests/windows-shell.spec.ts` 通过启动所用的 patch 算法组合真实交付的 bundle 层(从应用安装解析的 dsh-base + dsh-web-app),固定每个平台的有效清单——win32 pwsh 清单、POSIX bash 清单与 base-only profile——外加预设级 tool-bash 门控与冷启动解析闭包;`packages/bundle/base/tests/base.spec.ts` 固定四个 shell 行的对称 `!!js` 平台门控,并断言不再交付独立的平台 patch。 +- 单元:`apps/cli/tests/windows-shell.spec.ts` 通过启动所用的 patch 算法组合真实交付的 bundle 层(从应用安装解析的 dsh-base + dsh-web-app),固定每个平台的有效清单——win32 pwsh 清单、POSIX bash 清单与 base-only profile——外加预设级 shell 工具门控(`tool-bash`/`tool-pwsh`)与冷启动解析闭包;`packages/bundle/base/tests/base.spec.ts` 固定四个 shell 行的对称 `!!js` 平台门控,并断言不再交付独立的平台 patch。 - Keyless:`dsh --profile --dump-config` 在同一份共享 patch 层中显示两个栈,每个行以自己的 `disabled` 表达式在挂载时决定清单。 - 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。 diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml index 828a0debbd..f00e5beb21 100644 --- a/apps/cli/config/agent-presets/code/agent.cordis.yml +++ b/apps/cli/config/agent-presets/code/agent.cordis.yml @@ -45,14 +45,21 @@ # publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is # the criterion for host-plane ownership — injection resolves before any session # exists, so there is no agent to key by. Behind a preset realm those variables -# never reached the model's shell at all. `tool-bash` consumes the host registry -# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the -# sandbox policy owns it. +# never reached the model's shell at all. Both shell tools consume the host +# registry from here; the executors behind them (`bash-sandbox`/`pwsh-sandbox`) +# are host-plane too, where the sandbox policy owns them. The web-app overlay +# disables the host shell-tool rows, so exactly one of these rows mounts per +# host — `tool-bash` on POSIX, `tool-pwsh` on win32. - id: tool-bash name: '@deepseek-ai/dsh-tool-bash' # POSIX-only: the base composition swaps the bash stack for the pwsh stack on win32. disabled: !!js process.platform === 'win32' +- id: tool-pwsh + name: '@deepseek-ai/dsh-tool-pwsh' + # win32-only twin of tool-bash: bash has no Windows runner. + disabled: !!js process.platform !== 'win32' + # ── filesystem ────────────────────────────────────────────────────────────── # Both register into the host `tools` registry and provide nothing, so diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index 73ec4f0687..22c2588243 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -39,14 +39,21 @@ # publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is # the criterion for host-plane ownership — injection resolves before any session # exists, so there is no agent to key by. Behind a preset realm those variables -# never reached the model's shell at all. `tool-bash` consumes the host registry -# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the -# sandbox policy owns it. +# never reached the model's shell at all. Both shell tools consume the host +# registry from here; the executors behind them (`bash-sandbox`/`pwsh-sandbox`) +# are host-plane too, where the sandbox policy owns them. The web-app overlay +# disables the host shell-tool rows, so exactly one of these rows mounts per +# host — `tool-bash` on POSIX, `tool-pwsh` on win32. - id: tool-bash name: '@deepseek-ai/dsh-tool-bash' # POSIX-only: the base composition swaps the bash stack for the pwsh stack on win32. disabled: !!js process.platform === 'win32' +- id: tool-pwsh + name: '@deepseek-ai/dsh-tool-pwsh' + # win32-only twin of tool-bash: bash has no Windows runner. + disabled: !!js process.platform !== 'win32' + # ── filesystem ────────────────────────────────────────────────────────────── # Both register into the host `tools` registry and provide nothing, so diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml index d4fd218086..74832ac03f 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -38,14 +38,21 @@ # publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is # the criterion for host-plane ownership — injection resolves before any session # exists, so there is no agent to key by. Behind a preset realm those variables -# never reached the model's shell at all. `tool-bash` consumes the host registry -# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the -# sandbox policy owns it. +# never reached the model's shell at all. Both shell tools consume the host +# registry from here; the executors behind them (`bash-sandbox`/`pwsh-sandbox`) +# are host-plane too, where the sandbox policy owns them. The web-app overlay +# disables the host shell-tool rows, so exactly one of these rows mounts per +# host — `tool-bash` on POSIX, `tool-pwsh` on win32. - id: tool-bash name: '@deepseek-ai/dsh-tool-bash' # POSIX-only: the base composition swaps the bash stack for the pwsh stack on win32. disabled: !!js process.platform === 'win32' +- id: tool-pwsh + name: '@deepseek-ai/dsh-tool-pwsh' + # win32-only twin of tool-bash: bash has no Windows runner. + disabled: !!js process.platform !== 'win32' + # ── filesystem ────────────────────────────────────────────────────────────── # Both register into the host `tools` registry and provide nothing, so diff --git a/apps/cli/tests/windows-shell.spec.ts b/apps/cli/tests/windows-shell.spec.ts index 4109cab68e..4a44b9d106 100644 --- a/apps/cli/tests/windows-shell.spec.ts +++ b/apps/cli/tests/windows-shell.spec.ts @@ -5,9 +5,9 @@ * the launcher applies nothing beyond the bundle layers. The spec composes * the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the * app installation anchor) through the boot's patch algorithm and pins the - * effective per-platform roster, the preset-level gate that keeps tool-bash - * out of win32 sessions, and the cold-start resolution closure for the pwsh - * rows' bare plugin names. + * effective per-platform roster, the preset-level gates that keep tool-bash + * out of win32 sessions and tool-pwsh out of POSIX sessions, and the + * cold-start resolution closure for the pwsh rows' bare plugin names. */ import { afterEach, describe, expect, it } from 'vitest' @@ -53,18 +53,18 @@ describe('the shipped shell composition (real bundle layers)', () => { ) const byId = new Map(rows.map(row => [row.id, row])) // One shared patch set, two rosters: the shell stacks gate themselves. - for (const id of ['bash-sandbox', 'pwsh-sandbox', 'tool-pwsh']) { + for (const id of ['bash-sandbox', 'pwsh-sandbox', 'tool-bash', 'tool-pwsh']) { expect(byId.has(id), `row ${id}`).toBe(true) } expect(disabledOn(byId.get('bash-sandbox')!, 'win32'), 'bash-sandbox on win32').toBe(true) expect(disabledOn(byId.get('bash-sandbox')!, 'linux'), 'bash-sandbox on linux').toBe(false) expect(disabledOn(byId.get('pwsh-sandbox')!, 'win32'), 'pwsh-sandbox on win32').toBe(false) expect(disabledOn(byId.get('pwsh-sandbox')!, 'linux'), 'pwsh-sandbox on linux').toBe(true) - expect(disabledOn(byId.get('tool-pwsh')!, 'win32'), 'tool-pwsh on win32').toBe(false) - expect(disabledOn(byId.get('tool-pwsh')!, 'linux'), 'tool-pwsh on linux').toBe(true) - // The Web surface owns the host tool-bash row on every platform: sessions - // mount their own preset rows instead. + // The Web surface owns both host shell TOOL rows on every platform: the + // executors stay host-plane with their platform gates, while sessions + // mount the shell tools from their preset rows instead. expect(byId.get('tool-bash')?.disabled).toBe(true) + expect(byId.get('tool-pwsh')?.disabled).toBe(true) // The permission surface never moves: the sandbox/policy rows, the // permission switcher, fs-sandbox, and the approval service stay enabled // exactly as on POSIX — the confined pwsh executor is what changes. @@ -103,37 +103,41 @@ describe('the shipped shell composition (real bundle layers)', () => { }) }) -describe('shipped agent presets keep tool-bash off the win32 roster', () => { +describe('shipped agent presets gate both shell tools by platform', () => { const presetRoot = resolve(fileURLToPath(new URL('../package.json', import.meta.url)), '..', 'config', 'agent-presets') - it.each(['standard', 'code', 'cordis'])('preset %s gates its tool-bash row by platform', (preset) => { + it.each(['standard', 'code', 'cordis'])('preset %s gates its shell tool rows by platform', (preset) => { const entries: unknown = yaml.load( readFileSync(join(presetRoot, preset, 'agent.cordis.yml'), 'utf8'), { schema: entryListSchema }, ) if (!Array.isArray(entries)) throw new TypeError(`preset ${preset} must parse to an entry array`) - const row = entries.find((entry): entry is Record => ( - typeof entry === 'object' && entry !== null && (entry as Record).id === 'tool-bash' - )) - if (row === undefined) throw new TypeError(`preset ${preset} must mount tool-bash`) - expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string }) - // The base patch gates the host tool-bash row on win32; the preset row - // must not re-enable it there. Evaluate the shipped expression with a - // platform-scoped context (the `with` scope shadows the global - // `process`) so both outcomes pin on every host. - const expression = (row.disabled as { __jsExpr: string }).__jsExpr - expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression))).toBe(true) - expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression))).toBe(false) + for (const [id, win32] of [['tool-bash', true], ['tool-pwsh', false]] as const) { + const row = entries.find((entry): entry is Record => ( + typeof entry === 'object' && entry !== null && (entry as Record).id === id + )) + if (row === undefined) throw new TypeError(`preset ${preset} must mount ${id}`) + expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string }) + // The base patch gates the host shell-tool rows by platform; the preset + // rows must not re-enable them on the wrong host. Evaluate the shipped + // expression with a platform-scoped context (the `with` scope shadows + // the global `process`) so both outcomes pin on every host. + const expression = (row.disabled as { __jsExpr: string }).__jsExpr + expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression)), `${id} on win32`).toBe(win32) + expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression)), `${id} on linux`).toBe(!win32) + } }) - it('minimal mounts no tool-bash row at all (its shell is the PTY stack)', () => { + it('minimal mounts no shell tool row at all (its shell is the PTY stack)', () => { const entries: unknown = yaml.load( readFileSync(join(presetRoot, 'minimal', 'agent.cordis.yml'), 'utf8'), { schema: entryListSchema }, ) if (!Array.isArray(entries)) throw new TypeError('minimal preset must parse to an entry array') - expect(entries.some(entry => ( - typeof entry === 'object' && entry !== null && (entry as Record).id === 'tool-bash' - ))).toBe(false) + for (const id of ['tool-bash', 'tool-pwsh']) { + expect(entries.some(entry => ( + typeof entry === 'object' && entry !== null && (entry as Record).id === id + )), `${id} must be absent from minimal`).toBe(false) + } }) }) diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index c0750ddf95..3d51ce9e7a 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -253,10 +253,18 @@ # the criterion for host-plane ownership — injection resolves before any session # exists, so there is no agent to key by. Behind a preset realm those variables # would never reach the model's shell at all. +# +# Both shell TOOL rows move behind presets on every platform: the executors +# (`bash-sandbox`/`pwsh-sandbox`) stay host-plane with their platform gates in +# the base patch, while each session's preset declares the shell tool its agent +# sees and gates it by platform — so the host tool rows are disabled here. - id: tool-bash disabled: true +- id: tool-pwsh + disabled: true + # The background-task REGISTRY stays on the host plane; only the model-facing # `task_*` controls move. Its producers — `tool-bash` here, `tool-pty` and a # non-continuable `tool-subagent` elsewhere — are preset rows that resolve it