Sfoglia il codice sorgente

Merge remote-tracking branch 'origin/worktree/code-diff-card' into worktree/changes-diff-preview

creatixchu 1 mese fa
parent
commit
2875e28a0e
100 ha cambiato i file con 872 aggiunte e 2293 eliminazioni
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
  2. 2 0
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
  3. 2 0
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md
  7. 6 0
      .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml
  8. 31 0
      .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
  9. 31 0
      .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml
  11. 9 64
      .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
  12. 13 68
      .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md
  13. 2 2
      .agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.i18n.yaml
  14. 3 3
      .agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md
  15. 2 2
      .agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md
  16. 0 19
      apps/cli/config/examples/cordis/cordis.yml
  17. 3 0
      apps/cli/src/profile-boot.ts
  18. 14 6
      apps/cli/tests/desktop-host.e2e.ts
  19. 7 62
      apps/cli/tests/profiles/headless/tests/ptc.e2e.ts
  20. 105 0
      apps/cli/tests/profiles/web/tests/creator-plugin-manager.expected.e2e.ts
  21. 59 0
      apps/cli/tests/profiles/web/tests/fixtures/creator-plugin-manager.mjs
  22. 12 4
      apps/cli/tests/web-agent-presets.e2e.ts
  23. 12 1
      apps/desktop-host/src/index.ts
  24. 2 2
      apps/desktop/README.i18n.yaml
  25. 2 0
      apps/desktop/README.md
  26. 2 0
      apps/desktop/README.zh.md
  27. 3 0
      apps/desktop/src/host-process.ts
  28. 1 1
      apps/desktop/src/main.ts
  29. 3 3
      apps/desktop/tests/host-process.spec.ts
  30. 1 0
      apps/desktop/tests/main-startup.spec.ts
  31. 1 0
      apps/web/tests/agent-preset-authoring.e2e.ts
  32. 31 187
      apps/web/tests/cordis-tool-round.e2e.ts
  33. 71 2
      apps/web/tests/details-session-lifecycle.e2e.ts
  34. 1 1
      apps/web/tests/expected/agent-preset-authoring/created.expected.md
  35. 1 1
      apps/web/tests/expected/agent-preset-authoring/damaged.expected.md
  36. 1 1
      apps/web/tests/expected/agent-preset-authoring/section.expected.md
  37. 1 1
      apps/web/tests/expected/agent-preset-selection/menu.expected.md
  38. 6 6
      apps/web/tests/expected/cordis-history/ui.expected.md
  39. 0 13
      apps/web/tests/scaffold.ts
  40. 2 2
      docs/config-catalog.i18n.yaml
  41. 1 1
      docs/config-catalog.md
  42. 1 1
      docs/config-catalog.zh.md
  43. 2 2
      docs/event-producer-consumer.i18n.yaml
  44. 1 1
      docs/event-producer-consumer.md
  45. 1 1
      docs/event-producer-consumer.zh.md
  46. 2 2
      docs/module-graph.i18n.yaml
  47. 5 5
      docs/module-graph.md
  48. 5 5
      docs/module-graph.zh.md
  49. 2 2
      docs/tool-catalog.i18n.yaml
  50. 6 187
      docs/tool-catalog.md
  51. 8 189
      docs/tool-catalog.zh.md
  52. 2 2
      docs/user/develop/practice/dynamic-cordis.i18n.yaml
  53. 8 8
      docs/user/develop/practice/dynamic-cordis.md
  54. 8 8
      docs/user/develop/practice/dynamic-cordis.zh.md
  55. 2 3
      packages/boot/app-boot/src/index.ts
  56. 9 0
      packages/boot/app-boot/src/profile-context.ts
  57. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  58. 4 2
      packages/boot/plugin-manager/README.md
  59. 4 2
      packages/boot/plugin-manager/README.zh.md
  60. 14 2
      packages/boot/plugin-manager/package.json
  61. 3 2
      packages/boot/plugin-manager/src/index.ts
  62. 12 4
      packages/boot/plugin-manager/src/operations.ts
  63. 13 3
      packages/boot/plugin-manager/src/tools.ts
  64. 27 2
      packages/boot/plugin-manager/tests/manager.spec.ts
  65. 14 0
      packages/boot/plugin-manager/tests/operations.spec.ts
  66. 118 3
      packages/boot/plugin-manager/tests/tools.spec.ts
  67. 9 0
      packages/boot/plugin-manager/tsconfig.json
  68. 2 2
      packages/client/ui-agent-preset/src/client/locales.ts
  69. 2 2
      packages/client/ui-conversation/README.i18n.yaml
  70. 2 0
      packages/client/ui-conversation/README.md
  71. 2 0
      packages/client/ui-conversation/README.zh.md
  72. 8 13
      packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css
  73. 5 15
      packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
  74. 6 2
      packages/client/ui-conversation/tests/skeleton.client.spec.tsx
  75. 3 3
      packages/core/tools/tests/gen-tool-catalog.spec.ts
  76. 2 2
      packages/extensions/README.i18n.yaml
  77. 5 5
      packages/extensions/README.md
  78. 5 5
      packages/extensions/README.zh.md
  79. 2 2
      packages/extensions/cordis-client-runner/README.i18n.yaml
  80. 7 7
      packages/extensions/cordis-client-runner/README.md
  81. 7 7
      packages/extensions/cordis-client-runner/README.zh.md
  82. 2 2
      packages/extensions/cordis-host-runner/README.i18n.yaml
  83. 7 7
      packages/extensions/cordis-host-runner/README.md
  84. 7 7
      packages/extensions/cordis-host-runner/README.zh.md
  85. 5 8
      packages/extensions/cordis-host-runner/src/index.ts
  86. 2 2
      packages/extensions/cordis-host-runner/src/lifecycle.ts
  87. 1 1
      packages/extensions/cordis-host-runner/tests/composition.spec.ts
  88. 2 2
      packages/extensions/tool-cordis/README.i18n.yaml
  89. 11 131
      packages/extensions/tool-cordis/README.md
  90. 15 135
      packages/extensions/tool-cordis/README.zh.md
  91. 1 7
      packages/extensions/tool-cordis/package.json
  92. 10 1
      packages/extensions/tool-cordis/src/api-catalog.ts
  93. 0 31
      packages/extensions/tool-cordis/src/fiber-state.ts
  94. 11 462
      packages/extensions/tool-cordis/src/index.ts
  95. 0 332
      packages/extensions/tool-cordis/src/inspect.ts
  96. 1 84
      packages/extensions/tool-cordis/src/present.ts
  97. 3 106
      packages/extensions/tool-cordis/src/prompt.ts
  98. 1 1
      packages/extensions/tool-cordis/tests/cordis-lifecycle.spec.ts
  99. 0 9
      packages/extensions/tool-cordis/tsconfig.json
  100. 2 2
      packages/extensions/ui-cordis/README.i18n.yaml

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.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-07-25-web-client-session-scope-and-provide-channel.md
-2026-07-25-web-client-session-scope-and-provide-channel.md: 620102f9f5e7fd76f38bf0031b856b26c2a7840d
-2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 3f00658c672908ba92627416ba5d5db381c0d9cd
+2026-07-25-web-client-session-scope-and-provide-channel.md: 0f003edc4a53e7a04ea4f5613abe42c2d3926566
+2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 1bae16cfaa1c54eebc6c8709ac6db5323b68d222

+ 2 - 0
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md

@@ -95,6 +95,8 @@ Slot scope is the closed set `root | session-maybe | session`:
 
 `conversation` is the resident `session-maybe` shell: `ConversationRoot`, HeroShell, the Workspace picker, the root-owned scrollport and composer stack, and the overlay chain's fallback frame retain their React instances across the no-session → blank-session switch. Two strict entries fill fixed regions without reparenting that tree: `conversation.session.header` carries breadcrumb/tabs/actions above the scrollport, while `conversation.session` carries the view ring and draft mirror inside it; both share the same session-scoped chat store. The composer bar (`conversation.composer.bar`) is itself `session-maybe`: with no session its machine faces and message actions are inert, while the whole dashed card opens the existing Workspace picker by pointer and its read-only textarea does the same through Enter or Space. The same instance — textarea included — goes live when a session appears; the remaining input slots stay strict `session` and dispatch nothing until then. The blank → engaging/active transition never rebuilds the InputBar on a phase flip.
 
+Blank Sessions retain the header's leading and corner slots so navigation controls, including the right-sidebar opener, are available before the first message. Title, actions, utilities, and View tabs remain hidden in the blank phase. The header still requires a selected Session; the Files and Terminal entries use that Session's workspace and execution services without requiring a recorded Turn.
+
 - The runtime's first built-in entry: the `'session'` hook — `useSession` itself rides the same mechanism, no special-casing.
 - Concurrent discipline: the render plane reads only from the hooks compartment (uSES consistency guarantee); props-compartment callbacks are used only in event-handler space; descriptor resolution is render-safe (idempotent caching, with prune reaping residue from abandoned renders).
 - Third-party components take zero value dependencies; types are a one-line type-only import (declaration merging into `SessionStandardProps` / `SessionMaybeStandardProps`).

+ 2 - 0
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md

@@ -95,6 +95,8 @@ slot scope 是闭集 `root | session-maybe | session`:
 
 `conversation` 是 `session-maybe` 的常驻外壳:`ConversationRoot`、HeroShell、Workspace picker、root 持有的 scrollport 与 composer stack,以及 overlay chain 的 fallback 外框,在无会话 → blank 会话的切换中保持 React 实例。两个严格 session entry 只填入固定区域,不改变该树的父级:`conversation.session.header` 在 scrollport 上方承载 breadcrumb/tab/action,`conversation.session` 在其内部承载 view ring 与 draft mirror;二者共享同一个 session scope chat store。composer bar(`conversation.composer.bar`)本身即为 `session-maybe`:无 session 时,其 machine faces 和消息动作保持惰性,整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。session 出现后同一实例(含 textarea)转为 live;其余输入 slot 保持严格 `session`,在此之前不派发任何内容。blank → engaging/active 的 InputBar 不因 phase 翻转而重建。
 
+blank Session 保留 header 的 leading 与 corner slot,让右侧栏展开入口等导航控件在首条消息之前即可使用。标题、actions、utilities 和 View tabs 在 blank phase 中继续隐藏。header 仍要求已选中的 Session;Files 与 Terminal 入口使用该 Session 的工作区和执行服务,无需已有 Turn 记录。
+
 - 运行时内建第一条:`'session'` 钩子——`useSession` 本身走同一机制,无特判。
 - Concurrent 纪律:渲染平面只从 hooks 格读(uSES 一致性保证);props 格回调只在事件 handler 空间用;描述符解析 render-safe(幂等缓存、废弃渲染残留由 prune 收尸)。
 - 第三方组件值零依赖,类型一行 type-only import(declaration merging 进 `SessionStandardProps` / `SessionMaybeStandardProps`)。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.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-09-14-current-profile-plugin-management.md
-2026-09-14-current-profile-plugin-management.md: a59a79022bce88dcee7b1629ec35c9759489e34a
-2026-09-14-current-profile-plugin-management.zh.md: 7c3e8d470fcf87fb32cda8d9f68dd2ffe4b7fe15
+2026-09-14-current-profile-plugin-management.md: 8d4d4c40f033e490f09ebf8d367e1fb621037a6d
+2026-09-14-current-profile-plugin-management.zh.md: a8dce606d00321bd96e643a9ba842581c961d6c1

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md

@@ -16,7 +16,7 @@ Configuration watches use Chokidar write stabilization by default. Its ordinary
 
 Profile files remain the persisted state: entry toggles edit only `disabled` in the last override matching the entry id and any module-name assertion, appending when none matches, and bundle toggles edit the ordered string list. Dependency updates do not reactivate retained disabled bundles. A service removal first applies the composition without the bundle and waits for old fibers to finish before deleting the dependency. Saved configuration, pnpm completion and runtime activation have separate outcomes; a failed removal preserves the actual partial state and a diagnostic path, while a failed or cancelled installation restores the profile files it snapshotted.
 
-This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service. Management operations return results to callers without adding messages to live Agents. The agent tool is disabled by default in the base bundle and shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
+This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service. Management operations return results to callers without adding messages to live Agents. The agent tool is enabled in Creator mode and disabled by default in the base bundle and other shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
 
 CLI calls inherit the terminal and authentication environment; service calls retain the subprocess credential scrub and bounded diagnostics. Management records carry error codes and parameters for locale-owned Web presentation. Reconciliation compares entry identity, fiber identity, configuration and diagnostics before and after updating: unchanged inactive entries remain warnings, while newly affected failures reject the operation. Explicit enablement targets must activate.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md

@@ -16,7 +16,7 @@ Web 和 Agent 控件需要修改运行中的 profile,同时避免另建包安
 
 profile 文件保持为持久状态:条目开关只修改最后一条符合条目 id 及模块名称断言的覆盖项中的 `disabled`,没有匹配项时追加,组合包开关修改有序字符串列表。更新依赖不会重新激活保留的已停用组合包。service 删除组合包时,先应用去掉该组合包的配置,等待旧 fiber 完成卸载后再删除依赖。已保存配置、pnpm 完成状态与运行时激活分别报告;失败的删除保留实际的部分状态与诊断路径,失败或被取消的安装则恢复它快照的 profile 文件。
 
-这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service。管理操作向调用方返回结果,不向存活 Agent 添加消息。base 组合包和内置预设默认禁用该 Agent 工具。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
+这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service。管理操作向调用方返回结果,不向存活 Agent 添加消息。创造模式启用该 Agent 工具;base 组合包和其他内置预设默认禁用。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
 
 CLI 调用继承终端和认证环境;service 调用保留子进程凭据清理与有界诊断。管理结果提供错误码和参数,由 Web 词典呈现文案。重载前后比较 entry、fiber、配置与诊断:未变化的已有故障保留为警告,本次影响到的新故障使操作失败。显式启用的目标必须成功激活。
 

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.i18n.yaml

@@ -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-09-16-creator-persistent-plugin-management.md
+2026-09-16-creator-persistent-plugin-management.md: 04ccbb83fe6d2f5c460f53100a9087cce119ea4d
+2026-09-16-creator-persistent-plugin-management.zh.md: 91afa00cd3f0adb8b21a52ee958bac61bc0912fc

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md

@@ -0,0 +1,31 @@
+# Agent Note: Creator mode installs persistent plugin bundles
+
+Status: implemented
+
+English | [中文](2026-09-16-creator-persistent-plugin-management.zh.md)
+
+## Problem
+
+Agents need to install capabilities and use them during the same conversation. Generated-code tools create a second plugin lifecycle alongside ordinary installed bundles.
+
+## Decision
+
+Creator mode enables the existing `plugin_manager` tool. Agents author packages and Loader YAML patches as workspace files, then install them through `install_bundle`. An MCP connection is a configuration-only bundle inserting the installed `dsh-mcp-client`; a UI bundle includes a Host entry and a Client artifact. Profile locking, package installation, enablement and HMR remain owned by the existing manager. The entire management tool requires `danger-full-access` or approval for one call: profile changes can load code with host permissions and affect other sessions. Each execution, including inventory reads, uses the shared sandbox escalation helper and approval service before accessing the manager. Under lower sandbox modes, `ask` requests approval and `never` denies; a full-access session needs no additional approval. A grant does not change session permissions, but its profile changes persist. This keeps shell-equivalent Host access explicit without requiring a permanent session-wide elevation. Human-operated Web and CLI controls retain their existing behavior.
+
+The model sees two read-only Cordis inspection tools. Generated-code define/run/stop/undefine and dynamic self-inspection tool APIs are absent. Existing runtime and Client consumers retain their services; historical session cards remain readable. This supersedes only the model-facing mutation workflow in the [self-referential toolset decision](../feature/2026-07-08-self-referential-cordis-toolset.md); its runtime ownership and sandbox rationale remain independently relevant. The [profile transaction decision](2026-09-14-current-profile-plugin-management.md) continues to govern locking, package installation, and partial failures.
+
+Visual creation requests default to an installed Client plugin rendered in the current Web page unless the user names another destination. The development skill supplies a minimal package and effect-owned Client registration. Discovery ends when the required APIs are known; a working first version is installed before optional visual refinement. Verification uses the connected page where available. Browser authentication or operating-system setup is not a prerequisite for plugin installation, and a mock preview cannot establish an in-app result.
+
+Desktop supplies its bundled pnpm entry and Electron Node invocation through launcher-owned profile facts. Plugin Manager uses that invocation for installation, removal, and registry inspection, retaining the ordinary profile transaction and activation behavior. Its environment applies only to package subprocesses; ordinary Host subprocesses do not inherit the private Node launcher path. CLI profiles retain their configured PATH command.
+
+Historical Cordis sessions declare `retired-tools` coverage at their exact format version, including when it equals the current writer. They are immutable replay inputs; their tool call/result data is compared after persistence and their cards render without registering retired tools. This coverage does not count toward migration coverage. The retained runner write APIs remain for programmatic and browser consumers; removing them requires replacing those consumers together.
+
+## Alternatives considered
+
+Generic entry CRUD and an MCP-specific management API duplicate operations expressible as bundle files plus existing installation and enablement. They are unnecessary for prompt-driven installation. Moving generated-code versioning into Plugin Manager retains two lifecycles without providing ordinary package persistence.
+
+## Consequences
+
+Selected bundles affect all sessions in the profile and survive restart. HMR activates new bundles on live profiles; installed package replacement requires restart. The agent reports saved state separately from activation and verifies the requested capability. Side effects belong to the plugin lifecycle, including stylesheet cleanup.
+
+A built Web profile test installs an MCP bundle, checks existing and new Creator sessions, restarts the process, and verifies tool disposal after bundle removal. A recorded session replays manager enablement of a configured MCP entry followed by an actual local MCP request without model credentials. Historical-card tests retain the removed tools' saved presentation.

+ 31 - 0
.agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.zh.md

@@ -0,0 +1,31 @@
+# Agent Note: 创造模式安装持久化插件组合包
+
+Status: implemented
+
+[English](2026-09-16-creator-persistent-plugin-management.md) | 中文
+
+## Problem
+
+agent 需要安装能力并在同一段对话中使用。生成代码工具在普通已安装组合包之外引入了第二套插件生命周期。
+
+## Decision
+
+创造模式启用现有的 `plugin_manager` 工具。agent 将包和 Loader YAML patch 写入工作区文件,再通过 `install_bundle` 安装。MCP 连接是插入已安装 `dsh-mcp-client` 的纯配置组合包;UI 组合包包含 Host 入口和 Client 产物。profile 锁、包安装、启停和 HMR 继续由现有管理器负责。 整个管理工具要求 `danger-full-access` 或单次调用的批准:profile 变更能以宿主权限加载代码,并影响其他会话。每次执行(包括查询列表)都会先使用共享沙箱提权函数和审批服务,再访问管理器。在较低沙箱模式下,`ask` 请求审批,`never` 拒绝;完整权限会话无需额外审批。批准不改变会话权限,但本次操作的 profile 变更会持久化。这让与 shell 等价的宿主访问需要明确授权,同时不必永久提升整个会话的权限。由用户直接操作的 Web 和 CLI 控件保持原有行为。
+
+模型可见两个只读 Cordis 检查工具,不再提供生成代码的 define/run/stop/undefine 和动态自省工具 API。现有运行时和 Client 消费者保留其服务;历史会话卡片仍然可读。这仅取代[自引用工具集决策](../feature/2026-07-08-self-referential-cordis-toolset.zh.md)中的模型侧变更流程;其运行时所有权和沙箱依据仍有独立价值。[profile 事务决策](2026-09-14-current-profile-plugin-management.zh.md)继续规定锁、包安装和部分失败行为。
+
+除非用户指定其他目标,视觉创建请求默认通过已安装的 Client 插件显示在当前 Web 页面。开发 skill 提供最小包和由 effect 管理的 Client 注册示例。已知所需 API 后结束探查,在可选视觉优化之前先安装能工作的初版。有条件时使用已连接页面验证。浏览器认证或操作系统设置不是安装插件的前提,mock 预览不能证明应用内结果。
+
+Desktop 通过启动器提供的 profile 信息传入内置 pnpm 入口与 Electron Node 调用方式。Plugin Manager 使用该调用完成安装、移除和 registry 检查,保留通常的 profile 事务与激活行为。其环境仅应用于包管理子进程;普通 Host 子进程不继承私有 Node 启动器路径。CLI profile 保留其配置的 PATH 命令。
+
+历史 Cordis 会话以准确的格式版本声明 `retired-tools` 覆盖,即使该版本等于当前 writer 也如此。它们是不可改写的重放输入;持久化后的工具调用及结果数据会进行比较,卡片渲染不注册已移除工具。此覆盖不计入迁移覆盖。保留的 runner 写入 API 供程序和浏览器消费者使用;移除时必须一起替换这些消费者。
+
+## Alternatives considered
+
+通用条目增删改查和 MCP 专用管理 API 重复了组合包文件及现有安装、启停操作能够表达的能力,提示驱动的安装不需要这些接口。将生成代码的版本管理搬入 Plugin Manager 会保留两套生命周期,且无法提供普通包的持久化方式。
+
+## Consequences
+
+已选择的组合包影响 profile 中的所有会话,并在重启后保留。HMR 在实时 profile 中激活新组合包;替换已安装的包需要重启。agent 分别报告保存状态和激活结果,并验证所需能力。副作用归属于插件生命周期,包括样式表清理。
+
+构建后的 Web profile 测试安装 MCP 组合包,检查现有和新建创造模式会话,重启进程,并验证移除组合包后的工具释放。录制会话无需模型凭据即可回放管理器启用已配置 MCP 条目及后续真实本地 MCP 请求。历史卡片测试保留已移除工具的已保存展示。

+ 2 - 2
.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.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-07-08-self-referential-cordis-toolset.md
-2026-07-08-self-referential-cordis-toolset.md: d8ae7d26666964c24245021249473d4160b35220
-2026-07-08-self-referential-cordis-toolset.zh.md: d6fac5b59d1d057ea11b607cba115ceb1c37664a
+2026-07-08-self-referential-cordis-toolset.md: 278c71051de1fb897c8735b39dff0b66c43a5626
+2026-07-08-self-referential-cordis-toolset.zh.md: 8a8e197e197fb6b53b52426d5ad9a33fd09e78c9

+ 9 - 64
.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md

@@ -1,4 +1,4 @@
-# Agent Note: The self-referential cordis toolset
+# Agent Note: Cordis runtime inspection and runner isolation
 
 Status: implemented
 
@@ -6,79 +6,24 @@ English | [中文](2026-07-08-self-referential-cordis-toolset.zh.md)
 
 ## Problem
 
-Everything in this harness is a cordis plugin, but the agent running inside that plugin runtime cannot see or touch it: it cannot enumerate the services and events around it, cannot extend itself with a new tool mid-session, and cannot compose capabilities it invents. Handing the model that power is worth exploring — a self-referential agent that inspects and modifies its own runtime — but it raises three correctness problems at once, and the design is about answering them rather than the raw "let the model run code" mechanic.
-
-First, model-written registration must be validated where it happens: a malformed tool schema has to fail at registration, not when a later request tries to assemble it into a prompt. Second, model-written code has to call service APIs whose source it has never seen — guessed method signatures and, worse, guessed return-value shapes cost many steps of blind probing. Third, everything the model mounts must be fully disposable, by the model on demand and by the ordinary plugin lifecycle when the host plugin reloads, or a long session accretes orphaned listeners and tools.
+Runtime API discovery must describe the APIs a plugin can actually call. Process-local generated definitions also need registration validation and complete effect disposal; isolating JavaScript globals alone does not constrain the authority of injected services.
 
 ## Decision
 
-The toolset ships as [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.md), with its runnable overlay and usage in the [runtime Cordis guide](../../../../docs/user/develop/practice/dynamic-cordis.md). It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence.
-
-The vm isolates accidental global pollution, and the context façade hides framework internals. Neither restricts the authority of exposed services: a temporary Plugin can call `ctx.shell` with the host executor's privileges and reach the real filesystem and web services. It runs in the shared DSH runtime and may affect other sessions in that process. This is an opt-in development tool with bash-equivalent trust, not a security boundary or product default.
-
-### The three tools
-
-| Tool | Contract |
-|---|---|
-| `cordis_inspect` | Read-only report over the live current-process runtime, one Markdown section per `what` value (omit `what` for all sections). `plugins` lists every live fiber; `temporary` lists only the temporary Plugins created by `cordis_mount`. An exact `name` with `what: "api"` or `what: "events"` narrows to one source-documented target. |
-| `cordis_mount` | Evaluates `code` now as an async JavaScript-function body in a `node:vm` sandbox and saves it nowhere. The returned Plugin is mounted under the internal `cordis-dynamic` group and tracked under a fresh process-local id (`dyn-1`, `dyn-2`, …). |
-| `cordis_unmount` | Unmounts one `cordis_mount` temporary Plugin by id and returns only after every owned tool, listener, service, timer, and effect reaches quiescence. It cannot remove Loader, configured, or installed Plugins. |
-
-`cordis_inspect` sections are `services` (every provided ctx service and owning fiber), `plugins` (every live plugin fiber), `tools` (what the model can call), `temporary` (the `cordis_mount` subset with id, running/pending state, provided and awaited services, and lifetime), `api` (live service signatures and referenced types), and `events` (harness events with dispatch mode and signature). Temporary Plugins remain active across later turns and disappear after `cordis_unmount`, toolset unload, or DSH restart; they are never restored automatically. Broad `api` and `events` reports omit full JSDoc to stay compact; an exact `name` returns one service or event with its original method/declaration JSDoc. A name is invalid with other sections, unknown targets fail, and an API target must be live. The model-facing tool descriptions carry the operational rules needed at call time; [the generated tool catalog](../../../../docs/tool-catalog.md) is their exhaustive rendering.
-
-### Sandbox semantics
-
-Mount code runs as an async-function body in a fresh vm realm. Its documented API steers file, network, process, and timer access through Cordis services so mounts remain inspectable and disposable. Host-realm helpers still make Node escape possible, consistent with the trusted posture. `vmTimeoutMs` bounds only synchronous evaluation.
-
-Sandbox globals are deliberately small: a tagged write-through `console` (`[cordis:<id>] …` on the host stdout/stderr, so a listener that fires long after the mount call still lands somewhere the user sees), the `harness.defineTool` / `harness.registerTool` registration pair, the encoding primitives fresh vm contexts lack (`btoa`/`atob` as host closures over `Buffer` — a sanctioned exception, `Buffer` itself is never exposed — plus `TextEncoder`/`TextDecoder`), and callable traps over the withheld Node APIs (`require`, `setTimeout`/`setInterval`/`setImmediate`/`clearTimeout`/`clearInterval`, `fetch`) that throw a redirect naming the cordis alternative. Only function-shaped globals are trapped; `process` and `Buffer` stay `undefined` so a `typeof` feature probe stays inert rather than detonating a throwing accessor.
-
-Mount code crosses the vm boundary through three controls. Dual-realm `instanceof` recognizes both host and vm objects. `harness.defineTool` rebuilds the output schema/projectors in the host realm, snapshots the body value as host-owned JSON, and lets the registry enforce the [canonical tool-output contract](../architecture/2026-07-20-canonical-tool-output-contract.md) before observation. The mounted plugin receives a whitelist context façade, not a raw or pass-through `Context`; framework plumbing and context-valued returns are rejected. Service reads require a declared `inject`, preserving Cordis activation and unload semantics. `ctx.tools.get` exposes only the schema view, so mounted code cannot bypass `ToolRuntime.execute` by calling a definition directly.
-
-The boundary normalizes unambiguous JSON-Schema forms into `ParameterSchemaSpec`, preserving `integer`, raw object openness, and required arrays. Direct DSL object nodes must declare `additionalProperties`; invalid vocabulary fails with the accepted alternatives. Parse, TypeScript, missing-return, Node-API, and duplicate-tool errors include the relevant source line or corrective contract without narrating implementation internals.
-
-### The internal group and temporary-Plugin lifecycle
+`cordis_inspect_list` and `cordis_inspect_query` expose read-only runtime discovery. Generated catalogs retain source-owned declarations and JSDoc, intersected with live providers. The catalog generator rejects freshness drift; detailed queries avoid charging every request for complete API declarations.
 
-Every temporary Plugin is a child of one internal `cordis-dynamic` group beneath the tool plugin, so ordinary fiber disposal handles toolset reload and unload. `cordis_mount` awaits settlement; startup failure disposes the fiber before returning an error. A settled pending Plugin remains visible with its missing injections. `cordis_unmount` awaits the Plugin fiber's disposal.
+The Host and Client runners retain their programmatic lifecycle and browser consumers. A Host definition evaluates in a fresh vm realm and receives a context façade: service access requires declared injection, framework internals are hidden, and registrations belong to the definition's fiber. Tool output normalization crosses back into the Host realm before validation. Disposal awaits the fiber's owned effects. The vm prevents accidental global pollution; injected filesystem, shell, and network services still have real authority, so it is not a security boundary.
 
-Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal project Plugin or installable profile bundle through the regular development workflow.
-
-### Cross-mount composition via provide/inject
-
-Mounts relate to each other through ordinary cordis service semantics, with their ids as the lifecycle handles: mount A calls `ctx.provide('foo', value)`, mount B declares `inject: ['foo']` and activates the moment `foo` exists; mounted first, B stays pending and names the missing service; unmounting A sends B back to pending (its registrations unwound) and a later re-provide re-runs B's `apply` through a fresh sandbox façade; a duplicate provide fails loud with the owning fiber named. One realm caveat: a service value provided by a mount is a vm-realm object — method calls on it work from anywhere, but consumers must not assume host prototypes on it.
-
-### The generated API catalog
-
-`cordis_inspect` serves API and event data from a generated catalog rather than a duplicated table. The generator reuses the Cordis catalog AST scan and emits service summaries, signatures, original service-method and event JSDoc, event modes, referenced type declarations, and the inherited context API. Ambiguous type names are omitted and oversized declarations are marked as truncated.
-
-Freshness is gated like every generated artifact: `pnpm run verify-cordis-api` (in `doc-sync`) regenerates in memory and fails on any diff, so a JSDoc or public-signature edit cannot ship without regenerating the catalog the model reads. At runtime the inspect tool intersects the catalog with the live runtime rather than dumping it: broad reports render live catalogued services as summary + signatures, live services without a catalog entry (mount-provided ones) as name + owning fiber, catalogued services with no live provider tersely, and then the referenced type shapes. Exact-name reports render one live service or event with the original JSDoc immediately before each signature; keeping that detail opt-in avoids charging its token cost on exploratory listings.
-
-### Configuration, rendering, and observability
-
-The plugin exposes one config field, validated by schemastery and documented in [the config catalog](../../../../docs/config-catalog.md): `vmTimeoutMs` (default 5000), the millisecond bound on the synchronous portion of code evaluation. The current model-facing names are `cordis_inspect`, `cordis_mount`, and `cordis_unmount`; the internal `cordis-dynamic` group name and `dyn-` id prefix remain structural vocabulary. All three tools render as `generic` cards per [the tool cookbook](../../../../docs/cookbook/adding-a-tool.md): inspect is `read`, mount is `execute` carrying code as `rawInput`, and unmount is `delete`. Web conversation rows preserve those generic mechanics while giving the tools the action titles `Inspect`, `Mount temporary Plugin`, and `Unmount temporary Plugin` plus one shared Cordis accent; the mount row retains the shared JavaScript expansion and syntax highlighting.
-
-Model-visible ⟺ logged holds with no new session event type: mount and unmount are visible through their logged `tool/call` / `tool/result` pairs, and any changed tool set is logged by the full changed request header emitted when schemas change between steps. Temporary Plugins are process memory, not session state: session resume rehydrates conversation history but never recreates them.
+Definitions remain process-local. Restart and session resume do not recreate them from historical calls. The [Creator persistent plugin decision](../architecture/2026-09-16-creator-persistent-plugin-management.md) owns agent-authored installation, approval, and profile persistence. Shipped model tools do not create or mutate runner definitions.
 
 ## Alternatives considered
 
-**A structured per-capability registration tool instead of `cordis_mount`.** The most tempting alternative is a `cordis_register_tool` with explicit `name` / `description` / `parameters` / `code` fields (and siblings `cordis_register_listener`, `cordis_register_service`, …) rather than a single "mount a plugin" primitive. It was rejected because its one real win — no plugin boilerplate for the single commonest case — does not pay for its costs, while a single mount primitive answers every capability at once.
-
-| Dimension | Structured per-capability tools | Single `cordis_mount` |
-|---|---|---|
-| Schema correctness | `parameters` is still model-written JSON needing unified-schema validation, merely one step earlier | The same validation runs at the sandbox boundary, with the same instructive errors |
-| The code field | An `execute` body is still model-written JS in a vm; the realm and service-call correctness problems are unchanged | One sandbox, one normalization path, one guarded registration |
-| Capability coverage | Tools only; listeners, services, `inject` relations each need another structured tool — an API that grows without bound | One vocabulary (a cordis plugin) covers every effect, present and future |
-| Cross-mount composition | Not expressible in a tool-registration payload | Native `provide`/`inject`, ordinary cordis semantics |
-| Inspectability | Registers something the plugin list cannot show as a plugin | What the model mounts is exactly what `cordis_inspect` renders |
-| Model ergonomics | Wins for the single most common case (no plugin boilerplate) | Mitigated by the canonical recipe in the mount description plus boundary errors that teach the fix |
-
-The correctness investment therefore goes where it pays for every capability at once: the generated API catalog surfaced through `cordis_inspect`, and sandbox-boundary validation whose error messages teach the correct call. A structured registration tool remains addable later as sugar that synthesizes mount code; nothing here forecloses it.
-
-**A hand-maintained service/event reference in the tool.** The first cut of the inspect tool carried a hand-written table of service method signatures. It was replaced by the generated `api-catalog.ts` because a hand table drifts from the JSDoc the moment a signature changes and nothing gates the drift, whereas the generated artifact is freshness-checked against the same AST the docs use.
+**Hand-maintained API tables.** Rejected because they drift independently from service declarations; generated catalogs and freshness checks share one source.
 
-**A new `cordis/mount` session event.** A durable event recording each mount's source and name has clear precedent (`hook/invoked`, `compaction/start`). Rejected: mount and unmount are already visible as `tool/call` / `tool/result` pairs and the tool-set change is already logged as a full changed request header, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs the mount source and name outside the tool call.
+**Treat the vm as a security sandbox.** Rejected because services exposed through the façade can reach the Host's real resources. Restricted globals improve lifecycle correctness, not permission enforcement.
 
-**A hardened / capability-restricted sandbox.** Trapping Node built-ins and handing mount code a whitelist façade rather than the raw context might suggest an intent to sandbox for safety. It is explicitly not that: the traps and the façade narrow the *API* mount code sees — steering it onto cordis services and away from leak-prone Node built-ins and framework internals — for correctness and to close the unguarded-context escape, but the capabilities the façade exposes (`ctx.shell`, `ctx.fs`, `ctx.web`) reach the real runtime, so it is not a security boundary. A real one (separate process, permission prompts) was out of scope for a dev/opt-in toolset and would fight the entire point — handing the model the live runtime.
+**Recreate definitions from session logs.** Rejected because replaying source would execute historical side effects. Historical cards display persisted source and outcomes without restoring a live definition.
 
 ## Consequences
 
-The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` short-circuits the chain, so a mounted listener can stop the agent's own tool dispatch ([waterfall semantics](../../../../docs/cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume.
+Inspection remains usable without Creator or Plugin Manager. Runner lifecycle tests cover guarded registration, startup failure cleanup, and awaited disposal. Existing browser consumers retain their lifecycle APIs; removing their write-side registry and activation machinery requires a coordinated replacement of those consumers, rather than deleting historical rendering or assuming session data recreates live state.

+ 13 - 68
.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md

@@ -1,84 +1,29 @@
-# Agent Note: 自引用 cordis 工具集
+# Agent Note:Cordis 运行时检查与 runner 隔离
 
 Status: implemented
 
 [English](2026-07-08-self-referential-cordis-toolset.md) | 中文
 
-## 问题
+## Problem
 
-本 harness 中的一切都是 cordis 插件,但运行在该插件运行时内部的 agent(智能体)既看不到也碰不到它:它无法枚举周围的服务和事件,无法在会话中途为自己添加新工具,也无法组合自己发明的能力。赋予模型这种能力值得探索——一个能审视并修改自身运行时的自引用 agent——但这同时引发三个正确性问题,本设计的核心正是回答这些问题,而非单纯的「让模型执行代码」机制。
+运行时 API 发现必须描述插件实际能调用的 API。进程内生成定义还需要注册校验和完整的 effect 释放;仅隔离 JavaScript 全局变量并不能限制注入服务的权限。
 
-第一,模型编写的注册必须在注册发生时就完成校验:格式错误的工具 schema 必须在注册时失败,而不是等到后续请求尝试将其组装进提示词时才报错。第二,模型编写的代码需要调用它从未见过源码的服务 API——靠猜测方法签名、更糟糕的是猜测返回值结构,会消耗大量盲目试探的步骤。第三,模型挂载的一切都必须完全可释放:模型可以按需释放,普通的插件生命周期在宿主插件重载时也会释放,否则长会话会积累遗留的监听器和工具。
+## Decision
 
-## 决策
+`cordis_inspect_list` 和 `cordis_inspect_query` 提供只读运行时发现。生成目录保留源码中的声明和 JSDoc,并按存活 provider 筛选。目录生成器拒绝过期产物;精确查询避免每次请求都承担完整 API 声明的 token 成本。
 
-该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.zh.md) 发布,其可运行 overlay 与用法位于[运行时 Cordis 指南](../../../../docs/user/develop/practice/dynamic-cordis.zh.md)。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时插件,再将该插件卸载至完全停稳。
+Host 和 Client runner 保留程序侧生命周期和浏览器消费者。Host 定义在新的 vm realm 中求值,并接收 context façade:服务访问需要声明注入,框架内部结构不可见,注册归定义的 fiber 所有。工具输出先回到 Host realm 进行归一化,再接受校验。释放会等待 fiber 所有的 effect。vm 防止意外污染全局变量;注入的文件系统、shell 和网络服务仍有真实权限,因此它不是安全边界。
 
-vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节。但二者都不限制已暴露服务的权限:临时插件可以调用 `ctx.shell` 以宿主执行器的权限运行命令,也能访问真实的文件系统和网络服务。它运行在共享 DSH 运行时中,可能影响同一进程的其他会话。这是一个需要显式启用的开发工具,信任等级与 bash 相当,不是安全边界,也不是产品默认配置。
+定义仅存在于进程中。重启和会话恢复不会从历史调用重建它们。[Creator 持久化插件决策](../architecture/2026-09-16-creator-persistent-plugin-management.zh.md)负责 agent 安装、审批和 profile 持久化。内置模型工具不创建或修改 runner 定义。
 
-### 三个工具
+## Alternatives considered
 
-| 工具 | 约定 |
-|---|---|
-| `cordis_inspect` | 当前进程活跃运行时的只读报告,每个 `what` 值对应一个 Markdown 小节(省略 `what` 则输出全部小节)。`plugins` 列出全部存活 fiber,`temporary` 只列 `cordis_mount` 创建的临时插件。精确 `name` 搭配 `what: "api"` 或 `what: "events"` 可收窄到一个带源码文档的目标。 |
-| `cordis_mount` | 立即在 `node:vm` 沙箱中把 `code` 作为异步 JavaScript 函数体求值,且不保存到任何位置。返回的插件挂在内部 `cordis-dynamic` 分组下,并用新的进程内 id(`dyn-1`、`dyn-2`……)跟踪。 |
-| `cordis_unmount` | 按 id 卸载一个 `cordis_mount` 临时插件,并只在其自有工具、监听器、服务、定时器和其他 effect 完全停稳后返回。它不能删除 Loader、已配置或已安装的插件。 |
+**手写 API 表。** 拒绝,因为它会独立于服务声明漂移;生成目录与新鲜度检查共享同一源码。
 
-`cordis_inspect` 的小节是 `services`(每个已提供的 ctx 服务及所属 fiber)、`plugins`(全部存活插件 fiber)、`tools`(模型可调用的工具)、`temporary`(`cordis_mount` 子集,包含 id、running/pending 状态、提供与等待的服务和生命周期)、`api`(活跃服务签名及其引用类型)和 `events`(harness 事件及分发模式和签名)。临时插件可跨后续轮次保持活跃,并在 `cordis_unmount`、工具集卸载或 DSH 重启后消失;系统绝不会自动恢复它们。宽泛的 `api` 和 `events` 报告省略完整 JSDoc;精确 `name` 返回一个服务或事件及其原始 JSDoc。其他小节不能搭配 name,未知目标会失败,而 API 目标必须处于活跃状态。面向模型的工具描述包含调用时所需的操作规则;[生成的工具目录](../../../../docs/tool-catalog.zh.md)是这些规则的完整呈现。
+**将 vm 视为安全沙箱。** 拒绝,因为 façade 暴露的服务可以访问 Host 的真实资源。受限全局变量改善生命周期正确性,不执行权限控制。
 
-### 沙箱语义
+**从会话日志重建定义。** 拒绝,因为重放源码会执行历史副作用。历史卡片展示持久化源码和结果,不恢复运行中的定义。
 
-挂载代码以异步函数体的形式在一个新的 vm realm 中运行。其文档化的 API 将文件、网络、进程和定时器访问引导至 Cordis 服务,使挂载保持可审视和可释放。宿主 realm 的辅助手段仍然使 Node 逃逸成为可能,这与信任姿态一致。`vmTimeoutMs` 仅约束同步执行部分。
+## Consequences
 
-沙箱全局变量刻意精简:一个带标签的直写 `console`(在宿主 stdout/stderr 上输出 `[cordis:<id>] …`,这样在挂载调用之后很久才触发的监听器输出仍能落到用户可见的地方)、`harness.defineTool`/`harness.registerTool` 注册对、新 vm 上下文缺少的编码原语(`btoa`/`atob` 作为基于 `Buffer` 的宿主闭包——这是一个明确允许的例外,`Buffer` 本身从不暴露——加上 `TextEncoder`/`TextDecoder`),以及对未暴露的 Node API 设置的可调用陷阱(`require`、`setTimeout`/`setInterval`/`setImmediate`/`clearTimeout`/`clearInterval`、`fetch`),这些陷阱会抛出一条重定向消息指明 cordis 替代方案。只有函数形态的全局变量才设陷阱;`process` 和 `Buffer` 保持 `undefined`,这样 `typeof` 特性探测仍然无害,而不会触发会抛出异常的访问器。
-
-挂载代码通过三道控制跨越 vm 边界。双 realm `instanceof` 同时识别宿主和 vm 对象。`harness.defineTool` 在宿主 realm 中重建输出 schema/投影器,将工具体返回值快照为宿主自有的 JSON,并让注册表在观测前强制执行[规范工具输出约定](../architecture/2026-07-20-canonical-tool-output-contract.zh.md)。挂载的插件接收的是一个白名单上下文门面,而非原始或透传的 `Context`;框架内部机制和以上下文为值的返回会被拒绝。服务读取需要声明 `inject`,保留 Cordis 的激活与卸载语义。`ctx.tools.get` 仅暴露 schema 视图,因此挂载代码无法绕过 `ToolRuntime.execute` 直接调用定义。
-
-边界将无歧义的 JSON Schema 形式规范化为 `ParameterSchemaSpec`,同时保留 `integer`、原始对象开放性和 required 数组。直接使用 DSL 的对象节点必须声明 `additionalProperties`;无效词汇会报错并给出可接受的替代方案。解析错误、TypeScript 错误、缺少 return、Node API 误用和重复工具名等错误信息包含相关源码行或纠正性约定,不叙述实现内部细节。
-
-### 内部分组与临时插件生命周期
-
-每个临时插件都是工具插件下方内部 `cordis-dynamic` 分组的子节点,因此普通的 fiber 释放即可处理工具集重载和卸载。`cordis_mount` 会等待 settlement;启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的插件仍然可见,并列出其缺失的注入。`cordis_unmount` 等待插件 fiber 的释放完成。
-
-临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的项目 Plugin 或可安装的 profile 组合包。
-
-### 通过 provide/inject 实现跨挂载组合
-
-挂载之间通过普通的 cordis 服务语义相互关联,以各自的 id 作为生命周期句柄:挂载 A 调用 `ctx.provide('foo', value)`,挂载 B 声明 `inject: ['foo']` 并在 `foo` 存在的瞬间激活;如果 B 先挂载,它保持 pending 状态并列出缺失的服务;卸载 A 使 B 回到 pending(其注册被撤销),之后重新 provide 会通过一个新的沙箱门面重新运行 B 的 `apply`;重复 provide 会明确报错并指出拥有该服务的 fiber。一个 realm 注意事项:由挂载 provide 的服务值是 vm realm 对象——从任何地方调用其方法都能工作,但消费方不得假设它具有宿主原型。
-
-### 生成的 API 目录
-
-`cordis_inspect` 从生成的目录提供 API 和事件数据,而非维护一份重复的表格。生成器复用 Cordis 目录的 AST 扫描,输出服务摘要、签名、原始服务方法与事件 JSDoc、事件模式、引用的类型声明以及继承的上下文 API。有歧义的类型名被省略,过大的声明被标记为截断。
-
-新鲜度像所有生成产物一样受门禁约束:`pnpm run verify-cordis-api`(在 `doc-sync` 中)在内存中重新生成并在有任何 diff 时失败,因此 JSDoc 或公开签名变更如果不重新生成模型读取的目录就无法合入。运行时 inspect 工具将目录与活跃运行时取交集而非直接转储:宽泛报告把有目录条目的活跃服务渲染为摘要 + 签名,把没有目录条目的活跃服务(挂载提供的)渲染为名称 + 所属 fiber,简要列出有目录条目但无活跃提供方的服务,再附上引用的类型结构。精确名称报告渲染一个活跃服务或事件,并把原始 JSDoc 紧靠在每个签名之前;让该细节按需出现,避免探索性列表承担其 token 成本。
-
-### 配置、渲染与可观测性
-
-该插件暴露一个配置字段,由 schemastery 校验并记录在[配置目录](../../../../docs/config-catalog.zh.md)中:`vmTimeoutMs`(默认 5000),代码同步求值部分的毫秒上限。当前面向模型的名称是 `cordis_inspect`、`cordis_mount` 和 `cordis_unmount`;内部 `cordis-dynamic` 分组名和 `dyn-` id 前缀仍是结构性词汇。三个工具均按[工具实操手册](../../../../docs/cookbook/adding-a-tool.zh.md)渲染为 `generic` 卡片:inspect 为 `read`,mount 为携带代码 `rawInput` 的 `execute`,unmount 为 `delete`。Web 对话行保留这些通用机制,同时为各工具设置操作标题 `Inspect`、`Mount temporary Plugin` 和 `Unmount temporary Plugin` 以及统一的 Cordis 强调色;mount 行仍使用共用的 JavaScript 展开视图和语法高亮。
-
-「模型可见 ⟺ 已记录」成立,且无需新的会话事件类型:mount 与 unmount 通过已记录的 `tool/call`/`tool/result` 对可见,当步骤之间的 schema 发生变化时,系统发出的完整 request header 会记录工具集的任何变化。临时插件属于进程内存,而非会话状态:恢复持久化会话只会重建对话历史,绝不会重新创建它们。
-
-## 曾考虑的替代方案
-
-**用结构化的逐能力注册工具替代 `cordis_mount`。** 最具吸引力的替代方案是一个带有显式 `name`/`description`/`parameters`/`code` 字段的 `cordis_register_tool`(以及配套工具 `cordis_register_listener`、`cordis_register_service`……),而非单一的「挂载一个插件」原语。否决原因:它唯一的真正优势——对最常见的单一场景免去插件样板代码——不足以抵偿其代价,而单一的 mount 原语能一次性覆盖所有能力。
-
-| 维度 | 结构化逐能力工具 | 单一 `cordis_mount` |
-|---|---|---|
-| schema 正确性 | `parameters` 仍然是模型编写的 JSON,需要统一 schema 校验,只是提前了一步 | 同样的校验在沙箱边界运行,同样的指导性错误信息 |
-| 代码字段 | `execute` 函数体仍然是 vm 中模型编写的 JS;realm 和服务调用的正确性问题不变 | 一个沙箱、一条规范化路径、一处受保护的注册 |
-| 能力覆盖面 | 仅限工具;监听器、服务、`inject` 关系各需另一个结构化工具——API 无限增长 | 一套词汇(cordis 插件)覆盖当前和未来的所有效果 |
-| 跨挂载组合 | 在工具注册载荷中无法表达 | 原生 `provide`/`inject`,普通的 cordis 语义 |
-| 可审视性 | 注册的东西无法在插件列表中显示为插件 | 模型挂载的正是 `cordis_inspect` 渲染的 |
-| 模型易用性 | 对最常见的单一场景有优势(无插件样板) | 通过 mount 描述中的规范示例加边界错误信息教会正确调用来缓解 |
-
-因此正确性投入放在能一次性为所有能力带来回报的地方:通过 `cordis_inspect` 呈现的生成 API 目录,以及沙箱边界校验(其错误信息教会正确的调用方式)。结构化注册工具日后仍可作为语法糖添加,由它合成 mount 代码;本设计不排斥这一可能。
-
-**在工具中手工维护服务/事件参考。** inspect 工具的第一版携带了一份手写的服务方法签名表。它被生成的 `api-catalog.ts` 取代,因为手写表在签名变化的瞬间就会与 JSDoc 脱节且没有门禁约束这种漂移,而生成产物的新鲜度由文档使用的同一套 AST 检查。
-
-**新增 `cordis/mount` 会话事件。**一个持久事件记录每次挂载的源码和名称,有明确先例(`hook/invoked`、`compaction/start`)。不予采纳:挂载和卸载已经作为 `tool/call`/`tool/result` 对可见,工具集变化已经作为完整的变更 request header 被记录,因此专用事件只会重复记录。如果审计用例需要在工具调用之外取得挂载的源码和名称,日后仍可添加。
-
-**加固的/能力受限的沙箱。** 对 Node 内置模块设陷阱并向挂载代码提供白名单门面而非原始上下文,可能暗示意图是为安全而沙箱化。这里明确不是:陷阱和门面收窄的是挂载代码所见的 *API*——将其引导至 cordis 服务、远离易泄漏的 Node 内置模块和框架内部——目的是正确性和封堵未受保护的上下文逃逸,但门面暴露的能力(`ctx.shell`、`ctx.fs`、`ctx.web`)触及真实运行时,因此它不是安全边界。真正的安全边界(独立进程、权限提示)超出了一个开发/显式启用工具集的范围,且会与其核心目的——将活跃运行时交给模型——相冲突。
-
-## 后果
-
-该工具集是刻意的显式启用设计,具有完整权限的 `ctx`,因此部署方采用它的意识程度应与 bash 工具相当。以下几个事实由工具描述直接告知模型:一个 waterfall(瀑布式事件)监听器(如 `tools/pre-execute`)如果不调用 `next()` 就返回,会短路整条链,因此一个挂载的监听器可以阻止 agent 自身的工具分发([waterfall 语义](../../../../docs/cordis-primer.zh.md#cordis-waterfall-semantics));挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会导致死锁;`vmTimeoutMs` 仅约束同步执行;挂载不会在会话恢复后存活。
+检查能力不依赖 Creator 或 Plugin Manager。Runner 生命周期测试覆盖受保护注册、启动失败清理和等待释放。现有浏览器消费者保留生命周期 API;移除其写入注册表与激活机制,需要协调替换这些消费者,不能删除历史渲染或假定会话数据会重建运行状态。

+ 2 - 2
.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.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/testing/2026-07-30-web-browser-snapshot-ci-gate.md
-2026-07-30-web-browser-snapshot-ci-gate.md: 719ac059d1208601e3fdd7ef10ae48464bd92877
-2026-07-30-web-browser-snapshot-ci-gate.zh.md: 57e23491e2123f9e14f478b98331dac2726ef2a2
+2026-07-30-web-browser-snapshot-ci-gate.md: d1c8dcdd859d0515804cc6aed0d2f3341beb6dd0
+2026-07-30-web-browser-snapshot-ci-gate.zh.md: 9d642819e09abc63384f4f4eb105d13261a9eb95

+ 3 - 3
.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md

@@ -16,7 +16,7 @@ The consumer job owns the [single Linux build](../../archived/process/2026-07-30
 
 Local `pnpm run test:web` continues to build first and then run the full browser suite serially; `test:web:built` is the serial entry point for existing build artifacts. Developers explicitly run `DSH_SNAPSHOT=refresh pnpm run test:web` only after confirming that user-visible output changed intentionally, review every expected-output diff, and then verify again in replay mode that no files are written.
 
-CI's `scripts/run-web-snapshots.ts` first runs `hmr-live.e2e.ts` and `cordis-tool-round.e2e.ts` as separate serial Vitest invocations. The HMR scenario mutates built workspace state, while the Cordis scenario owns a lifecycle-sensitive approval and steering sequence whose turn grouping is made deterministic by waiting for the initial turn to settle before approval. After both pass, one six-worker Vitest pool runs every remaining file. Every child inherits stdio, and the enclosing gate streams that output through `run-gates`.
+CI’s `scripts/run-web-snapshots.ts` runs `hmr-live.e2e.ts` separately because it mutates built workspace state. One six-worker Vitest pool then runs every remaining file, including historical Cordis-card rendering. Every child inherits stdio, and `run-gates` streams its output.
 
 For pull requests, the gate runs only in the Linux consumer job: these scenarios target POSIX, and the other PR jobs do not provision Chromium. The self-hosted default-branch Linux serial standby also includes the comparison, while the macOS and Windows serial jobs remain browser-free (there is no hosted Linux serial aggregate). A PR's `all checks passed` verdict already depends on the consumer job, so a browser compare failure blocks the merge without requiring a new branch-protection check name.
 
@@ -30,10 +30,10 @@ Completed local replays measured the six-worker browser command at about 65–71
 
 **Create a standalone browser job and rebuild the entire repository.** Rejected: it would duplicate dependency installation and the publishable build. The existing Linux consumer job already owns that build and is part of the unified required verdict.
 
-**Run HMR and Cordis inside the parallel pool.** Rejected because HMR mutates shared built state and the Cordis approval continuation requires a serial preflight. Every other file shares one bounded pool; dedicated long-file processes add scheduling code and leave part of a reduced worker budget idle after those files complete.
+**Run HMR inside the parallel pool.** Rejected because it mutates shared built state. Other browser fixtures own isolated state and share one bounded pool.
 
 **Replace real Chromium with jsdom snapshots.** Rejected: jsdom does not cover the browser, HTTP/SSE carriage, or the composition of real client plugin bundles. It remains useful for fast lower-layer feedback, but cannot replace the assembled browser chain.
 
 ## Consequences
 
-Before merge, every PR proves that the current web assembly matches all committed browser expected outputs; a missing refresh fails in the same PR that changes the assembly. The cost is Chromium provisioning, two serial scenarios, and one bounded six-worker pool in the consumer job; the consumer-owned build and browser cache avoid duplicate builds and downloads on reruns. Parallel-file failures stream immediately, but a worker-budget change still requires a completed end-to-end measurement rather than an elapsed-time guess. The gate makes no claim of cross-platform browser consistency, and if a Playwright/Chromium upgrade changes the ARIA format, the upgrade PR must explicitly refresh the expected outputs and review the churn.
+Before merge, every PR proves that the current web assembly matches all committed browser expected outputs; a missing refresh fails in the same PR that changes the assembly. The cost is Chromium provisioning, one serial scenario, and one bounded six-worker pool in the consumer job; the consumer-owned build and browser cache avoid duplicate builds and downloads on reruns. Parallel-file failures stream immediately, but a worker-budget change still requires a completed end-to-end measurement rather than an elapsed-time guess. The gate makes no claim of cross-platform browser consistency, and if a Playwright/Chromium upgrade changes the ARIA format, the upgrade PR must explicitly refresh the expected outputs and review the churn.

+ 2 - 2
.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md

@@ -16,7 +16,7 @@ Linux PR 的 `node 24 / snapshots and artifacts` 必须运行完整 Web 浏览
 
 本地 `pnpm run test:web` 仍先构建,再串行运行完整浏览器套件;`test:web:built` 是已有构建产物的串行执行入口。开发者只在确认用户可见输出有意变化后显式运行 `DSH_SNAPSHOT=refresh pnpm run test:web`,评审每一处预期输出 diff,再以 replay 模式复验不再写文件。
 
-CI 的 `scripts/run-web-snapshots.ts` 先用相互独立的 Vitest 调用串行运行 `hmr-live.e2e.ts` 与 `cordis-tool-round.e2e.ts`。HMR 场景会修改已构建工作区状态;Cordis 场景则拥有一条对生命周期时序敏感的批准与 steering(中途引导)序列,它通过在批准前等待初始轮次结束来确定轮次分组。两者通过后,其余全部文件进入同一个 6-worker Vitest 池。所有子进程都继承 stdio,外围门禁再通过 `run-gates` 流式传递输出。
+CI 的 `scripts/run-web-snapshots.ts` 单独运行会修改构建产物的 `hmr-live.e2e.ts`。随后由一个六 worker Vitest 池运行其余文件,包括历史 Cordis 卡片渲染。所有子进程继承 stdio,由 `run-gates` 流式转发输出。
 
 对 PR 而言,门禁仅在 Linux 消费方 job 中运行:这些场景面向 POSIX,其他 PR job 不安装 Chromium。自托管的默认分支 Linux 串行热备也包含该比较,而 macOS 和 Windows 串行 job 仍不使用浏览器(不存在托管的 Linux 串行聚合)。PR 的 `all checks passed` 已依赖消费方 job,因此浏览器比较失败会阻止合并,无需新增 branch-protection check 名称。
 
@@ -30,7 +30,7 @@ CI 的 `scripts/run-web-snapshots.ts` 先用相互独立的 Vitest 调用串行
 
 **新建独立 browser job 并重新构建全仓。** 已否决:它会重复依赖安装和发布构建。现有 Linux 消费方 job 已负责该构建,并已被统一的 required verdict 聚合。
 
-**把 HMR 与 Cordis 也放进并行池。** 不予采用,因为 HMR 会修改共享的已构建状态,Cordis 批准 continuation 则需要串行预检。其余全部文件共用一个有界池;专用长文件进程会增加调度代码,并在这些文件结束后让缩减后的部分 worker 预算闲置。
+**在并行池中运行 HMR。** 拒绝,因为它会修改共享构建产物。其他浏览器 fixture 拥有隔离状态,共用一个有界池。
 
 **用 jsdom 快照代替真实 Chromium。** 已否决:jsdom 不覆盖浏览器、HTTP/SSE 承载及真实客户端插件包的组合;它仍可用于快速的下层反馈,但不能替代组装后的浏览器链路。
 

+ 0 - 19
apps/cli/config/examples/cordis/cordis.yml

@@ -1,19 +0,0 @@
-# Opt-in Web composition for inspecting the self-referential Cordis tools.
-# Temporary Plugin code can reach every injected live capability; treat this
-# deployment like shell access, not as a security boundary.
-# This file is a PATCH OVERLAY over the web profile (dsh-base + dsh-web-app
-# bundle layers), not a tree: `dsh web --patch` applies it as one more sibling
-# patch list at the same include level, so these patches reach every bundle
-# row. A patch replaces the targeted row's whole `config`.
-
-# Pinning the port here keeps this demo off the default 3080.
-- id: webserver
-  config:
-    host: 127.0.0.1
-    port: 3081
-
-- insert:
-    - id: cordis-host-runner
-      name: '@deepseek-ai/dsh-cordis-host-runner'
-    - id: tool-cordis
-      name: '@deepseek-ai/dsh-tool-cordis'

+ 3 - 0
apps/cli/src/profile-boot.ts

@@ -235,6 +235,8 @@ export interface RunProfileOptions {
   patchFiles: readonly string[]
   /** The invocation's inner arguments, handed to the tree through `ctx.cmdlineArgs`. */
   args: readonly string[]
+  /** Application-owned package runtime, scoped to plugin package operations. */
+  packageManager?: ProfileContext['packageManager']
   /** Module fallback backend; pkg executables always use runtime resolution. */
   resolutionMode?: ProfileResolutionMode
 }
@@ -293,6 +295,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
     const rootConfig = join(composed.profile.dir, PROFILE_ROOT_FILENAME)
     const profileContext: ProfileContext = {
       name: options.profile,
+      ...(options.packageManager === undefined ? {} : { packageManager: options.packageManager }),
       dir: composed.profile.dir, patchPath: composed.profile.patchPath,
       installAnchor: options.resolvedProfile?.installAnchor ?? INSTALL_ANCHOR,
       startedBundles: composed.profile.layers.map(layer => layer.packageName),

+ 14 - 6
apps/cli/tests/desktop-host.e2e.ts

@@ -3,7 +3,7 @@
 import { fork } from 'node:child_process'
 import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
-import { dirname, join } from 'node:path'
+import { delimiter, dirname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { finished } from 'node:stream/promises'
 import { expect, it, onTestFinished } from 'vitest'
@@ -33,8 +33,8 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
   writeFileSync(join(modules, 'dsh', 'package.json'), '{"type":"module","exports":{"./profile-boot":"./profile-boot.js"}}')
   writeFileSync(join(modules, 'dsh', 'profile-boot.js'), `
     import { writeFileSync } from 'node:fs';
-    export function runProfile() {
-      process.send({ type: 'booting' });
+    export function runProfile(options) {
+      process.send({ type: 'booting', packageManager: options.packageManager });
       return new Promise((resolve, reject) => process.once('disconnect', () => {
         if (${String(fail)}) { reject(new Error('fixture boot failure')); return; }
         resolve({ ctx: { plugin: async () => {}, connection: { authenticatedUrl: value => value }, webServer: { port: 19387 } },
@@ -44,7 +44,9 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
   `)
   const entry = join(root, 'index.js')
   copyFileSync(join(hostDirectory, 'lib', 'index.js'), entry)
-  const child = fork(entry, [root, root], { execArgv: [], stdio: ['ignore', 'ignore', 'pipe', 'ipc'] })
+  const pnpm = join(root, 'bundled-pnpm.mjs')
+  const nodeBin = join(root, 'bin')
+  const child = fork(entry, [root, root, root, 'runtime', pnpm, nodeBin], { execArgv: [], stdio: ['ignore', 'ignore', 'pipe', 'ipc'] })
   let stderr = ''
   child.stderr!.setEncoding('utf8').on('data', (chunk: string) => { stderr += chunk })
   const exited = new Promise<number | null>(resolve => child.once('exit', resolve))
@@ -55,11 +57,17 @@ it.each([false, true])('settles startup after parent IPC disconnect (boot failur
     rmSync(root, { recursive: true, force: true })
   })
   try {
-    await new Promise<void>((resolve, reject) => {
-      child.once('message', () => { resolve() })
+    const boot = await new Promise<{
+      packageManager: { command: string; args: string[]; env: Record<string, string> }
+    }>((resolve, reject) => {
+      child.once('message', resolve)
       child.once('error', reject)
       child.once('exit', (code) => { reject(new Error(`Host exited before booting: ${String(code)} ${stderr}`)) })
     })
+    expect(boot.packageManager.command).toBe(process.execPath)
+    expect(boot.packageManager.args).toEqual(['--expose-internals', pnpm])
+    expect(boot.packageManager.env.ELECTRON_RUN_AS_NODE).toBe('1')
+    expect(boot.packageManager.env.PATH).toBe(`${nodeBin}${delimiter}${process.env.PATH ?? ''}`)
     child.disconnect()
     expect(await exited).toBe(fail ? 1 : 0)
     await drained

+ 7 - 62
apps/cli/tests/profiles/headless/tests/ptc.e2e.ts

@@ -277,7 +277,7 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
     expect(ctx.jobs.list()).toEqual([])
   }, 15_000)
 
-  it('uses versioned Cordis DTO ids directly for running and pending Plugins, then confirms removal', async () => {
+  it('uses runtime inspection results directly through PTC', async () => {
     ctx = await typedPtcModeHarness()
     await ctx.plugin(CordisHostRunner)
     await ctx.plugin(ToolCordis)
@@ -287,70 +287,15 @@ describe('PTC mode typed values: keyless real-process contracts', () => {
     } as unknown as Agent
 
     const value = completion(await runCode(ctx, `
-      const activeDefinition = await tools.cordis_define({
-        plugin: { kind: 'new', idPrefix: 'active' },
-        name: 'active-ptc-plugin',
-        purpose: 'prove an active Host half',
-        code: { host: "return { name: 'active-ptc-plugin', apply(ctx) {} }" },
+      const listed = await tools.cordis_inspect_list({});
+      const provider = listed.providers.find(item => item.id === 'Tool');
+      const inspected = await tools.cordis_inspect_query({
+        platform: provider.platform, provider: provider.id, method: provider.methods[0].name,
       });
-      const active = await tools.cordis_run({
-        pluginId: activeDefinition.pluginId,
-        packageId: activeDefinition.packageId,
-        mode: 'run',
-      });
-      const pendingDefinition = await tools.cordis_define({
-        plugin: { kind: 'new', idPrefix: 'queue' },
-        name: 'pending-ptc-plugin',
-        purpose: 'prove a Host half waiting for a Service',
-        code: { host: "return { name: 'pending-ptc-plugin', inject: ['missing-ptc-service'], apply(ctx) {} }" },
-      });
-      const pending = await tools.cordis_run({
-        pluginId: pendingDefinition.pluginId,
-        packageId: pendingDefinition.packageId,
-        mode: 'run',
-      });
-      const before = await tools.cordis_inspect_self({});
-      const removed = await tools.cordis_undefine({ pluginId: active.pluginId });
-      const after = await tools.cordis_inspect_self({});
-      await tools.cordis_undefine({ pluginId: pending.pluginId });
-      return {
-        active: {
-          pluginId: active.pluginId,
-          packageId: active.packageId,
-          pluginRunId: active.pluginRunId,
-          status: active.host.status,
-        },
-        pending: {
-          pluginId: pending.pluginId,
-          packageId: pending.packageId,
-          pluginRunId: pending.pluginRunId,
-          status: pending.host.status,
-          waitingFor: pending.host.waitingFor,
-        },
-        removed,
-        beforeContainsId: before.plugins.some(plugin => plugin.pluginId === active.pluginId),
-        afterContainsId: after.plugins.some(plugin => plugin.pluginId === active.pluginId),
-      };
+      return { provider: provider.id, names: inspected.data.tools.map(tool => tool.name) };
     `, testToolSignal, agent))
 
-    expect(value).toEqual({
-      active: {
-        pluginId: 'active-1',
-        packageId: 'pkg-1',
-        pluginRunId: 'run-1',
-        status: 'running',
-      },
-      pending: {
-        pluginId: 'queue-2',
-        packageId: 'pkg-2',
-        pluginRunId: 'run-2',
-        status: 'waiting',
-        waitingFor: ['missing-ptc-service'],
-      },
-      removed: { pluginId: 'active-1', wasRunning: true },
-      beforeContainsId: true,
-      afterContainsId: false,
-    })
+    expect(value).toEqual({ provider: 'Tool', names: ['cordis_inspect_list', 'cordis_inspect_query', 'run_code'] })
   })
 })
 

+ 105 - 0
apps/cli/tests/profiles/web/tests/creator-plugin-manager.expected.e2e.ts

@@ -0,0 +1,105 @@
+/** Built Web profile: MCP reaches existing/new Creator sessions and survives a process restart. */
+import { spawn } from 'node:child_process'
+import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { expect, it } from 'vitest'
+import { readProfileManifest } from '@deepseek-ai/dsh-app-boot'
+import { startHttpMcpFixture } from '../../../../../../packages/mcp/mcp-client/tests/http-fixture.ts'
+
+interface Observation {
+  approvals: string[]
+  permission: string
+  before: string[]
+  denied: { isError: boolean; content: unknown }
+  afterDenied: string[]
+  bundlesAfterDenied: { name: string }[]
+  after: string[]
+  other: string[]
+  remaining: string[]
+  result: unknown
+  ping: unknown
+  removed?: unknown
+}
+
+const repo = fileURLToPath(new URL('../../../../../../', import.meta.url))
+
+it('configures MCP on a live profile, restores it on restart, and removes its tools', async (test) => {
+  const root = await mkdtemp(join(tmpdir(), 'creator-manager-'))
+  test.onTestFinished(() => rm(root, { recursive: true, force: true }))
+  const mcp = await startHttpMcpFixture()
+  test.onTestFinished(mcp.close)
+  await mkdir(join(root, 'workspace'))
+  const bundle = join(root, 'demo-mcp')
+  await mkdir(bundle)
+  await writeFile(join(bundle, 'package.json'), JSON.stringify({ name: '@test/creator-mcp', version: '1.0.0',
+    dsh: { bundle: { patch: './cordis.patch.yml' } } }))
+  await writeFile(join(bundle, 'cordis.patch.yml'), JSON.stringify([{ insert: [{ id: 'demo',
+    name: '@deepseek-ai/dsh-mcp-client', config: { serverName: 'demo', transport: 'streamable-http',
+      url: mcp.url, failOnStartupError: true },
+  }] }]))
+  const patch = join(root, 'test.patch.yml')
+  await writeFile(patch, JSON.stringify([{ insert: [{ id: 'creator-manager-observer',
+    name: new URL('./fixtures/creator-plugin-manager.mjs', import.meta.url).href, config: { bundle },
+  }] }]))
+  const start = async () => {
+    const child = spawn(process.execPath, [join(repo, 'apps/cli/lib/bin.js'), '--profile', 'web', '--patch', patch,
+      '--port', '0', '--no-open'], { cwd: join(root, 'workspace'),
+      env: { ...process.env, DSH_HOME: join(root, 'home'), DSH_AGENTS_HOME: join(root, 'agents'),
+        DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'keyless-no-model-calls' },
+      stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
+    })
+    let output = ''
+    const completion = new Promise<void>((resolve, reject) => {
+      child.once('close', () => { resolve() })
+      child.once('error', reject)
+    })
+    const stop = async () => { if (child.exitCode === null) child.kill('SIGTERM'); await completion }
+    test.onTestFinished(stop)
+    for (const stream of [child.stdout, child.stderr]) stream!.on('data', (data) => { output = (output + String(data)).slice(-30_000) })
+    await expect.poll(() => {
+      if (child.exitCode !== null) throw new Error(output)
+      return output.includes('dsh web: http://')
+    }, { timeout: 60_000 }).toBe(true)
+    return { stop, request: (phase: string): Promise<Observation> => new Promise((resolve, reject) => {
+      child.once('message', (value: { result: Observation; error?: string }) => {
+        if (value.error !== undefined) reject(new Error(value.error))
+        else resolve(value.result)
+      })
+      child.send(phase)
+    }) }
+  }
+  const first = await start()
+  const initial = await first.request('initial')
+  expect(initial.before).toContain('plugin_manager')
+  for (const retired of ['cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'cordis_inspect_self']) {
+    expect(initial.before).not.toContain(retired)
+  }
+  expect(initial.before).not.toContain('mcp__demo__ping')
+  expect(initial.denied.isError).toBe(true)
+  expect(JSON.stringify(initial.denied.content)).toContain('the user rejected escalating')
+  expect(initial.afterDenied).toEqual(initial.before)
+  expect(initial.bundlesAfterDenied.map(bundle => bundle.name)).not.toContain('@test/creator-mcp')
+  expect(initial.approvals).toEqual(['rejected', 'allowed-once'])
+  expect(initial.permission).toBe('workspace-write')
+  expect(initial.result).toMatchObject({ application: 'applied', changed: true })
+  expect(initial.after).toContain('mcp__demo__ping')
+  expect(initial.other).toContain('mcp__demo__ping')
+  expect(JSON.stringify(initial.ping)).toContain('pong')
+  const saved = readProfileManifest('dsh', join(root, 'home/profiles/web'))
+  expect(saved.dsh?.profile?.bundles).toContain('@test/creator-mcp')
+  expect(saved.dependencies).toHaveProperty('@test/creator-mcp')
+  await first.stop()
+  const second = await start()
+  const restarted = await second.request('restart')
+  expect(restarted.result).toEqual(expect.arrayContaining([expect.objectContaining({ name: '@test/creator-mcp' })]))
+  expect(restarted.approvals).toEqual(['rejected', 'allowed-once'])
+  expect(restarted.permission).toBe('workspace-write')
+  expect(restarted.before).toContain('mcp__demo__ping')
+  expect(JSON.stringify(restarted.ping)).toContain('pong')
+  expect(restarted.removed).toMatchObject({ application: 'applied', changed: true })
+  expect(restarted.remaining).not.toContain('mcp__demo__ping')
+  expect(mcp.calls).toEqual(['ping', 'ping'])
+  await second.stop()
+})

+ 59 - 0
apps/cli/tests/profiles/web/tests/fixtures/creator-plugin-manager.mjs

@@ -0,0 +1,59 @@
+/** Test-only IPC assertions over real Creator presets and profile management. */
+export const inject = ['agents', 'agentPresets', 'tools', 'pluginManager', 'permissionPresets']
+
+export function apply(ctx, config) {
+  const receive = message => {
+    if (message !== 'initial' && message !== 'restart') return
+    void inspect(message).then(result => process.send({ result }), error => process.send({ error: String(error.stack ?? error) }))
+  }
+  ctx.effect(() => {
+    process.on('message', receive)
+    return () => process.off('message', receive)
+  })
+  async function inspect(phase) {
+    const handles = []
+    let activeSession
+    let answer = 'rejected'
+    const approvals = []
+    const disposeApproval = ctx.on('approval/request', (request, next) => {
+      if (request.toolName !== 'plugin_manager') return next()
+      approvals.push(answer)
+      return Promise.resolve(answer)
+    }, { prepend: true })
+    const make = async id => {
+      const handle = await ctx.agents.create({ sessionId: id, cwd: process.cwd(),
+        setup: scope => ctx.agentPresets.mount(scope, 'cordis').then(() => undefined) })
+      handles.push(handle)
+      return handle.agent
+    }
+    const names = agent => ctx.tools.schemas(agent).map(tool => tool.name)
+    try {
+      const first = await make(`${phase}-first`)
+      const before = names(first)
+      const manage = args => ctx.tools.execute({ name: 'plugin_manager', arguments: args, agent: first,
+        callId: `${phase}-manage`, signal: new AbortController().signal })
+      ctx.permissionPresets.set(first.session, 'workspace-write')
+      first.session.append('turn/start', { turn: 1 })
+      activeSession = first.session
+      const denied = await manage({ action: 'install_bundle', target: config.bundle })
+      const afterDenied = names(first)
+      const bundlesAfterDenied = await ctx.pluginManager.listBundles()
+      answer = 'allowed-once'
+      const result = phase === 'initial'
+        ? JSON.parse((await manage({ action: 'install_bundle', target: config.bundle })).value)
+        : await ctx.pluginManager.listBundles()
+      const second = await make(`${phase}-second`)
+      const after = names(first)
+      const other = names(second)
+      const ping = await ctx.tools.execute({ name: 'mcp__demo__ping', arguments: {}, agent: first,
+        callId: `${phase}-ping`, signal: new AbortController().signal })
+      const removed = phase === 'restart'
+        ? JSON.parse((await manage({ action: 'remove_bundle', target: '@test/creator-mcp' })).value) : undefined
+      return { approvals, permission: ctx.permissionPresets.current(first.session), before, denied, afterDenied, bundlesAfterDenied, result, after, other, ping, removed, remaining: names(first) }
+    } finally {
+      disposeApproval()
+      activeSession?.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
+      await Promise.all(handles.map(handle => handle.dispose()))
+    }
+  }
+}

+ 12 - 4
apps/cli/tests/web-agent-presets.e2e.ts

@@ -6,6 +6,7 @@ import { dirname, join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
 import {
   boot,
+  initProfile,
   createProfileResolutionGeneration,
   loadOverlayPatches,
   loadProfile,
@@ -79,6 +80,8 @@ async function bootWeb(
     // moved into the presets that a host row still waits for. The boot audit
     // is that assertion.
     { id: 'webserver', disabled: true },
+    // This composition has no application readiness or file-watching lifecycle.
+    { id: 'hmr', disabled: true },
     // The web bundle's runtime row injects `webServer`, so it cannot
     // activate without the bound port disabled above. It owns dist serving
     // and the URL prompt line — surface glue, not anything that decides an
@@ -122,6 +125,7 @@ async function bootWeb(
   const home = dirname(settingsFile)
   const profileDir = join(home, 'profiles', 'spec')
   await mkdir(profileDir, { recursive: true })
+  if (profileBundles === undefined) initProfile(profileDir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
   // Product Bundles are installed into the Profile, not the dsh app. Model
   // pnpm's package link for only the selected products; their own production
   // dependencies resolve from the linked workspace packages, while shared
@@ -156,6 +160,10 @@ async function bootWeb(
   const rootConfig = join(profileDir, 'cordis.yml')
   await writeFile(rootConfig, '[]\n')
   return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], async (bootCtx) => {
+    bootCtx.provide('profileContext', { name: 'spec', dir: profileDir, patchPath: profile.patchPath,
+      installAnchor: INSTALL_ANCHOR, home, cwd: home,
+      startedBundles: profileBundles ?? ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'],
+      overlays: overrides, telemetryDisabledEnv: '1' })
     await bootCtx.plugin(PluginPackages, { generation: resolution })
     bootCtx.provide('connection', {
       fetch: { register: () => () => {} },
@@ -351,12 +359,12 @@ describe('the shipped Web composition', () => {
     })
     try {
       const tools = toolNames(ctx, handle.agent)
-      // The self-referential toolset is what distinguishes this preset.
       expect(tools).toEqual(expect.arrayContaining([
-        'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self',
-        'cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine',
+        'cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager',
       ]))
-      // And it keeps the standard agent's own tools rather than replacing them.
+      for (const removed of ['cordis_define', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'cordis_inspect_self']) {
+        expect(tools).not.toContain(removed)
+      }
       expect(tools).toEqual(expect.arrayContaining(['bash', 'read', 'edit', 'skill']))
       expect(tools).not.toContain('str_replace_editor')
       expect(ctx.commands.find(handle.agent, 'goal')).toBeDefined()

+ 12 - 1
apps/desktop-host/src/index.ts

@@ -1,6 +1,6 @@
 /** Launch the Desktop profile through the Web application and report its URL to Electron. */
 
-import { join } from 'node:path'
+import { delimiter, join } from 'node:path'
 import { loadLayeredEnv, loadProfileDirectory } from '@deepseek-ai/dsh-app-boot'
 import { runProfile } from '@deepseek-ai/dsh/profile-boot'
 import type {} from '@deepseek-ai/dsh-client-connection'
@@ -20,6 +20,17 @@ async function main(): Promise<void> {
     resolvedProfile: { profile, installAnchor },
     patchFiles: [],
     args: ['--no-open', '--port', '19387'],
+    ...(process.argv[6] === undefined ? {} : {
+      packageManager: {
+        command: process.execPath,
+        args: ['--expose-internals', process.argv[6]],
+        env: {
+          ELECTRON_RUN_AS_NODE: '1',
+          DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
+          PATH: `${process.argv[7] ?? ''}${delimiter}${process.env.PATH ?? ''}`,
+        },
+      },
+    }),
   })
   const stop = async (): Promise<void> => {
     // Startup failure is reported by main; shutdown only owns a tree that booted.

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: d19304ae123aefe57af649c5d6a02c4db867314e
-README.zh.md: 3d5b84c23701c4915fb1983fd6363c1ce2dc3059
+README.md: c16e56f511cee6a875fcf2a5e8871c8e174bd2a2
+README.zh.md: 42d687ad46ff47f6032e2c322d64e04ecd88ac28

+ 2 - 0
apps/desktop/README.md

@@ -6,6 +6,8 @@ The desktop application is an Electron shell around the complete dsh Web applica
 
 Desktop’s local native directory flow opens an Electron folder dialog attached to the application window, restoring, showing, and focusing that window first. Concurrent requests share one dialog; cancellation returns no path and failures remain retryable. Ordinary Web uses the Host chooser. Browse mode lists Host directories. On Linux without zenity or kdialog, automatic selection uses browse instead of the Electron dialog.
 
+Creator and the Web Plugin Manager use Desktop’s bundled pnpm under Electron Node mode without requiring pnpm on PATH. The private Node launcher environment applies only to package operations.
+
 ## Key technical decisions
 
 The original artwork lives in `resources/icon.png` and `resources/icon.svg`; platform adaptations retain the whale and gradients in `resources/icon-windows.*` and `resources/icon-macos.*`. Export each platform SVG as a transparent 1024×1024 PNG. Electron-builder generates the multi-size ICO for the Windows application, installer, and uninstaller ([Windows icon requirements](https://learn.microsoft.com/en-us/windows/apps/design/iconography/app-icon-construction)). The installation pages use matching artwork in both themes; the uninstaller's welcome and finish pages share `installer/assets/uninstaller-sidebar.png`, converted to a 164×314 BMP during preparation.

+ 2 - 0
apps/desktop/README.zh.md

@@ -6,6 +6,8 @@
 
 Desktop 的本地原生目录流程打开绑定应用窗口的 Electron 文件夹对话框,并先恢复、显示和聚焦该窗口。并发请求共用一个对话框;取消不返回路径,失败后可以重试。普通 Web 使用 Host 选择器。浏览模式列出 Host 目录。Linux 缺少 zenity 或 kdialog 时,自动选择使用浏览模式,不使用 Electron 对话框。
 
+Creator 和 Web Plugin Manager 在 Electron Node 模式下使用 Desktop 内置 pnpm,无需 PATH 中存在 pnpm。私有 Node 启动器环境仅应用于包操作。
+
 ## 关键技术决策
 
 设计师原稿位于 `resources/icon.png` 和 `resources/icon.svg`;平台适配保留鲸鱼与渐变,分别位于 `resources/icon-windows.*` 和 `resources/icon-macos.*`。将各平台 SVG 导出为透明的 1024×1024 PNG。electron-builder 为 Windows 应用、安装程序和卸载程序生成多尺寸 ICO([Windows 图标要求](https://learn.microsoft.com/en-us/windows/apps/design/iconography/app-icon-construction))。安装页面在两种主题下使用匹配的图案;卸载程序的欢迎和完成页共用 `installer/assets/uninstaller-sidebar.png`,准备阶段将其转换为 164×314 BMP。

+ 3 - 0
apps/desktop/src/host-process.ts

@@ -74,6 +74,7 @@ export class DesktopHostProcess {
    * @param onFailure - Receives the first unexpected child failure, including after readiness.
    * @param primaryRuntime - Optional bundled dependency payload; when supplied, missing sibling
    *   `office-skills` resources fail Host startup.
+   * @param packageManager - Bundled pnpm entry and Node launcher directory, scoped to package operations.
    * @param profileResolution - Package resolution mode for the application-owned profile.
    */
   constructor(
@@ -85,6 +86,7 @@ export class DesktopHostProcess {
     private readonly onFailure?: (error: Error) => void,
     private readonly primaryRuntime?: string,
     private readonly profileResolution: 'link' | 'runtime' = 'link',
+    private readonly packageManager?: { readonly pnpm: string; readonly nodeBin: string },
   ) {}
 
   /**
@@ -102,6 +104,7 @@ export class DesktopHostProcess {
       this.projectDir,
       this.primaryRuntime ?? join(this.runtimeDir, '..', 'runtime', 'primary-runtime'),
       this.profileResolution,
+      ...this.packageManager === undefined ? [] : [this.packageManager.pnpm, this.packageManager.nodeBin],
     ], {
       cwd: this.projectDir,
       env: desktopNodeEnvironment(this.node, undefined, this.environment),

+ 1 - 1
apps/desktop/src/main.ts

@@ -219,7 +219,7 @@ async function main(): Promise<void> {
       hostInspectPort, process.env, onFailure,
       development ? join(app.getAppPath(), '.desktop-build', 'targets', `${process.platform === 'darwin' ? 'mac' : 'win'}-${process.arch}`, 'runtime', 'primary-runtime')
         : join(process.resourcesPath, 'runtime', 'primary-runtime'),
-      development ? 'link' : 'runtime')
+      development ? 'link' : 'runtime', resources)
     return {
       start: async () => {
         const ready = await host.start()

+ 3 - 3
apps/desktop/tests/host-process.spec.ts

@@ -79,13 +79,13 @@ describe('desktop host process', () => {
 
   it('passes external dependencies and runtime profile resolution to the Host', async () => {
     const runtime = projectWithHost(HTTP_HOST.replace('runtime: process.argv[2]',
-      'primaryRuntime: process.argv[4], profileResolution: process.argv[5], runtime: process.argv[2]'))
+      'pnpm: process.argv[6], nodeBin: process.argv[7], primaryRuntime: process.argv[4], profileResolution: process.argv[5], runtime: process.argv[2]'))
     const primaryRuntime = join(runtime, 'external-primary-runtime')
     const host = new DesktopHostProcess(process.execPath, runtime, runtime, undefined, process.env,
-      undefined, primaryRuntime, 'runtime')
+      undefined, primaryRuntime, 'runtime', { pnpm: join(runtime, 'pnpm.mjs'), nodeBin: join(runtime, 'bin') })
     hosts.push(host)
     const { url } = await host.start()
-    expect(await (await fetch(url)).json()).toMatchObject({ primaryRuntime, profileResolution: 'runtime' })
+    expect(await (await fetch(url)).json()).toMatchObject({ primaryRuntime, profileResolution: 'runtime', pnpm: join(runtime, 'pnpm.mjs'), nodeBin: join(runtime, 'bin') })
   })
 
   it('reports a fatal event after readiness once', async () => {

+ 1 - 0
apps/desktop/tests/main-startup.spec.ts

@@ -64,6 +64,7 @@ const harness = await vi.hoisted(async () => {
       readonly node: string, readonly runtime: string, readonly profile: string,
       readonly inspectPort?: number, readonly environment?: NodeJS.ProcessEnv, readonly onFailure?: (error: Error) => void,
       readonly primaryRuntime?: string, readonly profileResolution?: string,
+      readonly packageManager?: { pnpm: string; nodeBin: string },
     ) { hosts.push(this) }
   }
   const app = Object.assign(new EventEmitter(), {

+ 1 - 0
apps/web/tests/agent-preset-authoring.e2e.ts

@@ -48,6 +48,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
     userRoot = await realpath(await mkdtemp(join(tmpdir(), 'dsh-web-e2e-presets-')))
     scaffold = await launchWebScaffold({
       extraOverlayPath: OVERLAY,
+      profile: { packages: [] },
       agentPresets: {
         // The shipped root is the plugin's own, prepended before this.
         roots: [{ path: userRoot, trust: 'user' }],

+ 31 - 187
apps/web/tests/cordis-tool-round.e2e.ts

@@ -1,99 +1,36 @@
-// Web e2e scenario for the opt-in Cordis tools. Record mode drives a real
-// model through inspect, define, run, and stop; replay pins the same shipped Web
-// composition, durable calls, Cordis-owned rows, the define card's own source view,
-// and conversation accessibility tree.
-//
-// The approval is never in the fixture. The fixture pins what the MODEL said;
-// tools execute for real, and this test answers the approval before starting the
-// stop turn. The package therefore carries a browser half whose only
-// job is to be visible (`[data-snapshot-probe]`): its absence before the answer
-// and presence after it is the v3 user gate, proven rather than described.
+/** Historical generated-plugin cards remain readable after their tool APIs are removed. */
 import { readFile } from 'node:fs/promises'
 import { fileURLToPath } from 'node:url'
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
-import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
-import type { SessionEvent } from '@deepseek-ai/dsh-session'
+import { SessionId } from '@deepseek-ai/dsh-session/types'
+import { afterAll, beforeAll, describe, expect, it } from 'vitest'
 import {
-  captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
-  launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
+  captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, readPersistedEvents, parseSeedFixture, realizeSeedFixture,
+  watchConsole, webSnapshotMode, type WebScaffold,
 } from './scaffold.ts'
-import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, saveFailureShot } from './support.ts'
+import { expandOwningTurnProcess, newEnglishPage } from './support.ts'
 
 const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/cordis-tool-round/session.v3.jsonl', import.meta.url))
-const UI_EXPECTED = fileURLToPath(new URL('../../../snapshots/web/cordis-tool-round/ui.expected.md', import.meta.url))
+const UI_EXPECTED = fileURLToPath(new URL('./expected/cordis-history/ui.expected.md', import.meta.url))
 const MODE = webSnapshotMode()
-const CORDIS_TOOLS = ['cordis_inspect_self', 'cordis_define', 'cordis_run', 'cordis_stop'] as const
-const PACKAGE_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
-// The browser half is the PROBE this scenario turns on: it renders a marker into
-// the frame-wide overlay, so "did the plugin actually run in this page" becomes a
-// DOM fact. A host-only package would sidestep the approval round trip entirely
-// (the host runs those immediately), which would drop the v3 user gate out of
-// coverage — the one thing this scenario exists to prove.
-const CLIENT_CODE = 'return { inject: ["slots"], apply(ctx) { ctx.slots.register('
-  + '{ name: "shell.overlay", id: "snapshot-probe" }, '
-  + '() => React.createElement("div", { "data-snapshot-probe": "loaded" })) } }'
-const PROMPT = 'Use only Cordis tools. First call cordis_inspect_self with no arguments. '
-  + 'Then call cordis_define with plugin kind "new", idPrefix "snap", name "snapshot noop", '
-  + 'purpose "does nothing, for the snapshot", '
-  + `code.host exactly ${JSON.stringify(PACKAGE_CODE)} and code.client exactly ${JSON.stringify(CLIENT_CODE)}. `
-  + 'Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode "run". '
-  + 'After the run request returns, reply exactly CORDIS_UI_READY and stop.'
-const STOP_PROMPT = 'Use only Cordis tools. Call cordis_stop with pluginId "snap-1". '
-  + 'After it succeeds, reply exactly CORDIS_UI_DONE and stop.'
+const SEED_ID = 'cordis-history'
 
-function assertCompleteCordisLifecycle(events: readonly SessionEvent[]): void {
-  const turnEnd = events.findLast(
-    (event): event is Extract<SessionEvent, { type: 'turn/end' }> => event.type === 'turn/end',
-  )
-  const reason = turnEnd?.data.reason
-  expect(reason).toEqual({ kind: 'completed' })
-
-  const calls = events.filter(
-    (event): event is Extract<SessionEvent, { type: 'tool/call' }> => event.type === 'tool/call',
-  )
-  expect(calls.map(event => event.data.name)).toEqual(CORDIS_TOOLS)
-
-  const callIds = new Set(calls.map(event => String(event.data.callId)))
-  const results = events.filter(
-    (event): event is Extract<SessionEvent, { type: 'tool/result' }> =>
-      event.type === 'tool/result' && callIds.has(String(event.data.message.source.callId)),
-  )
-  expect(results).toHaveLength(CORDIS_TOOLS.length)
-  expect(results.every(event => !event.data.message.content[0].isError)).toBe(true)
-}
-
-describe('web e2e: Cordis tools use their owned cards', () => {
+describe.skipIf(MODE === 'record')('web e2e: historical Cordis cards', () => {
   let scaffold: WebScaffold
   let browser: Browser
   let page: Page
   let tripwire: ReturnType<typeof watchConsole>
-  const sessionEvents: SessionEvent[] = []
-  const modelFrames: string[] = []
-  const modelChanges: string[] = []
 
   beforeAll(async () => {
-    scaffold = await launchWebScaffold({
-      cordisTools: true,
-      compareReplaySession: true,
-      ...(MODE === 'record' ? {} : { replayFixture: FIXTURE, paceMs: 15 }),
-    })
-    scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
-    scaffold.ctx.sessionProjections.onChanged((_session, key, value, seq) => {
-      if (key === 'modelSelection') modelChanges.push(`${String(seq)}:${JSON.stringify(value)}`)
-    })
+    scaffold = await launchWebScaffold({})
+    await seedSession(scaffold, await readFile(FIXTURE, 'utf8'), SEED_ID)
     browser = await chromium.launch()
     page = await newEnglishPage(browser)
-    page.on('websocket', (socket) => {
-      socket.on('framereceived', (frame) => {
-        const payload = String(frame.payload)
-        if (payload.includes('modelSelection')) modelFrames.push(payload)
-      })
-    })
     tripwire = watchConsole(page)
     await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
-    await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
-    await connectFreshWorkspace(page, scaffold.workspaceCwd)
+    await page.locator('[role="treeitem"]').first().click()
+    await page.locator('[role="treeitem"]').nth(1).click()
   }, 120_000)
 
   afterAll(async () => {
@@ -101,118 +38,25 @@ describe('web e2e: Cordis tools use their owned cards', () => {
     await scaffold?.close()
   })
 
-  it('drives the recorded Cordis lifecycle to a settled turn (all modes)', async () => {
-    onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-drive'))
-    if (MODE !== 'record') {
-      expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT, STOP_PROMPT])
-    }
-    const input = page.locator('[data-composer-input]').first()
-    await input.waitFor({ timeout: 10_000 })
-    const runTurnSettled = scaffold.whenTurnSettled()
-    await input.fill(PROMPT)
-    await input.press('Enter')
-
-    // The approval is the TEST's action in every mode: the fixture pins what the
-    // model said, and the gate is a real round trip through the real panel.
-    const approve = page.locator('[data-cordis-approve]').first()
-    await approve.waitFor({ timeout: 90_000 })
-    // The one assertion this scenario cannot give up: the model asking to run is
-    // NOT the plugin running. Until a person answers, the browser half has not
-    // been fetched, evaluated, or mounted anywhere on this page.
-    expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
-    const sessionId = await runTurnSettled
-    // Approving from idle makes the run-outcome steer a distinct continuation
-    // turn, matching the recorded replay and keeping turn grouping deterministic.
-    const approvalTurnSettled = scaffold.whenTurnSettled()
-    await approve.click()
-    await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
-    await approvalTurnSettled
-    await expect.poll(() => page.getByText('The Cordis Plugin is running.', { exact: true }).count(), { timeout: 15_000 })
-      .toBeGreaterThanOrEqual(1)
-    await expect.poll(() => input.isEnabled(), { timeout: 15_000 }).toBe(true)
-    const stopTurnSettled = scaffold.whenTurnSettled()
-    await input.fill(STOP_PROMPT)
-    await input.press('Enter')
-    await stopTurnSettled
-    await expect.poll(() => {
-      const stop = sessionEvents.find(
-        (event): event is Extract<SessionEvent, { type: 'tool/call' }> =>
-          event.type === 'tool/call' && event.data.name === 'cordis_stop',
-      )
-      return stop !== undefined && sessionEvents.some(
-        event => event.type === 'tool/result'
-          && String(event.data.message.source.callId) === String(stop.data.callId),
-      )
-    }, { timeout: 15_000 }).toBe(true)
-    if (MODE === 'record') {
-      assertCompleteCordisLifecycle(sessionEvents)
-      await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
-        .toBeGreaterThanOrEqual(1)
-      await recordFixture(scaffold, sessionId, FIXTURE)
-    }
-  }, 200_000)
-
-  it.skipIf(MODE === 'record')('the durable log carries one complete Cordis lifecycle', () => {
-    assertCompleteCordisLifecycle(sessionEvents)
-  })
-
-  it.skipIf(MODE === 'record')('renders localized Cordis lifecycle cards', async () => {
-    onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-rows'))
-    await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
-      .toBeGreaterThanOrEqual(1)
-
-    const inspectRow = page.locator('[data-tool="cordis_inspect_self"]').filter({ hasText: 'Inspect' }).first()
-    await expandOwningTurnProcess(page, inspectRow)
-    await inspectRow.waitFor({ timeout: 10_000 })
-
-    // cordis_define does NOT go through the generic row: ui-cordis registers a
-    // keyed toolview for it, and a keyed hit replaces the generic card. So the
-    // title here is the CARD's ("Cordis Plugin"), and the expanded body is the
-    // card's own two code sections rather than a generic args dump.
-    const defineRow = page.locator('[data-tool="cordis_define"]').filter({ hasText: 'Cordis Plugin' }).first()
-    await expandOwningTurnProcess(page, defineRow)
-    await defineRow.waitFor({ timeout: 10_000 })
-    // The whole summary row is the expand toggle (unified tool-row interaction).
-    await defineRow.locator('[aria-expanded]').first().click()
-    await expect.poll(() => defineRow.textContent(), { timeout: 10_000 }).toContain('data-snapshot-probe')
-    await defineRow.getByRole('tab', { name: 'Host' }).click()
-    await expect.poll(() => defineRow.textContent()).toContain(PACKAGE_CODE)
-
-    const runRow = page.locator('[data-tool="cordis_run"]').filter({ hasText: 'Run Cordis Plugin' }).first()
-    await expandOwningTurnProcess(page, runRow)
-    await runRow.waitFor({ timeout: 10_000 })
-    await expect.poll(() => runRow.textContent()).toContain('snap-')
-
-    const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop Cordis Plugin' }).first()
-    await expandOwningTurnProcess(page, stopRow)
-    await stopRow.waitFor({ timeout: 10_000 })
-    await expect.poll(() => stopRow.textContent()).toContain('snap-')
-    await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok')
-    // Stopping withdraws the browser half from every page, probe included.
-    await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 15_000 }).toBe(0)
-  })
-
-  it.skipIf(MODE === 'record')('matches the conversation aria golden', async () => {
-    onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-aria'))
-    console.log('MODEL_TRACE', { modelChanges, frameCount: modelFrames.length, modelFrames })
-    // Final Assistant text precedes turn/end. Three footers prove every turn
-    // reached the render state covered by the ARIA golden.
-    await expect.poll(
-      () => page.getByRole('button', { name: 'Branch into a new conversation', exact: true }).count(),
-      { timeout: 15_000 },
-    ).toBe(3)
-    await page.locator('[data-conversation-scroll]').evaluate((host) => { host.scrollTop = host.scrollHeight })
-    await expect.poll(
-      async () => page.getByRole('button', { name: 'Back to bottom', exact: true }).count(),
-      { timeout: 10_000 },
-    ).toBe(0)
-    await page.mouse.move(0, 0)
-    const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+  it('renders recorded definition source and lifecycle results without registering retired tools', async () => {
+    const persisted = await readPersistedEvents(scaffold, SessionId(SEED_ID))
+    const expected = parseSeedFixture(realizeSeedFixture(scaffold, await readFile(FIXTURE, 'utf8'), SEED_ID)).events
+    const tools = (events: typeof persisted) => events.filter(event => event.type === 'tool/call' || event.type === 'tool/result')
+      .map(event => ({ type: event.type, data: event.data }))
+    expect(tools(persisted)).toEqual(tools(expected))
+    const names = scaffold.ctx.tools.schemas().map(tool => tool.name)
+    expect(names).not.toEqual(expect.arrayContaining(['cordis_define']))
+    const define = page.locator('[data-tool="cordis_define"]').first()
+    await expandOwningTurnProcess(page, define)
+    await define.locator('[aria-expanded]').first().click()
+    await define.getByRole('tab', { name: 'Host' }).click()
+    await expect.poll(() => define.textContent()).toContain('snapshot-noop')
+    const stop = page.locator('[data-tool="cordis_stop"]').first()
+    await expandOwningTurnProcess(page, stop)
+    await expect.poll(() => stop.getAttribute('data-state')).toBe('ok')
+    const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
+      .split(SEED_ID).join('{{seededId}}')
     await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
-  })
-
-  it.skipIf(MODE === 'record')('stayed clean: no page errors or reconnect churn', () => {
     expect(tripwire.pageErrors).toEqual([])
-    expect(tripwire.warnings).toEqual([])
   })
 })

+ 71 - 2
apps/web/tests/details-session-lifecycle.e2e.ts

@@ -1,5 +1,5 @@
 // Recorded-session Sidebar geometry and per-Session view state through the shipped browser composition.
-import { mkdir, readFile } from 'node:fs/promises'
+import { mkdir, readFile, writeFile } from 'node:fs/promises'
 import { fileURLToPath } from 'node:url'
 import { join } from 'node:path'
 import type { Browser, Locator, Page } from 'playwright'
@@ -15,6 +15,7 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
 const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/details-session-lifecycle', import.meta.url))
 const HANDLES_EXPECTED = join(SNAPSHOT_DIR, 'handles.expected.md')
 const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md')
+const BLANK_EXPECTED = join(SNAPSHOT_DIR, 'blank-session.expected.md')
 const SHOT_DIR = fileURLToPath(new URL('../../../.artifacts/screenshots/0907-sidebar-rules', import.meta.url))
 const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v3.jsonl', import.meta.url))
 const SEED_FIXTURE = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url))
@@ -127,17 +128,20 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
   let browser: Browser
   let page: Page
   let tripwire: ReturnType<typeof watchConsole>
+  const sessionEvents: string[] = []
 
   beforeAll(async () => {
     const fixture = await readFile(FIXTURE, 'utf8')
     expect(fixtureUserPrompts(fixture)).toEqual([PROMPT])
     scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5, compareReplaySession: false })
     await seedSession(scaffold, await readFile(SEED_FIXTURE, 'utf8'), 'details-session-lifecycle-seed')
+    scaffold.ctx.on('session/event', (_session, event) => { sessionEvents.push(event.type) })
     browser = await chromium.launch()
     page = await newEnglishPage(browser)
     tripwire = watchConsole(page)
     await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
     await appFrame(page).waitFor({ timeout: 30_000 })
+    expect(await page.locator('[data-sidebar-right-expand]').count()).toBe(0)
     await connectFreshWorkspace(page, scaffold.workspaceCwd)
   }, 120_000)
 
@@ -154,12 +158,77 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
       await mkdir(SHOT_DIR, { recursive: true })
       await saveFailureShot(page, `screenshots/0907-sidebar-rules/details-session-lifecycle-${MODE}-${process.pid}`)
     })
+    const blankColumn = page.locator('[data-rightbar-col]')
+    const workspace = join(scaffold.workspaceCwd, 'workspace')
+    await writeFile(join(workspace, 'before-chat.md'), '# Before the first message\n\nWorkspace preview is available.\n')
+    await page.locator('[data-sidebar-right-expand]').waitFor({ state: 'visible' })
+    await page.screenshot({ path: join(SHOT_DIR, `blank-collapsed-${MODE}-${process.pid}.png`), fullPage: true })
+    await page.locator('[data-sidebar-right-expand]').click()
+    await blankColumn.locator('[data-sidebar-right-guide-entry="files"]').click()
+    await blankColumn.locator('[data-files-entry="file"]').getByRole('button', { name: 'before-chat.md', exact: true }).click()
+    await blankColumn.getByText('Workspace preview is available.', { exact: true }).waitFor()
+    await page.getByText('Into the Unknown', { exact: false }).waitFor()
+    const blankPanes = await paneSnapshot(page)
+    expect(blankPanes.map(pane => pane.tabs.map(tab => tab.title))).toEqual([['Files', 'before-chat.md']])
+    await page.screenshot({ path: join(SHOT_DIR, `blank-preview-${MODE}-${process.pid}.png`), fullPage: true })
+
+    const blankViewport = page.viewportSize()!
+    try {
+      await blankColumn.locator('[data-sidebar-right-toggle]').click()
+      await page.setViewportSize({ width: 767, height: blankViewport.height })
+      await page.locator('[data-sidebar-right-expand]').click()
+      await expect.poll(() => blankColumn.locator('[data-sidebar-right-panel]').boundingBox())
+        .toEqual({ x: 0, y: 0, width: 767, height: blankViewport.height })
+      await blankColumn.getByText('Workspace preview is available.', { exact: true }).waitFor()
+      await blankColumn.locator('[data-sidebar-right-toggle]').click()
+    } finally {
+      await page.setViewportSize(blankViewport)
+    }
+    await page.locator('[data-sidebar-right-expand]').click()
+    await blankColumn.locator('[data-dockkit-add-tab]').click()
+    await blankColumn.locator('[data-sidebar-right-guide-entry="terminal"]')
+      .getByRole('button', { name: /^New terminal/u }).click()
+    const agent = scaffold.ctx.agents.list().find(agent => agent.session.header.cwd === workspace)
+    if (agent === undefined) throw new Error('Blank Session has no workspace Agent')
+    await expect.poll(() => scaffold.ctx.terminalController.list(agent.id).map(terminal => terminal.state)).toEqual(['running'])
+    await page.locator('.xterm-helper-textarea:visible').click()
+    await page.keyboard.insertText('node -e "require(\'fs\').writeFileSync(\'before-chat-terminal.txt\',\'READY\')"')
+    await page.keyboard.press('Enter')
+    await expect.poll(() => readFile(join(workspace, 'before-chat-terminal.txt'), 'utf8')).toBe('READY')
+    expect(sessionEvents).not.toContain('turn/start')
+    expect(sessionEvents).not.toContain('user/message')
+    await blankColumn.locator('[data-dockkit-tab][aria-selected="true"]').hover()
+    // The tab disappears before the Host finishes process cleanup; close responds after quiescence.
+    const [closed] = await Promise.all([
+      page.waitForResponse('**/api/terminal/close'),
+      blankColumn.locator('[data-dockkit-tab][aria-selected="true"] [data-dockkit-tab-close]').click(),
+    ])
+    expect(await closed.json()).toMatchObject({ result: { ok: true } })
+    expect(scaffold.ctx.terminalController.list(agent.id)).toEqual([])
+    await blankColumn.locator('[data-dockkit-tab]').filter({ hasText: 'before-chat.md' }).click()
+    await compareOrRefreshGolden(BLANK_EXPECTED, [
+      '# Blank Session workspace sidebar', '',
+      '- No selected Session: expand control absent',
+      '- Selected workspace before first message: expand control visible',
+      '- Files: before-chat.md opens as a Markdown preview',
+      '- Narrow viewport: reopened preview fills the viewport',
+      '- Terminal: writes a file in the selected workspace before any user message or turn', '',
+      `\`\`\`json\n${JSON.stringify(await paneSnapshot(page), null, 2)}\n\`\`\``,
+    ].join('\n'), MODE)
+
     const settled = scaffold.whenTurnSettled()
     const input = page.locator('[data-composer-input]').first()
     await input.fill(PROMPT)
     await input.press('Enter')
     await settled
     await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
+    expect(await paneSnapshot(page)).toEqual(blankPanes)
+    await blankColumn.getByText('Workspace preview is available.', { exact: true }).waitFor()
+    for (const title of ['before-chat.md', 'Files']) {
+      const tab = blankColumn.locator('[data-dockkit-tab]').filter({ hasText: title })
+      await tab.hover()
+      await tab.locator('[data-dockkit-tab-close]').click()
+    }
 
     await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
     expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
@@ -356,6 +425,6 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
     await compareOrRefreshGolden(SIDEBAR_EXPECTED, checkpoints.join('\n\n'), MODE)
     expect(tripwire.pageErrors).toEqual([])
     expect(tripwire.warnings).toEqual([])
-    await assertFixtureInventory(SNAPSHOT_DIR, ['handles.expected.md', 'sidebar.expected.md'])
+    await assertFixtureInventory(SNAPSHOT_DIR, ['handles.expected.md', 'sidebar.expected.md', 'blank-session.expected.md'])
   })
 })

+ 1 - 1
apps/web/tests/expected/agent-preset-authoring/created.expected.md

@@ -59,7 +59,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 创造模式"':
-        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
+        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、持久化插件管理和 preset 创作指导。
         - code: cordis
       - 'button "查看: 创造模式"':
         - img

+ 1 - 1
apps/web/tests/expected/agent-preset-authoring/damaged.expected.md

@@ -59,7 +59,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 创造模式"':
-        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
+        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、持久化插件管理和 preset 创作指导。
         - code: cordis
       - 'button "查看: 创造模式"':
         - img

+ 1 - 1
apps/web/tests/expected/agent-preset-authoring/section.expected.md

@@ -59,7 +59,7 @@
         - text: 复制
     - listitem:
       - 'button "设为默认: 创造模式"':
-        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
+        - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、持久化插件管理和 preset 创作指导。
         - code: cordis
       - 'button "查看: 创造模式"':
         - img

+ 1 - 1
apps/web/tests/expected/agent-preset-selection/menu.expected.md

@@ -4,5 +4,5 @@
     - img
   - menuitem "PTC mode Full coding agent without the workflow tool; other tools are exposed through the PTC mode SDK so the model can combine multi-step operations in one TypeScript program."
   - menuitem "Minimal mode Single-tool coding agent with a persistent shell."
-  - menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
+  - menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, persistent plugin management, and preset-authoring guidance."
   - menuitem "Refusing mode Resolves, then refuses to start."

+ 6 - 6
snapshots/web/cordis-tool-round/ui.expected.md → apps/web/tests/expected/cordis-history/ui.expected.md

@@ -1,8 +1,6 @@
 - banner:
   - navigation "Session hierarchy":
     - button "Use only Cordis tools. First" [disabled]
-  - img
-  - text: Standard mode
   - button "More actions":
     - img
   - button "Open right sidebar":
@@ -18,7 +16,7 @@
   - img
   - img
   - text: System prompt
-- text: "Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY and stop. {{clock}}"
+- text: "Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY and stop. 9/1 {{clock}}"
 - button "Copy":
   - img
 - button "3 tool calls" [expanded]:
@@ -76,7 +74,7 @@
 - button "Ran for {{duration}}":
   - img
   - text: Ran for {{duration}}
-- text: {{clock}}
+- text: 9/1 {{clock}}
 - button "Thought for a while":
   - text: Thought for a while
   - img
@@ -92,7 +90,7 @@
 - button "Ran for {{duration}}":
   - img
   - text: Ran for {{duration}}
-- text: {{clock}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}}
+- text: 9/1 {{clock}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. 9/1 {{clock}}
 - button "Copy":
   - img
 - button "1 tool call" [expanded]:
@@ -114,7 +112,9 @@
 - button "Ran for {{duration}}":
   - img
   - text: Ran for {{duration}}
-- text: {{clock}}
+- text: 9/1 {{clock}}
+- button "Back to bottom":
+  - img
 - textbox "Message or run a task, / commands, @ files or sessions"
 - button "Add files or run commands":
   - img

+ 0 - 13
apps/web/tests/scaffold.ts

@@ -382,12 +382,6 @@ export interface LaunchOptions {
    * insertion is needed.
    */
   toolsMode?: 'native' | 'ptc' | 'both'
-  /**
-   * Insert the opt-in model-facing Cordis tool provider into the shipped tree.
-   * Record and replay use the same tool surface, so captured request headers
-   * remain reconstructable without making the tools a product default.
-   */
-  cordisTools?: boolean
   /**
    * Keep the shipped DeepSeek adapter mounted while masking the process
    * environment's DEEPSEEK_API_KEY for this scaffold lifetime. This is the
@@ -662,13 +656,6 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
       // be able to change a golden, whatever roots a scenario asks for.
       : [{ id: 'agent-presets', config: { ...options.agentPresets, includeUserRoot: false } }],
     ...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
-    // The shipped Web bundle already owns both runners and the Cordis UI. This
-    // scenario adds only the model-facing tools that exercise those services.
-    ...options.cordisTools === true
-      ? [{ insert: [
-        { id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' },
-      ] }]
-      : [],
     ...options.deepSeekSearch === undefined
       ? []
       : [{

+ 2 - 2
docs/config-catalog.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 docs/config-catalog.md
-config-catalog.md: 1f30bbb98544660671d0c1e226096efbfe1d866c
-config-catalog.zh.md: 5155dc556c6f5c628d8ccef2883766324222065a
+config-catalog.md: a0cddf0cc0228c5b2906654879dc29abc41c1468
+config-catalog.zh.md: 346dcc9522650132b638ddf6e9215cf7bd57f2ed

+ 1 - 1
docs/config-catalog.md

@@ -3769,7 +3769,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-terminal` ([`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts))
 - `@deepseek-ai/dsh-tool-ask-user` — requires `tools` · `userQuestions` ([`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts))
 - `@deepseek-ai/dsh-tool-call-timeout-policy` — requires `tools` ([`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts))
-- `@deepseek-ai/dsh-tool-cordis` — requires `tools` · `systemPrompt` · `dynamicCordisRunner` · `cordisInspect` ([`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts))
+- `@deepseek-ai/dsh-tool-cordis` — requires `tools` · `systemPrompt` · `cordisInspect` ([`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts))
 - `@deepseek-ai/dsh-tool-subagent-control` — requires `tools` · `subagents` ([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts))
 - `@deepseek-ai/dsh-user-questions` ([`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts))
 - `@deepseek-ai/dsh-webhook` — requires `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry` ([`packages/webhook/webhook/src/index.ts`](../packages/webhook/webhook/src/index.ts))

+ 1 - 1
docs/config-catalog.zh.md

@@ -3771,7 +3771,7 @@ export interface Config {
 - `@deepseek-ai/dsh-terminal`([`packages/terminal/terminal/src/index.ts`](../packages/terminal/terminal/src/index.ts))
 - `@deepseek-ai/dsh-tool-ask-user` — 需要 `tools` · `userInteraction`([`packages/interaction/tool-ask-user/src/index.ts`](../packages/interaction/tool-ask-user/src/index.ts))
 - `@deepseek-ai/dsh-tool-call-timeout-policy` — 需要 `tools`([`packages/guard/timeout-policy/src/index.ts`](../packages/guard/timeout-policy/src/index.ts))
-- `@deepseek-ai/dsh-tool-cordis` — 需要 `tools` · `systemPrompt` · `dynamicCordisRunner` · `cordisInspect`([`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts))
+- `@deepseek-ai/dsh-tool-cordis` — 需要 `tools` · `systemPrompt` · `cordisInspect`([`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts))
 - `@deepseek-ai/dsh-tool-subagent-control` — 需要 `tools` · `subagents`([`packages/subagent/tool-subagent-control/src/index.ts`](../packages/subagent/tool-subagent-control/src/index.ts))
 - `@deepseek-ai/dsh-user-questions`([`packages/interaction/user-questions/src/index.ts`](../packages/interaction/user-questions/src/index.ts))
 - `@deepseek-ai/dsh-webhook` — 需要 `agents` · `agentDefaultModel` · `agentPresets` · `permissionPresets` · `sessionTitle` · `workspaceRegistry`([`packages/webhook/webhook/src/index.ts`](../packages/webhook/webhook/src/index.ts))

+ 2 - 2
docs/event-producer-consumer.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 docs/event-producer-consumer.md
-event-producer-consumer.md: 85b4fceb59447bf9ba27a3f627f50a123e2b4e02
-event-producer-consumer.zh.md: 1605f32ae101b9ff8c536d39a5c8c43a8a18cb7c
+event-producer-consumer.md: 397ad7a77e49ef8da8bc3a43171a494eb42917fe
+event-producer-consumer.zh.md: d5768b0842c0e821173e3d80535988f91454114f

+ 1 - 1
docs/event-producer-consumer.md

@@ -16,7 +16,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
 | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
 | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
 | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
 | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |

+ 1 - 1
docs/event-producer-consumer.zh.md

@@ -18,7 +18,7 @@
 | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
 | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
 | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
 | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
 | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |

+ 2 - 2
docs/module-graph.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 docs/module-graph.md
-module-graph.md: b9269b0966999fe28b9a61087480516983653989
-module-graph.zh.md: 84254d4025fad25db8bf4b45b11ab443d68d4ce5
+module-graph.md: b17729e48e1415cd3eb15d2f84f3cb5df54c4c29
+module-graph.zh.md: bcfa6291e4968d000a47ee55d3713fb23705726f

+ 5 - 5
docs/module-graph.md

@@ -989,9 +989,6 @@ flowchart TD
   pkg_experimental_auto_review --> pkg_tools
   pkg_tool_cordis --> pkg_agent
   pkg_tool_cordis --> pkg_cordis_host_runner
-  pkg_tool_cordis --> pkg_llm
-  pkg_tool_cordis --> pkg_scope
-  pkg_tool_cordis --> pkg_session
   pkg_tool_cordis --> pkg_system_prompt
   pkg_tool_cordis --> pkg_tools
   pkg_host_plugin_inventory --> pkg_agent_presets
@@ -1098,9 +1095,12 @@ flowchart TD
   pkg_plugin_manager --> pkg_brand
   pkg_plugin_manager --> pkg_hmr
   pkg_plugin_manager --> pkg_host_plugin_inventory
+  pkg_plugin_manager --> pkg_sandbox
+  pkg_plugin_manager --> pkg_sandbox_policy
   pkg_plugin_manager --> pkg_subprocess
   pkg_plugin_manager --> pkg_tools
   pkg_plugin_manager --> pkg_typert_protocol
+  pkg_plugin_manager --> pkg_user_approval
   pkg_headless --> pkg_agent
   pkg_headless --> pkg_agent_default_model
   pkg_headless --> pkg_fs
@@ -1542,7 +1542,7 @@ flowchart TD
 | [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
-| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) |
 | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
@@ -1557,7 +1557,7 @@ flowchart TD
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
-| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
 | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |

+ 5 - 5
docs/module-graph.zh.md

@@ -991,9 +991,6 @@ flowchart TD
   pkg_experimental_auto_review --> pkg_tools
   pkg_tool_cordis --> pkg_agent
   pkg_tool_cordis --> pkg_cordis_host_runner
-  pkg_tool_cordis --> pkg_llm
-  pkg_tool_cordis --> pkg_scope
-  pkg_tool_cordis --> pkg_session
   pkg_tool_cordis --> pkg_system_prompt
   pkg_tool_cordis --> pkg_tools
   pkg_host_plugin_inventory --> pkg_agent_presets
@@ -1100,9 +1097,12 @@ flowchart TD
   pkg_plugin_manager --> pkg_brand
   pkg_plugin_manager --> pkg_hmr
   pkg_plugin_manager --> pkg_host_plugin_inventory
+  pkg_plugin_manager --> pkg_sandbox
+  pkg_plugin_manager --> pkg_sandbox_policy
   pkg_plugin_manager --> pkg_subprocess
   pkg_plugin_manager --> pkg_tools
   pkg_plugin_manager --> pkg_typert_protocol
+  pkg_plugin_manager --> pkg_user_approval
   pkg_headless --> pkg_agent
   pkg_headless --> pkg_agent_default_model
   pkg_headless --> pkg_fs
@@ -1544,7 +1544,7 @@ flowchart TD
 | [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
-| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) |
 | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
@@ -1559,7 +1559,7 @@ flowchart TD
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
-| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
 | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |

+ 2 - 2
docs/tool-catalog.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 docs/tool-catalog.md
-tool-catalog.md: c345df5560b3ba288fcad73fc4eeb859eb69b00a
-tool-catalog.zh.md: 8554d83f993b84fae1107d16a5f7d1c5cb2c7b2a
+tool-catalog.md: eae53047f91a2a86fd9d5e68ff2f84d8cf62faa6
+tool-catalog.zh.md: 15dda3b508e78dc3eeaddabed05b60f318886064

+ 6 - 187
docs/tool-catalog.md

@@ -15,7 +15,7 @@ This table connects model-visible tool names to the plugin package and service s
 
 | Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note |
 | --- | --- | --- | --- | --- | --- |
-| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager` | `tool/call`, `tool/result`, `user/message` | - | - |
+| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager`, `ctx.sandboxPolicy` | `tool/call`, `tool/result`, `user/message` | - | - |
 | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
@@ -24,7 +24,7 @@ This table connects model-visible tool names to the plugin package and service s
 | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.jobs` runtime and is collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. |
 | `@deepseek-ai/dsh-tool-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. |
 | `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. |
-| `@deepseek-ai/dsh-tool-cordis` | `cordis_define`, `cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_run`, `cordis_stop`, `cordis_undefine` | `ctx.tools`, `ctx.dynamicCordisRunner` | `tool/call`, `tool/result`, `process-local dynamic package lifecycle` | - | Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. |
+| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect_list`, `cordis_inspect_query` | `ctx.tools`, `ctx.cordisInspect` | `tool/call`, `tool/result` | - | Creator mode provides two read-only runtime inspection tools. The Cordis host runner supplies the inspection registry; Client queries require a connected page. Author persistent changes as bundles and install them with plugin_manager. |
 | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. |
 | `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent pwsh tool, the Windows counterpart of the persistent bash tool; deployment composition supplies a pwsh-dialect PTY backend and may override the model-facing environment description. |
 | `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`, `ctx.fs` | `tool/call`, `fs/observed after view presence/absence, edit absence, or successful mutation`, `tool/result` | - | Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. |
@@ -51,7 +51,7 @@ This table connects model-visible tool names to the plugin package and service s
 
 ### `plugin_manager`
 
-List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.
+List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.
 
 ```json
 {
@@ -712,91 +712,9 @@ The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for W
 
 ## `@deepseek-ai/dsh-tool-cordis`
 
-### `cordis_define`
-
-Define an immutable Cordis Package. For a new Plugin, use kind:"new" and provide only a semantic prefix of 3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing Plugin, use kind:"existing" with its exact pluginId to append a Package without overwriting older versions. Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the returned IDs.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "plugin": {
-      "oneOf": [
-        {
-          "type": "object",
-          "additionalProperties": false,
-          "properties": {
-            "kind": {
-              "type": "string",
-              "const": "new"
-            },
-            "idPrefix": {
-              "type": "string",
-              "description": "Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."
-            }
-          },
-          "required": [
-            "kind",
-            "idPrefix"
-          ]
-        },
-        {
-          "type": "object",
-          "additionalProperties": false,
-          "properties": {
-            "kind": {
-              "type": "string",
-              "const": "existing"
-            },
-            "pluginId": {
-              "type": "string",
-              "description": "Exact ID of an existing Plugin; the new Package is appended to that instance."
-            }
-          },
-          "required": [
-            "kind",
-            "pluginId"
-          ]
-        }
-      ]
-    },
-    "name": {
-      "type": "string",
-      "description": "Short, readable Package name."
-    },
-    "purpose": {
-      "type": "string",
-      "description": "One-sentence, user-facing description of the Package purpose."
-    },
-    "code": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "host": {
-          "type": "string",
-          "description": "Plain JavaScript function body that returns the Host-half Cordis Plugin."
-        },
-        "client": {
-          "type": "string",
-          "description": "Plain JavaScript function body that returns the browser Client-half Cordis Plugin."
-        }
-      }
-    }
-  },
-  "required": [
-    "plugin",
-    "name",
-    "purpose",
-    "code"
-  ]
-}
-```
-
-Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
 ### `cordis_inspect_list`
 
-List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.
+List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before writing or configuring a plugin, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call.
 
 ```json
 {
@@ -809,7 +727,7 @@ Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/
 
 ### `cordis_inspect_query`
 
-Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before cordis_define to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.
+Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate the compact signature directory, then query the exact service or event for its structured contract and referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the exact root for its complete registration contract and props.
 
 ```json
 {
@@ -845,106 +763,7 @@ Run a read-only query explicitly declared by an Inspect Provider. platform, prov
 
 Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
 
-### `cordis_inspect_self`
-
-Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package summary. Only pluginId plus packageId returns that immutable Package's Host/Client source and runtime diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code nor changes version pointers.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."
-    },
-    "packageId": {
-      "type": "string",
-      "description": "Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."
-    }
-  }
-}
-```
-
-Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_run`
-
-Activate one exact Package of a dynamic Plugin. Use mode:"run" for the first activation, restarting currentPackageId, or rollback. When current exists, use mode:"update" to switch to a different Package, even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or technical failure is reported through state and steering. After a technical failure, read diagnostics with cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after the user rejects it.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable Plugin ID returned by cordis_define."
-    },
-    "packageId": {
-      "type": "string",
-      "description": "Exact immutable Package ID to activate under that Plugin."
-    },
-    "mode": {
-      "type": "string",
-      "description": "Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.",
-      "enum": [
-        "run",
-        "update"
-      ]
-    }
-  },
-  "required": [
-    "pluginId",
-    "packageId",
-    "mode"
-  ]
-}
-```
-
-Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_stop`
-
-Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects temporarily; use cordis_undefine for permanent removal.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable dynamic Plugin ID to stop."
-    }
-  },
-  "required": [
-    "pluginId"
-  ]
-}
-```
-
-Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_undefine`
-
-Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, first stop it and cancel the request, then delete every Package, grant, and version pointer. After this returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards retain only a "Plugin removed" record. Do not call this Tool when versions must remain available for restart or rollback; use cordis_stop instead.
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable dynamic Plugin ID to remove permanently."
-    }
-  },
-  "required": [
-    "pluginId"
-  ]
-}
-```
-
-Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes.
+Creator mode provides two read-only runtime inspection tools. The Cordis host runner supplies the inspection registry; Client queries require a connected page. Author persistent changes as bundles and install them with plugin_manager.
 
 <a id="deepseek-aidsh-tool-bash-persistent"></a>
 

+ 8 - 189
docs/tool-catalog.zh.md

@@ -19,7 +19,7 @@
 
 | 工具包 | 模型可见名称 | 依赖 | 写入/影响 | 随产品发布的别名 | 部署说明 |
 | --- | --- | --- | --- | --- | --- |
-| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager` | `tool/call`, `tool/result`, `user/message` | - | - |
+| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager`, `ctx.sandboxPolicy` | `tool/call`, `tool/result`, `user/message` | - | - |
 | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after a UI/provider answers the question` | - | ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 |
@@ -28,7 +28,7 @@
 | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;禁用 `enableRunInBackground` 配置(默认为 true)后,该参数会被完全移除。 |
 | `@deepseek-ai/dsh-tool-present` | `present` | `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 |
 | `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 |
-| `@deepseek-ai/dsh-tool-cordis` | `cordis_define`、`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_run`、`cordis_stop`、`cordis_undefine` | `ctx.tools`、`ctx.dynamicCordisRunner` | `tool/call`、`tool/result`、`process-local dynamic package lifecycle` | - | 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 |
+| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect_list`, `cordis_inspect_query` | `ctx.tools`, `ctx.cordisInspect` | `tool/call`, `tool/result` | - | 创造模式提供两个只读运行时检查工具。Cordis host runner 提供检查注册表;Client 查询需要已连接页面。持久化变更编写为组合包,再通过 plugin_manager 安装。 |
 | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 |
 | `@deepseek-ai/dsh-tool-pwsh-persistent` | `pwsh` | `ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 pwsh 工具,持久 bash 工具的 Windows 对应物;部署组合提供 pwsh 方言的 PTY 后端,并可覆盖面向模型的环境描述。 |
 | `@deepseek-ai/dsh-tool-str-replace-editor` | `str_replace_editor` | `ctx.tools`、`ctx.fs` | `tool/call`、`fs/observed after view presence/absence, edit absence, or successful mutation`、`tool/result` | - | 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 |
@@ -55,7 +55,7 @@
 
 ### `plugin_manager`
 
-列出当前 profile 的插件或组合包、启用或禁用它们、安装组合包或删除已安装的组合包。改动影响该 profile 中的每个会话。先查询列表以获取准确标识。包安装可能执行获准的构建脚本。live profile 立即应用配置变化;startup profile 需要重启。
+列出当前 profile 中的插件或组合包,启用或禁用它们,安装组合包或移除已安装的组合包。每项操作都要求 danger-full-access 权限或本次调用的批准。批准不改变会话权限模式。变更影响该 profile 的所有会话。先列出条目以获取准确标识。包安装可能运行已获批准的构建脚本。支持热更新的 profile 立即应用变更;仅启动时加载的 profile 需要重启。
 
 ```json
 {
@@ -716,91 +716,9 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
 
 ## `@deepseek-ai/dsh-tool-cordis`
 
-### `cordis_define`
-
-定义一个不可变的 Cordis Package。新建 Plugin 时使用 kind:"new",只提供 3 至 6 位小写英文字母组成的语义前缀;Host 返回最终 pluginId 和 packageId。修改现有 Plugin 时使用 kind:"existing" 并传入精确 pluginId,以追加 Package 而不覆盖旧版本。code.host 与 code.client 至少提供一个;每个值都是返回 Cordis Plugin 的 plain JavaScript 函数体,不经过 TypeScript、JSX 或 import 转换。依赖 Service、Event、Builtin、Slot 或 token 前先查询 Inspect。Define 只校验参数和语法并记录源码,不申请审批、不执行 apply,也不改变 currentPackageId。成功后用返回的 ID 调用 cordis_run。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "plugin": {
-      "oneOf": [
-        {
-          "type": "object",
-          "additionalProperties": false,
-          "properties": {
-            "kind": {
-              "type": "string",
-              "const": "new"
-            },
-            "idPrefix": {
-              "type": "string",
-              "description": "Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix."
-            }
-          },
-          "required": [
-            "kind",
-            "idPrefix"
-          ]
-        },
-        {
-          "type": "object",
-          "additionalProperties": false,
-          "properties": {
-            "kind": {
-              "type": "string",
-              "const": "existing"
-            },
-            "pluginId": {
-              "type": "string",
-              "description": "Exact ID of an existing Plugin; the new Package is appended to that instance."
-            }
-          },
-          "required": [
-            "kind",
-            "pluginId"
-          ]
-        }
-      ]
-    },
-    "name": {
-      "type": "string",
-      "description": "Short, readable Package name."
-    },
-    "purpose": {
-      "type": "string",
-      "description": "One-sentence, user-facing description of the Package purpose."
-    },
-    "code": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "host": {
-          "type": "string",
-          "description": "Plain JavaScript function body that returns the Host-half Cordis Plugin."
-        },
-        "client": {
-          "type": "string",
-          "description": "Plain JavaScript function body that returns the browser Client-half Cordis Plugin."
-        }
-      }
-    }
-  },
-  "required": [
-    "plugin",
-    "name",
-    "purpose",
-    "code"
-  ]
-}
-```
-
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
 ### `cordis_inspect_list`
 
-列出 Host 当前已知的全部 Cordis Inspect Provider,包括本地 Host Provider 和 Client 最近同步的 manifest。每项包含所属平台、用途、只读方法及输入/输出 schema。创建或修改 Package 前先调用本 Tool,再从结果中选择 cordis_inspect_query 的 provider 和 method。不要猜测名称,也不要把 Inspect method 当作 Plugin 代码可调用的业务 Service。
+列出 Host 当前已知的所有 Cordis Inspect Provider,包括本地 Host Provider 和 Client 同步的最新清单。每项包含平台、用途、只读方法以及输入输出 schema。编写或配置插件前先调用本工具,再从结果选择 cordis_inspect_query 的 provider 和方法。不要猜测名称,也不要把 Inspect 方法当作插件代码可调用的业务 Service。
 
 ```json
 {
@@ -809,11 +727,11 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
 }
 ```
 
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
+来源: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
 
 ### `cordis_inspect_query`
 
-执行 Inspect Provider 显式声明的只读查询。platform、provider 和 method 必须来自 cordis_inspect_list,input 必须符合该方法的 schema。在 cordis_define 前用本 Tool 读取精确 Service 方法、Event mode、Builtin 签名、Tool schema、主题 token,或实时 Slot 树及 props。Host 查询在本地执行;Client 查询等待首个有效页面响应,在页面回答或 Tool 被取消前保持 pending。本 Tool 不能调用业务 Service 方法或修改运行时。查询 Service.listService 和 Event.listEvents 时,先不传 input 浏览紧凑签名目录,再查询精确 service 或 event 获取结构化约定和引用类型。查询 Slots.listSubTree 时,先不传 root 浏览紧凑树,再查询精确 root 获取完整注册约定和 props。
+执行 Inspect Provider 明确声明的只读查询。platform、provider 和 method 必须来自 cordis_inspect_list,input 必须符合该方法的 schema。编写插件代码前,用本工具读取准确的 Service 方法、Event 模式、Builtin 签名、Tool schema、主题 token,或实时 Slot 树与 props。Host 查询在本地运行。Client 查询等待页面首个有效响应,直到页面回应或工具取消。本工具不能调用业务 Service 方法或修改运行时。对于 Service.listService 和 Event.listEvents,不传 input 可浏览精简签名目录,再查询准确服务或事件以获得完整约定及引用类型。对于 Slots.listSubTree,不传 root 可浏览精简树,再查询准确 root 以获得完整注册约定和 props。
 
 ```json
 {
@@ -847,108 +765,9 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
 }
 ```
 
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_inspect_self`
-
-按逐层增加的详细程度检查当前 Session 拥有的动态 Cordis 对象。不传 ID 时只列 Plugin 摘要;只传 pluginId 时返回版本指针、最新 Run 和全部 Package 摘要;只有同时传 pluginId 与 packageId 才返回该不可变 Package 的 Host/Client 源码和运行诊断。packageId 不能单独传入。处理 @pluginId、修复异步失败或定义更新版本前,先查询精确 Package。本 Tool 只读,不执行代码,也不改变版本指针。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin."
-    },
-    "packageId": {
-      "type": "string",
-      "description": "Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned."
-    }
-  }
-}
-```
-
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_run`
-
-激活动态 Plugin 的一个精确 Package。首次激活、重启 currentPackageId 或回退使用 mode:"run";已有 current 时,即使 Plugin 当前已停止,切换到其他 Package 也使用 mode:"update"。未授权的 Client Package 创建审批请求并返回 awaiting-approval;已授权的 Package 返回 starting,并在浏览器中异步继续。两种结果都不会在 Tool 内等待最终结局。currentPackageId 只在完整成功后改变;失败时保留旧 current 和目标 next。异步成功、拒绝或技术失败通过状态与 steering 报告。技术失败后,用 cordis_inspect_self 读取诊断,修正同一 Plugin 并自主重试。用户拒绝后不要再次申请审批。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable Plugin ID returned by cordis_define."
-    },
-    "packageId": {
-      "type": "string",
-      "description": "Exact immutable Package ID to activate under that Plugin."
-    },
-    "mode": {
-      "type": "string",
-      "description": "Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.",
-      "enum": [
-        "run",
-        "update"
-      ]
-    }
-  },
-  "required": [
-    "pluginId",
-    "packageId",
-    "mode"
-  ]
-}
-```
-
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_stop`
-
-停止动态 Plugin 的当前 Run,并取消尚未完成的审批或激活请求。保留 Plugin、全部不可变 Package、授权、currentPackageId 和 nextPackageId,以便之后直接运行或更新。停止已处于停止状态的 Plugin 会幂等成功。临时禁用副作用使用本 Tool;永久移除使用 cordis_undefine。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable dynamic Plugin ID to stop."
-    }
-  },
-  "required": [
-    "pluginId"
-  ]
-}
-```
-
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
-
-### `cordis_undefine`
-
-永久移除当前 Session 拥有的动态 Plugin。如果它正在运行或等待审批,先停止并取消请求,再删除全部 Package、授权和版本指针。返回后,其 pluginId、packageIds、@ 引用和 Package 业务视图均失效;历史卡片只保留“Plugin 已移除”记录。需要保留版本以便重启或回退时不要调用本 Tool,应改用 cordis_stop。
-
-```json
-{
-  "type": "object",
-  "properties": {
-    "pluginId": {
-      "type": "string",
-      "description": "Stable dynamic Plugin ID to remove permanently."
-    }
-  },
-  "required": [
-    "pluginId"
-  ]
-}
-```
-
-来源:[`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
+来源: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/tool-cordis/src/index.ts)
 
-不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。
+创造模式提供两个只读运行时检查工具。Cordis host runner 提供检查注册表;Client 查询需要已连接页面。持久化变更编写为组合包,再通过 plugin_manager 安装。
 
 <a id="deepseek-aidsh-tool-bash-persistent"></a>
 

+ 2 - 2
docs/user/develop/practice/dynamic-cordis.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 docs/user/develop/practice/dynamic-cordis.md
-dynamic-cordis.md: 5e324f88c9fc5ac8f770749cf4ab2c97d175121d
-dynamic-cordis.zh.md: 69486cd0d2bacf7c0831e12801128be22fb209ef
+dynamic-cordis.md: c9065027f3449355aa7a19e16672fa5812cab633
+dynamic-cordis.zh.md: 21d0a410d8e27931786a76b9383230a3d30004e5

+ 8 - 8
docs/user/develop/practice/dynamic-cordis.md

@@ -1,15 +1,15 @@
-# Extend a running agent with Cordis tools
+# Configure persistent plugins from a prompt
 
 English | [中文](dynamic-cordis.zh.md)
 
-This practice guide enables [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.md). The agent can inspect its current Cordis process and mount or unmount model-authored plugins in memory. Temporary plugins disappear when they are unmounted or the process exits and may affect other sessions in the same process.
+Creator mode provides [Plugin Manager](../../../../packages/boot/plugin-manager/README.md) and read-only [runtime inspection](../../../../packages/extensions/tool-cordis/README.md). Plugin configuration belongs to the current profile, affects its sessions, and survives process restarts.
 
-## Run it
+## Connect an MCP server
 
-Start the browser interface with the checked-in overlay:
+Start the Web profile and select Creator mode. With a reachable Streamable HTTP MCP server that exposes `ping`, send this prompt using its actual endpoint:
 
-```sh
-pnpm dsh web --patch apps/cli/config/examples/cordis/cordis.yml
-```
+> Configure the MCP server at `<endpoint>` in this profile as `demo`. Make its tools available now, then call its ping tool and tell me the result.
 
-The command requires a model credential. The [Cordis tool reference](../../../../packages/extensions/tool-cordis/README.md) defines the tool arguments, lifetime, cleanup, and safety contracts.
+The agent writes a configuration-only bundle whose patch inserts `@deepseek-ai/dsh-mcp-client`, then installs it with `plugin_manager install_bundle`. With HMR enabled, the tools appear in the same running session. Verify both the management result (`application: applied`) and a successful `mcp__demo__ping` call. A saved entry with `restart-required` has not activated yet; a failed entry needs configuration repair.
+
+Read the bundle patch before editing its configuration. Use Plugin Manager to disable entries or remove the bundle. See the [MCP client reference](../../../../packages/mcp/mcp-client/README.md) for accepted configuration and connection failure behavior.

+ 8 - 8
docs/user/develop/practice/dynamic-cordis.zh.md

@@ -1,15 +1,15 @@
-# 用 Cordis 工具扩展运行中的智能体
+# 通过提示词配置持久化插件
 
 [English](dynamic-cordis.md) | 中文
 
-本实战指南启用 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.zh.md)。智能体可以检查当前 Cordis 进程,并在内存中挂载或卸载模型编写的插件。临时插件会在卸载或进程退出时消失,并可能影响同一进程中的其他会话。
+创造模式提供 [Plugin Manager](../../../../packages/boot/plugin-manager/README.zh.md) 和只读[运行时检查](../../../../packages/extensions/tool-cordis/README.zh.md)。插件配置属于当前 profile,影响其会话,并在进程重启后保留。
 
-## 运行
+## 连接 MCP 服务器
 
-使用仓库内 overlay 启动浏览器界面:
+启动 Web profile 并选择创造模式。准备一个可访问且提供 `ping` 的 Streamable HTTP MCP 服务器,将其实际端点填入以下提示词:
 
-```sh
-pnpm dsh web --patch apps/cli/config/examples/cordis/cordis.yml
-```
+> 将 `<endpoint>` 处的 MCP 服务器配置到当前 profile,命名为 `demo`。立即启用它的工具,然后调用它的 ping 工具并告诉我结果。
 
-该命令需要模型凭据。[Cordis 工具参考](../../../../packages/extensions/tool-cordis/README.zh.md)定义了四类约定:工具参数、存续时间、清理行为和安全性。
+agent 编写纯配置组合包,在 patch 中插入 `@deepseek-ai/dsh-mcp-client`,再通过 `plugin_manager install_bundle` 安装。启用 HMR 时,工具会出现在同一个运行中的会话里。同时检查管理结果(`application: applied`)和成功的 `mcp__demo__ping` 调用。返回 `restart-required` 的已保存条目尚未激活;失败条目需要修复配置。
+
+修改配置前先读取组合包 patch。使用 Plugin Manager 停用条目或移除组合包。可接受的配置及连接失败行为见 [MCP client 参考](../../../../packages/mcp/mcp-client/README.zh.md)。

+ 2 - 3
packages/boot/app-boot/src/index.ts

@@ -17,7 +17,7 @@ import Include, { applyEntryPatches, entryListSchema, type PatchOptions } from '
 import Group from '@deepseek-ai/cordis-plugin-group'
 import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-home-paths'
 import { createLaunchEnvironmentSnapshot, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
-export { readProfilePatches, resolveTelemetryPatch, type ProfileContext } from './profile-context.ts'
+export { readProfilePatches, resolveTelemetryPatch, type ProfileContext, type ProfilePnpmInvocation } from './profile-context.ts'
 import type {} from '@deepseek-ai/dsh-system-prompt'
 
 export {
@@ -673,8 +673,7 @@ export function installFailLoud(
 
 /**
  * Value mirrors used because Cordis's const enum has no runtime object to import.
- * Keep aligned with `packages/extensions/tool-cordis/src/fiber-state.ts` and
- * `packages/client/web/src/loader-status.ts`.
+ * Keep aligned with `packages/client/web/src/loader-status.ts`.
  */
 const FIBER_PENDING = 0 as FiberState.PENDING
 const FIBER_ACTIVE = 2 as FiberState.ACTIVE

+ 9 - 0
packages/boot/app-boot/src/profile-context.ts

@@ -4,9 +4,18 @@ import { composeEntries, loadProfileDirectory, PROFILE_PATCH_FILENAME, type Prof
 import { loadOptionalPatches } from './index.ts'
 import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 
+/** Application-owned package manager executable; environment applies only to package operations. */
+export interface ProfilePnpmInvocation {
+  readonly command: string
+  readonly args: readonly string[]
+  readonly env: Readonly<Record<string, string>>
+}
+
 /** Current profile facts; scheduling and mutation belong to their callers. */
 export interface ProfileContext {
   readonly name: string
+  /** Packaged applications supply their bundled runtime instead of a PATH executable. */
+  readonly packageManager?: ProfilePnpmInvocation
   readonly dir: string
   readonly patchPath: string
   readonly installAnchor: string

+ 2 - 2
packages/boot/plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md
-README.md: 66408c85273e57d78d94c98a3e8cdcefab1286fb
-README.zh.md: 2b10860867b9ff992a96222b24432ebb99025015
+README.md: 46b5447a8b3548b548bf844e1d753296861b17da
+README.zh.md: d4c285b72f8d31357a12f222d741323e9b4658e6

+ 4 - 2
packages/boot/plugin-manager/README.md

@@ -7,6 +7,8 @@ kind: "package-reference"
 
 English | [中文](README.zh.md)
 
+Application-owned profiles supply their bundled package-manager invocation through launcher facts. It takes precedence over `pnpmCommand` for package operations and registry inspection; its environment applies only to those subprocesses.
+
 ## Summary
 
 Manage the current profile's plugins without editing configuration by hand. Enable or disable individual plugin entries, select installed bundles, and install or remove external bundles. With HMR enabled in YAML, configuration changes apply immediately; without HMR, the running composition remains until restart. Changes affect every session using the profile.
@@ -26,9 +28,9 @@ Manage the current profile's plugins without editing configuration by hand. Enab
 <a id="use-this-package"></a>
 ## Use this package
 
-Base-backed profiles provide the manager. In Web, the sidebar's **Plugins** page ([ui-plugin-manager](../../client/ui-plugin-manager/README.md)) manages the profile's bundles and their uniquely addressable rows; the Settings Plugin list stays read-only. Agent-preset rows remain read-only. The `plugin_manager` tool exposes the same operations and is disabled by default.
+Base-backed profiles provide the manager. In Web, the sidebar's **Plugins** page ([ui-plugin-manager](../../client/ui-plugin-manager/README.md)) manages the profile's bundles and their uniquely addressable rows; the Settings Plugin list stays read-only. Agent-preset rows remain read-only. The `plugin_manager` tool exposes the same operations and is enabled in Creator mode. Other presets keep it disabled by default. Every tool action requires `danger-full-access` or approval for that call. Under lower sandbox modes, `ask` requests approval; `never`, rejection, cancellation, or an unavailable approval channel prevents execution. An approval leaves the session permission mode unchanged. Profile changes persist across sessions, and installed Host code executes in-process outside the workspace sandbox. Dependency build-script approval remains separate.
 
-Enable the tool explicitly in the profile patch; agents using a preset also need its `tool-plugin-manager` entry enabled.
+For a deployment without agent presets, enable the tool in the profile patch. Preset-backed sessions use their preset’s `tool-plugin-manager` entry.
 
 ```yaml
 - id: tool-plugin-manager

+ 4 - 2
packages/boot/plugin-manager/README.zh.md

@@ -7,6 +7,8 @@ kind: "package-reference"
 
 [English](README.md) | 中文
 
+应用拥有的 profile 通过启动器信息提供内置包管理器调用方式。它在包操作和 registry 检查中优先于 `pnpmCommand`;其环境仅应用于这些子进程。
+
 ## 概述
 
 管理当前 profile 的插件,无需手动编辑配置。启停单个插件条目、选择已安装的组合包,以及安装或删除外部组合包。在 YAML 中启用 HMR 时,配置变化立即生效;未启用 HMR 时,运行中的组合保留到重启。改动影响使用该 profile 的全部会话。
@@ -26,9 +28,9 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-基于 base 的 profile 提供管理服务。在 Web 中,侧边栏的**插件**页([ui-plugin-manager](../../client/ui-plugin-manager/README.zh.md))管理 profile 的组合包及其能唯一定位的行;设置页的插件列表保持只读。Agent 预设条目保持只读。`plugin_manager` 工具提供相同操作,默认禁用。
+基于 base 的 profile 提供管理服务。在 Web 中,侧边栏的**插件**页([ui-plugin-manager](../../client/ui-plugin-manager/README.zh.md))管理 profile 的组合包及其能唯一定位的行;设置页的插件列表保持只读。Agent 预设条目保持只读。`plugin_manager` 工具提供相同操作,在 Creator 模式中启用。其他预设仍默认禁用。 每个工具操作都要求 `danger-full-access` 或本次调用的批准。在较低沙箱模式下,`ask` 会请求审批;`never`、拒绝、取消或审批渠道不可用时均不执行。批准不改变会话的权限模式。profile 变更跨会话持久化,已安装的 Host 代码在宿主进程内运行,不受工作区沙箱限制。依赖构建脚本仍需单独批准。
 
-在 profile patch 中显式启用工具;使用预设的 Agent 还需要启用该预设中的 `tool-plugin-manager` 条目。
+未使用 Agent 预设的部署在 profile patch 中启用工具;使用预设的会话由其预设中的 `tool-plugin-manager` 条目控制。
 
 ```yaml
 - id: tool-plugin-manager

+ 14 - 2
packages/boot/plugin-manager/package.json

@@ -68,11 +68,17 @@
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-hmr": "workspace:^",
-    "@deepseek-ai/dsh-brand": "workspace:^"
+    "@deepseek-ai/dsh-brand": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
+    "@deepseek-ai/dsh-sandbox": "workspace:^",
+    "@deepseek-ai/dsh-user-approval": "workspace:^"
   },
   "peerDependenciesMeta": {
     "@deepseek-ai/dsh-hmr": {
       "optional": true
+    },
+    "@deepseek-ai/dsh-user-approval": {
+      "optional": true
     }
   },
   "devDependencies": {
@@ -85,6 +91,12 @@
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-hmr": "workspace:^",
     "@deepseek-ai/cordis-plugin-timer": "workspace:^",
-    "@deepseek-ai/dsh-brand": "workspace:^"
+    "@deepseek-ai/dsh-brand": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
+    "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
+    "@deepseek-ai/dsh-sandbox": "workspace:^",
+    "@deepseek-ai/dsh-user-approval": "workspace:^"
   }
 }

+ 3 - 2
packages/boot/plugin-manager/src/index.ts

@@ -266,7 +266,8 @@ export class PluginManager extends TypertRemoteService {
       case 'registry': {
         if (known.has(parsed.name)) return refused('already-installed', `${parsed.name} is already installed`)
         const view = await viewProfilePackage(this.profile.dir, spec.trim(), {
-          command: this.pnpmCommand, timeoutMs: this.inspectTimeoutMs, ...signal === undefined ? {} : { signal },
+          ...this.profile.packageManager ?? { command: this.pnpmCommand },
+          timeoutMs: this.inspectTimeoutMs, ...signal === undefined ? {} : { signal },
         })
         const log = `${view.stderr}${view.cause === undefined ? '' : `${messageOf(view.cause)}\n`}`.trim()
         if (view.exitCode !== 0 || view.cause !== undefined || view.timedOut) {
@@ -476,7 +477,7 @@ export class PluginManager extends TypertRemoteService {
     const cwd = this.profile.dir
     const identity = requestId === undefined ? {} : { requestId }
     const task = runProfilePnpm({ ...this.profile, profile: this.profile.name }, args, {
-      execution: 'service', command: this.pnpmCommand,
+      execution: 'service', ...this.profile.packageManager ?? { command: this.pnpmCommand },
       signal: signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, signal]),
       outputBytes: this.outputBytes, activateNewBundles: false,
       onOutput: (text, stream) => {

+ 12 - 4
packages/boot/plugin-manager/src/operations.ts

@@ -25,6 +25,10 @@ export interface PackageOperationContext {
 export interface PackageOperationOptions {
   /** The pnpm executable name or path; resolved through `PATH` like the `dsh plugin` command. Defaults to `pnpm`. */
   command?: string
+  /** Prefix arguments for an application-owned executable. */
+  args?: readonly string[]
+  /** Application runtime environment, applied only to this package operation. */
+  env?: Readonly<Record<string, string>>
   /** CLI inherits authentication and terminal descriptors; service scrubs secrets and captures output. */
   execution: 'cli' | 'service'
   signal?: AbortSignal
@@ -111,8 +115,8 @@ export async function runProfilePnpm(
   let output = Buffer.alloc(0)
   let truncated = false
   const cancellation = new AbortController()
-  const child = execa(options.command ?? 'pnpm', args.map(arg => anchorPathSpec(arg, context.cwd)), {
-    cwd: dir, env: options.execution === 'cli' ? process.env : scrubbedParentEnv(), extendEnv: false, reject: false,
+  const child = execa(options.command ?? 'pnpm', [...options.args ?? [], ...args.map(arg => anchorPathSpec(arg, context.cwd))], {
+    cwd: dir, env: { ...(options.execution === 'cli' ? process.env : scrubbedParentEnv()), ...options.env }, extendEnv: false, reject: false,
     stdout: options.execution === 'cli' ? 'inherit' : 'pipe',
     stderr: options.execution === 'cli' ? 'inherit' : 'pipe',
     buffer: false, stdin: options.execution === 'cli' ? 'inherit' : 'ignore', cancelSignal: options.signal === undefined
@@ -197,6 +201,10 @@ export interface PackageViewResult {
 export interface PackageViewOptions {
   /** The pnpm executable name or path. Defaults to `pnpm`. */
   command?: string
+  /** Prefix arguments for an application-owned executable. */
+  args?: readonly string[]
+  /** Application runtime environment, applied only to this package operation. */
+  env?: Readonly<Record<string, string>>
   /** Ends the lookup early; the caller's signal, when it has one. */
   signal?: AbortSignal
   /** Bound on the lookup, in milliseconds. */
@@ -212,8 +220,8 @@ export interface PackageViewOptions {
  * @returns pnpm's exit, output, and how the lookup ended.
  */
 export async function viewProfilePackage(dir: string, spec: string, options: PackageViewOptions): Promise<PackageViewResult> {
-  const result = await execa(options.command ?? 'pnpm', ['view', spec, 'name', 'version', 'description', 'dsh', '--json'], {
-    cwd: dir, env: scrubbedParentEnv(), extendEnv: false, reject: false, stdin: 'ignore',
+  const result = await execa(options.command ?? 'pnpm', [...options.args ?? [], 'view', spec, 'name', 'version', 'description', 'dsh', '--json'], {
+    cwd: dir, env: { ...scrubbedParentEnv(), ...options.env }, extendEnv: false, reject: false, stdin: 'ignore',
     timeout: options.timeoutMs, ...options.signal === undefined ? {} : { cancelSignal: options.signal },
   })
   const cause = result.exitCode === undefined && !result.timedOut && !result.isCanceled

+ 13 - 3
packages/boot/plugin-manager/src/tools.ts

@@ -2,11 +2,14 @@
 import { assertNever } from '@deepseek-ai/dsh-util-values'
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from './index.ts'
+import type {} from '@deepseek-ai/dsh-sandbox-policy'
+import type {} from '@deepseek-ai/dsh-user-approval'
+import { approveEscalation } from '@deepseek-ai/dsh-sandbox'
 import type { PluginEntryId } from './types.ts'
 import { defineTool } from '@deepseek-ai/dsh-tools'
 
 /** Required services for the management tool. */
-export const inject = ['tools', 'pluginManager']
+export const inject = ['tools', 'pluginManager', 'sandboxPolicy']
 
 /** Register one management tool for discovery and the four persistent actions.
  * @param ctx Agent-scoped tool registration context.
@@ -14,7 +17,7 @@ export const inject = ['tools', 'pluginManager']
 export function apply(ctx: Context): void {
   ctx.tools.register(defineTool({
     name: 'plugin_manager',
-    description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.',
+    description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.',
     parameters: {
       action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle'], description: 'Management operation.' },
       target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
@@ -27,7 +30,14 @@ export function apply(ctx: Context): void {
       schema: { type: 'string' },
       render: (_args, value) => [{ type: 'text', text: value }],
     },
-    async execute(args) {
+    async execute(args, exec) {
+      const policy = ctx.sandboxPolicy.resolve(exec.agent === undefined ? {} : { session: exec.agent.session })
+      await approveEscalation({
+        requestedMode: 'danger-full-access', effectiveMode: policy.mode, subject: 'plugin management operation',
+        justification: `plugin_manager ${JSON.stringify(args)}. Profile changes persist across sessions; installed Host code runs outside the workspace sandbox.`,
+      }, { approver: ctx.get('approval'), agent: exec.agent, callId: exec.callId,
+        toolName: 'plugin_manager', signal: exec.signal })
+      exec.signal.throwIfAborted()
       const manager = ctx.pluginManager
       switch (args.action) {
         case 'list_plugins':

+ 27 - 2
packages/boot/plugin-manager/tests/manager.spec.ts

@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
 import { realpath } from 'node:fs/promises'
 import { join } from 'node:path'
 import { tmpdir } from 'node:os'
-import { pathToFileURL } from 'node:url'
+import { fileURLToPath, pathToFileURL } from 'node:url'
 import type { Context } from '@deepseek-ai/cordis'
 import { expect, it, onTestFinished, vi } from 'vitest'
 import {
@@ -18,7 +18,7 @@ import { Group } from '@deepseek-ai/cordis-plugin-loader'
 import * as operations from '../src/operations.ts'
 import { parse, parseDocument } from 'yaml'
 
-async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}) {
+async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}, packageManager?: ProfileContext['packageManager']) {
   // pnpm resolves workspace roots through native realpath, including Windows 8.3 aliases.
   const home = await realpath(mkdtempSync(join(tmpdir(), 'plugin-manager-')))
   const dir = join(home, 'profiles', 'test')
@@ -41,6 +41,7 @@ async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, pre
   const overlays: PatchOptions[] = overlay ? [{ id: 'managed', disabled: true }] : []
   const profile: ProfileContext = {
     name: 'test',
+    ...(packageManager === undefined ? {} : { packageManager }),
     startedBundles: ['core', 'extra'],
     dir, patchPath: join(dir, 'cordis.patch.yml'), installAnchor: anchor, cwd: home, home,
     overlays, telemetryDisabledEnv: undefined,
@@ -742,3 +743,27 @@ it('applies watched configuration while pnpm installation is still running', asy
   expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core', 'extra', 'new-bundle'])
   expect(ctx.get('managedProbe')).toBeUndefined()
 })
+
+it('installs and removes with the bundled pnpm when PATH contains no pnpm', async () => {
+  const pnpm = fileURLToPath(new URL('../../../../apps/desktop/node_modules/pnpm/bin/pnpm.mjs', import.meta.url))
+  const { manager, dir } = await fixture('startup', false, undefined, { pnpmCommand: 'must-not-be-used' }, {
+    command: process.execPath, args: ['--expose-internals', pnpm], env: { PATH: '', ELECTRON_RUN_AS_NODE: '1' },
+  })
+  const target = join(dir, 'local-bundle')
+  mkdirSync(target)
+  writeFileSync(join(target, 'package.json'), JSON.stringify({ name: '@test/desktop-manager', version: '1.0.0',
+    dsh: { bundle: { patch: './cordis.patch.yml' } } }))
+  writeFileSync(join(target, 'cordis.patch.yml'), '[]\n')
+  // Fixture-only packages need no registry resolution during this local install.
+  const manifest = readProfileManifest('test', dir)
+  delete manifest.dependencies
+  writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
+  const installed = await manager.installBundle(target)
+  expect(installed.error).toBeUndefined()
+  expect(installed.packageResult?.exitCode).toBe(0)
+  expect(readProfileManifest('test', dir).dependencies).toHaveProperty('@test/desktop-manager')
+  const removed = await manager.removeBundle('@test/desktop-manager')
+  expect(removed.error).toBeUndefined()
+  expect(removed.packageResult?.exitCode).toBe(0)
+  expect(readProfileManifest('test', dir).dependencies ?? {}).not.toHaveProperty('@test/desktop-manager')
+})

+ 14 - 0
packages/boot/plugin-manager/tests/operations.spec.ts

@@ -233,3 +233,17 @@ it('asks the registry through pnpm view in the profile directory and reports how
   expect(missing).toMatchObject({ exitCode: null, timedOut: false })
   expect(missing.cause).toMatchObject({ message: 'spawn pnpm ENOENT', code: 'ENOENT' })
 })
+
+it('uses application-owned executable arguments and environment for package operations and inspection', async () => {
+  const { dir, context } = fixture()
+  const runtime = { command: '/app/electron', args: ['--expose-internals', '/app/pnpm.mjs'], env: { ELECTRON_RUN_AS_NODE: '1', PATH: '/app/bin' } }
+  command.run.mockImplementationOnce(() => result(0, ''))
+  await runProfilePnpm(context, ['add', './extra'], { ...runtime, execution: 'service', outputBytes: 100, activateNewBundles: false })
+  expect(command.run).toHaveBeenLastCalledWith(runtime.command, [...runtime.args, 'add', resolve(context.cwd, 'extra')],
+    expect.objectContaining({ env: expect.objectContaining(runtime.env) as unknown }))
+  command.run.mockResolvedValueOnce(Object.assign({ exitCode: 0, failed: false }, { stdout: '{}', stderr: '', timedOut: false }))
+  await viewProfilePackage(dir, 'example', { ...runtime, timeoutMs: 1000 })
+  expect(command.run).toHaveBeenLastCalledWith(runtime.command,
+    [...runtime.args, 'view', 'example', 'name', 'version', 'description', 'dsh', '--json'],
+    expect.objectContaining({ env: expect.objectContaining(runtime.env) as unknown }))
+})

+ 118 - 3
packages/boot/plugin-manager/tests/tools.spec.ts

@@ -3,6 +3,11 @@ import { Context } from '@deepseek-ai/cordis'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import { ToolCallId } from '@deepseek-ai/dsh-llm'
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import SandboxPolicy, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
+import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
+import SessionProjections from '@deepseek-ai/dsh-session-projection'
+import ApprovalService, { type ApprovalOutcome } from '@deepseek-ai/dsh-user-approval'
 import { expect, it, onTestFinished, vi } from 'vitest'
 import type PluginManager from '../src/index.ts'
 import * as tool from '../src/tools.ts'
@@ -12,7 +17,7 @@ function resultText(result: Awaited<ReturnType<ToolRuntime['execute']>>): string
   return result.value
 }
 
-async function fixture() {
+async function fixture(mode: 'read-only' | 'workspace-write' | 'danger-full-access' = 'danger-full-access', approval?: 'ask' | 'never') {
   const ctx = new Context()
   onTestFinished(() => ctx.fiber.dispose())
   const manager = {
@@ -26,12 +31,122 @@ async function fixture() {
   ctx.provide('pluginManager', manager as unknown as PluginManager)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
+  await ctx.plugin(SessionProjections)
+  await ctx.plugin(SandboxPolicy, { mode })
+  if (approval !== undefined) await ctx.plugin(ApprovalService, { policy: approval })
   const fiber = await ctx.plugin(tool)
-  const call = (args: unknown) => ctx.tools.execute({ name: 'plugin_manager', arguments: args,
-    callId: ToolCallId('manager-call'), signal: new AbortController().signal })
+  const call = (args: unknown, agent?: Agent, signal = new AbortController().signal) => ctx.tools.execute({ name: 'plugin_manager', arguments: args,
+    ...agent === undefined ? {} : { agent },
+    callId: ToolCallId('manager-call'), signal })
   return { ctx, manager, call, fiber }
 }
 
+it.each(['read-only', 'workspace-write'] as const)('denies every management action in %s before accessing the manager', async (mode) => {
+  const { call, manager } = await fixture(mode)
+  for (const action of ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle']) {
+    const result = await call({ action, target: 'bundle', enabled: true })
+    expect(result.isError).toBe(true)
+    expect(JSON.stringify(result.content)).toContain('requires approval, but no approval service is composed')
+  }
+  for (const method of Object.values(manager)) expect(method).not.toHaveBeenCalled()
+})
+
+it('checks the calling session on each execution, including after permission is revoked', async () => {
+  const { call, manager } = await fixture()
+  const id = SessionId('manager-permissions')
+  const session = Session.create(id, undefined, { version: SESSION_FORMAT_VERSION, id, createdAt: 0, isSeeded: false })
+  const agent = { session } as unknown as Agent
+  setSandboxMode(session, 'workspace-write')
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(true)
+  expect(manager.listPlugins).not.toHaveBeenCalled()
+  setSandboxMode(session, 'danger-full-access')
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(false)
+  setSandboxMode(session, 'read-only')
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(true)
+  expect(manager.listPlugins).toHaveBeenCalledTimes(1)
+})
+
+function activeAgent(): Agent {
+  const id = SessionId('manager-approval')
+  const session = Session.create(id, undefined, { version: SESSION_FORMAT_VERSION, id, createdAt: 0, isSeeded: false })
+  session.append('turn/start', { turn: 1 })
+  return { session } as unknown as Agent
+}
+
+it.each(['read-only', 'workspace-write'] as const)('approves each action once in %s without changing session permissions', async (mode) => {
+  const { ctx, call, manager } = await fixture(mode, 'ask')
+  const agent = activeAgent()
+  const prompted = vi.fn(async () => 'allowed-once' as const)
+  const dispose = ctx.on('approval/request', prompted)
+  for (const action of ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle']) {
+    expect((await call({ action, target: 'bundle', enabled: true }, agent)).isError).toBe(false)
+  }
+  expect(prompted).toHaveBeenCalledTimes(6)
+  for (const method of Object.values(manager)) expect(method).toHaveBeenCalledTimes(1)
+  expect(ctx.sandboxPolicy.resolve({ session: agent.session }).mode).toBe(mode)
+  const audit = agent.session.snapshotEvents().filter(event => event.type.startsWith('approval/'))
+  expect(audit).toHaveLength(12)
+  expect(audit[0]).toMatchObject({ type: 'approval/asked', data: {
+    toolName: 'plugin_manager', callId: 'manager-call',
+  } })
+  const request = audit[0]
+  if (request?.type !== 'approval/asked') throw new Error('Expected an approval request')
+  expect(request.data.reason).toContain('"action":"list_plugins"')
+  expect(audit[1]).toMatchObject({ type: 'approval/decided', data: { outcome: 'allowed-once' } })
+  dispose()
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(true)
+  expect(manager.listPlugins).toHaveBeenCalledTimes(1)
+})
+
+it.each(['rejected', 'cancelled', 'unavailable'] as const)('does not mutate the profile when approval is %s', async (outcome) => {
+  const { ctx, call, manager } = await fixture('workspace-write', 'ask')
+  ctx.on('approval/request', async () => outcome)
+  const agent = activeAgent()
+  expect((await call({ action: 'install_bundle', target: 'bundle' }, agent)).isError).toBe(true)
+  expect(manager.installBundle).not.toHaveBeenCalled()
+  expect(agent.session.snapshotEvents().filter(event => event.type === 'approval/decided')
+    .map(event => event.data.outcome)).toEqual([outcome])
+})
+
+it('rejects never policy without prompting and keeps full-access calls prompt-free', async () => {
+  const { ctx, call, manager } = await fixture('workspace-write', 'never')
+  const prompted = vi.fn(async () => 'allowed-once' as const)
+  ctx.on('approval/request', prompted, { prepend: true })
+  const agent = activeAgent()
+  expect((await call({ action: 'set_plugin', target: 'include:demo', enabled: true }, agent)).isError).toBe(true)
+  expect(manager.setPluginEnabled).not.toHaveBeenCalled()
+  setSandboxMode(agent.session, 'danger-full-access')
+  expect((await call({ action: 'set_plugin', target: 'include:demo', enabled: true }, agent)).isError).toBe(false)
+  expect(manager.setPluginEnabled).toHaveBeenCalledTimes(1)
+  expect(prompted).not.toHaveBeenCalled()
+})
+
+it('cancels an approval wait before any manager operation', async () => {
+  const { ctx, call, manager } = await fixture('workspace-write', 'ask')
+  const asked = Promise.withResolvers<undefined>()
+  const answer = Promise.withResolvers<ApprovalOutcome>()
+  ctx.on('approval/request', () => { asked.resolve(undefined); return answer.promise })
+  const controller = new AbortController()
+  const result = call({ action: 'install_bundle', target: 'bundle' }, activeAgent(), controller.signal)
+  await asked.promise
+  expect(manager.installBundle).not.toHaveBeenCalled()
+  controller.abort()
+  answer.resolve('allowed-once')
+  expect((await result).isError).toBe(true)
+  expect(manager.installBundle).not.toHaveBeenCalled()
+})
+
+it('does not apply a grant when the call was cancelled before dispatch', async () => {
+  const { ctx, call, manager } = await fixture('workspace-write', 'ask')
+  const controller = new AbortController()
+  vi.spyOn(ctx.approval, 'request').mockImplementation(async () => {
+    controller.abort()
+    return 'allowed-once'
+  })
+  expect((await call({ action: 'install_bundle', target: 'bundle' }, activeAgent(), controller.signal)).isError).toBe(true)
+  expect(manager.installBundle).not.toHaveBeenCalled()
+})
+
 it('paginates inventories with an explicit continuation and total', async () => {
   const { call } = await fixture()
   const first = await call({ action: 'list_plugins' })

+ 9 - 0
packages/boot/plugin-manager/tsconfig.json

@@ -52,6 +52,15 @@
     },
     {
       "path": "../../util/brand"
+    },
+    {
+      "path": "../../sandbox/sandbox-policy"
+    },
+    {
+      "path": "../../sandbox/sandbox"
+    },
+    {
+      "path": "../../interaction/user-approval"
     }
   ]
 }

+ 2 - 2
packages/client/ui-agent-preset/src/client/locales.ts

@@ -44,7 +44,7 @@ export const en: Record<AgentPresetSettingsKey, string> = {
     'Single-tool coding agent with a persistent shell.',
   presetCordisName: 'Creator mode',
   presetCordisDescription:
-    'Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance.',
+    'Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, persistent plugin management, and preset-authoring guidance.',
   duplicate: 'Duplicate',
   duplicateUnavailable: 'This deployment has no writable preset directory',
   delete: 'Delete',
@@ -109,7 +109,7 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
   presetMinimalName: '极简模式',
   presetMinimalDescription: '仅提供持久 shell 的单工具编码 Agent。',
   presetCordisName: '创造模式',
-  presetCordisDescription: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。',
+  presetCordisDescription: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、持久化插件管理和 preset 创作指导。',
   duplicate: '复制',
   duplicateUnavailable: '此部署未配置可写的预设目录',
   delete: '删除',

+ 2 - 2
packages/client/ui-conversation/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
-README.md: 802ba3b8d853457e00cab3180c251ffec5291f19
-README.zh.md: 83199417289770640c9c038d84ee3e0fa6ac7866
+README.md: e07c2f66e55d0bfe55b869d5b395914670278be3
+README.zh.md: 81e5300e5bac3d6d54e83ead165598cb2c378a99

+ 2 - 0
packages/client/ui-conversation/README.md

@@ -46,6 +46,8 @@ Workspace selection uses `uiWorkspace.openWorkspace` to prepare the target and c
 
 The package occupies the root-scoped `main` key `conversation`, whose wrapper declares the optional-Session `main.conversation` shell. It registers strict Session header/body entries, View list, composer chain and bar, input regions, Hero regions, queue dock, draft persistence, and phase calculation. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
 
+A blank Session retains the header's leading and corner controls, including the right-sidebar opener, while hiding its title, actions, utilities, and View tabs. Selecting a Workspace creates the Session needed by these controls; the first message is not required. Without a selected Session, the strict header is absent. Sidebar entries retain their own data and execution prerequisites.
+
 View selection is deterministic: a registered persisted selection wins, otherwise registered `chat` wins, otherwise no View renders. It never chooses the first registered View. Shell phase combines Session lifecycle with the active-target set; no target-specific snapshot is read by the shell.
 
 The shell reads the persisted View preference before rendering when a Session first binds or a cached Session becomes current, activates the registered preferred View or Chat fallback, and activates later tab or focus selections before committing them to the store. A blank Session still omits the `conversation.view` slot; no unselected target is activated.

+ 2 - 0
packages/client/ui-conversation/README.zh.md

@@ -46,6 +46,8 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
 
 本包占据 root 作用域 `main` 中的 `conversation` key,其包装层声明 optional-Session `main.conversation` shell。本包注册 strict Session header/body、View list、composer chain 与 bar、输入区域、Hero 区域、queue dock、草稿持久化和 phase 计算。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
 
+blank Session 保留 header 的 leading 与 corner 控件,包括右侧栏展开入口,同时隐藏标题、actions、utilities 和 View tabs。选择 Workspace 会创建这些控件所需的 Session,无需先发送消息。没有选中 Session 时,strict header 不挂载。侧栏各入口仍遵循自身的数据与执行环境要求。
+
 View 选择规则固定:有效且已注册的持久化选择优先,其次是已注册的 `chat`,否则不渲染 View;绝不选择第一个已注册 View。Shell phase 只组合 Session lifecycle 与 active-target set,不读取任何 target-specific 快照。
 
 Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染前读取持久化 View 偏好,激活已注册的偏好 View 或 Chat fallback,并在后续 tab 或 focus 选择写入 store 前先激活对应 target。blank Session 仍不渲染 `conversation.view` slot;未选中的 target 不会激活。

+ 8 - 13
packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css

@@ -46,10 +46,14 @@
   border-bottom: 0.5px solid var(--dsw-alias-border-l3);
 }
 
-/* Blank hero/settling: keep the strict Session header mounted without taking
-   column space; the root-owned scrollport and composer remain below it. */
-.headerHidden {
-  display: none;
+/* Blank Sessions retain navigation controls without conversation title or tabs. */
+.headerBlank {
+  min-height: 0;
+  border-bottom: none;
+}
+
+.headerBlank .headerCorner {
+  margin-left: auto;
 }
 
 /* macOS hiddenInset titlebar: the title row doubles as the window drag
@@ -67,15 +71,6 @@
   -webkit-app-region: no-drag;
 }
 
-/* Blank state on macOS desktop: the chrome rows are gone but the leading seat
-   (the hidden sidebar's reopen control) stays on screen, so the header keeps
-   its layout without the rule. */
-:global([data-platform='darwin']) .headerHidden {
-  display: block;
-  min-height: 0;
-  border-bottom: none;
-}
-
 .titleRow {
   display: flex;
   align-items: center;

+ 5 - 15
packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx

@@ -1,7 +1,6 @@
 /** Strict per-session header/body content inserted into the resident conversation layout. */
 
 import clsx from 'clsx'
-import { isDarwinDesktop } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type {
@@ -55,8 +54,7 @@ function equalBreadcrumbs(left: readonly Breadcrumb[], right: readonly Breadcrum
 /**
  * Renders Session header chrome above the resident conversation scrollport.
  * @param props - Strict Session store, view ledger, navigation, render, and locale shares.
- * @returns the header: title and tabs when visible, and on macOS desktop the
- *   always-mounted leading seat even while blank-session chrome hides.
+ * @returns Session navigation controls, with title and tabs after conversation starts.
  */
 export function ConversationSessionHeader({
   sessionId, useSession, useSessions, useConversation, useConversationViews, useStore,
@@ -69,16 +67,8 @@ export function ConversationSessionHeader({
   const session = useSession(s => s)
   const conversation = useConversation(s => s)
   const hideChrome = session.blank && conversationPhase(session, conversation) === 'blank'
-  // macOS desktop keeps the leading seat mounted through the blank state (the
-  // hidden sidebar's reopen control lives there), so the header may not leave
-  // the layout or the accessibility tree while its chrome hides.
-  const darwinDesktop = isDarwinDesktop()
-
   return (
-    <header
-      className={clsx(css.header, hideChrome && css.headerHidden)}
-      aria-hidden={(hideChrome && !darwinDesktop) || undefined}
-    >
+    <header className={clsx(css.header, hideChrome && css.headerBlank)}>
       <div className={css.titleRow}>
         <div className={css.headerLeading} data-conversation-header-leading="">
           {renderSlot('conversation.session.header.leading', {})}
@@ -142,11 +132,11 @@ export function ConversationSessionHeader({
             <div className={css.headerUtilities}>
               {renderSlot('conversation.session.header.utilities', {})}
             </div>
-            <div className={css.headerCorner} data-conversation-header-corner="">
-              {renderSlot('conversation.session.header.corner', {})}
-            </div>
           </>
         )}
+        <div className={css.headerCorner} data-conversation-header-corner="">
+          {renderSlot('conversation.session.header.corner', {})}
+        </div>
       </div>
       {!hideChrome && tabs.length > 1 && (
         <div className={css.tabs} role="tablist">

+ 6 - 2
packages/client/ui-conversation/tests/skeleton.client.spec.tsx

@@ -462,7 +462,7 @@ describe('ConversationRoot resident composer', () => {
     expect(seat?.contains(fallback)).toBe(true)
   })
 
-  it('hero phase: same textarea, hero chrome, no header, picker switches the workspace', () => {
+  it('hero phase: keeps sidebar controls accessible while hiding conversation chrome', () => {
     const b = mount(
       sessionSnapshotOf({ blank: true }),
       [
@@ -474,7 +474,11 @@ describe('ConversationRoot resident composer', () => {
     const host = b.view.container.querySelector('[data-conversation-scroll]')
     const header = b.view.container.querySelector('header')
     expect(host).not.toBeNull()
-    expect(header?.getAttribute('aria-hidden')).toBe('true')
+    expect(header?.getAttribute('aria-hidden')).not.toBe('true')
+    expect(b.view.getByTestId('view-conversation.session.header.corner')).toBeTruthy()
+    expect(b.view.queryByRole('tablist')).toBeNull()
+    expect(b.slotCalls).not.toContain('conversation.session.header.utilities')
+    expect(b.slotCalls).not.toContain('conversation.session.header.actions')
     expect(b.view.getByText('探索未至之境')).toBeTruthy()
     expect(b.view.getByText('预览版')).toBeTruthy()
     expect(b.view.queryByTestId('view-chat')).toBeNull()

+ 3 - 3
packages/core/tools/tests/gen-tool-catalog.spec.ts

@@ -26,9 +26,9 @@ describe('gen-tool-catalog collectToolCatalog', () => {
     const catalog = await collectToolCatalog()
     const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
     expect(names).toEqual([
-      'ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_inspect_list',
-      'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop',
-      'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep',
+      'ask_user_question', 'bash', 'bash', 'cordis_inspect_list',
+      'cordis_inspect_query',
+      'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep',
       'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output',
       'list_agents', 'list_agents', 'list_mcp_resource_templates', 'list_mcp_resources',
       'list_subagent_models', 'lsp', 'plugin_manager', 'present', 'pwsh', 'pwsh', 'ralph',

+ 2 - 2
packages/extensions/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/extensions/README.md
-README.md: e8422f39053bd853f4f54d55015504d16f3b775f
-README.zh.md: ec788a64c47b94d8c4d12014ed867ed9efd2e349
+README.md: 56ef38506d7ea99595200da5574f6df6b3f93f3d
+README.zh.md: 62d5a15ea3f9bec8396ad0b80fde44d6f0b3dc95

+ 5 - 5
packages/extensions/README.md

@@ -1,5 +1,5 @@
 ---
-description: "The extensions group map: model-facing tools and dual-half runners for defining, running, and removing dynamic Cordis packages, for users and maintainers navigating the group."
+description: "Runtime API inspection, process-local runners, and historical Cordis cards."
 kind: "package-group"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The extensions group lets an agent inspect and modify the live DSH runtime without editing repository files or configuration. It can define, run, update, stop, and remove dynamic Cordis packages from model tools or a browser panel. A package may affect the host, browser, or both, and immutable versions support controlled updates. Definitions exist only in process memory and disappear when DSH restarts. Choose the child package for model tooling, host execution, browser execution, or browser controls.
+The extensions group provides read-only runtime API discovery for agents, process-local runners for programmatic and browser consumers, and historical generated-plugin cards. Creator mode installs persistent plugins through [Plugin Manager](../boot/plugin-manager/README.md). Choose a child package for inspection, Host execution, Client execution, or browser controls.
 
 ## Table of Contents
 
@@ -24,10 +24,10 @@ The extensions group lets an agent inspect and modify the live DSH runtime witho
 
 | Package | Role | ctx key |
 |---|---|---|
-| [`tool-cordis`](tool-cordis/README.md) | Seven model-facing tools: inspect the live runtime, define, run, stop, and remove dynamic packages | registers on `ctx.tools` |
+| [`tool-cordis`](tool-cordis/README.md) | Two read-only tools for runtime API discovery | registers on `ctx.tools` |
 | [`cordis-host-runner`](cordis-host-runner/README.md) | Host half: definition registry, sandboxed host-half lifecycle, and the inspect registry that answers browser queries | provides `ctx.dynamicCordisRunner` and `ctx.cordisInspect` |
 | [`cordis-client-runner`](cordis-client-runner/README.md) | Browser half: evaluates a browser-half source into a live plugin and answers run requests | client face; provides browser `ctx.dynamicCordisRunner` |
-| [`ui-cordis`](ui-cordis/README.md) | Browser surfaces: the frame-wide panel, lifecycle tool cards, and the `@pluginId` input source | client face; registers slots |
+| [`ui-cordis`](ui-cordis/README.md) | Browser panel and historical lifecycle tool cards | client face; registers slots |
 
 -----
 
@@ -35,7 +35,7 @@ The extensions group lets an agent inspect and modify the live DSH runtime witho
 ## Related documentation
 
 - [Extensions subsystem](../../docs/subsystems/extensions.md) — the generated `ctx.cordisInspect` and `ctx.dynamicCordisRunner` service API.
-- [Generated tool catalog](../../docs/tool-catalog.md#deepseek-aidsh-tool-cordis) — the seven model-facing tool schemas.
+- [Generated tool catalog](../../docs/tool-catalog.md#deepseek-aidsh-tool-cordis) — the two read-only tool schemas.
 - [Generated configuration catalog](../../docs/config-catalog.md#deepseek-aidsh-cordis-host-runner) — the runner's accepted config fields.
 - [Self-referential Cordis toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md) — design home for sandbox semantics, lifecycle, and composition.
 - [Client shells and dynamic packages Agent Note](../../.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md) — package placement and build faces for the client halves.

+ 5 - 5
packages/extensions/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "extensions 组地图:用于定义、运行与移除动态 Cordis 包的模型侧工具和双半 runner,供浏览本组的用户与维护者阅读。"
+description: "运行时 API 检查、进程内 runner 与历史 Cordis 卡片。"
 kind: "package-group"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-extensions 组让 agent(智能体)检查并修改实时 DSH 运行时,而不编辑仓库文件或配置。该组支持通过模型工具或浏览器面板定义、运行、更新、停止和移除动态 Cordis 包。包可以作用于 host、浏览器或两者,不可变版本支持受控更新。定义只存在于进程内存中,并在 DSH 重启时消失。按模型工具、host 执行、浏览器执行或浏览器控件选择对应的子包。
+extensions 组为 agent 提供只读运行时 API 发现、供程序和浏览器消费者使用的进程内 runner,以及历史生成插件卡片。Creator 模式通过 [Plugin Manager](../boot/plugin-manager/README.zh.md) 安装持久化插件。按需选择检查、Host 执行、Client 执行或浏览器控件子包。
 
 ## 目录
 
@@ -24,10 +24,10 @@ extensions 组让 agent(智能体)检查并修改实时 DSH 运行时,而
 
 | 包 | 职责 | ctx 键 |
 |---|---|---|
-| [`tool-cordis`](tool-cordis/README.zh.md) | 七个模型侧工具:检查实时运行时,定义、运行、停止并移除动态包 | 注册到 `ctx.tools` |
+| [`tool-cordis`](tool-cordis/README.zh.md) | 两个只读运行时 API 发现工具 | 注册到 `ctx.tools` |
 | [`cordis-host-runner`](cordis-host-runner/README.zh.md) | host 半:定义注册表、沙箱化的 host 半生命周期,以及用于应答浏览器查询的 inspect 注册表 | 提供 `ctx.dynamicCordisRunner` 与 `ctx.cordisInspect` |
 | [`cordis-client-runner`](cordis-client-runner/README.zh.md) | 浏览器半:将浏览器半源码求值为运行中的插件,并应答运行请求 | client 面;提供浏览器侧 `ctx.dynamicCordisRunner` |
-| [`ui-cordis`](ui-cordis/README.zh.md) | 浏览器面:全局面板、生命周期工具卡片与 `@pluginId` 输入源 | client 面;注册 slot |
+| [`ui-cordis`](ui-cordis/README.zh.md) | 浏览器面板与历史生命周期工具卡片 | client 侧;注册 slot |
 
 -----
 
@@ -35,7 +35,7 @@ extensions 组让 agent(智能体)检查并修改实时 DSH 运行时,而
 ## 相关文档
 
 - [extensions 子系统](../../docs/subsystems/extensions.zh.md)——生成的 `ctx.cordisInspect` 与 `ctx.dynamicCordisRunner` 服务 API。
-- [生成的工具目录](../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-cordis)——七个模型侧工具 schema。
+- [生成的工具目录](../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-cordis)——两个只读工具 schema。
 - [生成的配置目录](../../docs/config-catalog.zh.md#deepseek-aidsh-cordis-host-runner)——runner 的受支持配置字段。
 - [自引用 Cordis 工具集 Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md)——沙箱语义、生命周期与组合的设计居所。
 - [客户端外壳与动态包 Agent Note](../../.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md)——浏览器半的包归属与构建面。

+ 2 - 2
packages/extensions/cordis-client-runner/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/extensions/cordis-client-runner/README.md
-README.md: 153a8bf3b71d2c4c8edf3423dd0b7004083d1ecc
-README.zh.md: 207b3d40c56110d734499bb516e41872c53b262a
+README.md: aa87e2919be899539bea8aff00d072553f339c38
+README.zh.md: f4925e4bd52098884c6a25f2d9504ce9a9d667f0

+ 7 - 7
packages/extensions/cordis-client-runner/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-cordis-client-runner` lets a page run the browser half of a dynamic Cordis package: it answers the host's run requests, loads the browser-half source into the page as a live plugin, and removes it when the host retracts the run. A person approves or declines a run — or starts one directly — and the result this package reports back becomes the `cordis_run` tool result the model reads. Nothing loads at activation and nothing is restored after a refresh; a page runs a dynamic package only when someone answers a run request or asks for it here.
+`dsh-cordis-client-runner` runs the browser half of process-local dynamic packages for programmatic callers and existing browser controls. It loads a definition after an approved request or explicit user gesture, and removes it when the Host retracts the run. Page refresh does not restore definitions. Creator UI plugins use installed Client modules through Plugin Manager.
 
 ## Table of Contents
 
@@ -80,7 +80,7 @@ A `cordis/request-run` event asks this page whether to run a definition. Whoever
 Read these pages when the package-level contract is not enough. They move from the browser half to the host that asks it, the tools whose runs it answers, and the surface that renders it.
 
 - [Host runner](../cordis-host-runner/README.md) — the registry and run round trip this package answers.
-- [Tool package](../tool-cordis/README.md) — the model-facing tools whose run requests reach this page.
+- [Tool package](../tool-cordis/README.md) — read-only runtime API discovery.
 - [UI package](../ui-cordis/README.md) — the panel and cards that operate this face.
 - [Extensions subsystem](../../../docs/subsystems/extensions.md) — the generated `ctx.dynamicCordisRunner` API and `cordis/*` events.
 - [Client shells and dynamic packages Agent Note](../../../.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md) — package placement and build faces for the client halves.
@@ -90,25 +90,25 @@ Read these pages when the package-level contract is not enough. They move from t
 <a id="model-experience"></a>
 ## Model Experience
 
-### Run resolution, when a model asked for the run
+### Run resolution relayed by the Host
 
 #### What the model sees
 
-This package contributes no tool, prompt, or context of its own; the first thing it authors that reaches a model is the resolution it sends back for a `cordis/request-run` round trip, which the host turns into the blocked `cordis_run` result. A success carries the loaded revision and, for a browser half parked on services this page does not have, their names. A failure carries one reason — `rejected` when the user refused, `host-half-failed`, or `client-half-failed` — and, for the browser half, this package's own text: the failing stage (`evaluate`, `module-import`, or `activate`) followed by the closure's, guard's, or fiber's message. The guard's teaching errors (an undeclared service, a shadowed browser global, a plugin that returned no `apply`) reach the model through exactly that field. A crash that happens later, while React renders the loaded half, travels the separate post-settle path below.
+This package contributes no tool or prompt. It resolves `cordis/request-run` with activation success, missing services, rejection, or Host/Client failures. The Host runner owns any message relayed to the session.
 
 #### Token effect
 
-Conditional and bounded: at most one resolution per run request, spent inside the `cordis_run` tool result the host already emits. The text is data-dependent (a definition's own error message) and this package retains nothing across requests — a page's later load failures are page-local diagnostics with no model-visible carrier.
+Conditional and bounded: at most one resolution per run request, spent inside the runner result the host already emits. The text is data-dependent (a definition's own error message) and this package retains nothing across requests — a page's later load failures are page-local diagnostics with no model-visible carrier.
 
 #### KV Cache effect
 
-Append-only. A resolution reaches the model only as the tool result for the request that was already in flight, extending the history tail; nothing this package authors rewrites or reorders earlier request tokens, so an otherwise reusable prefix stays reusable. Repeated runs of the same definition each produce their own result rather than replacing an earlier one.
+Host steering appends to the session history; this package does not rewrite earlier messages.
 
 ### Render failure, after the run settled
 
 #### What the model sees
 
-A browser half that loads cleanly can still crash when React renders it, and that crash lands after the run was answered — so the model would otherwise be told "ok" and never learn. Every entry-boundary crash of a package this page seated is sent to the host (`reportRenderFailure`) naming the slot, whether the crash retired the entry from its cell (`abdicated`: the package's UI is gone, not merely broken), and a message written for the author. The host keeps the last one per package, steers the owning session with it, and exposes it through `cordis_inspect_self`; nothing here reaches a run resolution.
+React can fail after a successful load. The Client reports each owned entry failure with its slot, message, and whether the entry was removed. The Host retains the latest failure and steers the owning session; the page also displays its local failure.
 
 #### Token effect
 

+ 7 - 7
packages/extensions/cordis-client-runner/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-cordis-client-runner` 让页面运行动态 Cordis 包的浏览器半:它应答 host 的运行请求、把浏览器半源码装载进页面成为活插件,并在 host 撤回该次运行时把它移除。人可以批准或拒绝一次运行——也可以直接启动一次——而本包回报的结果变成模型读到的 `cordis_run` 工具结果。激活时什么都不装载,刷新后也不恢复;一页只在有人应答运行请求或在此主动要求时,才运行动态包。
+`dsh-cordis-client-runner` 为程序调用方和现有浏览器控件运行进程内动态包的浏览器部分。它在请求获批或用户显式操作后加载定义,并在 Host 撤销运行时移除定义。页面刷新不会恢复定义。Creator UI 插件通过 Plugin Manager 使用已安装的 Client 模块。
 
 ## 目录
 
@@ -80,7 +80,7 @@ kind: "package-reference"
 当包级约定不够用时阅读以下页面。它们从浏览器半逐步进入发问的 host、其运行被应答的工具,以及渲染它的界面。
 
 - [Host runner](../cordis-host-runner/README.zh.md)——本包应答的注册表与运行往返。
-- [工具包](../tool-cordis/README.zh.md)——运行请求到达本页的模型侧工具。
+- [工具包](../tool-cordis/README.zh.md)——只读运行时 API 发现。
 - [UI 包](../ui-cordis/README.zh.md)——操作这个面的面板与卡片。
 - [extensions 子系统](../../../docs/subsystems/extensions.zh.md)——生成的 `ctx.dynamicCordisRunner` API 与 `cordis/*` 事件。
 - [客户端外壳与动态包 Agent Note](../../../.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md)——浏览器半的包归属与构建面。
@@ -90,25 +90,25 @@ kind: "package-reference"
 <a id="model-experience"></a>
 ## 模型体验
 
-### 由模型发起那次 run 的最终回答
+### Host 转发的运行结果
 
 #### 模型看到的内容
 
-本包自己不贡献任何工具、提示词或上下文;它撰写并到达模型的第一样内容,是为一次 `cordis/request-run` 往返发回的回答——host 把它变成那个被阻塞的 `cordis_run` 的结果。成功时带上已装载的 revision,以及(当浏览器半挂在这一页没有的服务上时)那些服务的名字。失败时带一个 reason:用户拒绝的 `rejected`、`host-half-failed` 或 `client-half-failed`;后者还带上本包自己的文本——出错阶段(`evaluate`、`module-import` 或 `activate`)加上闭包、guard 或 fiber 的消息。guard 的教学错误(未声明的服务、被遮蔽的浏览器全局、返回值里没有 `apply`)正是经这个字段到达模型的。而装载之后、React 渲染时才发生的崩溃,走下面那条独立的事后通道。
+本包不提供工具或提示。它以激活成功、缺失服务、拒绝或 Host/Client 失败响应 `cordis/request-run`。发送给会话的任何消息由 Host runner 负责。
 
 #### Token 影响
 
-有条件且有界:每次 run 请求最多一个回答,花在 host 本来就会发出的那个 `cordis_run` 结果里。文本随数据而定(某个定义自己的错误消息),本包跨请求不留存任何东西——一页后续的装载失败是页面本地诊断,在模型侧没有任何承载物。
+有条件且有界:每次 run 请求最多一个回答,花在 host 本来就会发出的那个 `run` 结果里。文本随数据而定(某个定义自己的错误消息),本包跨请求不留存任何东西——一页后续的装载失败是页面本地诊断,在模型侧没有任何承载物。
 
 #### KV Cache 影响
 
-只追加。回答只作为「本来就在途的那次请求」的工具结果到达模型、延长历史尾部;本包撰写的内容不会重写或重排更早的请求 token,因此原本可复用的前缀仍然可复用。同一定义的多次运行各自产出各自的结果,而不是替换更早那一个。
+Host steering 追加到会话历史;本包不改写更早的消息。
 
 ### run 落定之后的渲染期失败
 
 #### 模型看到的内容
 
-一个装载得干干净净的浏览器半,仍可能在 React 渲染时崩溃,而那次崩溃发生在 run 已经被回答之后——否则模型只会被告知「ok」,永远学不到。凡是本页落座过的包,其 entry 边界的每一次崩溃都会发回 host(`reportRenderFailure`):点名槽位、说明这次崩溃是否已把 entry 从格位上摘掉(`abdicated`:包的 UI 是没了、而不只是坏了),以及一条写给作者的 message。host 每包只留最后一条,用它 steer 所属会话,并经由 `cordis_inspect_self` 暴露;这条通道上的任何东西都不会进入 run 的最终回答。
+React 可能在加载成功后失败。Client 报告其拥有的每个 entry 失败,包括 slot、消息及 entry 是否已移除。Host 保留最新失败并向所属会话发送 steering;页面也显示本地失败。
 
 #### Token 影响
 

+ 2 - 2
packages/extensions/cordis-host-runner/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/extensions/cordis-host-runner/README.md
-README.md: c9f573557607163103adc0ab7277f3ab569d7ab8
-README.zh.md: 10c38095ef30025a8b513e7587b1df381d6316a5
+README.md: 67df264fe2b4a76df6587a726e72e6300c507912
+README.zh.md: 0c8659078e3a78d3ce7284ac4d2e988902f89143

+ 7 - 7
packages/extensions/cordis-host-runner/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-cordis-host-runner` makes dynamic packages runnable in this process: definitions the model records with `cordis_define` stay here, host halves run in a `node:vm` sandbox, a package with a browser half waits for a person to approve or decline it on a page, and the model can inspect the live runtime and its definitions here. The model-facing tools live in `@deepseek-ai/dsh-tool-cordis`, and the browser half loads through `@deepseek-ai/dsh-cordis-client-runner`. Definitions live only in process memory, so a DSH restart clears them and nothing is written to disk. One config field, `vmTimeoutMs`, bounds synchronous sandbox evaluation.
+`dsh-cordis-host-runner` exposes runtime inspection and keeps process-local dynamic definitions available to programmatic callers and browser controls. Host halves run in a `node:vm` realm; browser halves use the Client runner and approval UI. Definitions disappear on restart. Agents discover APIs through `tool-cordis` and install persistent bundles through Plugin Manager; no model tool creates dynamic definitions.
 
 ## Table of Contents
 
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
 <a id="use-this-package"></a>
 ## Use this package
 
-Mount this plugin in any composition that should support dynamic packages — it powers the model's `cordis_*` tools, and packages with a browser half additionally need the client runner plus the UI package composed on the client side. The common path is explicit: load this package, optionally set `vmTimeoutMs`, and let the tools and the browser do the rest.
+Mount this plugin for the inspection registry or programmatic dynamic-package lifecycle. Browser lifecycle consumers also require the Client runner and UI package. The shipped Creator workflow uses installed bundles instead of this definition registry.
 
 ### Minimal configuration
 
@@ -43,11 +43,11 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
 
 ### What a run does
 
-A definition is recorded by `cordis_define` and activated by `cordis_run`. A package with only a host half activates directly in this process: its code runs in the sandbox. A package with a browser half becomes a request: it waits until a person allows or declines it on a page, or the asking turn is cancelled; the answering page then loads the host half first and the browser half second. `mode: "run"` starts the current package or restarts it, `mode: "update"` switches to a different package version. `cordis_stop` ends the live run — removing the package's handlers and any loaded browser UI — while keeping the definition runnable; `cordis_undefine` stops and forgets it.
+Programmatic callers use `define`, `run`, `stop`, and `undefine`; the browser panel operates existing definitions. Host-only packages activate in this process. A package with a browser half waits for approval or cancellation, then loads Host before Client. `mode: "run"` starts the current version; `mode: "update"` replaces it. Stop disposes the live effects and retains the definition; undefine also forgets it.
 
 ### What happens to definitions
 
-Definitions are session-scoped and process-local: a package is visible only to the session that defined it, other sessions read it as absent, and everything disappears on DSH restart. The session log keeps the define call's arguments — including the code it submitted — and the receipt; only the in-memory registry holds the parsed definition. A browser half reaches a page only through a run, so a reloaded page holds nothing until someone runs the package again.
+Definitions are session-scoped and process-local: other sessions read them as absent, and restart clears them. Historical logs retain tool arguments and receipts but do not restore the registry. Reloading a browser page requires another explicit run to load its Client half.
 
 ### Trust stance
 
@@ -92,7 +92,7 @@ The runner is built on two separations. **Registry and sandbox are one service.*
 
 Read these pages when the package-level contract is not enough. They move from the runner to the tools that call it, the browser half that answers it, and the generated surface.
 
-- [Tool package](../tool-cordis/README.md) — the model-facing tools that call this service.
+- [Tool package](../tool-cordis/README.md) — the read-only tools that use its inspection registry.
 - [Client runner](../cordis-client-runner/README.md) — the browser half that answers run requests and loads browser-half code.
 - [UI package](../ui-cordis/README.md) — the panel users approve and operate runs with.
 - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-cordis-host-runner) — every accepted config field.
@@ -108,7 +108,7 @@ Read these pages when the package-level contract is not enough. They move from t
 
 #### What the model sees
 
-Nothing directly: this package registers no tool and injects no prompt. It steers the owning session when a run settles — a success names the current package and says to continue, a user rejection says not to request the same activation again, and a technical failure names the reason, the version pointers, and the inspect-then-correct-and-update path. It also steers post-settle render failures (slot, whether the entry was removed), host guard rejections, and host handler failures. Panel stop and remove gestures inject a user-role message naming what the user did. Refusals from `run` or `stop` also reach the model through the calling tool's result.
+This package registers no tool or prompt. Programmatic `run` calls and browser controls can steer the owning session with outcomes and diagnostics; stop and remove gestures inject a user message. Shipped model tools cannot create or update dynamic definitions.
 
 #### Token effect
 
@@ -125,7 +125,7 @@ None of its own. A host half that registers tools changes the next request's too
 
 These limits define when the runner needs special care. They are current package constraints, not a task backlog.
 
-- **A successful run does not mean the UI rendered** — `run` returns once the answering page has loaded the browser half; React renders afterwards, so a component that throws cannot appear in the run receipt. The failure surfaces through steering and `cordis_inspect_self` diagnostics.
+- **A successful run does not mean the UI rendered** — React renders after the load receipt; failures reach the owning session through steering and appear in the browser panel.
 - **A browser-half package suspends where no page is connected** — headless and ACP deployments hold the run until the asking turn is cancelled; host-only packages are unaffected.
 - **A suspended run request has no timeout** — it waits for a person until the asking turn is cancelled, so unattended automation cannot use packages with a browser half.
 - **`vmTimeoutMs` bounds only synchronous evaluation** — an async host-half body escapes it, matching the toolset's cooperative trust stance.

+ 7 - 7
packages/extensions/cordis-host-runner/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-cordis-host-runner` 让动态包在本进程中可运行:模型用 `cordis_define` 记录的定义留在这里,host 半在 `node:vm` 沙箱中运行,带浏览器半的包会等待人在页面上批准或拒绝,模型也可以在这里检查实时运行时及其定义。面向模型的工具在 `@deepseek-ai/dsh-tool-cordis` 中,浏览器半经 `@deepseek-ai/dsh-cordis-client-runner` 装载。定义只存在于进程内存中,因此 DSH 重启即清空,也不会向磁盘写任何东西。唯一的配置字段 `vmTimeoutMs` 限制同步沙箱求值的时长。
+`dsh-cordis-host-runner` 提供运行时检查,并为程序调用方和浏览器控件保留进程内动态定义。Host 部分在 `node:vm` 中运行;浏览器部分使用 Client runner 和审批 UI。定义在重启后消失。Agent 通过 `tool-cordis` 发现 API,通过 Plugin Manager 安装持久化 bundle;没有模型工具创建动态定义。
 
 ## 目录
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-在任何一个应当支持动态包的组合中挂载本插件——它支撑模型的 `cordis_*` 工具,而带浏览器半的包还需要在客户端组合中额外挂载 client runner 与 UI 包。常用路径是显式的:加载本包,按需设置 `vmTimeoutMs`,其余交给工具与浏览器。
+需要检查注册表或程序侧动态包生命周期时挂载此插件。浏览器生命周期消费者还需要 Client runner 和 UI 包。内置 Creator 流程使用已安装 bundle,不使用此定义注册表。
 
 ### 最小配置
 
@@ -43,11 +43,11 @@ kind: "package-reference"
 
 ### run 会做什么
 
-定义由 `cordis_define` 记录、由 `cordis_run` 激活。只有 host 半的包直接在本进程中激活:它的代码在沙箱中运行。带浏览器半的包变成一次请求:它一直等到有人在一个页面上允许或拒绝,或提问的轮次被取消;作答页面随后先装载 host 半、再装载浏览器半。`mode: "run"` 启动当前包或重启它,`mode: "update"` 切换到另一个包版本。`cordis_stop` 结束一次存活运行——移除该包的 handler 与任何已装载的浏览器 UI——同时保留可再次运行的定义;`cordis_undefine` 停止并忘掉它。
+程序调用方使用 `define`、`run`、`stop` 和 `undefine`;浏览器面板操作已有定义。仅含 Host 的包在本进程激活。带浏览器部分的包等待审批或取消,批准后先加载 Host 再加载 Client。`mode: "run"` 启动当前版本,`mode: "update"` 替换版本。Stop 释放运行中的 effect 并保留定义;undefine 还会移除定义。
 
 ### 定义的去向
 
-定义以会话为界、以进程为本:包只对定义它的会话可见,其他会话读取时视其为不存在,DSH 重启后一切都消失。会话日志保留一次 define 调用的参数——包括它提交的代码——以及回执;解析出的定义只存于内存注册表。浏览器半只能经一次运行到达页面,因此刷新后的页面手上什么都没有,直到有人再次运行该包。
+定义按会话隔离且仅在进程内存在:其他会话无法读取,重启会清空。历史日志保留工具参数和回执,但不会恢复注册表。浏览器页面重载后,需要再次显式运行才能加载 Client 部分。
 
 ### 信任立场
 
@@ -92,7 +92,7 @@ runner 基于两项职责划分。**注册表与沙箱是同一个服务。** `D
 
 当包级约定不够用时阅读以下页面。它们从 runner 逐步进入调用它的工具、应答它的浏览器半与生成的表面。
 
-- [工具包](../tool-cordis/README.zh.md)——调用本服务的模型侧工具。
+- [工具包](../tool-cordis/README.zh.md)——使用其检查注册表的只读工具。
 - [Client runner](../cordis-client-runner/README.zh.md)——应答运行请求并装载浏览器半代码的浏览器半。
 - [UI 包](../ui-cordis/README.zh.md)——用户批准并操作运行的面板。
 - [生成的配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-cordis-host-runner)——每个受支持配置字段。
@@ -108,7 +108,7 @@ runner 基于两项职责划分。**注册表与沙箱是同一个服务。** `D
 
 #### 模型看到的内容
 
-没有直接可见的内容:本包不注册任何工具,也不注入提示词。当一次 run 结算时,它会向所属会话发送 steering——成功时点名当前包并指示继续,用户拒绝时指示不要再次请求同一激活,技术性失败则给出原因、版本指针与「检查—修正—更新」路径。它还会通过 steering 转达结算后的渲染失败(slot、条目是否已被移除)、host guard 拒绝与 host handler 失败。面板上的停止与移除手势会注入一条 user 角色消息,说明用户做了什么。`run` 或 `stop` 的拒绝还会经调用它的工具结果到达模型。
+本包不注册工具或提示。程序侧 `run` 调用和浏览器控件可向所属会话发送结果与诊断;停止和移除操作注入用户消息。内置模型工具无法创建或更新动态定义。
 
 #### Token 影响
 
@@ -125,7 +125,7 @@ runner 基于两项职责划分。**注册表与沙箱是同一个服务。** `D
 
 这些限制说明 runner 何时需要特别小心。它们是当前包约束,不是任务积压。
 
-- **run 成功不等于 UI 渲染成功**——只要作答页面已装载浏览器半,`run` 就会返回;React 是随后才渲染的,因此抛异常的组件不可能出现在 run 回执里。该失败经 steering 与 `cordis_inspect_self` 诊断浮现。
+- **运行成功不代表 UI 已渲染**——React 在加载回执之后渲染;失败通过 steering 发送到所属会话,并显示在浏览器面板中。
 - **带浏览器半的包在没有页面连接的地方挂起**——headless 与 ACP(Agent Client Protocol)部署会把 run 一直挂到提问的轮次被取消;纯 host 包不受影响。
 - **挂起的 run 请求没有超时**——它一直等人,直到提问的轮次被取消,因此无人值守的自动化用不了带浏览器半的包。
 - **`vmTimeoutMs` 只约束同步求值**——async 的 host 半函数体会逃出该上限,这与工具集基于协作的信任立场一致。

+ 5 - 8
packages/extensions/cordis-host-runner/src/index.ts

@@ -1034,11 +1034,11 @@ export class DynamicCordisRunnerService extends TypertRemoteService {
         + 'Do not request the same activation again unless the user asks.'
     } else {
       const returnedStatus = pending.requiresApproval ? 'awaiting-approval' : 'starting'
-      text = `Cordis ${pending.mode} ${identity} failed after cordis_run returned ${returnedStatus}: `
+      text = `Cordis ${pending.mode} ${identity} failed after the runner returned ${returnedStatus}: `
         + `${settled.reason}\n${formatErrorDetails(settled)}\n`
         + `currentPackageId: ${plugin?.currentPackageId ?? 'none'}\n`
         + `nextPackageId: ${plugin?.nextPackageId ?? pending.packageId}\n`
-        + 'Inspect the failed Package, correct it on the same Plugin when needed, and retry the activation autonomously.'
+        + 'Report the failure to the user; the definition can be managed through the Cordis panel.'
     }
     agent.steer(createUserMessage({
       content: [{ type: 'text', text }],
@@ -1060,8 +1060,7 @@ export class DynamicCordisRunnerService extends TypertRemoteService {
           + `Slot "${failure.slot}" after activation.\n`
           + `${formatErrorDetails(failure)}\n`
           + `entryAbdicated: ${failure.abdicated}\n`
-          + 'Inspect the failed Package, fix the Client code by defining a new Package on the same Plugin, and '
-          + 'activate that Package autonomously with cordis_run mode:"update".',
+          + 'Report the Client render failure to the user; the definition can be stopped through the Cordis panel.',
       }],
       source: { kind: 'plugin', plugin: 'cordis-host-runner' },
     }))
@@ -1084,8 +1083,7 @@ export class DynamicCordisRunnerService extends TypertRemoteService {
         text: `Cordis Host handler ${plugin.pluginId}/${run.packageId} (${run.pluginRunId}) failed when the Client called `
           + `host.call(${JSON.stringify(method)}).\n`
           + `${formatErrorDetails(failure)}\n`
-          + 'The Plugin remains running. Inspect this Package, correct the Host code on the same Plugin, and activate '
-          + 'the new Package autonomously with cordis_run mode:"update". If the handler needs a Service, either declare '
+          + 'The Plugin remains running. Report the Host handler failure to the user. If the handler needs a Service, either declare '
           + 'that Service in the returned Plugin inject list or read it with ctx.get(name) and handle undefined.',
       }],
       source: { kind: 'plugin', plugin: 'cordis-host-runner' },
@@ -1109,8 +1107,7 @@ export class DynamicCordisRunnerService extends TypertRemoteService {
         type: 'text',
         text: `Cordis ${platform} guard rejected runtime code in ${plugin.pluginId}/${run.packageId} `
           + `(${run.pluginRunId}) after activation.\n${formatErrorDetails(failure)}\n`
-          + 'The Plugin remains running. Inspect this Package, define a corrected Package on the same Plugin, and '
-          + 'activate it autonomously with cordis_run mode:"update".',
+          + 'The Plugin remains running. Report the guard rejection to the user; it can be stopped through the Cordis panel.',
       }],
       source: { kind: 'plugin', plugin: 'cordis-host-runner' },
     }))

+ 2 - 2
packages/extensions/cordis-host-runner/src/lifecycle.ts

@@ -35,8 +35,8 @@ export async function startHostHalf(
     // while the old run still holds the name — teach the replace recipe.
     if (message.includes('already registered')) {
       throw new Error(
-        `${message} — to REPLACE something an earlier dynamic package registered, first cordis_stop that package's id `
-        + '(find it with cordis_runtime_inspect what:"temporary"), then run the new version.',
+        `${message} — to REPLACE something an earlier dynamic package registered, first stop that package through its runner or the Cordis panel `
+        + 'before running the new version.',
       )
     }
     throw error instanceof Error ? error : new Error(message)

+ 1 - 1
packages/extensions/cordis-host-runner/tests/composition.spec.ts

@@ -175,6 +175,6 @@ describe('stop reaches quiescence', () => {
 
     // A second package registering the same tool name collides; the teaching
     // error points at the stop-then-run recipe rather than a bare conflict.
-    await expect(mount(harness, REVERSE_TOOL_CODE)).rejects.toThrow('first cordis_stop that package\'s id')
+    await expect(mount(harness, REVERSE_TOOL_CODE)).rejects.toThrow('first stop that package through its runner or the Cordis panel')
   })
 })

+ 2 - 2
packages/extensions/tool-cordis/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/extensions/tool-cordis/README.md
-README.md: d7fa7acf14ffbe5e9d3177af3ec354963d05e8db
-README.zh.md: 80d094252848cd11b175686963a15b86490fecd3
+README.md: a0152cf3dbdd08599ba431113123c6aaf097723d
+README.zh.md: cde328fce54b4c18f57b7a044e6afc24215417dd

+ 11 - 131
packages/extensions/tool-cordis/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Model-facing Cordis runtime tools for agents and maintainers choosing, composing, or debugging dynamic-package workflows."
+description: "Read-only runtime API discovery for agents developing and configuring installed Harness plugins."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-tool-cordis` lets a model inspect the live Cordis runtime and create, run, stop, update, or remove temporary dynamic packages with host code, browser code, or both. Package versions are immutable, so a failed package can be corrected by adding a new version and updating the active one. Definitions exist only in process memory and disappear when DSH restarts; the package does not write repository files, install dependencies, or change `cordis.yml`. It also teaches the model this workflow. Compose it with `@deepseek-ai/dsh-cordis-host-runner`, which provides the sandbox and run round trip.
+Inspect Host and Client runtime APIs before writing plugin code. Creator mode provides these read-only tools alongside Plugin Manager, which owns persistent profile changes. The inspection registry is supplied by the Cordis host runner; browser queries need a connected page.
 
 ## Table of Contents
 
@@ -25,38 +25,7 @@ English | [中文](README.zh.md)
 <a id="use-this-package"></a>
 ## Use this package
 
-Mount this plugin when a session should be able to extend its own runtime temporarily — for example, a model-written tool, service, or browser UI that helps the current work but should not become a repository plugin. Compose it with the host runner; without the runner the tools never activate, and no shipped bundle mounts the toolset (the web profile already mounts the host runner and the browser faces), so add the tool row explicitly.
-
-### Minimal composition
-
-```yaml
-- name: '@deepseek-ai/dsh-cordis-host-runner'
-  config:
-    vmTimeoutMs: 5000
-- name: '@deepseek-ai/dsh-tool-cordis'
-```
-
-The CLI example [`apps/cli/config/examples/cordis/cordis.yml`](../../../apps/cli/config/examples/cordis/cordis.yml) composes both. A package with a browser half additionally needs the browser runner and the UI package in the client composition; a host-only package needs none of them.
-
-### What the tools do
-
-The three inspect tools are read-only; the four lifecycle tools define and manage packages. All results are JSON rendered as text.
-
-- `cordis_inspect_list` — list the Inspect Providers (host and client) and their query methods.
-- `cordis_inspect_query` — run one provider query: exact service methods, event modes, builtin signatures, tool schemas, theme tokens, or live slot trees.
-- `cordis_inspect_self` — this session's dynamic plugins: version pointers, latest run, and, for one exact package, its source and runtime diagnostics.
-- `cordis_define` — record a package: a new plugin (`plugin.kind: "new"` with a 3–6-letter `idPrefix`) or a new version of an existing plugin (`plugin.kind: "existing"` with its `pluginId`). It validates parameters and syntax only; nothing runs and no approval is requested.
-- `cordis_run` — activate one package (`mode: "run"` for the first activation or restart, `mode: "update"` to switch versions). A package with a browser half may return `awaiting-approval` until a person allows it; the tool never waits for the final outcome.
-- `cordis_stop` — stop the current run and cancel any pending approval, keeping the plugin and every package version.
-- `cordis_undefine` — stop and permanently remove a plugin and all of its packages.
-
-### A typical workflow
-
-Inspect before writing, then define, then run: `cordis_inspect_query` reads the exact contract of the service or slot the package will use, `cordis_define` records the source (and the conversation shows a define card pointing to the panel where the run control lives), and `cordis_run` activates it. When the user types `@pluginId`, this package injects a context message that pins the referenced plugin, its base package, and the update path. After a technical failure, read the diagnostics with `cordis_inspect_self`, append a corrected package to the same plugin, and update to it.
-
-### Boundaries to plan around
-
-Definitions are session-scoped and process-local: a package is visible and controllable only in the session that defined it, stays active across later turns, and can affect other sessions in the same process while running. Stopping, removing, unloading the toolset, or restarting DSH clears it. The sandbox isolates globals but is not a security boundary — treat a dynamic package like bash access, and load this plugin as deliberately as you would grant one.
+Creator mode includes this toolset. Other compositions mount `@deepseek-ai/dsh-tool-cordis` alongside the host runner that provides `cordisInspect`. Call `cordis_inspect_list` to discover providers, then `cordis_inspect_query` for a provider's exact methods and types. Use [Plugin Manager](../../boot/plugin-manager/README.md) to install bundles containing plugin code or MCP configuration.
 
 -----
 
@@ -66,26 +35,7 @@ Definitions are session-scoped and process-local: a package is visible and contr
 <details>
 <summary>Implementation internals — click to expand</summary>
 
-This section explains the design behind the tools; the observable behavior is fully covered in [Use this package](#use-this-package).
-
-### Design philosophy
-
-The toolset is built on one separation: the tools are a thin, model-facing layer over the runner service. Inspection data comes from generated catalogs intersected with the live service store; definition and lifecycle verbs delegate to `ctx.dynamicCordisRunner`, which owns the registry, the vm sandbox, and the browser round trip. The tools add the model-facing judgments: only callable methods are shown, only keys a host half can reach are named, and every refusal is a teaching error the model can act on.
-
-### Source map
-
-| File | Role |
-|---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: tool registration, system-prompt section, `@pluginId` context injection |
-| [`src/inspect.ts`](src/inspect.ts) | Report rendering: joins the generated API catalog with the live service store |
-| [`src/api-catalog.ts`](src/api-catalog.ts) | Generated projection of the workspace's Cordis declarations (regenerated by `pnpm run gen-cordis-api`, gated by `verify-cordis-api`) |
-| [`src/prompt.ts`](src/prompt.ts) | The `tool:cordis` system-prompt section |
-| [`src/providers.ts`](src/providers.ts) | First-party host Inspect Providers: Service, Event, Builtin, Tool |
-| [`src/present.ts`](src/present.ts) | Replay-safe generic card render intents |
-
-### How a call flows
-
-An inspect call queries `ctx.cordisInspect`: host providers run locally, client providers wait for the first valid page response. Define prechecks each half's syntax by compiling it in the same wrapper the sandbox uses, so unparseable code is refused before an id exists. Run delegates to the runner, which activates host-only packages in-process and suspends browser-half packages on a `cordis/request-run` round trip; the tool returns the runner's receipt (`awaiting-approval`, `starting`, or `running`). When the user writes `@pluginId`, an `agent/pre-step` handler reads the reference and injects a user-role context message naming the base package and the required next steps.
+Host providers combine generated Service/Event catalogs and the requesting agent's tool registry. Client providers synchronize their manifests through the existing inspection registry and answer queries from a connected page. The tool plugin owns its registrations through Cordis effects; disposal removes both tools and prompt contributions. No invariant companion is published because inspection reads its providers directly and maintains no independent runtime projection.
 
 </details>
 
@@ -94,103 +44,33 @@ An inspect call queries `ctx.cordisInspect`: host providers run locally, client
 <a id="further-exploration"></a>
 ## Further Exploration
 
-Read these pages when the package-level contract is not enough. They move from the shared toolset to the runner internals, the generated schemas, and the subsystem surface.
-
-- [Host runner](../cordis-host-runner/README.md) — the registry, sandbox, and run round trip these tools delegate to.
-- [Client runner](../cordis-client-runner/README.md) — the browser half that answers run requests and loads browser-half code.
-- [UI package](../ui-cordis/README.md) — the panel and tool cards users operate definitions with.
-- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-cordis) — the exact schemas the model receives.
-- [Extensions subsystem](../../../docs/subsystems/extensions.md) — the generated `ctx.cordisInspect` and `ctx.dynamicCordisRunner` API.
-- [Self-referential Cordis toolset Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md) — design home: sandbox semantics, dynamic-package lifecycle, and composition.
-
------
+- [Plugin Manager](../../boot/plugin-manager/README.md) — persistent bundle installation and enablement.
+- [Cordis host runner](../cordis-host-runner/README.md) — inspection registry and existing runtime consumers.
 
 <a id="model-experience"></a>
 ## Model Experience
 
-### Tool schemas
+### Runtime inspection
 
 #### What the model sees
 
-The conversation model sees the generated [`cordis_inspect_list`, `cordis_inspect_query`, `cordis_inspect_self`, `cordis_define`, `cordis_run`, `cordis_stop`, and `cordis_undefine` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-cordis) whenever this plugin is visible.
+The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-cordis) describes two read-only inspection tools. The [prompt](src/prompt.ts) directs persistent changes through Plugin Manager and describes MCP setup. Creator visual requests default to an installed UI plugin displayed in the current Web page; the development skill covers Client packaging and slot registration. Query results contain the requested API declarations or live tool schemas.
 
 #### Token effect
 
-Fixed schema cost on every request in that tool view.
+Both tool schemas and the guidance section enter model requests while this plugin is visible. Query results append to the transcript; exact queries avoid loading unrelated declarations.
 
 #### KV Cache effect
 
-Prefix-stable while this tool view is unchanged. Scoping or plugin-lifecycle changes that hide these definitions may invalidate reuse from the first changed schema token.
-
-### System prompt section
-
-#### What the model sees
-
-This package registers one system-prompt section (`tool:cordis`, order 115) teaching when and how to use the dynamic-plugin workflow, the recommended tool sequence, and the high-frequency errors to avoid; the full text lives in [`src/prompt.ts`](src/prompt.ts). The section opens with:
-
-##### Section opening
-
-```markdown
-# Dynamic Cordis Plugins
-
-Dynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.
-```
-
-#### Token effect
-
-The section's rendered text repeats on every request while this plugin is visible.
-
-#### KV Cache effect
-
-Prefix-stable while the section text and order are unchanged; editing the prompt or changing its order may invalidate reuse from the first changed token.
-
-### Tool-call history and results
-
-#### What the model sees
-
-Inspect outputs are JSON rendered as text: `cordis_inspect_list` returns the provider directory, `cordis_inspect_query` the queried data, and `cordis_inspect_self` a plugin, version, and package summary with source and diagnostics for an exact package. Define answers that the package is defined and not running yet, with the ids to run. Run reports `awaiting-approval`, `starting`, or `running` with the run id and version pointers. Stop and undefine acknowledge in one line. Every refusal is a tool error carrying the runner's teaching text, and the submitted program stays in assistant tool-call history.
-
-#### Token effect
-
-Inspect output and submitted package code are data-dependent and resent until compaction; lifecycle acknowledgements are small.
-
-#### KV Cache effect
-
-Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV Cache entries.
-
-### Later requests after cordis_run
-
-#### What the model sees
-
-A running package may register tools, prompt contributions, or listeners that change later requests for the scopes it targets; `cordis_stop` and `cordis_undefine` remove those contributions after quiescence. When the user types `@pluginId`, the injected reference context also adds a user-role message naming the base package and the next steps.
-
-#### Token effect
-
-Indirect token impact equals the running package's contributions and lasts only for its process-local lifetime.
-
-#### KV Cache effect
-
-Running or stopping a prompt or tool contribution changes later request prefixes and may invalidate reuse from the first changed contribution; an unchanged running set remains prefix-stable.
+Unchanged schemas and guidance remain prefix-stable. Query results append to history; enabling other plugins can change subsequent tool schemas.
 
 ## Known Limitations and Deferred Work
 
 <a id="known-limitations-and-deferred-work"></a>
 
-
-These limits define when the toolset is a poor fit or needs special care. They are current package constraints, not a task backlog.
-
-- **The sandbox is containment for honest code, not a security boundary** — host-realm helpers on the sandbox global are reachable, so package code can reach Node; load this plugin as deliberately as you would grant a bash tool.
-- **Plain JavaScript only** — dynamic package code is not transformed: no TypeScript, JSX, or imports, and the sandbox withholds Node globals such as `require`, `setTimeout`, and `fetch`, redirecting filesystem, network, and process work to Cordis services.
-- **The vm and approval bounds belong to the runner** — see its [Known Limitations](../cordis-host-runner/README.md#known-limitations-and-deferred-work); an async host-half body escapes `vmTimeoutMs`.
+- Client queries wait for a responding page or cancellation. Inspection cannot invoke service methods, configure plugins, or execute generated code.
 
 <a id="dev-note"></a>
 ### Dev Note
 
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
 None.
-
-</details>
-
-**Runtime invariant:** No companion is published. This model-facing adapter has no independent lifecycle stream; execution relations are owned by the capability seam it calls.

+ 15 - 135
packages/extensions/tool-cordis/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "面向 agent(智能体)与维护者的 Cordis 运行时工具说明,用于选择、组合或排查动态包工作流。"
+description: "为开发和配置已安装 Harness 插件的 agent 提供只读运行时 API 查询。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-tool-cordis` 让模型检查实时 Cordis 运行时,并创建、运行、停止、更新或移除包含 host 代码、浏览器代码或两者的临时动态包。包版本不可变,因此包失败后,模型可以添加新版本并更新当前运行的版本。定义只存在于进程内存中,DSH 重启即消失;本包不写仓库文件、不安装依赖,也不改 `cordis.yml`。它还会把这套工作流教给模型。请与 `@deepseek-ai/dsh-cordis-host-runner` 一同组合,后者提供沙箱与运行往返。
+编写插件代码前查询 Host 和 Client 的运行时 API。创造模式同时提供这些只读工具与 Plugin Manager,后者负责持久化 profile 变更。检查注册表由 Cordis host runner 提供;浏览器查询需要已连接的页面。
 
 ## 目录
 
@@ -17,7 +17,7 @@ kind: "package-reference"
 - [理解实现](#understand-the-implementation)
 - [进一步探索](#further-exploration)
 - [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
+- [已知限制与待办](#known-limitations-and-deferred-work)
 - [开发备注](#dev-note)
 
 -----
@@ -25,38 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-当某个会话应当能临时扩展它自己的运行时——例如一个对当前工作有用、但不应成为仓库插件的模型编写的工具、服务或浏览器 UI——挂载本插件。请与 host runner 一同组合;没有 runner,这些工具永远不会激活,而且任何已发布的组合包都不会挂载这套工具集(web profile 已挂载 host runner 与浏览器侧组件),所以要显式地添加工具行。
-
-### 最小组合
-
-```yaml
-- name: '@deepseek-ai/dsh-cordis-host-runner'
-  config:
-    vmTimeoutMs: 5000
-- name: '@deepseek-ai/dsh-tool-cordis'
-```
-
-CLI 示例 [`apps/cli/config/examples/cordis/cordis.yml`](../../../apps/cli/config/examples/cordis/cordis.yml) 同时组合了这两者。带浏览器半的包还额外需要客户端组合里的浏览器 runner 与 UI 包;纯 host 包则两者都不需要。
-
-### 工具能做什么
-
-三个检查工具只读;四个生命周期工具定义并管理包。所有结果都是渲染成文本的 JSON。
-
-- `cordis_inspect_list`——列出 Inspect Provider(host 与 client)及其查询方法。
-- `cordis_inspect_query`——执行一次提供方查询:精确的服务方法、事件模式、builtin 签名、工具 schema、主题 token 或实时 slot 树。
-- `cordis_inspect_self`——本会话的动态插件:版本指针、最近一次运行,以及(对某个精确包而言)源码与运行时诊断。
-- `cordis_define`——登记一个包:新插件(`plugin.kind: "new"`,配 3–6 个字母的 `idPrefix`),或既有插件的新版本(`plugin.kind: "existing"`,配其 `pluginId`)。它只校验参数与语法;不运行任何东西,也不请求审批。
-- `cordis_run`——激活一个包(首次激活或重启用 `mode: "run"`,切换版本用 `mode: "update"`)。带浏览器半的包可能先返回 `awaiting-approval`,直到有人允许;工具从不等待最终结果。
-- `cordis_stop`——停止当前运行并取消任何待审批请求,保留插件与全部包版本。
-- `cordis_undefine`——停止并彻底移除一个插件及其全部包。
-
-### 典型工作流
-
-先检查、再定义、后运行:`cordis_inspect_query` 读取包要用的服务或 slot 的精确约定,`cordis_define` 记录源码(会话里会出现一张 define 卡片,指向存放运行控件的面板),`cordis_run` 激活它。当用户输入 `@pluginId` 时,本包注入一条上下文消息,钉住所引用的插件、其基准包与更新路径。技术性失败之后,用 `cordis_inspect_self` 读取诊断,向同一插件追加修正版,再更新到该版本。
-
-### 需要规划的边界
-
-定义以会话为界、以进程为本:包只在定义它的会话里可见可控,可跨后续轮次保持活跃,运行时也可能影响同一进程中的其他会话。停止、移除、卸载工具集或重启 DSH 都会清除它。沙箱隔离全局变量,但不是安全边界——对待动态包要像对待 bash 访问一样,加载本插件时也要像授予 bash 工具那样慎重。
+创造模式包含这组工具。其他组合需要同时挂载 `@deepseek-ai/dsh-tool-cordis` 和提供 `cordisInspect` 的 host runner。调用 `cordis_inspect_list` 发现 provider,再用 `cordis_inspect_query` 查询其具体方法和类型。通过 [Plugin Manager](../../boot/plugin-manager/README.zh.md) 安装包含插件代码或 MCP 配置的组合包。
 
 -----
 
@@ -64,28 +33,9 @@ CLI 示例 [`apps/cli/config/examples/cordis/cordis.yml`](../../../apps/cli/conf
 ## 理解实现
 
 <details>
-<summary>实现细节——点击展开</summary>
-
-本节解释工具背后的设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
-
-### 设计理念
-
-工具集基于一项职责分离原则:工具是在 runner 服务之上面向模型的轻量层。检查数据来自生成的目录与实时服务存储的交集;定义与生命周期操作委托给 `ctx.dynamicCordisRunner`,它拥有注册表、vm 沙箱与浏览器往返。工具层负责面向模型作出判断:只展示可调用的方法、只列出 host 侧可访问的键,并且每次拒绝都会提供可指导模型采取行动的错误信息。
-
-### 源码地图
+<summary>实现细节 — 点击展开</summary>
 
-| 文件 | 职责 |
-|---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:工具注册、系统提示词章节、`@pluginId` 上下文注入 |
-| [`src/inspect.ts`](src/inspect.ts) | 报告渲染:把生成的 API 目录与实时服务存储相交 |
-| [`src/api-catalog.ts`](src/api-catalog.ts) | 工作区 Cordis 声明的生成投影(由 `pnpm run gen-cordis-api` 重新生成,`verify-cordis-api` 守其新鲜度) |
-| [`src/prompt.ts`](src/prompt.ts) | `tool:cordis` 系统提示词章节 |
-| [`src/providers.ts`](src/providers.ts) | 第一方 host Inspect Provider:Service、Event、Builtin、Tool |
-| [`src/present.ts`](src/present.ts) | 可安全回放的通用卡片渲染意图 |
-
-### 一次调用的流程
-
-检查调用查询 `ctx.cordisInspect`:host 提供方在本地执行,client 提供方等待第一个有效的页面应答。define 用与沙箱相同的包装器编译每一半来做语法预检,因此无法解析的代码在拿到 id 之前就被拒绝。run 委托给 runner:纯 host 包在进程内激活,带浏览器半的包挂起在 `cordis/request-run` 往返上;工具返回 runner 的回执(`awaiting-approval`、`starting` 或 `running`)。当用户写下 `@pluginId` 时,一个 `agent/pre-step` 处理器读取引用,并注入一条 user 角色的上下文消息,点明基准包与必须的后续步骤。
+Host provider 结合生成的 Service/Event 目录与请求 agent 的工具注册表。Client provider 通过现有检查注册表同步清单,并从已连接页面回答查询。工具插件通过 Cordis effect 持有注册;释放时移除工具和提示词贡献。检查直接读取 provider,不维护独立运行时投影,因此不发布不变式配套插件。
 
 </details>
 
@@ -94,103 +44,33 @@ CLI 示例 [`apps/cli/config/examples/cordis/cordis.yml`](../../../apps/cli/conf
 <a id="further-exploration"></a>
 ## 进一步探索
 
-当包级约定不够用时阅读以下页面。它们从共享工具集逐步进入 runner 内部、生成 schema 与子系统接口。
-
-- [Host runner](../cordis-host-runner/README.zh.md)——这些工具委托的注册表、沙箱与运行往返。
-- [Client runner](../cordis-client-runner/README.zh.md)——应答运行请求并装载浏览器半代码的浏览器半。
-- [UI 包](../ui-cordis/README.zh.md)——用户操作定义所用的面板与工具卡片。
-- [生成的工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-cordis)——模型收到的确切 schema。
-- [extensions 子系统](../../../docs/subsystems/extensions.zh.md)——生成的 `ctx.cordisInspect` 与 `ctx.dynamicCordisRunner` API。
-- [自引用 Cordis 工具集 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md)——设计居所:沙箱语义、动态包生命周期与组合。
-
------
+- [Plugin Manager](../../boot/plugin-manager/README.zh.md) — 持久化组合包安装和启停。
+- [Cordis host runner](../cordis-host-runner/README.zh.md) — 检查注册表和现有运行时消费者。
 
 <a id="model-experience"></a>
 ## 模型体验
 
-### 工具 schema
-
-#### 模型看到的内容
-
-该插件可见时,会话模型会看到生成的 [`cordis_inspect_list`、`cordis_inspect_query`、`cordis_inspect_self`、`cordis_define`、`cordis_run`、`cordis_stop` 和 `cordis_undefine` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-cordis)。
-
-#### Token 影响
-
-该工具视图中的每次请求承担固定 schema 成本。
-
-#### KV Cache 影响
-
-只要该工具视图不变,前缀就保持稳定。隐藏这些定义的 scope 或插件生命周期变更,可能使从第一个变化的 schema token 起的复用失效。
-
-### 系统提示词章节
+### 运行时检查
 
-#### 模型看到的内容
+#### 模型所见
 
-本包注册一个系统提示词章节(`tool:cordis`,order 115),教模型何时以及如何使用动态包工作流、推荐的工具顺序与必须避免的高频错误;完整文本在 [`src/prompt.ts`](src/prompt.ts) 中。章节开头如下:
-
-##### 章节开头
-
-```markdown
-# Dynamic Cordis Plugins
-
-Dynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.
-```
-
-#### Token 影响
-
-该插件可见时,章节渲染出的文本会在每次请求中重复。
-
-#### KV Cache 影响
-
-只要章节文本与顺序不变,前缀就保持稳定;编辑提示词或改变其顺序可能使从第一个变化 token 起的复用失效。
-
-### 工具调用历史与结果
-
-#### 模型看到的内容
-
-检查输出是渲染成文本的 JSON:`cordis_inspect_list` 返回提供方目录,`cordis_inspect_query` 返回查询数据,`cordis_inspect_self` 返回插件、版本与包摘要,并在指定精确包时给出源码与诊断。define 返回该包已定义但尚未运行,并给出用于运行的 id。run 返回 `awaiting-approval`、`starting` 或 `running`,附运行 id 与版本指针。stop 与 undefine 各返回一行确认信息。每一次拒绝都是携带 runner 教学文本的工具错误,提交的程序保留在 assistant 工具调用历史中。
-
-#### Token 影响
-
-检查输出与提交的包代码取决于数据,并在压缩(compaction)前重复发送;生命周期确认文本很短。
-
-#### KV Cache 影响
-
-仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
-
-### cordis_run 之后的后续请求
-
-#### 模型看到的内容
-
-运行中的包可能注册工具、提示词贡献或监听器,改变其目标 scope 的后续请求;`cordis_stop` 与 `cordis_undefine` 会在完全停稳后移除这些贡献。当用户输入 `@pluginId` 时,注入的引用上下文还会增加一条 user 角色的消息,点明基准包与后续步骤。
+[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-cordis) 描述两个只读检查工具。[提示词](src/prompt.ts) 指引模型通过 Plugin Manager 进行持久化变更并说明 MCP 设置方式。创造模式的视觉请求默认通过已安装的 UI 插件显示在当前 Web 页面;开发技能说明 Client 打包和 slot 注册方法。查询结果包含所请求的 API 声明或当前工具 schema。
 
 #### Token 影响
 
-间接 token 影响等于运行中包的贡献,且只在其进程内生命周期内持续。
+插件可见时,两个工具 schema 和指导段落进入模型请求。查询结果追加到转录中;精确查询避免加载无关声明。
 
 #### KV Cache 影响
 
-运行或停止提示词/工具贡献会改变后续请求前缀,并可能使从第一个变化的贡献起的复用失效;运行集合不变时,前缀保持稳定。
+未改变的 schema 和指导保持前缀稳定。查询结果追加到历史中;启用其他插件可能改变后续工具 schema。
 
-## 已知限制与延期工作
+## 已知限制与待办
 
 <a id="known-limitations-and-deferred-work"></a>
 
-
-这些限制说明工具集何时不合适或需要特别小心。它们是当前包约束,不是任务积压。
-
-- **沙箱只用于约束诚实代码,并非安全边界**——可以触及沙箱全局变量上的 host realm helper,因此包代码可以触达 Node;加载本插件时,应当像授予 bash 工具一样慎重。
-- **只支持纯 JavaScript**——动态包代码不做任何转换:没有 TypeScript、JSX 或 import,沙箱还不提供 `require`、`setTimeout`、`fetch` 等 Node 全局变量,把文件、网络与进程工作重定向到 Cordis 服务。
-- **vm 与审批边界属于 runner**——见它的[已知限制](../cordis-host-runner/README.zh.md#known-limitations-and-deferred-work);async 的 host 半主体可逃出 `vmTimeoutMs`。
+- Client 查询等待页面响应或取消。检查不能调用服务方法、配置插件或执行生成代码。
 
 <a id="dev-note"></a>
 ### 开发备注
 
-<details>
-<summary>维护者的工作上下文——点击展开</summary>
-
 无。
-
-</details>
-
-**运行时不变式:** 不发布伴生入口。这个面向模型的适配器没有独立 lifecycle stream;执行关系由它调用的能力 seam 负责。

+ 1 - 7
packages/extensions/tool-cordis/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-tool-cordis",
-  "description": "Self-referential cordis toolset: inspect the live runtime, mount and dispose model-written plugins",
+  "description": "Read-only runtime API inspection for Harness plugin development",
   "version": "0.1.6-alpha.1",
   "publishConfig": {
     "access": "public"
@@ -29,9 +29,6 @@
   "peerDependencies": {
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-scope": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^"
@@ -40,9 +37,6 @@
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-cordis-host-runner": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-scope": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^"

+ 10 - 1
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1493,6 +1493,11 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
     summary: 'Current profile facts; scheduling and mutation belong to their callers.',
     description: 'Current profile facts; scheduling and mutation belong to their callers.',
     methods: [
+      {
+        signature: 'readonly packageManager?: ProfilePnpmInvocation',
+        description: 'Packaged applications supply their bundled runtime instead of a PATH executable.',
+        parameters: [],
+      },
       {
         signature: 'readonly startedBundles: readonly string[]',
         description: 'Bundle packages used to start this process, before any persisted edits.',
@@ -4531,7 +4536,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'FiberState',
-    declaration: 'export type FiberState = FiberStateEnum;',
+    declaration: 'export const enum FiberState {\n    PENDING,\n    LOADING,\n    ACTIVE,\n    FAILED,\n    DISPOSED,\n    UNLOADING\n}',
   },
   {
     name: 'FileAttachmentRef',
@@ -5193,6 +5198,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'PreToolDecision',
     declaration: 'export type PreToolDecision = {\n    kind: \'allow\';\n} | {\n    kind: \'deny\';\n    reason: string;\n    info?: ToolErrorInfo;\n} | {\n    kind: \'cancel\';\n} | {\n    kind: \'ask\';\n    reason?: string;\n};',
   },
+  {
+    name: 'ProfilePnpmInvocation',
+    declaration: 'export interface ProfilePnpmInvocation {\n    readonly command: string;\n    readonly args: readonly string[];\n    readonly env: Readonly<Record<string, string>>;\n}',
+  },
   {
     name: 'ProjectionChangeListener',
     declaration: 'export type ProjectionChangeListener = (session: Session, key: Extract<keyof SessionProjectionMap, string>, value: unknown, seq: SessionSeq) => void;',

+ 0 - 31
packages/extensions/tool-cordis/src/fiber-state.ts

@@ -1,31 +0,0 @@
-/**
- * Runtime mirror and labels for Cordis's `FiberState` const enum. A const enum has no runtime
- * object to import, so these values mirror the pinned vendored definition while retaining its
- * type.
- * @module @deepseek-ai/dsh-tool-cordis/fiber-state
- */
-
-import type { FiberState as FiberStateEnum } from '@deepseek-ai/cordis'
-
-/** Value mirror of the cordis `FiberState` const enum (see the module doc for why a mirror exists). */
-export const FiberState = {
-  PENDING: 0 as FiberStateEnum.PENDING,
-  LOADING: 1 as FiberStateEnum.LOADING,
-  ACTIVE: 2 as FiberStateEnum.ACTIVE,
-  FAILED: 3 as FiberStateEnum.FAILED,
-  DISPOSED: 4 as FiberStateEnum.DISPOSED,
-  UNLOADING: 5 as FiberStateEnum.UNLOADING,
-} as const
-
-/** The cordis `FiberState` enum type, re-exported so mirror consumers need one import. */
-export type FiberState = FiberStateEnum
-
-/** Human-readable label for each {@link FiberState}, keyed by member (inlining-safe — no reverse mapping). */
-export const STATE_LABELS = {
-  [FiberState.PENDING]: 'pending',
-  [FiberState.LOADING]: 'loading',
-  [FiberState.ACTIVE]: 'active',
-  [FiberState.FAILED]: 'failed',
-  [FiberState.DISPOSED]: 'disposed',
-  [FiberState.UNLOADING]: 'unloading',
-} as const satisfies Record<FiberState, string>

+ 11 - 462
packages/extensions/tool-cordis/src/index.ts

@@ -1,52 +1,35 @@
-/**
- * Model-facing Cordis runtime/package inspection, define, run, stop, and remove tools.
- * @module @deepseek-ai/dsh-tool-cordis
- */
-
+/** Read-only Host and Client runtime API discovery for plugin development. */
 import type { Context } from '@deepseek-ai/cordis'
-import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
-import {
-  CordisDynamicPackageId, CordisDynamicPluginId,
-} from '@deepseek-ai/dsh-cordis-host-runner'
-import type { DynamicCordisReference } from '@deepseek-ai/dsh-cordis-host-runner'
-import { createUserMessage } from '@deepseek-ai/dsh-llm'
+import type { Agent } from '@deepseek-ai/dsh-agent'
 import type { JsonValue } from '@deepseek-ai/dsh-util-values'
-import type { UserMessage } from '@deepseek-ai/dsh-session'
 import { defineTool } from '@deepseek-ai/dsh-tools'
 import type { ToolExecution } from '@deepseek-ai/dsh-tools'
-import { missingServices, providedServices } from './inspect.ts'
-import {
-  presentDefineCall, presentInspectListCall, presentInspectQueryCall, presentInspectSelfCall, presentRunCall,
-  presentStopCall, presentUndefineCall,
-} from './present.ts'
+import { presentInspectListCall, presentInspectQueryCall } from './present.ts'
 import { CORDIS_SYSTEM_PROMPT } from './prompt.ts'
 import { hostInspectProviders } from './providers.ts'
 
 export const name = 'tool-cordis'
-export const inject = ['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']
+export const inject = ['tools', 'systemPrompt', 'cordisInspect']
 
 function requireAgent(exec: ToolExecution): Agent {
-  if (exec.agent === undefined) throw new Error('Cordis dynamic tools require an Agent-backed session')
+  if (exec.agent === undefined) throw new Error('Cordis inspection requires an Agent-backed session')
   return exec.agent
 }
 
-/** Register the Cordis tools and explicit `@pluginId` context injection. */
+/** Register read-only runtime inspection tools.
+ * @param ctx Agent-scoped registration context.
+ */
 export function apply(ctx: Context): void {
-  ctx.systemPrompt.section({
-    name: 'tool:cordis',
-    order: ctx.systemPrompt.getSectionOrder('TOOL_CORDIS'),
-    text: CORDIS_SYSTEM_PROMPT,
-  })
+  ctx.systemPrompt.section({ name: 'tool:cordis', order: ctx.systemPrompt.getSectionOrder('TOOL_CORDIS'), text: CORDIS_SYSTEM_PROMPT })
   for (const provider of hostInspectProviders(ctx)) {
     ctx.effect(() => ctx.cordisInspect.register(provider), `tool-cordis: inspect ${provider.manifest.id}`)
   }
-
   ctx.tools.register(defineTool({
     name: 'cordis_inspect_list',
     description:
       'List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest '
       + 'manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and '
-      + 'input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and '
+      + 'input/output schemas. Call this Tool before writing or configuring a plugin, then select the provider and '
       + 'method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business '
       + 'Service that Plugin code can call.',
     parameters: {},
@@ -64,7 +47,7 @@ export function apply(ctx: Context): void {
     name: 'cordis_inspect_query',
     description:
       'Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come '
-      + 'from cordis_inspect_list, and input must satisfy that method\'s schema. Use this Tool before cordis_define '
+      + 'from cordis_inspect_list, and input must satisfy that method\'s schema. Use this Tool before writing plugin code '
       + 'to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot '
       + 'trees and props. Host queries run locally. A Client query waits for the first valid page response and '
       + 'remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service '
@@ -96,438 +79,4 @@ export function apply(ctx: Context): void {
     presentCall: presentInspectQueryCall,
   }))
 
-  ctx.tools.register(defineTool({
-    name: 'cordis_inspect_self',
-    description:
-      'Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, '
-      + 'list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package '
-      + 'summary. Only pluginId plus packageId returns that immutable Package\'s Host/Client source and runtime '
-      + 'diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing '
-      + 'an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code '
-      + 'nor changes version pointers.',
-    parameters: {
-      pluginId: { type: 'string', description: 'Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin.' },
-      packageId: { type: 'string', description: 'Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned.' },
-    },
-    output: {
-      schema: { type: 'json' },
-      render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
-    },
-    execute(args, exec): Promise<JsonValue> {
-      const agent = requireAgent(exec)
-      if (args.packageId !== undefined && args.pluginId === undefined) {
-        throw new Error('cordis_inspect_self packageId requires pluginId')
-      }
-      if (args.pluginId === undefined) {
-        return Promise.resolve({
-          mode: 'plugins',
-          plugins: ctx.dynamicCordisRunner.listPlugins(agent).map(reference => selfSummary(reference)),
-        } as unknown as JsonValue)
-      }
-      const pluginId = CordisDynamicPluginId(args.pluginId)
-      if (args.packageId === undefined) {
-        const plugin = ctx.dynamicCordisRunner.inspectPlugin(agent, pluginId)
-        return Promise.resolve({
-          mode: 'plugin',
-          ...selfSummary(plugin),
-          packages: plugin.packages.map(pkg => ({
-            ...pkg,
-            packageId: String(pkg.packageId),
-            isCurrent: pkg.packageId === plugin.currentPackageId,
-            isNext: pkg.packageId === plugin.nextPackageId,
-          })),
-        } as unknown as JsonValue)
-      }
-      return Promise.resolve(inspectSelfPackage(
-        ctx,
-        agent,
-        pluginId,
-        CordisDynamicPackageId(args.packageId),
-      ) as unknown as JsonValue)
-    },
-    presentCall: presentInspectSelfCall,
-  }))
-
-  ctx.tools.register(defineTool({
-    name: 'cordis_define',
-    description:
-      'Define an immutable Cordis Package. For a new Plugin, use kind:"new" and provide only a semantic prefix of '
-      + '3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing '
-      + 'Plugin, use kind:"existing" with its exact pluginId to append a Package without overwriting older versions. '
-      + 'Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns '
-      + 'a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a '
-      + 'Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it '
-      + 'does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the '
-      + 'returned IDs.',
-    parameters: {
-      plugin: {
-        required: true,
-        oneOf: [
-          {
-            type: 'object',
-            additionalProperties: false,
-            properties: {
-              kind: { type: 'string', const: 'new', required: true },
-              idPrefix: {
-                type: 'string',
-                required: true,
-                description: 'Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix.',
-              },
-            },
-          },
-          {
-            type: 'object',
-            additionalProperties: false,
-            properties: {
-              kind: { type: 'string', const: 'existing', required: true },
-              pluginId: { type: 'string', required: true, description: 'Exact ID of an existing Plugin; the new Package is appended to that instance.' },
-            },
-          },
-        ],
-      },
-      name: { type: 'string', required: true, description: 'Short, readable Package name.' },
-      purpose: { type: 'string', required: true, description: 'One-sentence, user-facing description of the Package purpose.' },
-      code: {
-        type: 'object',
-        additionalProperties: false,
-        required: true,
-        properties: {
-          host: { type: 'string', description: 'Plain JavaScript function body that returns the Host-half Cordis Plugin.' },
-          client: { type: 'string', description: 'Plain JavaScript function body that returns the browser Client-half Cordis Plugin.' },
-        },
-      },
-    },
-    output: {
-      schema: {
-        type: 'object',
-        additionalProperties: false,
-        properties: {
-          pluginId: { type: 'string', required: true },
-          packageId: { type: 'string', required: true },
-          name: { type: 'string', required: true },
-          purpose: { type: 'string', required: true },
-          hasHostHalf: { type: 'boolean', required: true },
-          hasClientHalf: { type: 'boolean', required: true },
-        },
-      },
-      render: (_args, value) => [{
-        type: 'text',
-        text: `Defined ${value.pluginId}/${value.packageId} (${value.name}); it is not running yet. `
-          + 'Use cordis_run to activate this Package.',
-      }],
-      presentationMeta: (_args, value) => ({ pluginId: value.pluginId, packageId: value.packageId }),
-    },
-    execute(args, exec) {
-      const plugin = args.plugin.kind === 'new'
-        ? { kind: 'new' as const, idPrefix: args.plugin.idPrefix }
-        : { kind: 'existing' as const, pluginId: CordisDynamicPluginId(args.plugin.pluginId) }
-      const receipt = ctx.dynamicCordisRunner.define({
-        sessionId: requireAgent(exec).id,
-        plugin,
-        name: args.name,
-        purpose: args.purpose,
-        code: {
-          ...args.code.host === undefined ? {} : { host: args.code.host },
-          ...args.code.client === undefined ? {} : { client: args.code.client },
-        },
-      })
-      return Promise.resolve({
-        ...receipt,
-        pluginId: String(receipt.pluginId),
-        packageId: String(receipt.packageId),
-      })
-    },
-    presentCall: presentDefineCall,
-  }))
-
-  ctx.tools.register(defineTool({
-    name: 'cordis_run',
-    description:
-      'Activate one exact Package of a dynamic Plugin. Use mode:"run" for the first activation, restarting '
-      + 'currentPackageId, or rollback. When current exists, use mode:"update" to switch to a different Package, '
-      + 'even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and '
-      + 'returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the '
-      + 'browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after '
-      + 'complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or '
-      + 'technical failure is reported through state and steering. After a technical failure, read diagnostics with '
-      + 'cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after '
-      + 'the user rejects it.',
-    parameters: {
-      pluginId: { type: 'string', required: true, description: 'Stable Plugin ID returned by cordis_define.' },
-      packageId: { type: 'string', required: true, description: 'Exact immutable Package ID to activate under that Plugin.' },
-      mode: {
-        type: 'string',
-        required: true,
-        enum: ['run', 'update'],
-        description: 'Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.',
-      },
-    },
-    output: {
-      schema: { type: 'json' },
-      render: (_args, value) => {
-        const result = requireJsonObject(value)
-        const pluginId = requireJsonString(result, 'pluginId')
-        const packageId = requireJsonString(result, 'packageId')
-        const pluginRunId = requireJsonString(result, 'pluginRunId')
-        return [{
-          type: 'text',
-          text: result.status === 'awaiting-approval'
-            ? `${pluginId}/${packageId} is awaiting user approval (${pluginRunId}).`
-            : result.status === 'starting'
-              ? `${pluginId}/${packageId} is starting asynchronously (${pluginRunId}).`
-              : `${pluginId}/${packageId} is running (${pluginRunId}).`,
-        }]
-      },
-      presentationMeta: (_args, value) => {
-        const result = requireJsonObject(value)
-        return {
-          pluginId: requireJsonString(result, 'pluginId'),
-          packageId: requireJsonString(result, 'packageId'),
-          pluginRunId: requireJsonString(result, 'pluginRunId'),
-        }
-      },
-    },
-    async execute(args, exec) {
-      const agent = requireAgent(exec)
-      const pluginId = CordisDynamicPluginId(args.pluginId)
-      const packageId = CordisDynamicPackageId(args.packageId)
-      const receipt = await ctx.dynamicCordisRunner.run(agent, pluginId, packageId, args.mode, exec.signal)
-      if (!receipt.ok) throw new Error(receipt.message)
-      if (receipt.status !== 'running') {
-        return {
-          status: receipt.status,
-          pluginId: args.pluginId,
-          packageId: args.packageId,
-          pluginRunId: String(receipt.pluginRunId),
-          mode: receipt.mode,
-          ...receipt.currentPackageId === undefined ? {} : { currentPackageId: String(receipt.currentPackageId) },
-          nextPackageId: String(receipt.nextPackageId),
-        }
-      }
-      const row = ctx.dynamicCordisRunner.snapshot(agent).find(candidate => candidate.pluginId === pluginId)
-      const fiber = row?.activeRun?.pluginRunId === receipt.pluginRunId ? row.activeRun.fiber : undefined
-      return {
-        status: 'running',
-        pluginId: args.pluginId,
-        packageId: args.packageId,
-        pluginRunId: String(receipt.pluginRunId),
-        currentPackageId: String(receipt.currentPackageId),
-        ...receipt.nextPackageId === undefined ? {} : { nextPackageId: String(receipt.nextPackageId) },
-        host: {
-          status: fiber === undefined ? 'absent' : missingServices(ctx, fiber).length === 0 ? 'running' : 'waiting',
-          provides: fiber === undefined ? [] : providedServices(ctx, fiber),
-          waitingFor: fiber === undefined ? [] : missingServices(ctx, fiber),
-        },
-        client: {
-          status: receipt.clientWaitingFor === undefined
-            ? 'absent'
-            : receipt.clientWaitingFor.length === 0 ? 'running' : 'waiting',
-          waitingFor: [...(receipt.clientWaitingFor ?? [])],
-        },
-      }
-    },
-    presentCall: presentRunCall,
-  }))
-
-  ctx.tools.register(defineTool({
-    name: 'cordis_stop',
-    description:
-      'Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the '
-      + 'Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update '
-      + 'directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects '
-      + 'temporarily; use cordis_undefine for permanent removal.',
-    parameters: {
-      pluginId: { type: 'string', required: true, description: 'Stable dynamic Plugin ID to stop.' },
-    },
-    output: {
-      schema: { type: 'object', additionalProperties: false, properties: { pluginId: { type: 'string', required: true } } },
-      render: (_args, value) => [{ type: 'text', text: `Dynamic Plugin ${value.pluginId} is stopped; its definition and versions remain.` }],
-    },
-    async execute(args, exec) {
-      const receipt = await ctx.dynamicCordisRunner.stop(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
-      if (!receipt.ok && receipt.reason !== 'not-running') throw new Error(receipt.message)
-      return { pluginId: args.pluginId }
-    },
-    presentCall: presentStopCall,
-  }))
-
-  ctx.tools.register(defineTool({
-    name: 'cordis_undefine',
-    description:
-      'Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, '
-      + 'first stop it and cancel the request, then delete every Package, grant, and version pointer. After this '
-      + 'returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards '
-      + 'retain only a "Plugin removed" record. Do not call this Tool when versions must remain available for restart '
-      + 'or rollback; use cordis_stop instead.',
-    parameters: {
-      pluginId: { type: 'string', required: true, description: 'Stable dynamic Plugin ID to remove permanently.' },
-    },
-    output: {
-      schema: {
-        type: 'object',
-        additionalProperties: false,
-        properties: {
-          pluginId: { type: 'string', required: true },
-          wasRunning: { type: 'boolean', required: true },
-        },
-      },
-      render: (_args, value) => [{ type: 'text', text: `Removed dynamic Plugin ${value.pluginId} and all of its Packages.` }],
-    },
-    async execute(args, exec) {
-      const receipt = await ctx.dynamicCordisRunner.undefine(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
-      if (!receipt.ok) throw new Error(receipt.message)
-      return { pluginId: args.pluginId, wasRunning: receipt.wasRunning }
-    },
-    presentCall: presentUndefineCall,
-  }))
-
-  ctx.on('agent/pre-step', async ({ agent, messages, signal }, next): Promise<PreStepDecision> => {
-    const decision = await next()
-    if (decision.kind === 'reject') return decision
-    const ids = referencedPluginIds(messages)
-    if (ids.length === 0) return decision
-    signal.throwIfAborted()
-    const contexts = ids.map((id) => {
-      const reference = ctx.dynamicCordisRunner.reference(agent, CordisDynamicPluginId(id))
-      return createUserMessage({
-        content: [{
-          type: 'text',
-          text: reference === undefined ? renderUnavailableReference(id) : renderReference(reference),
-        }],
-        source: { kind: 'plugin', plugin: name, form: 'instructions' },
-      })
-    })
-    return { ...decision, messages: [...decision.messages, ...contexts] }
-  })
-}
-
-function requireJsonObject(value: JsonValue): Record<string, JsonValue> {
-  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
-    throw new Error('expected a JSON object')
-  }
-  return value
-}
-
-function requireJsonString(value: Record<string, JsonValue>, key: string): string {
-  const field = value[key]
-  if (typeof field !== 'string') throw new Error(`expected JSON string field "${key}"`)
-  return field
-}
-
-type SelfState = 'defined' | 'awaiting-approval' | 'client-pending' | 'stopped' | 'running' | 'waiting' | 'failed'
-
-function selfSummary(reference: DynamicCordisReference & { packages?: readonly unknown[] }): Record<string, JsonValue> {
-  const latest = reference.latestRun
-  const state = selfState(reference)
-  return {
-    pluginId: String(reference.pluginId),
-    name: reference.name,
-    packageCount: reference.packages?.length ?? 1,
-    state,
-    ...reference.currentPackageId === undefined ? {} : { currentPackageId: String(reference.currentPackageId) },
-    ...reference.nextPackageId === undefined ? {} : { nextPackageId: String(reference.nextPackageId) },
-    ...reference.activeRun === undefined ? {} : {
-      activeRun: {
-        pluginRunId: String(reference.activeRun.pluginRunId),
-        packageId: String(reference.activeRun.packageId),
-      },
-    },
-    ...latest?.status !== 'awaiting-approval' ? {} : {
-      pendingApproval: {
-        pluginRunId: String(latest.pluginRunId),
-        packageId: String(latest.packageId),
-        mode: latest.mode,
-      },
-    },
-  }
-}
-
-function selfState(reference: DynamicCordisReference): SelfState {
-  const status = reference.latestRun?.status
-  if (status === 'awaiting-approval') return 'awaiting-approval'
-  if (status === 'client-pending' || status === 'starting-host') return 'client-pending'
-  if (status === 'failed' || status === 'rejected' || status === 'cancelled') return 'failed'
-  if (status === 'waiting') return 'waiting'
-  if (status === 'running') return 'running'
-  if (reference.activeRun !== undefined) return 'running'
-  return reference.currentPackageId === undefined ? 'defined' : 'stopped'
-}
-
-function inspectSelfPackage(
-  ctx: Context,
-  agent: Agent,
-  pluginId: ReturnType<typeof CordisDynamicPluginId>,
-  packageId: ReturnType<typeof CordisDynamicPackageId>,
-): Record<string, JsonValue> {
-  const inspected = ctx.dynamicCordisRunner.inspectPackage(agent, pluginId, packageId)
-  const row = ctx.dynamicCordisRunner.snapshot(agent).find(candidate => candidate.pluginId === pluginId)
-  const pkg = row?.packages.find(candidate => candidate.packageId === packageId)
-  const active = row?.activeRun?.packageId === packageId ? row.activeRun : undefined
-  const latest = inspected.latestRun?.packageId === packageId ? inspected.latestRun : undefined
-  const hostWaiting = active?.fiber === undefined ? [...(latest?.host.waitingFor ?? [])] : missingServices(ctx, active.fiber)
-  const hostStatus = pkg?.hasHostHalf !== true
-    ? 'absent'
-    : latest?.host.status ?? (active === undefined ? 'stopped' : hostWaiting.length === 0 ? 'running' : 'waiting')
-  const clientStatus = pkg?.hasClientHalf !== true
-    ? 'absent'
-    : latest?.client.status ?? 'stopped'
-  return {
-    mode: 'package',
-    plugin: selfSummary(inspected),
-    packageId: String(packageId),
-    name: inspected.name,
-    purpose: inspected.purpose,
-    code: inspected.code,
-    runtime: {
-      state: selfState(inspected),
-      host: {
-        status: hostStatus,
-        provides: active?.fiber === undefined ? [] : providedServices(ctx, active.fiber),
-        waitingFor: hostWaiting,
-        handlers: active?.handlers ?? [],
-        ...latest?.host.error === undefined ? {} : { error: latest.host.error },
-      },
-      client: {
-        status: clientStatus,
-        waitingFor: [...(latest?.client.waitingFor ?? [])],
-        ...latest?.client.error === undefined ? {} : { error: latest.client.error },
-        ...active?.renderFailure === undefined ? {} : { renderFailure: active.renderFailure },
-      },
-    },
-  } as unknown as Record<string, JsonValue>
-}
-
-function referencedPluginIds(messages: readonly UserMessage[]): string[] {
-  const found = new Set<string>()
-  const pattern = /(?:^|\s)@([a-z]{3,6}-\d+)(?=\s|$)/g
-  for (const message of messages) {
-    if (message.source.kind !== 'user') continue
-    const text = message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
-    for (const match of text.matchAll(pattern)) if (match[1] !== undefined) found.add(match[1])
-  }
-  return [...found]
-}
-
-function renderReference(reference: ReturnType<Context['dynamicCordisRunner']['reference']> & {}): string {
-  const mode = reference.currentPackageId === undefined ? 'run' : 'update'
-  return [
-    '<cordis_dynamic_plugin_context>',
-    JSON.stringify(reference, null, 2),
-    '',
-    `The user explicitly referenced @${reference.pluginId}. Use Package ${reference.packageId} as the base for this modification.`,
-    `Before modifying it, call cordis_inspect_self with pluginId="${reference.pluginId}" and packageId="${reference.packageId}" to read the exact metadata and source.`,
-    `Use cordis_define with plugin.kind="existing" and the original pluginId="${reference.pluginId}" to append an immutable Package.`,
-    `Do not create a new Plugin for this request. After cordis_define succeeds, call cordis_run mode="${mode}" with the returned packageId.`,
-    '</cordis_dynamic_plugin_context>',
-  ].join('\n')
-}
-
-function renderUnavailableReference(id: string): string {
-  return [
-    '<cordis_dynamic_plugin_context>',
-    `The user explicitly referenced @${id}, but this Plugin is unavailable in the current Session.`,
-    'It may have been removed, belong to another Session, or have been lost when the DSH process restarted.',
-    'Do not claim that it was updated or silently create a replacement Plugin. Tell the user that the reference is currently unavailable.',
-    '</cordis_dynamic_plugin_context>',
-  ].join('\n')
 }

+ 0 - 332
packages/extensions/tool-cordis/src/inspect.ts

@@ -1,332 +0,0 @@
-/**
- * Text renderers for `cordis_runtime_inspect`. Live facts come from the service store and
- * the plugin registry; what each service CAN DO comes from the generated
- * `api-catalog.ts`. This module owns the join of the two plus presentation: which
- * lines a section prints, how compact the default report stays, and what an exact
- * `name` adds.
- * @module @deepseek-ai/dsh-tool-cordis/inspect
- */
-
-import type { Context, Fiber } from '@deepseek-ai/cordis'
-import type { ScopeKey } from '@deepseek-ai/dsh-scope'
-import type { Agent } from '@deepseek-ai/dsh-agent'
-// Type-only: resolves `ctx.dynamicCordisRunner` (the registry this report reads).
-import type {} from '@deepseek-ai/dsh-cordis-host-runner'
-import { EVENT_API, INHERITED_CTX_API, SERVICE_API, TYPE_API } from './api-catalog.ts'
-import type { EventApiEntry, InheritedApiEntry, ServiceApiEntry, ServiceApiMethod, TypeApiEntry } from './api-catalog.ts'
-import { FiberState, STATE_LABELS } from './fiber-state.ts'
-
-/** One live service joined with what the generated catalog knows about it. */
-interface LiveService {
-  /** The `ctx.<name>` key. */
-  name: string
-  /** Plugin fiber providing it. */
-  owner: string
-  /** Lifecycle state of that fiber; `active` while it is serving. */
-  state: string
-  /** First sentence of the catalog summary; empty when the catalog has no entry. */
-  summary: string
-  /** Whether the generated catalog carries signatures for it. */
-  catalogued: boolean
-  /** Public method signatures from the catalog, empty for an uncatalogued service. */
-  methods: readonly string[]
-}
-
-/** The live service registrations, read from the reflect store. */
-function liveImpls(ctx: Context): { name: string; fiber: Fiber }[] {
-  const store = ctx.reflect.store
-  return Object.getOwnPropertySymbols(store)
-    .map(key => store[key])
-    .filter((impl): impl is NonNullable<typeof impl> => impl !== undefined)
-}
-
-/**
- * A summary as prose. JSDoc may name a symbol with an inline `{@link Foo.bar}`
- * tag, which the generated catalog retains verbatim; a report is read, not
- * compiled, so the link syntax is spent context and the bare symbol says the same
- * thing.
- */
-function plainSummary(summary: string): string {
-  return summary.replace(/\{@link\s+([^}]+)\}/g, '$1')
-}
-
-/**
- * Every service this process provides, joined with the generated catalog: what is
- * RUNNING comes from the store, what each service CAN DO comes from the catalog,
- * and a live service the catalog does not cover stays in the list as reachable
- * with no signatures rather than being dropped.
- */
-function liveServices(ctx: Context, api: readonly ServiceApiEntry[]): LiveService[] {
-  const catalogued = new Map(api.map(entry => [entry.key, entry]))
-  return liveImpls(ctx)
-    .map((impl) => {
-      const entry = catalogued.get(impl.name)
-      return {
-        name: impl.name,
-        owner: impl.fiber.name,
-        state: STATE_LABELS[impl.fiber.state],
-        summary: entry === undefined ? '' : plainSummary(entry.summary),
-        catalogued: entry !== undefined,
-        methods: entry === undefined ? [] : entry.methods.map(method => method.signature),
-      }
-    })
-    .sort((left, right) => left.name.localeCompare(right.name))
-}
-
-/** Catalogued services with no live provider: loadable in principle, absent here. */
-function absentServices(ctx: Context, api: readonly ServiceApiEntry[]): string[] {
-  const live = new Set(liveImpls(ctx).map(impl => impl.name))
-  return api.filter(entry => !live.has(entry.key)).map(entry => entry.key).sort()
-}
-
-/**
- * Whether a fiber is `root` itself or mounted anywhere inside `root`'s subtree.
- * @param fiber - the fiber to locate.
- * @param root - the subtree root to test against.
- * @returns true when `fiber` belongs to that subtree.
- */
-export function withinFiber(fiber: Fiber, root: Fiber): boolean {
-  let current = fiber
-  while (true) {
-    if (current === root) return true
-    const parent = current.parent.fiber
-    if (parent === current) return false
-    current = parent
-  }
-}
-
-/**
- * Service names provided by one mount's fiber subtree.
- * @param ctx - the runtime whose service registrations are inspected.
- * @param fiber - the root of the mounted fiber subtree.
- * @returns the provided service names in lexical order.
- */
-export function providedServices(ctx: Context, fiber: Fiber): string[] {
-  return liveImpls(ctx)
-    .filter(impl => withinFiber(impl.fiber, fiber))
-    .map(impl => impl.name)
-    .sort()
-}
-
-/**
- * Services a fiber declared in `inject` that do not exist yet — a settled fiber
- * that is not active is waiting on exactly these (legal cordis semantics: it
- * activates when the service appears).
- * @param ctx - the context to resolve service existence against.
- * @param fiber - the fiber whose `inject` declarations are checked.
- * @returns the missing service names, in declaration order.
- */
-export function missingServices(ctx: Context, fiber: Fiber): string[] {
-  return Object.keys(fiber.inject).filter(service => ctx.get(service) === undefined)
-}
-
-/**
- * The `services` section: every live ctx service with its owning fiber and, when
- * the generated catalog covers it, a one-line summary. The `api` section is the
- * one that carries signatures; this one answers what exists and who provides it.
- * @param ctx - the runtime to enumerate.
- * @param api - the generated service entries whose summaries annotate the live ones.
- * @returns one line per service, or a single placeholder line when none are provided.
- */
-export function describeServices(ctx: Context, api: readonly ServiceApiEntry[] = SERVICE_API): string[] {
-  const live = liveServices(ctx, api)
-  if (live.length === 0) return ['(no services provided)']
-  return live.map((service) => {
-    const state = service.state === STATE_LABELS[FiberState.ACTIVE] ? '' : `, ${service.state}`
-    const summary = service.summary === '' ? '' : ` — ${service.summary}`
-    return `- ${service.name} (provided by ${service.owner}${state})${summary}`
-  })
-}
-
-/**
- * The `plugins` section: a flat list of every fiber the registry knows, one line
- * per fiber with its lifecycle state, sorted by plugin name (a plugin mounted
- * more than once repeats — one line per instance). Temporary plugins are listed
- * like any other plugin; their ids live in the `temporary` section.
- * @param ctx - the runtime whose registry is enumerated.
- * @returns one line per loaded plugin fiber.
- */
-export function describePlugins(ctx: Context): string[] {
-  const fibers: Fiber[] = []
-  for (const runtime of ctx.registry.values()) {
-    for (const fiber of runtime.fibers) fibers.push(fiber)
-  }
-  return fibers
-    .sort((left, right) => left.name.localeCompare(right.name))
-    .map(fiber => `- ${fiber.name} [${STATE_LABELS[fiber.state]}]`)
-}
-
-/**
- * The `tools` section: the model-facing tool names the CALLING agent can see
- * (its scoped layer shadowing/joining the restricted global tool set) — the
- * honest answer to the tool description's "what you can call".
- * @param ctx - the runtime whose tool registry is read.
- * @param scope - the calling agent (the viewing scope); omitted = global view.
- * @returns one line per visible tool.
- */
-export function describeTools(ctx: Context, scope?: ScopeKey): string[] {
-  return ctx.tools.schemas(scope).map(schema => `- ${schema.name}`)
-}
-
-/**
- * The `temporary` section: one line per dynamic package this session defined,
- * with its metadata, which halves exist, the host half's lifecycle state and
- * provides/waits, the invoke methods it registered, and the last browser-half
- * load report. Session-scoped like every runner verb.
- * @param ctx - the runtime the packages live in.
- * @param agent - the calling agent; without one there is no definition space to report.
- * @returns one line per package, or a single placeholder line when none exist.
- */
-export function describeDynamic(ctx: Context, agent?: Agent): string[] {
-  const rows = agent === undefined ? [] : ctx.dynamicCordisRunner.snapshot(agent)
-  if (rows.length === 0) {
-    return ['No dynamic Plugins are defined in this session. Definitions live only in this process\'s memory, so a DSH restart clears them.']
-  }
-  return rows.flatMap((row) => {
-    const head = `- Plugin ${row.pluginId}; current: ${row.currentPackageId ?? 'none'}; next: ${row.nextPackageId ?? 'none'}`
-      + (row.activeRun === undefined
-        ? '; stopped'
-        : `; active: ${row.activeRun.packageId} as ${row.activeRun.pluginRunId}`)
-    const packages = row.packages.map((pkg) => {
-      const halves = [...pkg.hasHostHalf ? ['host'] : [], ...pkg.hasClientHalf ? ['client'] : []].join('+')
-      const active = row.activeRun?.packageId === pkg.packageId ? row.activeRun : undefined
-      if (active === undefined) return `    - ${pkg.packageId}: ${pkg.name} (${halves}) — ${pkg.purpose}`
-      const fiber = active.fiber
-      const state = fiber === undefined ? 'running' : fiber.state === FiberState.ACTIVE ? 'running' : STATE_LABELS[fiber.state]
-      const provides = fiber === undefined ? [] : providedServices(ctx, fiber)
-      const waiting = fiber === undefined ? [] : missingServices(ctx, fiber)
-      const failure = active.renderFailure
-      const rendered = failure === undefined
-        ? ''
-        : `; CLIENT RENDER FAILED at ${failure.slot}: ${failure.message}${failure.abdicated ? ' (entry removed)' : ''}`
-      return `    - ${pkg.packageId}: ${pkg.name} [${state}, ${active.pluginRunId}] (${halves}) — ${pkg.purpose}`
-        + `; provides: ${provides.join(', ') || 'none'}; waiting for: ${waiting.join(', ') || 'none'}`
-        + (active.handlers.length === 0 ? '' : `; host methods: ${active.handlers.join(', ')}`)
-        + rendered
-    })
-    return [head, ...packages]
-  })
-}
-
-/**
- * The transitive closure of catalogued type shapes referenced (word-bounded)
- * by the seed texts — the runtime scoping that keeps the `api` section to the
- * shapes the LIVE signatures actually mention.
- */
-function typeClosure(seeds: string[], types: readonly TypeApiEntry[]): TypeApiEntry[] {
-  const included = new Map<string, TypeApiEntry>()
-  let frontier = seeds
-  while (frontier.length > 0) {
-    const next: string[] = []
-    for (const entry of types) {
-      if (included.has(entry.name)) continue
-      const pattern = new RegExp(`\\b${entry.name}\\b`)
-      if (frontier.some(text => pattern.test(text))) {
-        included.set(entry.name, entry)
-        next.push(entry.declaration)
-      }
-    }
-    frontier = next
-  }
-  return [...included.values()].sort((left, right) => left.name.localeCompare(right.name))
-}
-
-/** Render one live catalogued service; `documented` is non-empty only for an exact-name report. */
-function serviceLines(
-  service: LiveService,
-  documented: readonly ServiceApiMethod[],
-): string[] {
-  const lines = [`- ${service.name} — ${service.summary}`]
-  for (const signature of service.methods) {
-    const contract = documented.find(entry => entry.signature === signature)
-    if (contract !== undefined) {
-      lines.push(`    ${contract.description}`)
-      for (const parameter of contract.parameters) lines.push(`    @param ${parameter.name} — ${parameter.description}`)
-      if (contract.returns !== undefined) lines.push(`    @returns ${contract.returns}`)
-      for (const failure of contract.throws ?? []) lines.push(`    @throws ${failure}`)
-    }
-    lines.push(`    ${signature}`)
-  }
-  return lines
-}
-
-/**
- * Render the generated catalog against the live runtime: live catalogued services with methods,
- * uncatalogued live services with owners, absent loadable services, referenced type shapes, and
- * inherited Context APIs.
- * @param ctx - the runtime to intersect the catalog with.
- * @param api - generated service entries, replaceable in tests.
- * @param name - exact live service key whose methods should include structured contracts; omitted for the compact catalog.
- * @param inherited - inherited `ctx` entries, replaceable in tests.
- * @param types - public type shapes, replaceable in tests.
- * @returns the section lines.
- */
-export function describeApi(
-  ctx: Context,
-  api: readonly ServiceApiEntry[] = SERVICE_API,
-  name?: string,
-  inherited: readonly InheritedApiEntry[] = INHERITED_CTX_API,
-  types: readonly TypeApiEntry[] = TYPE_API,
-): string[] {
-  const live = liveServices(ctx, api)
-  const byKey = new Map(api.map(entry => [entry.key, entry]))
-  const lines: string[] = []
-  let selected = live.filter(service => service.catalogued)
-  let documented: readonly ServiceApiMethod[] = []
-  if (name !== undefined) {
-    const entry = byKey.get(name)
-    if (entry === undefined) throw new Error(`no catalogued service named "${name}"`)
-    const service = live.find(candidate => candidate.name === name)
-    if (service === undefined) throw new Error(`catalogued service "${name}" is not running`)
-    selected = [service]
-    documented = entry.methods
-  }
-  for (const service of selected) lines.push(...serviceLines(service, documented))
-  if (name === undefined) {
-    for (const service of live.filter(candidate => !candidate.catalogued)) {
-      lines.push(`- ${service.name} (provided by ${service.owner}) — running, but this catalog has no signature for it;`
-        + ` inject: ['${service.name}'] still reaches it`)
-    }
-    const notRunning = absentServices(ctx, api)
-    if (notRunning.length > 0) lines.push(`not running (loadable services with no live provider): ${notRunning.join(', ')}`)
-  }
-  const shapes = typeClosure(selected.flatMap(service => [...service.methods]), types)
-  if (shapes.length > 0) {
-    lines.push('type shapes (referenced by the signatures above — read these before assuming a field is a string):')
-    for (const shape of shapes) {
-      for (const declLine of shape.declaration.split('\n')) lines.push(`    ${declLine}`)
-    }
-  }
-  if (name === undefined) {
-    lines.push('inherited ctx API:')
-    for (const entry of inherited) lines.push(`- ${entry.name} — ${entry.summary}`)
-  }
-  return lines
-}
-
-/**
- * The `events` section: every harness event with its dispatch mode, one-line
- * summary, and exact signature, closed by the waterfall caution.
- * @param events - the event catalog (the generated one by default; injectable for tests).
- * @param name - exact event name whose signature should include its structured contract; omitted for the compact catalog.
- * @returns the section lines.
- */
-export function describeEvents(events: readonly EventApiEntry[] = EVENT_API, name?: string): string[] {
-  let selected = events
-  if (name !== undefined) {
-    const event = events.find(candidate => candidate.name === name)
-    if (!event) throw new Error(`no catalogued event named "${name}"`)
-    selected = [event]
-  }
-  const lines = selected.flatMap((event) => {
-    const entry = [`- ${event.name} [${event.mode}] — ${event.summary}`]
-    if (name !== undefined) {
-      entry.push(`    ${event.description}`)
-      for (const parameter of event.parameters) entry.push(`    @param ${parameter.name} — ${parameter.description}`)
-    }
-    entry.push(`    ${event.signature}`)
-    return entry
-  })
-  lines.push('waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() short-circuits the chain.')
-  return lines
-}

+ 1 - 84
packages/extensions/tool-cordis/src/present.ts

@@ -1,17 +1,6 @@
-/** Pure replay-safe render intents for Cordis tools. */
-
+/** Pure replay-safe render intents for runtime inspection. */
 import type { GenericCallView } from '@deepseek-ai/dsh-tools'
 
-/**
- * Render a runtime-inspection call.
- * @param args - requested runtime category and optional member name.
- * @returns replay-safe generic call presentation.
- */
-export function presentRuntimeInspectCall(args: { what?: string; name?: string }): GenericCallView {
-  const target = args.name === undefined ? args.what : `${args.what}: ${args.name}`
-  return { card: 'generic', kind: 'read', title: target === undefined ? 'Inspect Cordis runtime' : `Inspect Cordis runtime: ${target}` }
-}
-
 /**
  * Render provider-directory inspection.
  * @returns replay-safe generic call presentation.
@@ -28,75 +17,3 @@ export function presentInspectListCall(): GenericCallView {
 export function presentInspectQueryCall(args: { platform: string; provider: string; method: string }): GenericCallView {
   return { card: 'generic', kind: 'read', title: `Query Cordis ${args.platform} ${args.provider}.${args.method}` }
 }
-
-/**
- * Render layered self-inspection.
- * @param args - optional Plugin and Package identity.
- * @returns replay-safe generic call presentation.
- */
-export function presentInspectSelfCall(args: { pluginId?: string; packageId?: string }): GenericCallView {
-  const target = args.pluginId === undefined
-    ? 'dynamic Cordis Plugins'
-    : args.packageId === undefined ? args.pluginId : `${args.pluginId}/${args.packageId}`
-  return { card: 'generic', kind: 'read', title: `Inspect ${target}` }
-}
-
-/**
- * Render an immutable Package source-inspection call.
- * @param args - exact Plugin and Package identity.
- * @returns replay-safe generic call presentation.
- */
-export function presentPackageInspectCall(args: { pluginId: string; packageId: string }): GenericCallView {
-  return { card: 'generic', kind: 'read', title: `Inspect Cordis Package ${args.pluginId}/${args.packageId}` }
-}
-
-/**
- * Render a new or appended Package definition.
- * @param args - target Plugin, Package metadata, and source halves.
- * @returns replay-safe generic call presentation with source in raw input.
- */
-export function presentDefineCall(args: {
-  plugin: { kind: 'new'; idPrefix: string } | { kind: 'existing'; pluginId: string }
-  name: string
-  purpose: string
-  code: { host?: string; client?: string }
-}): GenericCallView {
-  const target = args.plugin.kind === 'new' ? `new ${args.plugin.idPrefix}-*` : args.plugin.pluginId
-  return {
-    card: 'generic',
-    kind: 'execute',
-    title: `Register Cordis Plugin "${args.name}" for ${target}: ${args.purpose}`,
-    rawInput: args.code,
-  }
-}
-
-/**
- * Render Plugin removal.
- * @param args - Plugin identity to remove.
- * @returns replay-safe generic call presentation.
- */
-export function presentUndefineCall(args: { pluginId: string }): GenericCallView {
-  return { card: 'generic', kind: 'delete', title: `Remove Cordis Plugin ${args.pluginId}` }
-}
-
-/**
- * Render one exact Package activation.
- * @param args - Plugin, Package, and activation mode.
- * @returns replay-safe generic call presentation.
- */
-export function presentRunCall(args: { pluginId: string; packageId: string; mode: 'run' | 'update' }): GenericCallView {
-  return {
-    card: 'generic',
-    kind: 'execute',
-    title: `${args.mode === 'update' ? 'Update' : 'Run'} Cordis Plugin ${args.pluginId} · ${args.packageId}`,
-  }
-}
-
-/**
- * Render Plugin stop.
- * @param args - Plugin identity to stop.
- * @returns replay-safe generic call presentation.
- */
-export function presentStopCall(args: { pluginId: string }): GenericCallView {
-  return { card: 'generic', kind: 'execute', title: `Stop Cordis Plugin ${args.pluginId}` }
-}

+ 3 - 106
packages/extensions/tool-cordis/src/prompt.ts

@@ -1,107 +1,4 @@
-/** Model guidance shared by the Cordis dynamic-plugin tools. */
+/** Model guidance for read-only runtime API discovery. */
+export const CORDIS_SYSTEM_PROMPT = `# Harness runtime inspection
 
-export const CORDIS_SYSTEM_PROMPT = `# Dynamic Cordis Plugins
-
-Dynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.
-
-- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.
-- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.
-
-## Make the user-facing plan clear first
-
-- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.
-- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.
-- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.
-- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.
-- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.
-- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.
-- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.
-- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.
-- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.
-
-## Recommended workflow and Tools
-
-Before creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.
-
-1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.
-2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.
-3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.
-4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.
-5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.
-6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.
-7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.
-
-- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.
-- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.
-- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.
-
-## Identity, versions, and approval
-
-- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.
-- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.
-- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.
-- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.
-- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.
-- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.
-- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.
-
-When the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:
-
-1. Call cordis_inspect_self(pluginId, packageId) to read the target source.
-2. Use cordis_define in existing mode to append a Package to the same Plugin.
-3. Call cordis_run in run or update mode according to the version relationship.
-
-Never silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.
-
-## High-frequency errors that must be avoided
-
-### Services: ctx.get and inject
-
-- Read an optional Service with ctx.get('serviceName') by default and handle undefined.
-- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.
-- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.
-
-\`\`\`js
-return {
-  inject: ['requiredService'],
-  apply(ctx) {
-    ctx.requiredService.someMethod()
-    const optionalService = ctx.get('optionalService')
-    if (optionalService !== undefined) optionalService.someMethod()
-  },
-}
-\`\`\`
-
-### Code: use plain JavaScript only
-
-- Host and Client code is not transformed by TypeScript, JSX, or a bundler.
-- Do not use TypeScript types, as, decorators, import, require, or JSX.
-- Client React code must use React.createElement(...); never write <Component />.
-- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.
-
-### Data: do not serialize live data
-
-- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.
-- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.
-- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.
-
-### Lifecycle: every side effect must be reversible
-
-- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.
-- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.
-- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.
-
-## Host and Client
-
-- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.
-- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.
-- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.
-- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.
-- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.
-
-## Asynchronous results and recovery
-
-- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.
-- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.
-- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.
-- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.`
+Use cordis_inspect_list to discover Host and Client providers, then cordis_inspect_query to read exact Service, Event, Tool, Theme or Slot APIs. These tools are read-only; queries do not invoke business methods.`

+ 1 - 1
packages/extensions/tool-cordis/tests/cordis-lifecycle.spec.ts

@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
 
 /**
  * Direct regressions for the vendored Cordis ownership substrate used by
- * tool-cordis's dynamic plugin tree and every other harness plugin.
+ * cordis-host-runner and cordis-client-runner and every other harness plugin.
  */
 
 describe('Cordis effect ownership', () => {

+ 0 - 9
packages/extensions/tool-cordis/tsconfig.json

@@ -23,18 +23,9 @@
     {
       "path": "../../core/agent"
     },
-    {
-      "path": "../../core/scope"
-    },
     {
       "path": "../../core/tools"
     },
-    {
-      "path": "../../core/session"
-    },
-    {
-      "path": "../../llm/llm"
-    },
     {
       "path": "../cordis-host-runner"
     }

+ 2 - 2
packages/extensions/ui-cordis/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/extensions/ui-cordis/README.md
-README.md: 1cd6d778148d1afdcfc470b20b7e5ac437a21921
-README.zh.md: 2c15d6be3fcbfb4667cbdee7761f8e3716ce5c39
+README.md: 17fea955dae2ecc4b4549775d8f9176ee5eba1f5
+README.zh.md: 35726f2451a70586c0600789d1c27b15f2d779ea

Some files were not shown because too many files changed in this diff