mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge pull request #2234 from deepseek-harness/feat/loader-entry-disabled-interpolation
feat(loader): interpolate the entry disabled field
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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: fd760ea0f15f19e5f287aaddc36fb8eeb5f519ba
|
||||
2026-08-11-loader-entry-disabled-interpolation.zh.md: 15f5a80931c58555dceab359d05e8515334513b2
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Loader interpolates the entry `disabled` field
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-11-loader-entry-disabled-interpolation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Windows platform layer (then a separate `windows.cordis.patch.yml` beside the base patch, since folded into the base rows — see Decision) disabled `tool-bash` on win32, but the shipped presets each mount a `tool-bash` row. Preset rows compose last, so the same-id row re-enabled the tool on Windows — the session had both `tool-bash` (PowerShell-backed) and `tool-pwsh`, silently, because no spec pinned the composed preset layer. Entry metadata had no conditional mechanism: `!!js` interpolates only under plugin `config`, and [postmortem 0002](../../../../docs/postmortem/0002-js-expression-disabled-filesystem-tools.md) documents that `disabled: !!js ...` stays a truthy expression object, disabling the row everywhere.
|
||||
|
||||
## 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) 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.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A declarative `platform` field on the row.** Static and gate-checkable, but a second composition mechanism beside `!!js`, and platform is only today's condition.
|
||||
|
||||
**Preset-level platform overlays.** Rejected: the condition belongs on the row it governs — the same principle folds the launcher's separate Windows platform layer into the base rows.
|
||||
|
||||
## 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 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.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note:Loader 插值条目 `disabled` 字段
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-11-loader-entry-disabled-interpolation.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Windows 平台层(当时是 base patch 旁独立的 `windows.cordis.patch.yml`,现已折入 base 行——见「决策」)在 win32 上禁用 `tool-bash`,但 shipped 预设各自挂载了一行 `tool-bash`。预设行最后组合,同名行在 Windows 上重新启用了该工具——会话同时拥有 `tool-bash`(PowerShell 后端)与 `tool-pwsh`,且是静默的,因为没有 spec pin 组合后的预设层。条目元数据没有条件机制:`!!js` 只在插件 `config` 下插值,[postmortem 0002](../../../../docs/postmortem/0002-js-expression-disabled-filesystem-tools.md) 记录了 `disabled: !!js ...` 保持真值表达式对象、在所有平台上禁用该行的事故。
|
||||
|
||||
## 决策
|
||||
|
||||
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` 可插值后条件就落在它所治理的行上。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**行上的声明式 `platform` 字段。** 静态且可被门禁检查,但它是 `!!js` 之外的第二种组合机制,且平台只是今天的条件。
|
||||
|
||||
**预设级平台 overlay。** 被否:条件应当属于它所治理的行——同一原则把启动器独立的 Windows 平台层折入 base 行。
|
||||
|
||||
## 后果
|
||||
|
||||
行可以按平台或环境门控自身;错误的表达式在启动时响亮失败。其余元数据字段保持字面值,门禁继续拒绝那里的表达式——`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 栈是预设元数据的后续工作。
|
||||
@@ -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-07-20-dsh-cli-personal-config.md
|
||||
2026-07-20-dsh-cli-personal-config.md: ed04725e92848bbab550a27ef2f4c021536f765e
|
||||
2026-07-20-dsh-cli-personal-config.zh.md: cc97987f803f7fb513e94ce0ce079558f5e3dc75
|
||||
2026-07-20-dsh-cli-personal-config.md: bc2aff322de01bb9c6beebb1679b2ff9909d1fe3
|
||||
2026-07-20-dsh-cli-personal-config.zh.md: 507a7188a4a77d904e3204499290f3ed22abab2c
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-20-dsh-cli-personal-config.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A developer's own preferences — which provider and model the TUI uses, personal credentials, a private adapter route — had nowhere to live except edits to committed files. Pointing the TUI demo at a personal Anthropic-proxy Opus route meant patching `examples/tui-agent/cordis.yml` and `.env` in the working tree, which risks committing secrets and repeats per checkout. There was also no installable command: running the agent in an arbitrary project directory required invoking the repo's demo script from the repo root. Loader metadata is static, so "conditional composition uses overlays" (AGENTS.md) — but overlays only existed as committed sibling files, not as a machine-level layer.
|
||||
A developer's own preferences — which provider and model the TUI uses, personal credentials, a private adapter route — had nowhere to live except edits to committed files. Pointing the TUI demo at a personal Anthropic-proxy Opus route meant patching `examples/tui-agent/cordis.yml` and `.env` in the working tree, which risks committing secrets and repeats per checkout. There was also no installable command: running the agent in an arbitrary project directory required invoking the repo's demo script from the repo root. Loader metadata is static except the entry `disabled` field (see the [loader `disabled` interpolation decision](../architecture/2026-08-11-loader-entry-disabled-interpolation.md)), so "conditional composition uses overlays" (AGENTS.md) — but overlays only existed as committed sibling files, not as a machine-level layer.
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
开发者自己的偏好——TUI 使用哪个提供方和模型、个人凭证、私有的适配器路由——除了改动已提交的文件之外无处安放。要把 TUI 示例指向个人的 Anthropic 代理 Opus 路由,只能在工作区里改 `examples/tui-agent/cordis.yml` 和 `.env`,既有提交密钥的风险,又要在每个 checkout 里重复一遍。也没有可安装的命令:想在任意项目目录里运行这个 agent,必须回到仓库根目录调用示例脚本。Loader 元数据是静态的,所以「条件组合使用 overlay」(AGENTS.md)——但 overlay 此前只以已提交的同级文件形式存在,没有机器级的层。
|
||||
开发者自己的偏好——TUI 使用哪个提供方和模型、个人凭证、私有的适配器路由——除了改动已提交的文件之外无处安放。要把 TUI 示例指向个人的 Anthropic 代理 Opus 路由,只能在工作区里改 `examples/tui-agent/cordis.yml` 和 `.env`,既有提交密钥的风险,又要在每个 checkout 里重复一遍。也没有可安装的命令:想在任意项目目录里运行这个 agent,必须回到仓库根目录调用示例脚本。Loader 元数据是静态的——条目 `disabled` 字段除外(见 [loader `disabled` 插值决策](../architecture/2026-08-11-loader-entry-disabled-interpolation.md))——所以「条件组合使用 overlay」(AGENTS.md);但 overlay 此前只以已提交的同级文件形式存在,没有机器级的层。
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
@@ -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-07-26-code-dispatch-ui-foundation.md
|
||||
2026-07-26-code-dispatch-ui-foundation.md: 4115a1898de7d2cce01346c3f005fcd19c325f4c
|
||||
2026-07-26-code-dispatch-ui-foundation.zh.md: aeb57b93d781163dd0a4747ac03053c65deda1db
|
||||
2026-07-26-code-dispatch-ui-foundation.md: 94316e774f231a2f2d5e9bcc8d1a30fd4a2ec733
|
||||
2026-07-26-code-dispatch-ui-foundation.zh.md: 2c9ee5b93e20b5c09950e2896a888f90863b8bd0
|
||||
|
||||
@@ -16,7 +16,7 @@ Three changes, one per obstacle:
|
||||
|
||||
1. **`run_code` gains a required `description` parameter** (bash's exact contract: active voice, 5-10 words, shown in the UI; whitespace-only rejected at execute). `presentCall` now titles the card with the description and moves the program to `rawInput`. The prompt-side cost is a few tokens per call; the return is that every surface — TUI card, ACP title, web row — gets a human-readable label without parsing TypeScript.
|
||||
2. **`tool/code-dispatch` logs the sub-call's complete model-facing outcome** — `content: ContentBlock[]` + `isError`, the `tool/result` vocabulary — replacing `resultSummary` and deleting the summarize/cwd-normalization machinery outright. A UI renders a sub-call through the identical code path as a native result, including error text and non-text blocks. The event stays log-only (`deriveMessages()` ignores it): nothing about model context changes.
|
||||
3. **`DSH_TOOLS_MODE` env var on the `dsh` config tree** (`native`|`code`|`both`; unset keeps the schema default): the `tools` row reads it via `!!js`, and the worker code runtime is mounted unconditionally (Loader metadata is static, so no conditional row exists; a native boot only registers the service — workers spawn per run). This is an explicitly temporary configuration hook: per-session tool-mode selection owned by the web UI is the design goal, and the env var dies when that lands.
|
||||
3. **`DSH_TOOLS_MODE` env var on the `dsh` config tree** (`native`|`code`|`both`; unset keeps the schema default): the `tools` row reads it via `!!js`, and the worker code runtime is mounted unconditionally (Loader metadata was static when this shipped — no conditional row existed; the later [`disabled` interpolation decision](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) makes one possible but changes nothing here — a native boot only registers the service, workers spawn per run). This is an explicitly temporary configuration hook: per-session tool-mode selection owned by the web UI is the design goal, and the env var dies when that lands.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
1. **`run_code` 新增必填的 `description` 参数**(与 bash 完全相同的约定:主动语态、5-10 个词、展示在 UI 中;仅含空白的取值在执行时被拒绝)。`presentCall` 现在以该 description 作为卡片标题,并把程序文本移入 `rawInput`。提示词侧的成本是每次调用多出几个 token;换来的是每个表面——TUI 卡片、ACP(Agent Client Protocol)标题、Web 行——都无需解析 TypeScript 就能获得可供人阅读的标签。
|
||||
2. **`tool/code-dispatch` 记录子调用面向模型的完整结果**(`content: ContentBlock[]` 加 `isError`,即 `tool/result` 的词汇),取代 `resultSummary`,并把摘要与 cwd 归一化机制彻底删除。UI 渲染子调用走的代码路径与渲染原生结果完全相同,包括错误文本和非文本块。该事件保持仅日志(`deriveMessages()` 忽略它):模型上下文没有任何变化。
|
||||
3. **`dsh` 配置树上的 `DSH_TOOLS_MODE` 环境变量**(`native`|`code`|`both`;未设置时保持 schema 默认值):`tools` 行通过 `!!js` 读取它,worker 代码运行时则无条件挂载(loader 元数据是静态的,因此不存在条件行;native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的配置钩子:设计目标是让 Web UI 拥有按会话的工具模式选择,该目标落地后,这个环境变量随即退役。
|
||||
3. **`dsh` 配置树上的 `DSH_TOOLS_MODE` 环境变量**(`native`|`code`|`both`;未设置时保持 schema 默认值):`tools` 行通过 `!!js` 读取它,worker 代码运行时则无条件挂载(本项交付时 loader 元数据仍是静态的,因此不存在条件行;后来的 [`disabled` 插值决策](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) 让条件行成为可能,但此处不变——native 启动只是注册该服务,worker 要到每次运行时才 spawn)。这是一个明确标注为临时的配置钩子:设计目标是让 Web UI 拥有按会话的工具模式选择,该目标落地后,这个环境变量随即退役。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -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: 4e681b32088954d870df86898e26fe2cae669f14
|
||||
2026-08-01-windows-pwsh-default.zh.md: a9d600f8a8e47db49c3733f33091e667e341c6a7
|
||||
2026-08-01-windows-pwsh-default.md: c66e289c24d6024b1df53cd60f25c27d46fafc5a
|
||||
2026-08-01-windows-pwsh-default.zh.md: b2ad45ec96d546d01436a383ed7d8884b978aa31
|
||||
|
||||
@@ -12,9 +12,8 @@ The harness's shipped execution profile is bash-first on every platform. Windows
|
||||
|
||||
Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged.
|
||||
|
||||
- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`. It disables the POSIX-only `bash-sandbox`/`tool-bash` rows and inserts `pwsh-sandbox`/`tool-pwsh`. The later [Windows ACL sandbox decision](2026-08-08-windows-acl-restricted-token-sandbox.md) filled the win32 runner chain and superseded this note's original unconfined roster: `sandbox`, `sandbox-policy`, `fs-sandbox`, `permission`/`ui-permission`, and `approval` now stay enabled exactly as on POSIX, while the ACL backend truthfully reports its Everyone and hard-link gaps as partial enforcement.
|
||||
- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack re-enables the bash rows and disables both pwsh rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud.
|
||||
- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` declare `dsh-pwsh-sandbox`/`dsh-tool-pwsh`; the executor's dependency chain supplies `dsh-pwsh-local`, and the base bundle lists every row plugin as a dependency by house style.
|
||||
- **The base patch gates both shell stacks on its own rows** (the [loader `disabled` interpolation](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) note records the mechanism and the platform-layer fold): `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount only on win32 with the inverted expression — one shared patch file, exactly one shell stack per host. The confined pwsh stack runs over the ACL restricted-token runner, and the permission surface stays exactly as on POSIX (the [Windows ACL restricted-token sandbox](2026-08-08-windows-acl-restricted-token-sandbox.md) note owns that roster). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack or an unconfined pwsh executor overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel. The separate `windows.cordis.patch.yml` layer and the launcher's `apps/cli/src/windows-shell.ts` injection are deleted; the layer existed only because entry metadata was static.
|
||||
- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the pwsh rows. `apps/cli` and `dsh-base` declare `dsh-pwsh-sandbox`/`dsh-tool-pwsh`, and the executor's dependency chain supplies `dsh-pwsh-local`; the base bundle lists every row plugin as a dependency by house style.
|
||||
|
||||
The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior.
|
||||
|
||||
@@ -32,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 `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 are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows.
|
||||
- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their 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.
|
||||
- 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` pins the win32 default, custom-profile skip, missing-patch failure, cold-start dependency closure, and real composed roster; `packages/bundle/base/tests/base.spec.ts` pins that the Windows layer disables only the bash rows, inserts the confined pwsh rows, and leaves sandbox, permission, fs, and approval ownership untouched.
|
||||
- Keyless: a win32 `dsh --profile <name> --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged.
|
||||
- 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 <name> --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).
|
||||
|
||||
@@ -12,9 +12,8 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机
|
||||
|
||||
启动交付 profile(`dsh web`、`dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。
|
||||
|
||||
- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml)。它禁用仅限 POSIX 的 `bash-sandbox`/`tool-bash` 行,并插入 `pwsh-sandbox`/`tool-pwsh`。后续的 [Windows ACL 沙箱决策](2026-08-08-windows-acl-restricted-token-sandbox.md)填充了 win32 runner 链,并取代了本笔记最初的不限权清单:`sandbox`、`sandbox-policy`、`fs-sandbox`、`permission`/`ui-permission` 与 `approval` 均与 POSIX 上一样保持启用,而 ACL 后端则如实把 Everyone 与硬链接缺口报告为部分强制执行。
|
||||
- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行,并禁用两个 pwsh 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。
|
||||
- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 声明 `dsh-pwsh-sandbox`/`dsh-tool-pwsh`;执行器的依赖链提供 `dsh-pwsh-local`,按仓库惯例,base bundle 把每个行插件都列为依赖。
|
||||
- **base patch 在自身行上按平台门控两个 shell 栈**([loader `disabled` 插值](../architecture/2026-08-11-loader-entry-disabled-interpolation.md) note 记录了该机制与平台层折叠):`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。受限 pwsh 栈运行在 ACL 受限令牌 runner 之上,权限面与 POSIX 完全一致([Windows ACL 受限令牌沙箱](2026-08-08-windows-acl-restricted-token-sandbox.md) note 拥有该清单)。覆盖交付默认是组合决策:偏好 bash 栈或不限权 pwsh 执行器的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。独立的 `windows.cordis.patch.yml` 层与启动器的 `apps/cli/src/windows-shell.ts` 注入已删除;该层只因条目元数据是静态的而存在。
|
||||
- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到 pwsh 行。`apps/cli` 与 `dsh-base` 声明 `dsh-pwsh-sandbox`/`dsh-tool-pwsh`,执行器的依赖链提供 `dsh-pwsh-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。
|
||||
|
||||
pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。
|
||||
|
||||
@@ -32,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 栈仍是通用 `cordis.patch.yml` 的行。
|
||||
- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`(两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。
|
||||
- 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` 固定 win32 默认、自定义 profile 跳过、缺少 patch 时失败、冷启动依赖闭包和真实组合清单;`packages/bundle/base/tests/base.spec.ts` 固定 Windows 层仅禁用 bash 行、插入受限的 pwsh 行,并且不改变沙箱、权限、fs 与审批的归属。
|
||||
- Keyless:win32 上的 `dsh --profile <name> --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。
|
||||
- 单元:`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 <name> --dump-config` 在同一份共享 patch 层中显示两个栈,每个行以自己的 `disabled` 表达式在挂载时决定清单。
|
||||
- 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。
|
||||
|
||||
@@ -92,7 +92,7 @@ Run checks before pushes via [dsh-pre-push-checks](.agents/skills/dsh-pre-push-c
|
||||
|
||||
## Secrets / .env
|
||||
|
||||
Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and root `.env`. cordis.yml allows `!!js` (never `!js`) only under plugin `config`; Loader metadata is static, so conditional composition uses overlays ([primer](docs/cordis-primer.md#loader-configuration)). Never commit credentials. CI e2e skips without a key; [testing.md](docs/testing.md) owns key policy.
|
||||
Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and root `.env`. cordis.yml allows `!!js` (never `!js`) under plugin `config` and entry `disabled`; other metadata stays literal, so conditional composition also uses overlays ([primer](docs/cordis-primer.md#loader-configuration)). Never commit credentials. CI e2e skips without a key; [testing.md](docs/testing.md) owns key policy.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_sandbox_policy
|
||||
plugin_dsh_base_bash_sandbox["bash-sandbox<br/>@deepseek-ai/dsh-bash-sandbox"]
|
||||
cfg --> plugin_dsh_base_bash_sandbox
|
||||
plugin_dsh_base_pwsh_sandbox["pwsh-sandbox<br/>@deepseek-ai/dsh-pwsh-sandbox"]
|
||||
cfg --> plugin_dsh_base_pwsh_sandbox
|
||||
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
|
||||
cfg --> plugin_dsh_base_approval
|
||||
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
|
||||
@@ -68,6 +70,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_bash_env
|
||||
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
|
||||
cfg --> plugin_dsh_base_tool_bash
|
||||
plugin_dsh_base_tool_pwsh["tool-pwsh<br/>@deepseek-ai/dsh-tool-pwsh"]
|
||||
cfg --> plugin_dsh_base_tool_pwsh
|
||||
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
|
||||
cfg --> plugin_dsh_base_tool_tasks
|
||||
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
@@ -194,10 +198,12 @@ flowchart LR
|
||||
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
|
||||
| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` |
|
||||
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
|
||||
| `pwsh-sandbox` | `@deepseek-ai/dsh-pwsh-sandbox` |
|
||||
| `approval` | `@deepseek-ai/dsh-user-approval` |
|
||||
| `permission` | `@deepseek-ai/dsh-permission` |
|
||||
| `bash-env` | `@deepseek-ai/dsh-bash-env` |
|
||||
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
||||
| `tool-pwsh` | `@deepseek-ai/dsh-tool-pwsh` |
|
||||
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
|
||||
@@ -45,11 +45,16 @@
|
||||
# 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; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -39,11 +39,16 @@
|
||||
# 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; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -38,11 +38,16 @@
|
||||
# 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; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type ConfigDumpLayer,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -34,12 +33,6 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
|
||||
label: layer.packageName,
|
||||
patches: layer.patches,
|
||||
}))
|
||||
// The win32 shell platform layer rides between bundles and user layers,
|
||||
// exactly where the boot applies it.
|
||||
const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME)
|
||||
if (windowsShellLayer !== undefined) {
|
||||
layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches })
|
||||
}
|
||||
if (!defaultOnly) {
|
||||
if (existsSync(loaded.patchPath)) {
|
||||
layers.push({ label: loaded.patchPath, patches: loaded.patches })
|
||||
|
||||
@@ -39,7 +39,6 @@ const USER_PRESET_DIR = '.agent-presets'
|
||||
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -110,8 +109,6 @@ interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Bundle layers concatenated — the part below the user layers on a live reload. */
|
||||
bundlePatches: PatchOptions[]
|
||||
/** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */
|
||||
windowsShellPatches: PatchOptions[]
|
||||
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
|
||||
homePatches: PatchOptions[]
|
||||
/** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
|
||||
@@ -127,7 +124,6 @@ interface ComposedProfile {
|
||||
function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
return [
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...composed.profile.patches,
|
||||
...composed.homePatches,
|
||||
...composed.overlays,
|
||||
@@ -136,10 +132,10 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.profile.bundles` order, the win32 shell platform layer (when the host
|
||||
* is Windows), the profile's user layer, the home-level user layer
|
||||
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
|
||||
* every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
|
||||
* platform on its own rows), the profile's user layer, the home-level user
|
||||
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
|
||||
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* then the telemetry switch.
|
||||
* @param name - the profile name.
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
@@ -153,9 +149,8 @@ function composeProfile(
|
||||
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
|
||||
const rows = new Map<string, EntryOptions>()
|
||||
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
|
||||
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
const composedOverlays = [...overlays]
|
||||
@@ -174,7 +169,7 @@ function composeProfile(
|
||||
}
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
|
||||
return { profile, bundlePatches, windowsShellPatches, homePatches, overlays: composedOverlays, rows }
|
||||
return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
|
||||
}
|
||||
|
||||
/** Options for {@link runProfile}. */
|
||||
@@ -246,7 +241,6 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// removing the override could never revert the row to the bundle default.
|
||||
const composeLive = (): PatchOptions[] => structuredClone([
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
|
||||
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
|
||||
...composed.overlays,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* The Windows shell platform layer: on win32 hosts the shipped profile
|
||||
* compositions swap the POSIX-only bash stack for the sandbox-confined
|
||||
* PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox` +
|
||||
* `@deepseek-ai/dsh-tool-pwsh`). The layer is the base bundle's
|
||||
* `windows.cordis.patch.yml`, injected by the launcher between the bundle
|
||||
* layers and the user layers so a user patch can still override it — the
|
||||
* only override channel is composition config, like every other roster
|
||||
* decision. POSIX hosts never receive the layer.
|
||||
* @module @deepseek-ai/dsh/windows-shell
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
|
||||
/** The base bundle whose package carries the Windows shell patch. */
|
||||
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
|
||||
|
||||
/** The Windows shell patch filename inside the base bundle package. */
|
||||
export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml'
|
||||
|
||||
/** One Windows shell platform layer: its patch file and parsed patches. */
|
||||
export interface WindowsShellLayer {
|
||||
/** The patch file path, used as the config-dump provenance label. */
|
||||
label: string
|
||||
/** The parsed patch entries, applied after the bundle layers. */
|
||||
patches: PatchOptions[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Windows shell platform layer for a profile composition.
|
||||
* @param platform - the host platform (`process.platform` at call sites).
|
||||
* @param layers - the profile's bundle layers, in application order.
|
||||
* @param binName - the diagnostic prefix on thrown errors (`dsh`).
|
||||
* @returns the pwsh layer on win32, else `undefined`. A custom profile that
|
||||
* mounts no base bundle is skipped (it owns its shell stack); a base
|
||||
* bundle whose Windows shell patch is missing fails loud in
|
||||
* {@link loadOverlayPatches} — the shipped package always carries it, so
|
||||
* a miss is a broken installation.
|
||||
*/
|
||||
export function resolveWindowsShellLayer(
|
||||
platform: NodeJS.Platform,
|
||||
layers: readonly ProfileLayer[],
|
||||
binName: string,
|
||||
): WindowsShellLayer | undefined {
|
||||
if (platform !== 'win32') return undefined
|
||||
const base = layers.find(layer => layer.packageName === BASE_BUNDLE)
|
||||
if (base === undefined) return undefined
|
||||
const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME)
|
||||
return { label, patches: loadOverlayPatches(binName, label) }
|
||||
}
|
||||
@@ -1,73 +1,38 @@
|
||||
/**
|
||||
* The shipped shell composition: the base bundle gates both shell stacks by
|
||||
* platform on its own rows (`disabled: !!js process.platform`), so exactly
|
||||
* one shell stack mounts per host and no separate platform layer exists —
|
||||
* 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 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'
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, readFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
import yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
BASE_BUNDLE,
|
||||
resolveWindowsShellLayer,
|
||||
WINDOWS_SHELL_PATCH_FILENAME,
|
||||
} from '../src/windows-shell.ts'
|
||||
|
||||
const WINDOWS_PATCH = `- id: bash-sandbox
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: pwsh-sandbox
|
||||
name: '@deepseek-ai/dsh-pwsh-sandbox'
|
||||
`
|
||||
|
||||
/** One fake bundle layer rooted in a temp directory. */
|
||||
function fakeLayer(packageName: string, dir: string): ProfileLayer {
|
||||
return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
|
||||
}
|
||||
|
||||
/** A base bundle layer whose package carries the Windows shell patch. */
|
||||
function baseLayerWithPatch(dir: string): ProfileLayer {
|
||||
writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
|
||||
return fakeLayer(BASE_BUNDLE, dir)
|
||||
}
|
||||
|
||||
describe('resolveWindowsShellLayer', () => {
|
||||
let base: string
|
||||
afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
|
||||
const tempBase = (): string => {
|
||||
base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
|
||||
return base
|
||||
/**
|
||||
* The effective disabled state of one row on one platform: a `!!js` expression
|
||||
* evaluates with a platform-scoped `process` so both outcomes pin on any host.
|
||||
*/
|
||||
function disabledOn(row: { disabled?: unknown }, platform: 'win32' | 'linux'): boolean {
|
||||
const value = row.disabled
|
||||
if (value !== null && typeof value === 'object' && '__jsExpr' in value) {
|
||||
return Boolean(evaluate({ process: { platform } }, (value as { __jsExpr: string }).__jsExpr))
|
||||
}
|
||||
return value === true
|
||||
}
|
||||
|
||||
it('never applies on POSIX hosts', () => {
|
||||
expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
|
||||
expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('defaults Windows hosts to the pwsh platform layer', () => {
|
||||
const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
|
||||
expect(layer).toBeDefined()
|
||||
expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
|
||||
expect(layer?.patches).toEqual([
|
||||
{ id: 'bash-sandbox', disabled: true },
|
||||
{ insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('skips custom profiles without a base bundle', () => {
|
||||
const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
|
||||
expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails loud when the base bundle ships no Windows shell patch', () => {
|
||||
const base = tempBase()
|
||||
mkdirSync(base, { recursive: true })
|
||||
// The overlay loader owns the fail-loud contract: the caller named this
|
||||
// file, so its absence is a misconfiguration, not "no overlay".
|
||||
expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
|
||||
.toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
describe('the shipped shell composition (real bundle layers)', () => {
|
||||
let home: string
|
||||
afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
|
||||
// The app installation anchor, mirroring profile-boot.ts: the bundle layers
|
||||
@@ -75,65 +40,98 @@ describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
// suite composes the shipped patch files, not test fixtures.
|
||||
const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
|
||||
|
||||
it('composes the win32 confined roster through the real patch layers', () => {
|
||||
it('composes the confined pwsh roster on win32 and the bash roster on POSIX from the same rows', () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
|
||||
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
|
||||
const profile = loadProfile('dsh', 'web', anchor, home)
|
||||
const warnings: string[] = []
|
||||
const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
|
||||
expect(win32).toBeDefined()
|
||||
const rows = composeEntries(
|
||||
[...profile.layers.map(layer => layer.patches), win32!.patches],
|
||||
profile.layers.map(layer => layer.patches),
|
||||
message => warnings.push(message),
|
||||
)
|
||||
const byId = new Map(rows.map(row => [row.id, row]))
|
||||
// Only the POSIX bash stack leaves the roster: the permission surface
|
||||
// (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
|
||||
// exactly as on POSIX — the confined pwsh executor is what changes.
|
||||
for (const id of ['bash-sandbox', 'tool-bash']) {
|
||||
expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
|
||||
// One shared patch set, two rosters: the shell stacks gate themselves.
|
||||
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)
|
||||
// Host shell-tool rows are disabled on every platform; sessions mount
|
||||
// their own 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.
|
||||
for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
|
||||
expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
|
||||
}
|
||||
for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `inserted row ${id}`).toBe(true)
|
||||
}
|
||||
// The launcher's cold-start module fallback BFS-links the apps/cli
|
||||
// dependency closure into the profile's node_modules (the pwsh-local
|
||||
// precedent), so every inserted bare plugin must resolve from there.
|
||||
// dependency closure into the profile's node_modules, so every bare
|
||||
// plugin name in the base patch must resolve from there.
|
||||
const cliManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies?: Record<string, string> }
|
||||
for (const name of ['@deepseek-ai/dsh-pwsh-sandbox', '@deepseek-ai/dsh-tool-pwsh']) {
|
||||
expect(cliManifest.dependencies?.[name], `cold-start closure must reach ${name}`).toBeDefined()
|
||||
}
|
||||
// The patch touches only base-owned rows plus inserts, so the full web
|
||||
// profile composes without any no-match warning.
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
|
||||
it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
|
||||
it('base-only profiles carry both stacks with the same platform gating', () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
|
||||
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
|
||||
const profile = loadProfile('dsh', 'web', anchor, home)
|
||||
// POSIX: no platform layer, the bash stack stays enabled.
|
||||
const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
|
||||
const posixById = new Map(posixRows.map(row => [row.id, row]))
|
||||
expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
|
||||
expect(posixById.has('pwsh-local')).toBe(false)
|
||||
expect(posixById.has('pwsh-sandbox')).toBe(false)
|
||||
|
||||
// A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
|
||||
// patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
|
||||
// inserts, so the composition produces no no-match warning.
|
||||
initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
|
||||
const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
|
||||
const baseWarnings: string[] = []
|
||||
const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
|
||||
expect(win32).toBeDefined()
|
||||
composeEntries(
|
||||
[...baseOnly.layers.map(layer => layer.patches), win32!.patches],
|
||||
message => baseWarnings.push(message),
|
||||
const profile = loadProfile('dsh', 'base-only', anchor, home)
|
||||
const warnings: string[] = []
|
||||
const rows = composeEntries(
|
||||
profile.layers.map(layer => layer.patches),
|
||||
message => warnings.push(message),
|
||||
)
|
||||
expect(baseWarnings).toEqual([])
|
||||
const byId = new Map(rows.map(row => [row.id, row]))
|
||||
for (const id of ['bash-sandbox', 'tool-bash', 'pwsh-sandbox', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `row ${id}`).toBe(true)
|
||||
}
|
||||
// No web overlay: the tool rows keep their own gating too.
|
||||
expect(disabledOn(byId.get('tool-bash')!, 'win32'), 'tool-bash on win32').toBe(true)
|
||||
expect(disabledOn(byId.get('tool-bash')!, 'linux'), 'tool-bash on linux').toBe(false)
|
||||
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)
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
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 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`)
|
||||
for (const [id, win32] of [['tool-bash', true], ['tool-pwsh', false]] as const) {
|
||||
const row = entries.find((entry): entry is Record<string, unknown> => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
|
||||
))
|
||||
if (row === undefined) throw new TypeError(`preset ${preset} must mount ${id}`)
|
||||
expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string })
|
||||
// A platform-scoped context pins both outcomes 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 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')
|
||||
for (const id of ['tool-bash', 'tool-pwsh']) {
|
||||
expect(entries.some(entry => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
|
||||
)), `${id} must be absent from minimal`).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 docs/cordis-primer.md
|
||||
cordis-primer.md: d1e7c5fd8eaaa89fe448d238359389d945cd6346
|
||||
cordis-primer.zh.md: d6ce0f2024f65b006c9505daffaa06a08bb56875
|
||||
cordis-primer.md: c57055e9657ebc8a0c3f537825ddcbdda1ced68a
|
||||
cordis-primer.zh.md: 45cce2abb2117aef44028ab53a9836d24fab91d6
|
||||
|
||||
@@ -35,7 +35,7 @@ For single-decision events, short-circuiting is the design. A policy listener ca
|
||||
|
||||
## Loader Configuration
|
||||
|
||||
`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates only an entry's `config`, after declared injections activate, against that plugin context (`ctx.serviceName`); Include preserves nested row expressions until target activation. Entry metadata (`id`, `name`, `group`, `disabled`, `inject`, `intercept`, `isolate`) stays literal, so `disabled: !!js ...` always disables the entry. Use overlays when the environment selects plugins.
|
||||
`@deepseek-ai/cordis-plugin-include` parses `!!js` into expression nodes. Loader interpolates an entry's `config` (after declared injections activate, against that plugin context — `ctx.serviceName`) and its `disabled` field (at every mount decision, against the loader context); Include preserves nested row expressions until target activation. Other entry metadata stays literal. Use overlays when the environment selects plugins.
|
||||
|
||||
## Practical Rules
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
|
||||
|
||||
## Loader 配置
|
||||
|
||||
`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 只在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`;Include 会保留嵌套行表达式,直到目标行激活。条目元数据(`id`、`name`、`group`、`disabled`、`inject`、`intercept`、`isolate`)保持字面值,因此 `disabled: !!js ...` 始终禁用该条目。由环境选择插件时,请使用 overlay。
|
||||
`@deepseek-ai/cordis-plugin-include` 将 `!!js` 解析为表达式节点。Loader 在声明的注入激活后,基于该插件上下文(`ctx.serviceName`)插值条目的 `config`,并在每次挂载决策时基于 loader 上下文插值其 `disabled` 字段;Include 会保留嵌套行表达式,直到目标行激活。其余条目元数据保持字面值。由环境选择插件时,请使用 overlay。
|
||||
|
||||
## 实践规则
|
||||
|
||||
|
||||
@@ -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 docs/cordis-tutorial/05-config.md
|
||||
05-config.md: 2357f663135d6fc78a65f9d0952e0bc3f5eefae4
|
||||
05-config.zh.md: fbd94d179494ad0b6f73baff2ca525c786cc9e33
|
||||
05-config.md: 17cccce2ec43be65477ce527800ee6a636ee5d96
|
||||
05-config.zh.md: 87f1cb465d5f3e52a6f8e449cfb29818ba86dcb8
|
||||
|
||||
@@ -77,7 +77,7 @@ The loader used in this repo supports a `!!js` tag for config values that must b
|
||||
greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
|
||||
```
|
||||
|
||||
`!!js` works **only inside `config`**. Entry metadata (`name`, `id`, `disabled`, `inject`, ...) is static; `disabled: !!js ...` produces a truthy expression object that always disables the entry. See [loader configuration](../cordis-primer.md#loader-configuration).
|
||||
`!!js` works only inside `config` and in an entry's `disabled` field. `disabled: !!js ...` evaluates against the loader context at every mount decision (this repo's extension), so a row can gate itself on platform or environment; the other metadata (`name`, `id`, `inject`, ...) stays static, where an expression is ordinary truthy data. See [loader configuration](../cordis-primer.md#loader-configuration).
|
||||
|
||||
Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application.
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ ValidationError: invalid config:
|
||||
greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
|
||||
```
|
||||
|
||||
`!!js` **仅在 `config` 内有效**。Cordis 配置项的元数据(`name`、`id`、`disabled`、`inject` 等)是静态的;`disabled: !!js ...` 会生成一个真值表达式对象,始终禁用该 Cordis 配置项。详见 [loader 配置](../cordis-primer.md#loader-configuration)。
|
||||
`!!js` 仅在 `config` 与条目 `disabled` 字段内有效。`disabled: !!js ...` 在每次挂载决策时基于 loader 上下文求值(本仓库的扩展),可以按平台或环境门控一行;其余元数据(`name`、`id`、`inject` 等)保持静态,其中的表达式是普通真值数据。详见 [loader 配置](../cordis-primer.md#loader-configuration)。
|
||||
|
||||
下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
|
||||
|
||||
|
||||
@@ -199,6 +199,74 @@ describe('Loader config interpolation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loader entry disabled interpolation', () => {
|
||||
it('evaluates a !!js disabled expression against the loader context', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), [
|
||||
'- id: expr-off',
|
||||
' name: ./noop.mjs',
|
||||
' disabled: !!js process.version.length > 0',
|
||||
'- id: expr-on',
|
||||
' name: ./noop.mjs',
|
||||
' disabled: !!js process.version.length === 0',
|
||||
'',
|
||||
].join('\n'))
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
try {
|
||||
const off = [...ctx.loader.entries()].find(entry => entry.options.id === 'expr-off')
|
||||
const on = [...ctx.loader.entries()].find(entry => entry.options.id === 'expr-on')
|
||||
expect(off?.disabled).toBe(true)
|
||||
expect(off?.fiber).toBeUndefined()
|
||||
expect(on?.disabled).toBe(false)
|
||||
expect(on?.fiber).toBeDefined()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the raw expression in the options so write-back preserves the !!js form', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: expr\n name: ./noop.mjs\n disabled: !!js process.platform === "win32"\n')
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
try {
|
||||
const entry = [...ctx.loader.entries()].find(item => item.options.id === 'expr')
|
||||
// The evaluated boolean drives the mount decision; the serialized
|
||||
// expression node stays in the options for the file-backed tree.
|
||||
expect(entry?.options.disabled).toEqual({ __jsExpr: 'process.platform === "win32"' })
|
||||
expect(entry?.disabled).toBe(process.platform === 'win32')
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('re-evaluates when update() replaces the expression, mounting and unmounting', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'noop.mjs'), 'export function apply() {}\n')
|
||||
writeFileSync(join(dir, 'cordis.yml'), '- id: expr\n name: ./noop.mjs\n disabled: !!js process.version.length === 0\n')
|
||||
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
|
||||
try {
|
||||
const entry = [...ctx.loader.entries()].find(item => item.options.id === 'expr')
|
||||
expect(entry?.disabled).toBe(false)
|
||||
expect(entry?.fiber).toBeDefined()
|
||||
// The expression form is the file dialect; the typed programmatic API
|
||||
// carries booleans. Include reapplication feeds the raw node through
|
||||
// the untyped file path — simulated here with the serialized shape.
|
||||
const disabledTrue = { __jsExpr: 'process.version.length > 0' } as unknown as boolean
|
||||
const disabledFalse = { __jsExpr: 'process.version.length === 0' } as unknown as boolean
|
||||
await entry?.update({ disabled: disabledTrue })
|
||||
expect(entry?.disabled).toBe(true)
|
||||
expect(entry?.fiber).toBeUndefined()
|
||||
await entry?.update({ disabled: disabledFalse })
|
||||
expect(entry?.disabled).toBe(false)
|
||||
expect(entry?.fiber).toBeDefined()
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('boot with user patches', () => {
|
||||
it('applies id-targeted overrides, inserts, and interpolates !!js from the environment', async () => {
|
||||
const dir = tmp()
|
||||
|
||||
@@ -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/bundle/base/README.md
|
||||
README.md: 8b0db20274036a2601da19617a35e6bf4aeb30ca
|
||||
README.zh.md: ac5ab10a523fa211c1c1daf4c55d4dc8702eb782
|
||||
README.md: bd38f39f58ee1f765ff34d40cf57cc6daed2b32b
|
||||
README.zh.md: 2c6ff8513bae2b7d4b595733e83223bb7af34780
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and host-level subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Codex and Claude Code providers load dormant; Agent Presets independently decide whether their agent contributes either model-facing delegation tool. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
|
||||
|
||||
Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only bash stack (`bash-sandbox`/`tool-bash`) and inserts the sandbox-confined PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox`, `@deepseek-ai/dsh-tool-pwsh`). The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local` → `@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts never receive it.
|
||||
The patch gates both shell stacks by platform on its own rows: `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount on win32 only with the inverted expression — one shared patch file, exactly one shell stack per host. The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local` → `@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. A Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts see the pwsh rows disabled.
|
||||
|
||||
The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settings/credentials、遥测与宿主级 subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。Codex 与 Claude Code provider 以休眠状态加载;Agent Preset 分别决定自己的 agent 是否贡献任一面向模型的委派工具。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
|
||||
|
||||
启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的 bash 栈(`bash-sandbox`/`tool-bash`),并插入沙盒受限的 PowerShell 栈(`@deepseek-ai/dsh-pwsh-sandbox`、`@deepseek-ai/dsh-tool-pwsh`)。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner(`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。启动器在 win32 主机上把该层应用于 bundle 层与用户层之间;偏好不限权本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)。POSIX 主机永远不会收到它。
|
||||
patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner(`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。偏好不限权本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)。POSIX 主机看到的是被禁用的 pwsh 行。
|
||||
|
||||
行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。
|
||||
|
||||
|
||||
@@ -172,9 +172,14 @@
|
||||
|
||||
- id: bash-sandbox
|
||||
name: '@deepseek-ai/dsh-bash-sandbox'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
|
||||
- id: pwsh-sandbox
|
||||
name: '@deepseek-ai/dsh-pwsh-sandbox'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
- id: approval
|
||||
name: '@deepseek-ai/dsh-user-approval'
|
||||
config:
|
||||
@@ -199,6 +204,11 @@
|
||||
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
- id: tool-tasks
|
||||
name: '@deepseek-ai/dsh-tool-tasks'
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./windows.cordis.patch.yml": "./windows.cordis.patch.yml",
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
@@ -31,7 +30,6 @@
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"cordis.patch.yml",
|
||||
"windows.cordis.patch.yml",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
* field must name a real, parseable patch list.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
|
||||
|
||||
describe('dsh-base bundle', () => {
|
||||
it('declares a parseable patch list through the dsh.bundle.patch manifest field', () => {
|
||||
@@ -39,34 +40,37 @@ describe('dsh-base bundle', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('ships the Windows platform layer as the confined pwsh roster over the ACL runner chain', () => {
|
||||
it('gates each shell stack by platform with a symmetric disabled expression', () => {
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
const parsed = yaml.load(
|
||||
readFileSync(resolve(root, 'windows.cordis.patch.yml'), 'utf8'),
|
||||
readFileSync(resolve(root, 'cordis.patch.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
) as {
|
||||
id?: string
|
||||
disabled?: boolean
|
||||
insert?: { id?: string; name?: string }[]
|
||||
config?: { policy?: string }
|
||||
}[]
|
||||
const disables = parsed
|
||||
.filter(patch => patch.disabled === true)
|
||||
.map(patch => patch.id)
|
||||
// Only the POSIX bash stack is disabled: the Windows roster confines the
|
||||
// pwsh executor through the ACL runner chain, so the sandbox/policy rows,
|
||||
// the permission switcher, fs-sandbox, and the approval service all stay
|
||||
// enabled exactly as on POSIX — only the shell is swapped.
|
||||
expect(disables).toEqual(['bash-sandbox', 'tool-bash'])
|
||||
const inserted = parsed
|
||||
.flatMap(patch => patch.insert ?? [])
|
||||
.map(row => row.id)
|
||||
expect(inserted).toEqual(['pwsh-sandbox', 'tool-pwsh'])
|
||||
// The patch no longer touches the permission/approval surface at all.
|
||||
expect(parsed.find(patch => patch.id === 'approval')).toBeUndefined()
|
||||
expect(parsed.find(patch => patch.id === 'permission')).toBeUndefined()
|
||||
expect(parsed.find(patch => patch.id === 'sandbox')).toBeUndefined()
|
||||
expect(parsed.find(patch => patch.id === 'sandbox-policy')).toBeUndefined()
|
||||
expect(parsed.find(patch => patch.id === 'fs-sandbox')).toBeUndefined()
|
||||
)
|
||||
if (!Array.isArray(parsed)) throw new TypeError('base patch must parse to a patch list')
|
||||
const rows = parsed.flatMap((patch): Record<string, unknown>[] =>
|
||||
typeof patch === 'object' && patch !== null
|
||||
? (patch as { insert?: Record<string, unknown>[] }).insert ?? []
|
||||
: [],
|
||||
)
|
||||
// Symmetric gating: each stack's executor and tool rows carry the same
|
||||
// platform fact, inverted between the bash and pwsh twins, so exactly one
|
||||
// shell stack mounts per host. Evaluate with a platform-scoped context
|
||||
// (the `with` scope shadows the global `process`) so both outcomes pin on
|
||||
// every host.
|
||||
for (const [id, win32, linux] of [
|
||||
['bash-sandbox', true, false],
|
||||
['tool-bash', true, false],
|
||||
['pwsh-sandbox', false, true],
|
||||
['tool-pwsh', false, true],
|
||||
] as const) {
|
||||
const row = rows.find(candidate => candidate.id === id)
|
||||
if (row === undefined) throw new Error(`base patch must mount ${id}`)
|
||||
const expression = (row.disabled as { __jsExpr?: string } | undefined)?.__jsExpr
|
||||
if (expression === undefined) throw new Error(`${id} must gate on a !!js disabled expression`)
|
||||
expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression)), `${id} on win32`).toBe(win32)
|
||||
expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression)), `${id} on linux`).toBe(linux)
|
||||
}
|
||||
// The platform layer folded into these rows: no separate patch file ships.
|
||||
expect(existsSync(resolve(root, 'windows.cordis.patch.yml'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# The dsh-base Windows platform layer: applied by the dsh launcher on win32
|
||||
# hosts, between the bundle layers and the user layers. Windows confines
|
||||
# through the ACL restricted-token runner (the win32 chain of
|
||||
# dsh-sandbox-local → @deepseek-ai/dsh-sandbox-windows-acl), so the shipped
|
||||
# stack is the SANDBOXED PowerShell executor plus the full permission
|
||||
# surface: sandbox/sandbox-policy enforce the file-effect policy, the
|
||||
# permission switcher and the approval service run exactly as on POSIX, and
|
||||
# the fs row stays the base's sandboxed provider (fs-sandbox) — mounting
|
||||
# dsh-fs-local alongside it would double-register ctx.fs and fail the load.
|
||||
# Only the POSIX bash
|
||||
# stack (bash-sandbox/tool-bash) is disabled — bash has no Windows runner.
|
||||
# A Windows host that prefers the unconfined local pwsh executor or full
|
||||
# access overrides these rows through its profile or home cordis.patch.yml.
|
||||
# The bash-restore recipe must be complete: disable pwsh-sandbox and
|
||||
# tool-pwsh AND re-enable bash-sandbox and tool-bash — both executor
|
||||
# families register the same 'bash' service, so re-enabling the bash rows
|
||||
# while pwsh-sandbox stays inserted fails loud at load on a duplicate
|
||||
# registration.
|
||||
|
||||
- id: bash-sandbox
|
||||
disabled: true
|
||||
|
||||
- id: tool-bash
|
||||
disabled: true
|
||||
|
||||
- insert:
|
||||
- id: pwsh-sandbox
|
||||
name: '@deepseek-ai/dsh-pwsh-sandbox'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
@@ -262,6 +262,9 @@
|
||||
- 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
|
||||
|
||||
@@ -131,9 +131,8 @@ function workspaceManifests(): WorkspaceManifest[] {
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib;
|
||||
// dsh-base also ships the win32 shell platform layer the launcher reads.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml', 'windows.cordis.patch.yml'],
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
|
||||
|
||||
@@ -99,6 +99,8 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
|
||||
// the preset a model mounts, so the scoped name would send the model after an
|
||||
// id no roster reports.
|
||||
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', upstream: ['cordis'] },
|
||||
// The preset-roster loop names the `cordis` preset id, not a package.
|
||||
{ file: 'apps/cli/tests/windows-shell.spec.ts', upstream: ['cordis'] },
|
||||
// GROUP_ORDER holds `packages/<group>/` directory names, not package names.
|
||||
{ file: 'scripts/gen-module-graph.ts', upstream: ['cordis'] },
|
||||
{ file: 'scripts/gen-doc-graphs.ts', upstream: ['cordis'] },
|
||||
|
||||
39
scripts/verify-cordis-config.spec.ts
Normal file
39
scripts/verify-cordis-config.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* The verify-cordis-config metadata contract: `disabled` is the one entry
|
||||
* metadata field whose `!!js` expression the Loader interpolates; every other
|
||||
* metadata field must stay static, and a disabled expression must parse.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { metadataExpressionErrors } from './verify-cordis-config.ts'
|
||||
|
||||
describe('verify-cordis-config metadata expressions', () => {
|
||||
it('accepts a disabled !!js expression', () => {
|
||||
const problems = metadataExpressionErrors(
|
||||
{ id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', disabled: { __jsExpr: "process.platform === 'win32'" } },
|
||||
'[0]',
|
||||
)
|
||||
expect(problems).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an expression in a static metadata field', () => {
|
||||
const problems = metadataExpressionErrors({ id: { __jsExpr: 'process.platform' }, name: 'pkg' }, '[0]')
|
||||
expect(problems).toContain('[0].id: !!js is not interpolated here')
|
||||
})
|
||||
|
||||
it('rejects an expression nested below disabled (only the field itself interpolates)', () => {
|
||||
const problems = metadataExpressionErrors(
|
||||
{ id: 'tool-bash', name: 'pkg', disabled: { when: { __jsExpr: 'process.platform' } } },
|
||||
'[0]',
|
||||
)
|
||||
expect(problems).toContain('[0].disabled.when: !!js is not interpolated here')
|
||||
})
|
||||
|
||||
it('rejects a disabled expression that does not parse (the loader would fail the boot)', () => {
|
||||
const problems = metadataExpressionErrors(
|
||||
{ id: 'tool-bash', name: 'pkg', disabled: { __jsExpr: 'process.platform ===' } },
|
||||
'[0]',
|
||||
)
|
||||
expect(problems.some(problem => problem.includes('[0].disabled: disabled expression does not parse'))).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Validate Cordis Loader entry metadata and package resolution.
|
||||
*
|
||||
* The Loader interpolates only a plugin entry's `config`; expression objects in
|
||||
* fields such as `disabled` remain truthy data and silently change composition.
|
||||
* Example configs and the dsh Web composition resolve named plugins from their
|
||||
* owning workspace manifests. Local example packages must also be in the root
|
||||
* TypeScript project graph.
|
||||
* The Loader interpolates a plugin entry's `config` (after declared injections
|
||||
* activate, against that plugin context) and the entry `disabled` field (at
|
||||
* every mount decision, against the loader context). Every other entry
|
||||
* metadata field stays static, so an expression there remains truthy data and
|
||||
* silently changes composition. Example configs and the dsh Web composition
|
||||
* resolve named plugins from their owning workspace manifests. Local example
|
||||
* packages must also be in the root TypeScript project graph.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
@@ -36,7 +38,7 @@ const appOverlayFiles = new Set([
|
||||
'examples/web-schedule/cordis.yml',
|
||||
...globSync('examples/mcp-memory/*.cordis.yml', { cwd: root }),
|
||||
])
|
||||
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
|
||||
const metadataFields = ['id', 'name', 'group', 'inject', 'intercept', 'isolate'] as const
|
||||
|
||||
/** The adaptive directory-picker chooser package (mounts a backend row at boot). */
|
||||
const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||
@@ -64,33 +66,36 @@ const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
})
|
||||
const schema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
const files = cordisConfigFiles(root)
|
||||
const errors: string[] = []
|
||||
const pluginReferences: PluginReference[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
if (!isUnknownArray(document)) {
|
||||
errors.push(`${file}: root must be a Loader entry array`)
|
||||
continue
|
||||
}
|
||||
for (let index = 0; index < document.length; index++) {
|
||||
validateEntry(document[index], file, `[${index}]`)
|
||||
}
|
||||
}
|
||||
if (import.meta.main) {
|
||||
const files = cordisConfigFiles(root)
|
||||
|
||||
errors.push(...validateExampleResolution())
|
||||
errors.push(...validateAppResolution())
|
||||
errors.push(...validateSourcePlaneResolution())
|
||||
errors.push(...validatePresetPlaneSeparation())
|
||||
errors.push(...validateClientHalvesDeclared())
|
||||
for (const file of files) {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
if (!isUnknownArray(document)) {
|
||||
errors.push(`${file}: root must be a Loader entry array`)
|
||||
continue
|
||||
}
|
||||
for (let index = 0; index < document.length; index++) {
|
||||
validateEntry(document[index], file, `[${index}]`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
|
||||
for (const error of errors) console.error(`- ${error}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`verify-cordis-config: ${files.length} config files passed.`)
|
||||
errors.push(...validateExampleResolution())
|
||||
errors.push(...validateAppResolution())
|
||||
errors.push(...validateSourcePlaneResolution())
|
||||
errors.push(...validatePresetPlaneSeparation())
|
||||
errors.push(...validateClientHalvesDeclared())
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
|
||||
for (const error of errors) console.error(`- ${error}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
console.log(`verify-cordis-config: ${files.length} config files passed.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -409,11 +414,60 @@ function packageNameFromSpecifier(specifier: string): string | undefined {
|
||||
}
|
||||
|
||||
function validateMetadata(entry: Record<string, unknown>, file: string, path: string): void {
|
||||
for (const problem of metadataExpressionErrors(entry, path)) {
|
||||
errors.push(`${file}${problem}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expression-node diagnostics for one entry. `disabled` is the single
|
||||
* interpolated metadata field: its own `!!js` expression node is allowed and
|
||||
* must parse, while expressions nested below it stay truthy data; every other
|
||||
* metadata field must stay fully static.
|
||||
* @param entry - one loader entry (or patch row).
|
||||
* @param path - the entry's diagnostic path prefix.
|
||||
* @returns one diagnostic per offending expression.
|
||||
*/
|
||||
export function metadataExpressionErrors(entry: Record<string, unknown>, path: string): string[] {
|
||||
const problems: string[] = []
|
||||
for (const field of metadataFields) {
|
||||
if (!(field in entry)) continue
|
||||
const expressionPaths: string[] = []
|
||||
collectExpressionPaths(entry[field], `${path}.${field}`, expressionPaths)
|
||||
for (const expressionPath of expressionPaths) errors.push(`${file}${expressionPath}: !!js is not interpolated here`)
|
||||
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
|
||||
}
|
||||
const disabled = entry.disabled
|
||||
if (disabled !== undefined) {
|
||||
if (isJsExpr(disabled)) {
|
||||
const detail = disabledExpressionProblem(disabled.__jsExpr)
|
||||
if (detail !== undefined) problems.push(`${path}.disabled${detail}`)
|
||||
} else {
|
||||
// A non-expression value gates on Boolean() at mount; an expression
|
||||
// nested anywhere below it never evaluates, so it must stay literal.
|
||||
const expressionPaths: string[] = []
|
||||
collectExpressionPaths(disabled, `${path}.disabled`, expressionPaths)
|
||||
for (const expressionPath of expressionPaths) problems.push(`${expressionPath}: !!js is not interpolated here`)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse-only validation of a `disabled` expression: the Loader evaluates it
|
||||
* at every mount decision, and a syntax error would fail the boot — rejecting
|
||||
* it here moves that failure to the earliest resolvable point.
|
||||
* @param expression - the `!!js` expression text.
|
||||
* @returns the diagnostic suffix, or `undefined` when the expression parses.
|
||||
*/
|
||||
function disabledExpressionProblem(expression: string): string | undefined {
|
||||
try {
|
||||
// Compilation only — the constructor never executes the body.
|
||||
// oxlint-disable-next-line typescript/no-implied-eval
|
||||
new Function(`return (${expression})`)
|
||||
return undefined
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
return `: disabled expression does not parse: ${detail}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
vendor/README.md
vendored
1
vendor/README.md
vendored
@@ -47,6 +47,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
15. **Lazy Loader config resolution across `cordis/src/{events,fiber}.ts`, `loader/src/{index,config/entry}.ts`, `include/src/index.ts`, and `hmr/src/index.ts`**: ports [cordiverse/cordis#41](https://github.com/cordiverse/cordis/pull/41), retaining raw fiber config and resolving it through `internal/config` only after declared injections are active. Provider replacement re-resolves the raw expression, pending updates retain it, and HMR transfers it. Resolution applies only to the entry root, so child plugins mounted by a row keep caller-owned config identity. Include declares the `EntryGroup.key` tree-carrier marker (as Group does): its config is entry and patch lists, so interpolation keeps it literal and a `!!js` expression inside a nested row's config resolves lazily in that row's own fiber (Include's own `path` therefore stays literal too). Deferred failures retain the owning row diagnostic, and tree teardown does not persist failure-driven self-disposal. Covered by `packages/boot/app-boot/tests/{app-boot,user-patches}.spec.ts`, `packages/boot/cmdline/tests/cmdline.spec.ts`, `apps/cli/tests/web-agent-presets.e2e.ts`, and the built custom-profile cases in `apps/cli/tests/built-bin.e2e.ts`.
|
||||
16. **`cordis/package.json` publishes `src`**: added `src` to the `files` list, joining the other eight vendored packages. Cordis declares `"./src/*": "./src/*"` in its exports, so a tarball without `src` publishes an export map pointing at absent files; the release change judgement also reads `files` to decide whether a diff reaches the payload, and a package whose only published paths are build output has no tracked path to match.
|
||||
17. **`@deepseek-ai` rescope**: every vendored manifest `name`, every internal dependency entry among the vendored set, and every module specifier that reaches them use the scoped names in the manifest table's `npm name` column. Directory names, version numbers, and dependency ranges are unchanged, and no upstream runtime identifier is renamed — `Symbol.for('schemastery')` and Schemastery's `vendor:` metadata field keep their upstream values. Re-apply with `pnpm run rescope-vendor --apply` after a sync; the table's two name columns are the mapping, restated for consumers in [docs/rescope.md](../docs/rescope.md).
|
||||
18. **Entry `disabled` interpolation in `loader/src/config/entry.ts`**: a `disabled: !!js` expression evaluates against the loader context at every mount decision; the raw node stays in the options, so write-back keeps the `!!js` form. `disabled` is the only interpolated metadata field. Covered by `packages/boot/app-boot/tests/user-patches.spec.ts` and `apps/cli/tests/windows-shell.spec.ts`.
|
||||
|
||||
## Sync procedure
|
||||
|
||||
|
||||
16
vendor/loader/src/config/entry.ts
vendored
16
vendor/loader/src/config/entry.ts
vendored
@@ -3,7 +3,7 @@ import { deepEqual, isNullable } from '@deepseek-ai/cosmokit'
|
||||
import { Loader } from '../index.ts'
|
||||
import { EntryGroup } from './group.ts'
|
||||
import { EntryTree } from './tree.ts'
|
||||
import { evaluate } from './utils.ts'
|
||||
import { evaluate, isJsExpr } from './utils.ts'
|
||||
|
||||
/** Serialized plugin entry options stored in loader config files. */
|
||||
export interface EntryOptions {
|
||||
@@ -88,15 +88,25 @@ export class Entry {
|
||||
private _disabled(options: EntryOptions) {
|
||||
// group is always enabled
|
||||
if (options.group) return false
|
||||
if (options.disabled) return true
|
||||
if (this.disabledOf(options)) return true
|
||||
let entry = this.parent.ctx.fiber.entry
|
||||
while (entry) {
|
||||
if (entry.options.disabled) return true
|
||||
if (this.disabledOf(entry.options)) return true
|
||||
entry = entry.parent.ctx.fiber.entry
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective disabled state: a `!!js` expression evaluates against the loader
|
||||
* context. The raw node stays in the options, so write-back keeps the form.
|
||||
*/
|
||||
private disabledOf(options: EntryOptions): boolean {
|
||||
return isJsExpr(options.disabled)
|
||||
? Boolean(this.evaluate(options.disabled.__jsExpr))
|
||||
: Boolean(options.disabled)
|
||||
}
|
||||
|
||||
evaluate(expr: string) {
|
||||
return evaluate(this.ctx, expr)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user