Ver Fonte

refactor(web): manage bundles without module composition actions

Yichen Jiang há 1 semana atrás
pai
commit
d5155007e0
61 ficheiros alterados com 208 adições e 812 exclusões
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml
  5. 1 1
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md
  6. 1 1
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.zh.md
  7. 0 6
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml
  8. 6 0
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.i18n.yaml
  9. 7 9
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.md
  10. 7 9
      .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.zh.md
  11. 1 2
      apps/web/tests/expected/plugin-manager/manager.expected.md
  12. 4 15
      apps/web/tests/plugin-manager.e2e.ts
  13. 2 2
      docs/config-catalog.i18n.yaml
  14. 1 1
      docs/config-catalog.md
  15. 1 1
      docs/config-catalog.zh.md
  16. 1 1
      docs/event-producer-consumer.i18n.yaml
  17. 2 2
      docs/event-producer-consumer.md
  18. 2 2
      docs/subsystems/core.i18n.yaml
  19. 0 15
      docs/subsystems/core.md
  20. 0 15
      docs/subsystems/core.zh.md
  21. 2 2
      packages/boot/app-boot/README.i18n.yaml
  22. 1 1
      packages/boot/app-boot/README.md
  23. 1 1
      packages/boot/app-boot/README.zh.md
  24. 5 35
      packages/boot/app-boot/src/package-metadata.ts
  25. 13 13
      packages/boot/app-boot/tests/package-metadata.spec.ts
  26. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  27. 3 4
      packages/boot/plugin-manager/README.md
  28. 3 4
      packages/boot/plugin-manager/README.zh.md
  29. 1 2
      packages/boot/plugin-manager/package.json
  30. 0 77
      packages/boot/plugin-manager/src/manager.ts
  31. 0 48
      packages/boot/plugin-manager/src/modules.ts
  32. 4 31
      packages/boot/plugin-manager/src/types.ts
  33. 0 2
      packages/boot/plugin-manager/src/view.ts
  34. 36 99
      packages/boot/plugin-manager/tests/plugin-manager.spec.ts
  35. 0 3
      packages/boot/plugin-manager/tsconfig.json
  36. 2 2
      packages/client/ui-plugin-manager/README.i18n.yaml
  37. 8 12
      packages/client/ui-plugin-manager/README.md
  38. 8 12
      packages/client/ui-plugin-manager/README.zh.md
  39. 0 17
      packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
  40. 13 115
      packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
  41. 2 2
      packages/client/ui-plugin-manager/src/client/index.ts
  42. 6 24
      packages/client/ui-plugin-manager/src/client/locales.ts
  43. 6 26
      packages/client/ui-plugin-manager/src/client/manager-store.ts
  44. 0 1
      packages/client/ui-plugin-manager/src/client/presentation.ts
  45. 3 5
      packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx
  46. 11 49
      packages/client/ui-plugin-manager/tests/components.client.spec.tsx
  47. 9 41
      packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
  48. 2 21
      packages/extensions/tool-cordis/src/api-catalog.ts
  49. 2 2
      packages/host/plugin-manager/README.i18n.yaml
  50. 4 4
      packages/host/plugin-manager/README.md
  51. 4 4
      packages/host/plugin-manager/README.zh.md
  52. 1 27
      packages/host/plugin-manager/src/index.ts
  53. 1 3
      packages/host/plugin-manager/src/types.ts
  54. 5 10
      packages/host/plugin-manager/tests/plugin-manager.spec.ts
  55. 2 2
      packages/util/package-manifest/README.i18n.yaml
  56. 0 1
      packages/util/package-manifest/README.md
  57. 0 1
      packages/util/package-manifest/README.zh.md
  58. 0 1
      packages/util/package-manifest/src/index.ts
  59. 0 12
      packages/util/package-manifest/src/types.ts
  60. 4 3
      pnpm-lock.yaml
  61. 0 1
      scripts/gen-cordis-catalog.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.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-04-plugin-manager-over-the-profile-runtime.md
-2026-09-04-plugin-manager-over-the-profile-runtime.md: c68d7aedcd93ca4f179ab9df97e157ff5388b407
-2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: e01301a5f9e1f35e07471e86fb0df0e223c7f965
+2026-09-04-plugin-manager-over-the-profile-runtime.md: cfe1c63fc450761f4f740509f24e8ec64c804acd
+2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: ff83ea3dfa342b51cad19127bf62b9600eddf78e

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md

@@ -16,9 +16,9 @@ The CLI and Web need the same installation checks, while only a running applicat
 
 **pnpm retains its normal launch environment.** The installer uses `node:child_process` and Windows shell handling for the `.cmd` shim. Registry credentials and proxy configuration remain available. Output streams as `plugins/install-log` under a job id, with terminal colours when requested; spawn failures, timeouts and nonzero exits report `plugins/install-failed` with a bounded log tail. A failed add restores the saved manifest. Static declaration and bundle-conflict checks follow installation; unknown packages remain installed, and new bundles remain disabled unless the caller requests enablement. No discovery-cache directory is owned.
 
-**Enablement selects a layer; diagnostics describe actual rows.** Bundle enablement, nontransactional recomposition and explicit `dsh.plugins` discovery follow [native entry diagnostics and static declarations](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md). The manager keeps per-row issues distinct from the enabled choice. `retry` removes the complete layer, awaits cleanup, and adds it again; unchanged row options alone do not restart a failed plugin. `list` derives each package view from the manifest, static declarations and current Loader state.
+**Enablement selects a layer; diagnostics describe actual rows.** Bundle enablement and nontransactional recomposition follow [native entry diagnostics](2026-09-11-native-entry-diagnostics.md). The manager keeps per-row issues distinct from the enabled choice. `retry` removes the complete layer, awaits cleanup, and adds it again; unchanged row options alone do not restart a failed plugin. `list` derives each package view from the manifest, static declarations and current Loader state.
 
-**Rows go through the patch-file writer.** `addRow` inserts the declared module and config into the profile user patch. `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree.
+**Rows go through the patch-file writer.** `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree.
 
 **Runtime notifications follow diagnostics.** Native entry and fiber events share one pending reader. It waits for Loader and profile recomposition, then compares row identity, module, fiber phase, failure stage and message. Only a changed diagnostic set emits `plugins/changed` with reason `runtime`; healthy status churn and repeated identical failures stay silent. Management operations retain their own completion notifications. Events arriving during a read request another pass, and disposing the adapter cancels publication.
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md

@@ -16,9 +16,9 @@ CLI 与 Web 需要相同的安装检查,而只有运行中的应用才能应
 
 **pnpm 保留正常的启动环境。** 安装器使用 `node:child_process`,并在 Windows 上通过 shell 处理 `.cmd` 垫片。Registry 凭据与代理配置保持可用。输出以 job id 下的 `plugins/install-log` 流式发送,按请求保留终端颜色;启动失败、超时与非零退出以 `plugins/install-failed` 和有界日志尾部报告。add 失败时恢复保存的清单。安装后进行静态声明和组合包冲突检查;未知包保留安装,新组合包保持禁用,除非调用方请求启用。没有需要维护的发现缓存目录。
 
-**启用选择层,诊断描述实际行。** 组合包启用、非事务重组和显式 `dsh.plugins` 发现遵循[原生条目诊断与静态声明](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md)。管理器区分逐行问题与启用选择。`retry` 移除完整层,等待清理,再将其加入;仅保留不变的行选项不会重启失败插件。`list` 从清单、静态声明和当前 Loader 状态派生每个包的视图。
+**启用选择层,诊断描述实际行。** 组合包启用和非事务重组遵循[原生条目诊断](2026-09-11-native-entry-diagnostics.zh.md)。管理器区分逐行问题与启用选择。`retry` 移除完整层,等待清理,再将其加入;仅保留不变的行选项不会重启失败插件。`list` 从清单、静态声明和当前 Loader 状态派生每个包的视图。
 
-**行通过 patch-file 写入器修改。** `addRow` 将声明的模块与配置插入 profile 用户 patch。`setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树。
+**行通过 patch-file 写入器修改。** `setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树。
 
 **运行时通知跟随诊断。** 原生条目与 fiber 事件共用一次待完成的读取。读取等待 Loader 和 profile 重组完成,再比较行身份、模块、fiber 阶段、失败阶段与消息。只有诊断集合变化才发送原因是 `runtime` 的 `plugins/changed`;健康状态波动和重复的相同失败保持安静。管理操作仍保留自己的完成通知。读取期间到来的事件请求再次读取,适配器销毁时取消发布。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.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-09-plugin-management-in-the-web-sidebar.md
-2026-09-09-plugin-management-in-the-web-sidebar.md: 56c057e3d353efd9b6983a7883dad88d908aea22
-2026-09-09-plugin-management-in-the-web-sidebar.zh.md: 7b927db0a0ad231a5adbc2feda818bc8ae774be3
+2026-09-09-plugin-management-in-the-web-sidebar.md: e80a14d3bb805875bc32bb8aefca2fb2c73f4a41
+2026-09-09-plugin-management-in-the-web-sidebar.zh.md: dc86602ffdd9d5dafe1a84162b6918e5a0176641

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md

@@ -12,7 +12,7 @@ Installed packages belong to the running profile, while Settings is a modal over
 
 **Management is a sidebar entry; configuration stays in Settings.** `ui-plugin-manager` registers a `sidebar.panellist` entry and the `main` panel it opens under `plugins`. The page manages packages and global rows, displays install output and confirms dependency-sensitive actions. The Settings Plugins section keeps the global configuration cards and renders its single tab without a tab strip.
 
-**One store follows Host state.** The manager controller combines package views with global inventory, refreshes after management operations and reconnects, and keeps installation progress under the owning job. Configuration cards use the existing global settings bindings.
+**One store follows Host state.** The manager controller reads package views, refreshes after management operations and reconnects, and keeps installation progress under the owning job. Configuration cards use the existing global settings bindings.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 **管理位于侧栏,配置保留在设置中。** `ui-plugin-manager` 在 `plugins` 下注册 `sidebar.panellist` 入口与它打开的 `main` 面板。页面管理包与全局行、展示安装输出,并确认影响依赖的操作。设置的插件分区保留全局配置卡片,单个标签页直接展示内容,不显示标签栏。
 
-**一个 store 跟随 Host 状态。** 管理器控制器结合包视图与全局 inventory,在管理操作和重连后刷新,并按所属 job 保存安装进度。配置卡片使用原有全局 settings 绑定。
+**一个 store 跟随 Host 状态。** 管理器控制器读取包视图,在管理操作和重连后刷新,并按所属 job 保存安装进度。配置卡片使用原有全局 settings 绑定。
 
 ## 考虑过的替代方案
 

+ 0 - 6
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.i18n.yaml

@@ -1,6 +0,0 @@
-# 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-11-native-entry-diagnostics-and-static-plugin-declarations.md
-2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md: 0c83dc5e23515708d78747b1489847ac4f229705
-2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md: badb759753229def9e29c8b706b1dab03a798bb5

+ 6 - 0
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.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-11-native-entry-diagnostics.md
+2026-09-11-native-entry-diagnostics.md: 3f8e8eea3f9d436170579b7e77878e827fd76f55
+2026-09-11-native-entry-diagnostics.zh.md: f14430c3afab00ec1bb1f2301b494769ebbe0c3e

+ 7 - 9
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md → .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.md

@@ -1,8 +1,8 @@
-# Agent Note: Native entry diagnostics and static plugin declarations
+# Agent Note: Native entry diagnostics
 
 Status: implemented
 
-English | [中文](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md)
+English | [中文](2026-09-11-native-entry-diagnostics.zh.md)
 
 ## Problem
 
@@ -12,13 +12,13 @@ Third-party plugin failures must leave the application’s management endpoints
 
 **Startup strictness remains consumer-owned.** App-boot uses the [global required entry ids](2026-09-09-consumer-owned-startup-strictness.md) and the bootstrap Include identity. Package origin does not change the audit: all other rows may fail with a warning. Bundle metadata has no trust or stage policy. Row ownership only attributes diagnostics and management operations; nested Includes inherit their owning entry’s package without matching unrelated root ids.
 
-**Bundle enablement selects a whole patch layer.** Native rows and author-written groups retain their explicit ids and parents. Anonymous bundle rows receive deterministic ids on detached execution copies. Disabling a bundle removes its inserts and overrides across every target group; persistent user row overrides survive re-enablement. Ownership is checked before loading: strict layers claim ids first, optional collisions omit the whole losing bundle, and conflicting user inserts are omitted per row. Prefixing explicit ids would break author expressions and user patches; relying on authors to avoid collisions permits silent Loader reparenting.
+**Bundle enablement selects a whole patch layer.** Native rows and author-written groups retain their explicit ids and parents. Anonymous bundle rows receive deterministic ids on detached execution copies. Disabling a bundle removes its inserts and overrides across every target group; persistent user row overrides survive re-enablement. Ownership is checked before loading: bundle layers claim ids in their listed order, collisions omit the whole losing bundle, and conflicting user inserts are omitted per row. Prefixing explicit ids would break author expressions and user patches; relying on authors to avoid collisions permits silent Loader reparenting.
 
 **Diagnostics describe attempts and running instances separately.** `Entry.lastFailure` passively retains import, activation or update errors where the Loader already catches or propagates them. It does not restore transactions or change native continuation. Asynchronous activation failures come from `fiber.await()`, pending dependencies from injection state, and disabled-expression failures from evaluating the effective gate. An active old fiber can coexist with an update failure. Removed entries need no separate failure registry. Nested continuation failures remain advisory; readiness does not promise every nested plugin callback succeeded.
 
 **Recomposition is serialized and nontransactional.** The launcher provides `ProfileRuntime` before startup auditing. Preparation rejects malformed files before mutation. Accepted updates settle current Loader work and await removed fibers before publishing composition facts and returning issues. Old fiber references are necessary because removed entries are absent from Loader task enumeration. Successful sibling changes remain applied after another entry fails. Fresh patch copies prevent a later override from mutating data reused by another composition. `ProfileRuntime` declares Loader as a required injection so both root and plugin-context handles can reconcile and inspect the tree.
 
-**Discovery reads declarations without executing modules.** `readPackageMetadata` reads package identity, bundle patches and explicit `dsh.plugins` entries. `.` names the main export; subpaths and default configs remain available for composition editors. Undeclared packages remain installed and unknown. A declaration does not certify importability, Config validity or activation. Physical Cordis peer resolution is advisory and cannot detect an inlined copy. Runtime validation belongs to each actual mounted row, including separate mounts of one module in different presets.
+**Metadata discovery does not execute installed code.** `readPackageMetadata` reads package identity and bundle patch rows. A bundle’s metadata describes its declared composition; packages without a bundle patch remain installed and unknown. Physical Cordis peer resolution is advisory and cannot detect an inlined copy. Config validation and execution diagnostics belong to actual mounted rows.
 
 **Unhandled process failures stay fatal.** `installFailLoud` remains until shutdown. Only duplicate rejections already observed by an entry audit are coalesced through its process checkpoint. Unrelated detached rejections retain master’s teardown-and-exit policy. No in-process group protects against `process.exit`, a blocked event loop, native crashes or OOM. Preset generation owners retain their own strict mount-and-cleanup behavior.
 
@@ -26,7 +26,7 @@ Third-party plugin failures must leave the application’s management endpoints
 
 ## Alternatives considered
 
-**Keep contained groups and child probes.** Contained groups protected against transactional Loader rollback and supplied failure records for removed rows. Native entries retain both siblings and failed rows, so wrapping adds identities without supplying bundle-wide enablement that layer composition lacks. Child probes bounded discovery-time execution and discovered undeclared exports and schemas. Static declarations avoid that execution; automatic main-export detection and pre-mount schemas are given up. A future execution sandbox needs its own process ownership and teardown design, not a discovery probe presented as runtime isolation.
+**Wrap every bundle in a synthetic parent group.** Native entries retain both successful siblings and failed rows. A wrapper adds another entry identity, while whole-bundle enablement already follows the selected patch layers. Preserving author-written ids keeps their expressions and user patches valid.
 
 **Import inside the Host with try/catch.** Rejected because discovery could execute irreversible side effects, exit the Host or block its event loop. Catching exceptions cannot restore that isolation. Parsing source to infer plugin exports would add another JavaScript interpreter with incomplete results.
 
@@ -36,10 +36,8 @@ Third-party plugin failures must leave the application’s management endpoints
 
 ## Consequences
 
-Installed metadata, enabled layer selection and current runtime health are independent. An enabled bundle may be partially running, pending or failed; updating a row may leave old behavior active with a visible diagnostic. Whole-bundle disable removes its patch effects without requiring a synthetic parent group. Startup can still fail when an optional provider leaves a required consumer pending. Old probe records are not activation evidence, and plugins need explicit `dsh.plugins` declarations for discovery as addable modules.
+Installed metadata, enabled layer selection and current runtime health are independent. An enabled bundle may be partially running, pending or failed; updating a row may leave old behavior active with a visible diagnostic. Whole-bundle disable removes its patch effects without requiring a synthetic parent group. Startup can still fail when an optional provider leaves a required consumer pending.
 
 ## Verification
 
-`packages/boot/app-boot/tests/entry-issues.spec.ts` covers required-id policy, import/config/apply/disabled/pending failures, nested-entry and anonymous-row bundle ownership, active old config after failed updates, recovery, and awaited removed-fiber teardown. Composition tests cover duplicate ownership and unchanged parents. Static metadata tests use an import-time file-writing fixture and assert no execution, retain unknown packages, and reject malformed declarations. Inventory tests verify actual Loader failures and composition conflicts without a historical failure registry. Process-guard and user-patch tests retain fatal detached failures and best-effort live reload diagnostics.
-
-The [manager/Remote split](2026-09-04-plugin-manager-over-the-profile-runtime.md) remains in force. Manager integration tests cover retained unknown packages, static declarations, per-entry enable results, failed overrides with original ownership, and runtime-change notifications after dependency recovery.
+`packages/boot/app-boot/tests/entry-issues.spec.ts` covers required-id policy, import/config/apply/disabled/pending failures, nested-entry and anonymous-row bundle ownership, active old config after failed updates, recovery, and awaited removed-fiber teardown. Composition tests cover duplicate ownership and unchanged parents. Static metadata tests use an import-time file-writing fixture and assert no execution, retain unknown packages, and ignore unrelated manifest fields. Inventory tests verify actual Loader failures and composition conflicts without a historical failure registry. Process-guard and user-patch tests retain fatal detached failures and best-effort live reload diagnostics.

+ 7 - 9
.agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md → .agents/notes/implemented/architecture/2026-09-11-native-entry-diagnostics.zh.md

@@ -1,8 +1,8 @@
-# Agent Note: 原生条目诊断与静态插件声明
+# Agent Note: 原生条目诊断
 
 Status: implemented
 
-[English](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md) | 中文
+[English](2026-09-11-native-entry-diagnostics.md) | 中文
 
 ## Problem
 
@@ -12,13 +12,13 @@ Status: implemented
 
 **启动严格程度仍由消费者负责。** App-boot 使用[全局必需条目 id](2026-09-09-consumer-owned-startup-strictness.zh.md) 和启动 Include 的身份。包来源不改变审查规则:其他行失败只产生警告。组合包元数据没有 trust 或 stage 策略。行归属只用于诊断和管理操作;嵌套 Include 继承所属条目的包,不能匹配无关的根级 id。
 
-**组合包启停选择整份 patch 层。** 原生行和作者声明的组保留显式 id 与父组。匿名组合包行在独立执行副本中获得确定性的 id。禁用组合包会从所有目标组中移除其插入与覆盖;持久化的用户行覆盖在重新启用后保留。加载前检查所有权:严格层优先占有 id,可选层冲突时整包排除,用户插入冲突时逐行排除。为显式 id 加前缀会破坏作者表达式与用户 patch;依赖作者自行避免冲突则允许 Loader 静默改变父组。
+**组合包启停选择整份 patch 层。** 原生行和作者声明的组保留显式 id 与父组。匿名组合包行在独立执行副本中获得确定性的 id。禁用组合包会从所有目标组中移除其插入与覆盖;持久化的用户行覆盖在重新启用后保留。加载前检查所有权:组合包层按声明顺序占有 id,冲突时整包排除,用户插入冲突时逐行排除。为显式 id 加前缀会破坏作者表达式与用户 patch;依赖作者自行避免冲突则允许 Loader 静默改变父组。
 
 **诊断分别描述操作尝试与运行实例。** `Entry.lastFailure` 在 Loader 已有的捕获或传播位置被动保留导入、激活或更新错误。它不恢复事务,也不改变原生继续加载行为。异步激活失败取自 `fiber.await()`,等待依赖取自注入状态,disabled 表达式失败取自有效条件的求值。活跃的旧 fiber 可以与更新失败同时存在。移除的条目无需独立失败注册表。嵌套延续任务失败仍为提示信息;就绪不保证每个嵌套插件回调均成功。
 
 **重组串行执行且不具备事务性。** launcher 在启动审计前提供 `ProfileRuntime`。准备阶段在修改运行树前拒绝无效文件。已接受的更新等待当前 Loader 工作与已移除 fiber 完成后,发布组合信息并返回问题。必须保留旧 fiber 引用,因为已移除的条目不在 Loader 任务枚举中。某行失败后,其他行已成功的变化保留。新的 patch 副本避免后续覆盖修改其他重组会复用的数据。 `ProfileRuntime` 将 Loader 声明为必需注入,使根上下文与插件上下文取得的句柄都能重组并检查配置树。
 
-**发现阶段只读声明,不执行模块。** `readPackageMetadata` 读取包身份、组合包 patch 和显式 `dsh.plugins` 条目。`.` 表示主入口;子路径与默认配置继续供组合编辑器使用。未声明的包保持已安装和未知状态。声明不证明可导入、Config 有效或激活成功。Cordis 物理 peer 解析仅供提示,无法识别内联副本。运行校验属于每个实际挂载行,包括同一模块在不同预设中的独立挂载。
+**发现元数据时不执行安装包代码。** `readPackageMetadata` 读取包身份和组合包 patch 行。组合包元数据描述其声明的组合;没有组合包 patch 的包保持已安装和未知状态。Cordis 物理 peer 解析仅供提示,无法识别内联副本。Config 校验和执行诊断属于实际挂载的行。
 
 **未处理的进程级失败仍然致命。** `installFailLoud` 保持到应用关闭。只有条目审计已观测到的重复 rejection 会在其进程检查点内合并。无关的脱离管理 rejection 保留 master 的清理退出策略。进程内的组无法防护 `process.exit`、事件循环阻塞、原生崩溃或 OOM。预设代际的拥有者保留独立的严格挂载与清理行为。
 
@@ -26,7 +26,7 @@ Status: implemented
 
 ## Alternatives considered
 
-**保留 contained group 和子进程 probe。** contained group 防止事务式 Loader 回滚,并为已移除行提供失败记录。原生条目会同时保留成功行与失败行,包装只增加身份,并未提供整层组合所缺少的整包启停能力。子进程 probe 为发现阶段的执行设定边界,并发现未声明导出与 schema。静态声明避免这次执行,同时放弃自动主入口识别和挂载前 schema。未来的执行沙箱需要独立设计进程归属与清理,不能把发现 probe 视为运行隔离。
+**为每个组合包包装一个合成父组。** 原生条目同时保留成功行与失败行。包装会增加条目身份,而整包启停已经由选中的 patch 层决定。保留作者声明的 id,才能继续使用其表达式和用户 patch。
 
 **在 Host 内 import 并 try/catch。** 拒绝,因为发现阶段可能产生不可逆副作用、退出 Host 或阻塞事件循环。捕获异常无法恢复隔离。通过解析源码推断插件导出会增加一个结果不完整的 JavaScript 解释器。
 
@@ -36,10 +36,8 @@ Status: implemented
 
 ## Consequences
 
-已安装元信息、启用层选择与当前运行健康相互独立。启用的组合包可能部分运行、等待依赖或失败;行更新可能保留旧行为并显示诊断。整包禁用移除其 patch 效果,无需合成父组。可选提供方失败导致必需消费方等待时,启动仍可能失败。旧 probe 记录不是激活证据,插件需要显式 `dsh.plugins` 声明才能被发现为可添加模块。
+已安装元信息、启用层选择与当前运行健康相互独立。启用的组合包可能部分运行、等待依赖或失败;行更新可能保留旧行为并显示诊断。整包禁用移除其 patch 效果,无需合成父组。可选提供方失败导致必需消费方等待时,启动仍可能失败。
 
 ## Verification
 
-`packages/boot/app-boot/tests/entry-issues.spec.ts` 覆盖必需 id 策略、导入/配置/apply/disabled/pending 失败、嵌套条目与匿名行的组合包归属、失败更新后的活跃旧配置、恢复及已移除 fiber 的清理等待。组合测试覆盖重复归属与父组保持。静态元信息测试使用导入时写文件的 fixture,确认未执行、保留未知包并拒绝无效声明。inventory 测试直接验证 Loader 失败与组合冲突,不依赖历史失败注册表。进程处理与用户 patch 测试保留脱离管理失败的致命策略,以及尽力执行的实时重载诊断。
-
-[管理器与 Remote 的拆分](2026-09-04-plugin-manager-over-the-profile-runtime.zh.md)继续适用。管理器集成测试覆盖未知包保留、静态声明、逐行启用结果、保留原归属的失败覆盖,以及依赖恢复后的运行变化通知。
+`packages/boot/app-boot/tests/entry-issues.spec.ts` 覆盖必需 id 策略、导入/配置/apply/disabled/pending 失败、嵌套条目与匿名行的组合包归属、失败更新后的活跃旧配置、恢复及已移除 fiber 的清理等待。组合测试覆盖重复归属与父组保持。静态元信息测试使用导入时写文件的 fixture,确认未执行、保留未知包并忽略无关的 manifest 字段。inventory 测试直接验证 Loader 失败与组合冲突,不依赖历史失败注册表。进程处理与用户 patch 测试保留脱离管理失败的致命策略,以及尽力执行的实时重载诊断。

+ 1 - 2
apps/web/tests/expected/plugin-manager/manager.expected.md

@@ -9,10 +9,9 @@
     - button "查看 示例组合包": 示例组合包
     - text: "Web e2e fixture: a bundle whose one row is an inert plugin."
     - switch "启用 示例组合包"
-- heading "插件" [level=3]
+- heading "其他已安装包" [level=3]
 - text: 1 个
 - list:
   - listitem:
     - button "查看 示例插件": 示例插件
     - text: "Web e2e fixture: a plain plugin module with no bundle, added to a composition per row."
-    - button "加入全局"

+ 4 - 15
apps/web/tests/plugin-manager.e2e.ts

@@ -1,6 +1,5 @@
 // Web e2e scenario: the plugin manager page behind the sidebar's Plugins entry over a scaffold
-// profile runtime — one installed bundle switched on, one plain plugin added to
-// the global user layer. Zero model calls: everything is
+// profile runtime: installed packages and bundle enablement. Zero model calls: everything is
 // client state plus the profile files and the settings document, so there is
 // no fixture and a stray stream would fail loud on the open llm seam.
 import { readFile } from 'node:fs/promises'
@@ -76,17 +75,15 @@ describe('web e2e: plugin manager', () => {
 
     await panel.getByText('示例组合包', { exact: true }).waitFor({ timeout: 20_000 })
     expect(await panel.getByText('示例插件', { exact: true }).count()).toBe(1)
-    // The bundle is installed but not enabled; the plain plugin carries no
-    // switch, only its global add button; uninstall waits on the plugin's page,
-    // which names the module and where it is composed, and the crumb leads back.
+    // Non-bundle packages remain installed and expose their uninstall on the detail page.
     const toggle = panel.getByRole('switch', { name: '启用 示例组合包' })
     expect(await toggle.getAttribute('aria-checked')).toBe('false')
     expect(await panel.getByRole('switch', { name: '启用 示例插件' }).count()).toBe(0)
-    expect(await panel.getByRole('button', { name: '加入全局' }).count()).toBe(1)
+    expect(await panel.getByRole('button', { name: '加入全局' }).count()).toBe(0)
     expect(await panel.getByRole('button', { name: '卸载 示例插件' }).count()).toBe(0)
     await panel.getByRole('button', { name: '查看 示例插件' }).click()
     await panel.getByRole('button', { name: '卸载 示例插件' }).waitFor({ timeout: 5_000 })
-    await panel.getByText('尚未加入', { exact: true }).waitFor({ timeout: 5_000 })
+    await panel.getByText('此包未提供组合包 patch,可通过 Cordis 配置手动加载模块。', { exact: true }).waitFor({ timeout: 5_000 })
     await panel.getByRole('button', { name: '返回插件列表' }).click()
     await expect.poll(() => panel.getByRole('button', { name: '卸载 示例插件' }).count(), { timeout: 5_000 }).toBe(0)
 
@@ -123,14 +120,6 @@ describe('web e2e: plugin manager', () => {
     expect(tripwire.pageErrors).toEqual([])
   }, 60_000)
 
-  it('adds a declared plugin to the global user layer', async () => {
-    const panel = await openPluginsPanel()
-    const card = panel.locator('[data-plugin-package="@fixture/plain-plugin"]')
-    await card.getByRole('button', { name: '加入全局', exact: true }).click()
-    await expect.poll(() => homeFile('profiles', 'scaffold', 'cordis.patch.yml')).toContain('@fixture/plain-plugin')
-    expect(tripwire.pageErrors).toEqual([])
-  })
-
   it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
     expect(tripwire.warnings).toEqual([])
     await assertFixtureInventory(SNAPSHOT_DIR, ['manager.expected.md', 'live-enabled.expected.md'])

+ 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: 6f9e1de0be374f5e508223479e0e5fb7f02d4e25
-config-catalog.zh.md: b5164c6010ee2da8c8bcd9d0affe6214b850ab41
+config-catalog.md: 604da8f1422363b40cfe5902ded7e30319d82502
+config-catalog.zh.md: 8e707bd82b90d6006c76be28d68a038d8f207e30

+ 1 - 1
docs/config-catalog.md

@@ -942,7 +942,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/host/plugin-manager/src/index.ts:40`](../packages/host/plugin-manager/src/index.ts)
+Source: [`packages/host/plugin-manager/src/index.ts:39`](../packages/host/plugin-manager/src/index.ts)
 
 <a id="deepseek-aidsh-host-webserver"></a>
 

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

@@ -944,7 +944,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/host/plugin-manager/src/index.ts:40`](../packages/host/plugin-manager/src/index.ts)
+来源:[`packages/host/plugin-manager/src/index.ts:39`](../packages/host/plugin-manager/src/index.ts)
 
 
 <a id="deepseek-aidsh-host-webserver"></a>

+ 1 - 1
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: f30ae258881eee0cf3b220aaf760e17ac1a900dd
+event-producer-consumer.md: abfc811e02271eace2ee19965c76b6ad7caeef07
 event-producer-consumer.zh.md: 02645d1b5ba78a23f75a3d4c7e81fea2c2809f3a

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

@@ -48,8 +48,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
-| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:233`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:239`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:206`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:212`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 2 - 2
docs/subsystems/core.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/subsystems/core.md
-core.md: 9ffdfdac6bc4262a541eed214386d8649ff041c1
-core.zh.md: 45298191ecaf3d03894638e3167712bf6fe39a2a
+core.md: fcc2d3300e1e9fe7014f5c7f8a566b3b0a80f47a
+core.zh.md: e1879ce32a4f9b2ed301aa808cb12759e43e09a5

+ 0 - 15
docs/subsystems/core.md

@@ -948,21 +948,6 @@ The row injects only the Loader; the profile runtime and the agent registry are
  */
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
-/**
- * Add a row naming one of the package's modules to the profile user layer.
- * @param packageName - the installed package.
- * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
- * `id` overrides the derived row id, `config` overrides the declared default.
- * @returns where the row landed.
- */
-@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
-
-/**
- * Remove a row a user layer inserted.
- * @param rowId - the inserted row's id.
- */
-@Remote('removeRow') async removeRow(rowId: string): Promise<void>
-
 /**
  * Switch one row off or on in a user layer; deny-only.
  * @param rowId - the row's id as the composition declares it.

+ 0 - 15
docs/subsystems/core.zh.md

@@ -958,21 +958,6 @@ The row injects only the Loader; the profile runtime and the agent registry are
  */
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
-/**
- * Add a row naming one of the package's modules to the profile user layer.
- * @param packageName - the installed package.
- * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
- * `id` overrides the derived row id, `config` overrides the declared default.
- * @returns where the row landed.
- */
-@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
-
-/**
- * Remove a row a user layer inserted.
- * @param rowId - the inserted row's id.
- */
-@Remote('removeRow') async removeRow(rowId: string): Promise<void>
-
 /**
  * Switch one row off or on in a user layer; deny-only.
  * @param rowId - the row's id as the composition declares it.

+ 2 - 2
packages/boot/app-boot/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/app-boot/README.md
-README.md: e070f187379638dff75e1bcfc3bfe835195d08c0
-README.zh.md: 9d56700ca22743c7fc484bba113b718751f935e0
+README.md: b34d38cc928e01c320b502560ea82fa7817fa5c7
+README.zh.md: 4ba7b4d34f3aa80e505ac61798ffcf10a078dcc2

+ 1 - 1
packages/boot/app-boot/README.md

@@ -201,7 +201,7 @@ These limits describe when this boot library is a poor fit or needs special care
 - **Overrides retain the target’s owner** — changing a built-in row can make that required row fail; successful updates to other rows remain applied. Disabling the bundle removes its whole patch layer, including overrides.
 - **A conflict is decided by order, not merit** — among external bundles the earlier layer in `dsh.profile.bundles` keeps a contested id, so uninstalling that bundle lets the later one mount on the next boot; the plugin list shows which bundle lost and to whom.
 - **The nested-fiber audit is advisory** — a failed `ctx.inject()` continuation under a built-in entry is reported, not fatal, until shipped compositions are known clean.
-- **Declarations are not activation results** — `readPackageMetadata` reads `dsh.plugins` without importing modules. Declare `plugins: [{ name: "." }]` for a main export, or subpaths such as `./tools`; undeclared packages remain `unknown`. Config validation and execution diagnostics appear only after an actual mount. Physical Cordis resolution cannot detect an inlined copy.
+- **Metadata is not an activation result** — `readPackageMetadata` reads package identity and bundle patch rows without importing modules. Packages without a bundle patch remain `unknown`. Config validation and execution diagnostics appear only after an actual mount. Physical Cordis resolution cannot detect an inlined copy.
 
 <a id="dev-note"></a>
 ### Dev Note

+ 1 - 1
packages/boot/app-boot/README.zh.md

@@ -201,7 +201,7 @@ Loader 结算后,app-boot 将 optional 失败报告为警告;若已启用的
 - **覆盖保留目标的所有者**——修改内置行可能使该必需行失败;其他行成功的更新仍然生效。禁用组合包移除其整份 patch 层,包括覆盖。
 - **冲突按顺序判定,不看是非**——外部组合包之间,`dsh.profile.bundles` 里靠前的那层保住争议 id,卸掉它之后靠后的那层在下次启动时挂上;插件列表显示谁输给了谁。
 - **嵌套 fiber 审计只是提示**——内置条目下失败的 `ctx.inject()` 延续会被报告而非致命,直到确认随附组合都没有这类失败。
-- **声明不代表激活结果**——`readPackageMetadata` 读取 `dsh.plugins`,不导入模块。主入口声明为 `plugins: [{ name: "." }]`,子路径可写为 `./tools`;未声明的包保持 `unknown`。Config 校验和执行诊断只在实际挂载后产生。Cordis 物理路径解析无法识别被内联打包的副本。
+- **元数据不代表激活结果**——`readPackageMetadata` 读取包身份和组合包 patch 行,不导入模块。没有组合包 patch 的包保持 `unknown`。Config 校验和执行诊断只在实际挂载后产生。Cordis 物理路径解析无法识别被内联打包的副本。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 5 - 35
packages/boot/app-boot/src/package-metadata.ts

@@ -6,7 +6,6 @@ import { fileURLToPath } from 'node:url'
 import { loadOverlayPatches } from './index.ts'
 import { readProfileManifest, resolveBundleDir } from './profile.ts'
 import { visitPatchRows } from './patch-rows.ts'
-import type { DshPluginDeclaration } from '@deepseek-ai/dsh-package-manifest'
 
 /** One configured bundle row; conditions remain unevaluated until it mounts. */
 export interface PackageMetadataRow {
@@ -21,13 +20,12 @@ export interface PackageMetadata {
   readonly version?: string
   readonly description?: string
   readonly title?: string
-  readonly kind: 'bundle' | 'plugin' | 'unknown'
+  readonly kind: 'bundle' | 'unknown'
   readonly enginesDsh?: string
   /** Physical resolution only; null when unavailable, not an execution guarantee. */
   readonly cordisSameCopy: boolean | null
   readonly rows: readonly PackageMetadataRow[]
   readonly overrides: readonly string[]
-  readonly addable: readonly DshPluginDeclaration[]
 }
 
 /** Locations needed to read one installed package. */
@@ -39,39 +37,15 @@ export interface PackageMetadataOptions {
 }
 
 /**
- * Read package identity, bundle rows and explicitly addable modules without loading code.
+ * Read package identity and bundle rows without loading code.
  * @param options - installation and profile resolution anchors.
- * @returns declared metadata; an undeclared main export remains unknown.
+ * @returns declared metadata; packages without a bundle patch remain unknown.
  * @throws when the package or its declaration cannot be read or parsed.
  */
 export function readPackageMetadata(options: PackageMetadataOptions): PackageMetadata {
   const { binName, packageName, installAnchor, profileDir } = options
   const dir = resolveBundleDir(binName, packageName, installAnchor, profileDir)
   const manifest = readProfileManifest(binName, dir)
-  const declarations: unknown = manifest.dsh?.plugins
-  const addable: DshPluginDeclaration[] = []
-  if (declarations !== undefined) {
-    if (!Array.isArray(declarations)) throw new TypeError(`${packageName}: dsh.plugins must be an array`)
-    const names = new Set<string>()
-    for (const value of declarations as unknown[]) {
-      if (!isRecord(value) || typeof value.name !== 'string' || value.name.length === 0
-        || (value.title !== undefined && typeof value.title !== 'string')) {
-        throw new TypeError(`${packageName}: dsh.plugins entries require a name and an optional string title`)
-      }
-      const name = value.name
-      const subpath = name.replace(/^\.\//, '')
-      if (name !== '.' && (subpath.startsWith('/') || subpath.split('/').some(part => part === '..' || part === '.' || part === '')
-        || /[:\\]/.test(subpath))) throw new TypeError(`${packageName}: invalid plugin subpath ${JSON.stringify(name)}`)
-      const key = name === '.' ? '.' : subpath
-      if (names.has(key)) throw new TypeError(`${packageName}: repeated plugin subpath ${JSON.stringify(name)}`)
-      names.add(key)
-      addable.push({
-        name,
-        ...value.title === undefined ? {} : { title: value.title },
-        ...value.config === undefined ? {} : { config: value.config },
-      })
-    }
-  }
   const rows: PackageMetadataRow[] = []
   const own = new Set<string>()
   const declaredBundle = manifest.dsh?.bundle?.patch
@@ -88,15 +62,11 @@ export function readPackageMetadata(options: PackageMetadataOptions): PackageMet
     ...manifest.description === undefined ? {} : { description: manifest.description },
     ...manifest.dsh?.title === undefined ? {} : { title: manifest.dsh.title },
     ...manifest.engines?.dsh === undefined ? {} : { enginesDsh: manifest.engines.dsh },
-    kind: declaredBundle !== undefined ? 'bundle' : addable.length > 0 ? 'plugin' : 'unknown',
-    cordisSameCopy, rows, overrides, addable,
+    kind: declaredBundle !== undefined ? 'bundle' : 'unknown',
+    cordisSameCopy, rows, overrides,
   }
 }
 
-function isRecord(value: unknown): value is Record<string, unknown> {
-  return typeof value === 'object' && value !== null && !Array.isArray(value)
-}
-
 /** Resolve physical package identity through a packaged-runtime module proxy. */
 function cordisPackageDir(resolved: string): string | undefined {
   let dir = dirname(resolved.startsWith('file:') ? fileURLToPath(resolved) : resolved)

+ 13 - 13
packages/boot/app-boot/tests/package-metadata.spec.ts

@@ -63,20 +63,18 @@ describe('readPackageMetadata', () => {
     expect(metadata.cordisSameCopy).toBe(kind === 'foreign' ? false : kind === 'proxy' ? true : null)
   })
 
-  it('reads explicit main and subpath entries without importing them or reading a probe cache', () => {
+  it('reads package identity without executing the main export', () => {
     const staged = stage({ pkg: {
       main: "import { writeFileSync } from 'node:fs'; writeFileSync(new URL('./executed', import.meta.url), 'bad'); throw new Error('must never execute')",
-      manifest: { dsh: { title: 'Package', plugins: [{ name: '.', title: 'Main', config: { a: 1 } }, { name: './tools', title: 'Tools' }] } },
+      manifest: { dsh: { title: 'Package' } },
     } })
     const options = { ...staged, binName: NAME, packageName: 'pkg' }
     const result = readPackageMetadata(options)
-    expect(result).toMatchObject({ kind: 'plugin', title: 'Package', version: '1.0.0',
-      addable: [{ name: '.', title: 'Main', config: { a: 1 } }, { name: './tools', title: 'Tools' }],
-    })
+    expect(result).toMatchObject({ kind: 'unknown', title: 'Package', version: '1.0.0' })
     const dir = join(staged.profileDir, 'node_modules/pkg')
     expect(existsSync(join(dir, 'executed'))).toBe(false)
     writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'pkg', version: '2.0.0' }))
-    expect(readPackageMetadata(options)).toMatchObject({ kind: 'unknown', version: '2.0.0', addable: [] })
+    expect(readPackageMetadata(options)).toMatchObject({ kind: 'unknown', version: '2.0.0' })
   })
   it('lists bundle rows, nested groups, unevaluated gates and external overrides', () => {
     const staged = stage({ pkg: {
@@ -98,7 +96,7 @@ describe('readPackageMetadata', () => {
     expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toMatchObject({
       kind: 'bundle', description: 'Bundle', enginesDsh: '>=0.1.0',
       rows: [{ id: 'group', name: 'cordis:group', gated: false }, { id: 'tool', name: 'pkg/tool', gated: true }],
-      overrides: ['webserver'], addable: [],
+      overrides: ['webserver'],
     })
   })
   it.each(['export default () => {}', 'export function apply() {}', 'throw new Error("broken")'])('keeps an undeclared package unknown: %s', (main) => {
@@ -106,11 +104,13 @@ describe('readPackageMetadata', () => {
     expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' }).kind).toBe('unknown')
     expect(existsSync(join(staged.profileDir, 'node_modules/pkg/package.json'))).toBe(true)
   })
-  it.each([
-    'invalid', [{}], [{ name: '' }], [{ name: '../outside' }], [{ name: '/absolute' }],
-    [{ name: './tools' }, { name: 'tools' }], [{ name: '.', title: 1 }],
-  ])('rejects malformed or ambiguous plugin declarations: %j', (plugins) => {
-    const staged = stage({ pkg: { manifest: { dsh: { plugins } } } })
-    expect(() => readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toThrow()
+  it('ignores unrelated metadata while reading a bundle patch', () => {
+    const staged = stage({ pkg: {
+      manifest: { dsh: { bundle: { patch: './patch.yml' }, plugins: 'unrecognized metadata' } },
+      files: { 'patch.yml': '- insert:\n    - id: tool\n      name: pkg/tool\n' },
+    } })
+    expect(readPackageMetadata({ ...staged, binName: NAME, packageName: 'pkg' })).toMatchObject({
+      kind: 'bundle', rows: [{ id: 'tool', name: 'pkg/tool', gated: false }],
+    })
   })
 })

+ 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: 538f837dbaaec0b9b89d0ba2a70a44d5f67cf32f
-README.zh.md: 803dbd572b735f95f828260290fd7d39dd4e6fbe
+README.md: af12af8fb8f7839c9a2ec34275b8fbf65a21126e
+README.zh.md: a6569cfb5af5fc08f1d2595c48e3eb57f49e20af

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

@@ -70,17 +70,17 @@ const manager = new PluginManager(ctx, {
 console.log(await manager.list())
 ```
 
-`list` reports package identity, `bundle` / `plugin` / `unknown` classification, installation and enablement. Runtime rows carry actual phases and failures; disabled bundles show static patch declarations. `addable` comes only from `dsh.plugins`, including `.` for the main export and declared defaults. An active row can retain its previous config after an update fails. Package `issues` also names failed rows its patch overrides, without transferring their ownership.
+`list` reports package identity, `bundle` / `unknown` classification, installation and enablement. Runtime rows carry actual phases and failures; disabled bundles show static patch declarations. An active row can retain its previous config after an update fails. Package `issues` also names failed rows its patch overrides, without transferring their ownership.
 
 `enable` selects the whole bundle layer and recomposes live profiles. Per-row failures retain the enabled choice and successful siblings; results report `issues`, and the list can show `partial` or `failed`. Preparation failures revert the enable selection and raise `plugins/enable-failed`. `disable` removes the whole layer, including overrides. `retry` disables, awaits cleanup, and enables again. Startup-only profiles report `effect: restart`. Unapplied selections on live profiles report `failed`, preserving the rows actually running; `restart-required` is reserved for startup-only profiles. `uninstall` disables the bundle, removes user-inserted references, and runs pnpm remove.
 
-`addRow` writes an explicitly declared `dsh.plugins` module to the profile’s global `cordis.patch.yml`. It preserves declared defaults, checks the target row id, and performs no pre-mount import. `removeRow` removes a user insert. `setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles. `dependents` reports injection dependents and user-layer module references.
+`setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles. `dependents` reports injection dependents and user-layer module references.
 
 The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
 
 ### Failures
 
-Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
+Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
 
 -----
 
@@ -114,7 +114,6 @@ The manager reads Loader entries, the reflect store and entry-owned diagnostics.
 | [`src/installer.ts`](src/installer.ts) | `PluginInstaller`: pnpm runs with streamed output, static declarations, and the post-install checks |
 | [`src/manager.ts`](src/manager.ts) | `PluginManager`: every operation over the booted profile, the one-at-a-time mutex, user-layer edits, and dependents |
 | [`src/view.ts`](src/view.ts) | The view fold: manifest, declarations, and live-tree rows into one `PluginPackageView`, and the row-ownership walk |
-| [`src/modules.ts`](src/modules.ts) | Declared `dsh.plugins` modules: row naming, derived row ids, and their wire view |
 | [`src/helpers.ts`](src/helpers.ts) | Shared vocabulary: the diagnostic prefix, tooling bounds, the spawn seam, and the manifest readers |
 | [`src/types.ts`](src/types.ts) | Payloads, the `plugins/changed` and `plugins/install-log` events, and the `plugins/*` failure codes with their details |
 | [`src/errors.ts`](src/errors.ts) | `PluginOperationError` and the code-discriminated failure union |

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

@@ -70,17 +70,17 @@ const manager = new PluginManager(ctx, {
 console.log(await manager.list())
 ```
 
-`list` 报告包身份、`bundle` / `plugin` / `unknown` 分类、安装与启用状态。运行行携带实际阶段与失败;禁用组合包显示静态 patch 声明。`addable` 只来自 `dsh.plugins`,包括表示主入口的 `.` 和声明的默认配置。行更新失败后,活跃实例可能保留先前配置。包的 `issues` 还报告其 patch 覆盖的失败行,但不转移这些行的归属。
+`list` 报告包身份、`bundle` / `unknown` 分类、安装与启用状态。运行行携带实际阶段与失败;禁用组合包显示静态 patch 声明。行更新失败后,活跃实例可能保留先前配置。包的 `issues` 还报告其 patch 覆盖的失败行,但不转移这些行的归属。
 
 `enable` 选择整份组合包层,并在实时 profile 中重组。逐行失败保留启用选择与成功的其他行;结果报告 `issues`,列表可显示 `partial` 或 `failed`。准备失败会撤销启用选择,并抛出 `plugins/enable-failed`。`disable` 移除整层,包括覆盖。`retry` 先禁用并等待清理,再启用。仅启动时生效的 profile 报告 `effect: restart`。实时 profile 中尚未应用的选择报告 `failed`,并保留实际运行的行;`restart-required` 只用于仅启动时生效的 profile。`uninstall` 禁用组合包、删除用户插入的引用、执行 pnpm remove。
 
-`addRow` 将显式声明的 `dsh.plugins` 模块写入 profile 的全局 `cordis.patch.yml`。它保留声明的默认配置,检查目标行 id,不在挂载前 import。`removeRow` 删除用户插入。`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组。`dependents` 报告注入依赖方与用户层模块引用。
+`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组。`dependents` 报告注入依赖方与用户层模块引用。
 
 管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行与所在的 profile 目录,开了颜色时还带着 pnpm 的 SGR 转义。
 
 ### 失败
 
-每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
+每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
 
 -----
 
@@ -114,7 +114,6 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 | [`src/installer.ts`](src/installer.ts) | `PluginInstaller`:pnpm 流式输出、静态声明与装后检查 |
 | [`src/manager.ts`](src/manager.ts) | `PluginManager`:已启动 profile 上的每项操作、一次一个的互斥、用户层编辑与依赖查询 |
 | [`src/view.ts`](src/view.ts) | 将 manifest、声明与实际行状态组合为 `PluginPackageView`,并查询行归属 |
-| [`src/modules.ts`](src/modules.ts) | 声明的 `dsh.plugins` 模块:行命名、派生行 id 及其 wire 视图 |
 | [`src/helpers.ts`](src/helpers.ts) | 共享词汇:诊断前缀、工具边界、spawn 测试缝与 manifest 读取器 |
 | [`src/types.ts`](src/types.ts) | 载荷、`plugins/changed` 与 `plugins/install-log` 事件,以及 `plugins/*` 失败码及其 details |
 | [`src/errors.ts`](src/errors.ts) | `PluginOperationError` 与按码区分的失败联合 |

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

@@ -43,7 +43,6 @@
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
-    "@deepseek-ai/dsh-package-manifest": "workspace:^",
-    "@deepseek-ai/dsh-util-values": "workspace:^"
+    "@deepseek-ai/dsh-package-manifest": "workspace:^"
   }
 }

+ 0 - 77
packages/boot/plugin-manager/src/manager.ts

@@ -13,25 +13,21 @@ import {
   healProfilesModuleFallback,
   loadProfile,
   readProfileManifest,
-  rootIncludeEntry,
   inspectEntryIssues,
   type EntryIssue,
   type readPackageMetadata,
   type ProfileRuntime,
 } from '@deepseek-ai/dsh-app-boot'
 import { mutatePatchFile, readPatchListFile, type PatchRow } from '@deepseek-ai/dsh-app-boot/patch-file'
-import type { JsonValue } from '@deepseek-ai/dsh-util-values'
 import { PluginOperationError } from './errors.ts'
 import { bundlesOf, dependenciesOf, messageOf, NAME, optional, type PluginToolingConfig, type SpawnLike } from './helpers.ts'
 import { PluginInstaller } from './installer.ts'
-import { derivedRowId, moduleSpecifier } from './modules.ts'
 import type {
   PluginChangeReason,
   PluginDependents,
   PluginEnableResult,
   PluginInstallResult,
   PluginPackageView,
-  PluginRowAddition,
   PluginRowIssue,
   PluginRowReference,
   PluginServiceDependent,
@@ -328,72 +324,6 @@ export class PluginManager {
     return result
   }
 
-  /**
-   * Add a row naming one of the package's modules to the profile's `cordis.patch.yml`.
-   * @param packageName - the installed package.
-   * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
-   * `id` overrides the derived row id, `config` overrides the declared default.
-   * @returns where the row landed.
-   * @throws {PluginOperationError} `plugins/not-installed`, `plugins/not-enableable`
-   * when the module is absent from `dsh.plugins`, or `plugins/row-conflict`.
-   */
-  async addRow(
-    packageName: string,
-    options?: { module?: string; id?: string; config?: JsonValue },
-  ): Promise<PluginRowAddition> {
-    return this.exclusive('addRow', packageName, () => this.addRowNow(packageName, options))
-  }
-
-  private async addRowNow(
-    packageName: string,
-    options?: { module?: string; id?: string; config?: JsonValue },
-  ): Promise<PluginRowAddition> {
-    const runtime = this.runtime()
-    const installer = this.installer(runtime)
-    installer.assertInstalled(packageName)
-    const metadata = installer.metadata(packageName)
-    const declared = options?.module ?? '.'
-    const addable = metadata.addable.find(entry => entry.name === declared)
-    if (addable === undefined) {
-      const reason = `${declared} is not a module ${packageName} declares addable`
-      throw new PluginOperationError('plugins/not-enableable', `${NAME}: ${reason}`, { packageName, reason })
-    }
-    const moduleName = moduleSpecifier(packageName, declared)
-    const rowId = options?.id ?? derivedRowId(packageName, declared)
-    if (rowId.trim() === '' || rowId.includes(':')) {
-      throw new PluginOperationError('plugins/bad-request', `${NAME}: row ids must be nonempty and cannot contain ':'`, {})
-    }
-    const config = options?.config !== undefined ? options.config : addable.config !== undefined ? addable.config : {}
-    const row: PatchRow = { id: rowId, name: moduleName, config }
-    if (await this.rowExists(runtime, rowId)) {
-      throw new PluginOperationError('plugins/row-conflict', `${NAME}: a row ${JSON.stringify(rowId)} already exists`, { rowId })
-    }
-    const file = await this.editLayer(runtime, (document) => { document.appendInsert(row) })
-    this.changed('row', packageName)
-    return { rowId, file }
-  }
-
-  /**
-   * Remove a row a user layer inserted.
-   * @param rowId - the inserted row's id.
-   * @throws {PluginOperationError} `plugins/bad-request` when the layer inserts no such row.
-   */
-  async removeRow(rowId: string): Promise<void> {
-    return this.exclusive('removeRow', rowId, () => this.removeRowNow(rowId))
-  }
-
-  private async removeRowNow(rowId: string): Promise<void> {
-    const runtime = this.runtime()
-    // A holder rather than a `let`: the assignment happens inside the edit
-    // callback, which control-flow narrowing does not see.
-    const outcome = { removed: false }
-    await this.editLayer(runtime, (document) => { outcome.removed = document.removeInsert(rowId) })
-    if (!outcome.removed) {
-      throw new PluginOperationError('plugins/bad-request', `${NAME}: the layer inserts no row ${JSON.stringify(rowId)}`, {})
-    }
-    this.changed('row')
-  }
-
   /**
    * Switch one row off or on in a user layer. Deny-only: `true` writes
    * `disabled: true` for the row, `false` removes that key, so a bundle's
@@ -470,13 +400,6 @@ export class PluginManager {
     return found
   }
 
-  /** Whether the profile's composition already carries a row with `rowId`. */
-  private async rowExists(runtime: ProfileRuntime, rowId: string): Promise<boolean> {
-    const tree = rootIncludeEntry(this.ctx.root)?.subtree
-    return [...this.ctx.loader.entries()].some(entry => entry.parent.tree === tree && entry.options.id === rowId)
-      || (await readPatchListFile(NAME, runtime.patchPath, 'patches') ?? []).some(patch => patch.insert?.some(row => row.id === rowId))
-  }
-
   /** Edit one user layer file and, for the live global layer, recompose. */
   private async editLayer(
     runtime: ProfileRuntime,

+ 0 - 48
packages/boot/plugin-manager/src/modules.ts

@@ -1,48 +0,0 @@
-/**
- * The modules a package offers a composition: how a declared `dsh.plugins`
- * entry is named as a row and derived into a row id, and its wire view.
- * @module @deepseek-ai/dsh-plugin-manager/modules
- */
-
-import type { PackageMetadata } from '@deepseek-ai/dsh-app-boot'
-import type { JsonValue } from '@deepseek-ai/dsh-util-values'
-import { optional } from './helpers.ts'
-import type { PluginPackageAddableView } from './types.ts'
-
-/**
- * The row `name` for one declared addable module.
- * @param packageName - the package.
- * @param declared - the `dsh.plugins[].name`, `.` for the main export.
- * @returns the bare package for `.`, else `<package>/<subpath>`.
- */
-export function moduleSpecifier(packageName: string, declared: string): string {
-  if (declared === '.') return packageName
-  return `${packageName}/${declared.replace(/^\.\//, '')}`
-}
-
-/**
- * The row id derived from a package name and module: the unscoped name, then the subpath.
- * @param packageName - the package.
- * @param declared - the `dsh.plugins[].name`, `.` for the main export.
- * @returns the row id.
- */
-export function derivedRowId(packageName: string, declared: string): string {
-  const base = packageName.replace(/^@/, '')
-  return declared === '.' ? base : `${base}/${declared.replace(/^\.\//, '')}`
-}
-
-/**
- * The wire view of one declared addable module.
- * @param packageName - the package.
- * @param entry - the installed manifest’s module declaration.
- * @returns the view.
- */
-export function addableView(packageName: string, entry: PackageMetadata['addable'][number]): PluginPackageAddableView {
-  return {
-    moduleName: moduleSpecifier(packageName, entry.name),
-    declaredName: entry.name,
-    ...optional('title', entry.title),
-    // Config comes from the installed JSON manifest.
-    ...optional('config', entry.config as JsonValue | undefined),
-  }
-}

+ 4 - 31
packages/boot/plugin-manager/src/types.ts

@@ -5,10 +5,8 @@
  * @module @deepseek-ai/dsh-plugin-manager/types
  */
 
-import type { JsonValue } from '@deepseek-ai/dsh-util-values'
-
-/** What an installed package is: a bundle layer, declared plugin modules, or an unknown package. */
-export type PluginPackageKind = 'bundle' | 'plugin' | 'unknown'
+/** What an installed package is: a bundle layer or an unknown package. */
+export type PluginPackageKind = 'bundle' | 'unknown'
 
 /**
  * The state one package is in, folded from the manifest, declarations, and current entries:
@@ -19,7 +17,7 @@ export type PluginPackageKind = 'bundle' | 'plugin' | 'unknown'
  * - `disabled`: installed and not in the layer list;
  * - `not-enableable`: bundle declarations cannot be read;
  * - `restart-required`: its manifest state and the live tree disagree, which a profile without live reload resolves at the next start;
- * - `plain`: a bundle-less package; explicitly declared modules can be added to a composition.
+ * - `plain`: a package without a bundle patch.
  */
 export type PluginPackageStatus =
   | 'running'
@@ -51,19 +49,6 @@ export interface PluginPackageRowView {
   readonly failure?: { readonly stage: string; readonly message: string }
 }
 
-/** One module a package declares addable to the profile. */
-export interface PluginPackageAddableView {
-  /** The module as it is named in a row: the bare package for `.`, else `<package>/<subpath>`. */
-  readonly moduleName: string
-  /** The `dsh.plugins[].name` the package declared. */
-  readonly declaredName: string
-  /** Display title, when declared. */
-  readonly title?: string
-  /** Default row config, when declared. */
-  readonly config?: JsonValue
-
-}
-
 /** One package as the manager sees it. */
 export interface PluginPackageView {
   /** The installed package name. */
@@ -92,8 +77,6 @@ export interface PluginPackageView {
   readonly issues?: readonly PluginRowIssue[]
   /** Ids of built-in rows the bundle's patch overrides. */
   readonly overrides: readonly string[]
-  /** Modules the package declares addable. */
-  readonly addable: readonly PluginPackageAddableView[]
   /** Whether the profile applies user patch files while running; false means changes wait for a restart. */
   readonly liveReload: boolean
 }
@@ -162,14 +145,6 @@ export interface PluginDependents {
   readonly references: readonly PluginRowReference[]
 }
 
-/** Where a row was added. */
-export interface PluginRowAddition {
-  /** The id the row was given. */
-  readonly rowId: string
-  /** The user layer file the row was written to. */
-  readonly file: string
-}
-
 /** Why the manager changed something, for a listener deciding what to refresh. */
 export type PluginChangeReason = 'install' | 'uninstall' | 'enable' | 'disable' | 'retry' | 'row' | 'runtime'
 
@@ -200,14 +175,12 @@ export interface PluginOperationDetailsMap {
   'plugins/unavailable': { readonly reason: string }
   /** The package is not a profile dependency. */
   'plugins/not-installed': { readonly packageName: string }
-  /** The package cannot be enabled or added, with the declaration or request error. */
+  /** The package cannot be enabled, with the declaration or request error. */
   'plugins/not-enableable': { readonly packageName: string; readonly reason: string }
   /** Preparation or the root Include rejected enablement; the layer selection was reverted. */
   'plugins/enable-failed': { readonly packageName: string; readonly reason: string }
   /** pnpm exited non-zero, could not be spawned, or timed out. */
   'plugins/install-failed': { readonly spec: string; readonly exitCode: number | null; readonly log: string }
-  /** The row id is already taken in the target user layer. */
-  'plugins/row-conflict': { readonly rowId: string }
   /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
   'plugins/busy': {
     readonly operation: string

+ 0 - 2
packages/boot/plugin-manager/src/view.ts

@@ -10,7 +10,6 @@ import type { Entry } from '@deepseek-ai/cordis-plugin-loader'
 import { rootIncludeEntry, type EntryIssue, type PackageMetadata, type ProfileManifest, type ProfileRuntime } from '@deepseek-ai/dsh-app-boot'
 import { bundlesOf, dependenciesOf, messageOf, optional } from './helpers.ts'
 import type { PluginInstaller } from './installer.ts'
-import { addableView } from './modules.ts'
 import type { PluginPackageRowView, PluginPackageStatus, PluginPackageView, PluginRowPhase } from './types.ts'
 
 /** Runtime mirror: FiberState is a cross-package const enum. */
@@ -104,7 +103,6 @@ export function packageView(
     cordisSameCopy: metadata?.cordisSameCopy ?? null,
     rows,
     overrides: metadata?.overrides ?? [],
-    addable: (metadata?.addable ?? []).map(entry => addableView(name, entry)),
     ...affected.length === 0 ? {} : { issues: affected.map(issue => ({
       entryId: issue.entry.id, moduleName: issue.entry.options.name, stage: issue.stage, message: issue.message,
     })) },

+ 36 - 99
packages/boot/plugin-manager/tests/plugin-manager.spec.ts

@@ -100,7 +100,6 @@ interface StagedPackage {
   /** `index.js` text, exported as the package main. */
   main?: string
   version?: string
-  plugins?: { name: string; title?: string; config?: unknown }[]
   files?: Record<string, string>
 }
 
@@ -117,7 +116,6 @@ function stagePackage(profileDir: string, name: string, staged: StagedPackage):
     dsh: {
       title: `Title of ${name}`,
       ...staged.patch === undefined ? {} : { bundle: { patch: './cordis.patch.yml' } },
-      ...staged.plugins === undefined ? {} : { plugins: staged.plugins },
     },
   }, null, 2))
   if (staged.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), staged.patch)
@@ -331,27 +329,16 @@ describe('PluginManager', () => {
     expect(ctx.loader.resolve('include:hello').fiber?.state).toBe(2)
   })
 
-  it('does not execute a declared main module during install or list and refuses an undeclared entry', async () => {
+  it('installs and lists a non-bundle package without executing its main module', async () => {
     const staged = await stageHome()
     const marker = join(staged.profileDir, 'executed')
-    stagePackage(staged.profileDir, 'declared', {
-      plugins: [{ name: '.', config: { custom: 1 } }],
+    stagePackage(staged.profileDir, 'plain', {
       main: `import { writeFileSync } from 'node:fs'; writeFileSync(${JSON.stringify(marker)}, 'bad'); export function apply() {}`,
     })
-    stagePackage(staged.profileDir, 'unknown', { main: 'export function apply() {}' })
     const { manager } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir) })
-    await manager.add('declared')
-    await manager.add('unknown')
-    const views = await manager.list()
-    expect(views.find(view => view.name === 'declared')?.addable[0]).toMatchObject({ moduleName: 'declared', config: { custom: 1 } })
+    await manager.add('plain')
+    expect((await manager.list()).find(view => view.name === 'plain')?.kind).toBe('unknown')
     expect(existsSync(marker)).toBe(false)
-    for (const id of ['', 'nested:child']) {
-      await expect(manager.addRow('declared', { id })).rejects.toMatchObject({ code: 'plugins/bad-request' })
-    }
-    const added = await manager.addRow('declared', { config: null })
-    expect(readFileSync(added.file, 'utf8')).toContain('config: null')
-    expect(views.find(view => view.name === 'unknown')?.kind).toBe('unknown')
-    await expect(manager.addRow('unknown')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
   })
 
   it('reports plugins/unavailable without a profile runtime', async () => {
@@ -364,9 +351,9 @@ describe('PluginManager', () => {
   describe('list', () => {
     it('folds installed, enabled, declared and running facts into one view per package', async () => {
       const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, plugins: [{ name: './extra.js', title: 'Extra' }], files: { 'extra.js': 'export const name = "extra"\nexport function apply() {}\n' } })
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, files: { 'extra.js': 'export const name = "extra"\nexport function apply() {}\n' } })
       stagePackage(staged.profileDir, 'ext-lib', { main: 'export const x = 1\n' })
-      stagePackage(staged.profileDir, 'ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
+      stagePackage(staged.profileDir, 'ext-plugin', { main: 'export const name = "p"\nexport function apply() {}\n' })
       addDependency(staged.profileDir, 'ext-bundle')
       addDependency(staged.profileDir, 'ext-lib')
       addDependency(staged.profileDir, 'ext-plugin')
@@ -377,33 +364,12 @@ describe('PluginManager', () => {
       expect(views.map(view => [view.name, view.kind, view.status, view.installed, view.enabled])).toEqual([
         ['ext-bundle', 'bundle', 'disabled', true, false],
         ['ext-lib', 'unknown', 'plain', true, false],
-        ['ext-plugin', 'plugin', 'plain', true, false],
+        ['ext-plugin', 'unknown', 'plain', true, false],
       ])
       const bundle = views[0]
       expect(bundle).toMatchObject({ version: '1.0.0', title: 'Title of ext-bundle', description: 'staged ext-bundle', liveReload: true })
       // Rows come from static declarations while the bundle is not composed, under the ids the patch declares.
       expect(bundle?.rows).toEqual([{ entryId: 'hello', rowId: 'hello', moduleName: 'cordis:good', enabled: true, phase: null }])
-      expect(bundle?.addable).toEqual([{ moduleName: 'ext-bundle/extra.js', declaredName: './extra.js', title: 'Extra' }])
-    })
-
-    it('lists only declared modules, including an explicitly declared main export', async () => {
-      const staged = await stageHome()
-      const main = 'export const name = "p"\nexport function apply() {}\n'
-      stagePackage(staged.profileDir, 'ext-plugin', {
-        main,
-        plugins: [{ name: './tools/sql.js', title: 'SQL' }],
-        files: { 'tools/sql.js': 'export const name = "sql"\nexport function apply() {}\n' },
-      })
-      stagePackage(staged.profileDir, 'ext-explicit', { main, plugins: [{ name: '.', title: 'Main', config: { dsn: 'sqlite://' } }] })
-      addDependency(staged.profileDir, 'ext-plugin')
-      addDependency(staged.profileDir, 'ext-explicit')
-      const { manager } = await bootProfile(staged)
-
-      const views = await manager.list()
-
-      expect(views.find(view => view.name === 'ext-plugin')?.addable.map(entry => entry.declaredName)).toEqual(['./tools/sql.js'])
-      // A declared "." is the package's own word on its main export; nothing is prepended.
-      expect(views.find(view => view.name === 'ext-explicit')?.addable.map(entry => entry.declaredName)).toEqual(['.'])
     })
 
     it('reads a composed bundle\'s rows from the live tree with their failures', async () => {
@@ -496,7 +462,7 @@ describe('PluginManager', () => {
       const { manager } = await bootProfile(staged, {
         metadata: ({ packageName }) => {
           if (packageName === 'ext-odd') throw new Error('invalid declarations')
-          return { packageName, kind: 'bundle', cordisSameCopy: false, rows: [], overrides: [], addable: [] }
+          return { packageName, kind: 'bundle', cordisSameCopy: false, rows: [], overrides: [] }
         },
       })
 
@@ -898,67 +864,37 @@ describe('PluginManager', () => {
   })
 
   describe('rows in user layers', () => {
-    it('adds, disables, re-enables, and removes a row in the live global layer', async () => {
-      const staged = await stageHome()
-      stagePackage(staged.profileDir, '@acme/ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
-      addDependency(staged.profileDir, '@acme/ext-plugin')
-      const { ctx, manager, changes } = await bootProfile(staged)
-
-      const added = await manager.addRow('@acme/ext-plugin')
-
-      expect(added).toEqual({ rowId: 'acme/ext-plugin', file: join(staged.profileDir, 'cordis.patch.yml') })
-      expect(readFileSync(added.file, 'utf8')).toBe('- insert:\n    - id: acme/ext-plugin\n      name: "@acme/ext-plugin"\n      config: {}\n')
-      // Composed live: the row is in the tree, though its module cannot import from the temp home.
-      expect(entryIds(ctx)).toContain('include:acme/ext-plugin')
-      await expect(manager.addRow('@acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
-
-      await manager.setRowDisabled('acme/ext-plugin', true)
-      expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(true)
-      expect(readFileSync(added.file, 'utf8')).toContain('- id: acme/ext-plugin\n  disabled: true\n')
-      await manager.setRowDisabled('acme/ext-plugin', false)
-      expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(false)
-      expect(readFileSync(added.file, 'utf8')).not.toContain('disabled')
-
-      await manager.removeRow('acme/ext-plugin')
-      expect(entryIds(ctx)).not.toContain('include:acme/ext-plugin')
-      await expect(manager.removeRow('acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/bad-request' })
-      expect(changes.map(change => change.reason)).toEqual(['row', 'row', 'row', 'row'])
-    })
-
-    it('detects a conflict through the layer file when the tree was not recomposed', async () => {
+    it('persists a startup profile row toggle without changing its mounted tree', async () => {
       const staged = await stageHome('startup')
-      stagePackage(staged.profileDir, 'ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
-      addDependency(staged.profileDir, 'ext-plugin')
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW })
+      addDependency(staged.profileDir, 'ext-bundle')
+      const manifest = manifestOf(staged.profileDir)
+      manifest.dsh.profile.bundles.push('ext-bundle')
+      writeFileSync(join(staged.profileDir, 'package.json'), JSON.stringify(manifest))
       const { ctx, manager } = await bootProfile(staged)
 
-      await manager.addRow('ext-plugin')
+      await manager.setRowDisabled('hello', true)
 
-      expect(entryIds(ctx)).not.toContain('include:ext-plugin')
-      await expect(manager.addRow('ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
+      expect(readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')).toContain('disabled: true')
+      expect(ctx.loader.resolve('include:hello').disabled).toBe(false)
     })
 
-    it('adds a declared addable module with its default config and an explicit id, and refuses the rest', async () => {
+    it('disables and restores a bundle row through the live user layer', async () => {
       const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-bundle', {
-        patch: BUNDLE_ONE_ROW,
-        plugins: [
-          { name: './tools/sql.js', title: 'SQL', config: { dsn: 'sqlite://' } },
-          { name: './missing.js' },
-        ],
-        files: { 'tools/sql.js': 'export const name = "sql"\nexport function apply() {}\n' },
-      })
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW })
       addDependency(staged.profileDir, 'ext-bundle')
-      const { manager } = await bootProfile(staged)
+      const { ctx, manager, changes } = await bootProfile(staged)
+      await manager.enable('ext-bundle')
+      const file = join(staged.profileDir, 'cordis.patch.yml')
 
-      const added = await manager.addRow('ext-bundle', { module: './tools/sql.js', id: 'sql' })
-      expect(added.rowId).toBe('sql')
-      expect(readFileSync(added.file, 'utf8')).toContain('- id: sql\n      name: ext-bundle/tools/sql.js\n      config:\n        dsn: sqlite://\n')
-      await expect(manager.addRow('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
-      await expect(manager.addRow('ext-bundle', { module: './missing.js' })).resolves.toMatchObject({ rowId: 'ext-bundle/missing.js' })
-      await expect(manager.addRow('absent')).rejects.toMatchObject({ code: 'plugins/not-installed' })
+      await manager.setRowDisabled('hello', true)
+      expect(ctx.loader.resolve('include:hello').disabled).toBe(true)
+      expect(readFileSync(file, 'utf8')).toContain('disabled: true')
+      await manager.setRowDisabled('hello', false)
+      expect(ctx.loader.resolve('include:hello').disabled).toBe(false)
+      expect(readFileSync(file, 'utf8')).not.toContain('disabled')
+      expect(changes.map(change => change.reason)).toEqual(['enable', 'row', 'row'])
     })
-
-
   })
 
   describe('dependents and uninstall', () => {
@@ -1007,14 +943,15 @@ describe('PluginManager', () => {
       expect((await manager.dependents('ext-lib')).references).toEqual([])
     })
 
-    it('disables, drops references, removes the package, and forgets its probe', async () => {
+    it('disables a bundle and removes references from a handwritten user patch before uninstalling', async () => {
       const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, plugins: [{ name: './extra.js' }], files: { 'extra.js': 'export function apply() {}\n' } })
+      stagePackage(staged.profileDir, 'ext-bundle', { patch: BUNDLE_ONE_ROW, files: { 'extra.js': 'export function apply() {}\n' } })
       addDependency(staged.profileDir, 'ext-bundle')
       const calls: string[][] = []
       const { ctx, manager, changes } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir, calls) })
       await manager.enable('ext-bundle')
-      await manager.addRow('ext-bundle', { module: './extra.js' })
+      writeFileSync(join(staged.profileDir, 'cordis.patch.yml'), '- insert:\n    - id: ext-bundle/extra.js\n      name: ext-bundle/extra.js\n')
+      await ctx.profileRuntime.recompose()
       expect(entryIds(ctx)).toEqual(expect.arrayContaining(['include:hello', 'include:ext-bundle/extra.js']))
 
       await manager.uninstall('ext-bundle')
@@ -1024,7 +961,7 @@ describe('PluginManager', () => {
       expect(entryIds(ctx)).not.toContain('include:hello')
       expect(entryIds(ctx)).not.toContain('include:ext-bundle/extra.js')
       expect(readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')).toBe('[]\n')
-      expect(changes.map(change => change.reason)).toEqual(['enable', 'row', 'disable', 'uninstall'])
+      expect(changes.map(change => change.reason)).toEqual(['enable', 'disable', 'uninstall'])
       await expect(manager.uninstall('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-installed' })
     })
   })
@@ -1032,8 +969,8 @@ describe('PluginManager', () => {
 
 describe('PluginOperationError', () => {
   it('carries its code and details, and is what pluginOperationFailureOf narrows to', () => {
-    const failure = new PluginOperationError('plugins/row-conflict', 'taken', { rowId: 'x' })
-    expect(failure).toMatchObject({ name: 'PluginOperationError', code: 'plugins/row-conflict', message: 'taken', details: { rowId: 'x' } })
+    const failure = new PluginOperationError('plugins/not-installed', 'absent', { packageName: 'x' })
+    expect(failure).toMatchObject({ name: 'PluginOperationError', code: 'plugins/not-installed', message: 'absent', details: { packageName: 'x' } })
     expect(pluginOperationFailureOf(failure)).toBe(failure)
     expect(pluginOperationFailureOf(new Error('plain'))).toBeUndefined()
   })

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

@@ -20,9 +20,6 @@
     {
       "path": "../app-boot"
     },
-    {
-      "path": "../../util/values"
-    },
     {
       "path": "../../util/package-manifest"
     }

+ 2 - 2
packages/client/ui-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/client/ui-plugin-manager/README.md
-README.md: 50a29f8c77c9caf22f2e2eba3ecfd72bbe4fee28
-README.zh.md: 10e544ba47844ac3ddfa1120740d0d56963be79a
+README.md: 4aa1a8812397f878ea8d448bef2e3305480bd435
+README.zh.md: 871c55c1d00c3ad305d41f4cc45e34cb4e6e1210

+ 8 - 12
packages/client/ui-plugin-manager/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use the **Plugins** entry in the Web sidebar to manage installed packages. Switch bundle layers on and off, inspect their rows and failures, add declared modules to the global user layer, and install packages while watching pnpm output. Dependency-sensitive changes and uninstalling ask for confirmation. Global configuration remains in Settings.
+Use the **Plugins** entry in the Web sidebar to manage installed packages. Switch bundle layers on and off, inspect their rows and failures, install packages while watching pnpm output. Dependency-sensitive changes and uninstalling ask for confirmation. Global configuration remains in Settings.
 
 ## Table of Contents
 
@@ -25,7 +25,7 @@ Use the **Plugins** entry in the Web sidebar to manage installed packages. Switc
 <a id="use-this-package"></a>
 ## Use this package
 
-Select **Plugins** in the sidebar. The page reads packages and global composition through `api-remotes` when first opened. Global configuration remains in the Settings **Plugins** section.
+Select **Plugins** in the sidebar. The page reads packages through `api-remotes` when first opened. Global configuration remains in the Settings **Plugins** section.
 
 ### Installing a package
 
@@ -39,13 +39,9 @@ A pack’s switch changes its enabled layer selection. Live profiles recompose b
 
 A row's switch on the pack's page calls `plugins.setRowDisabled` against the profile's global user layer: off writes `disabled: true` for the row's id into the profile's `cordis.patch.yml`, on deletes that key again. The tree recomposes at once, so the row's host half unmounts or mounts while the rest of the pack keeps running; a browser half the pack's own client bundle mounts for every component stays until the page reloads. The switch appears only where it acts at once — an external pack that is switched on, on a profile that applies patches while it runs; built-in packs, packs that are off, and profiles that apply patches at their next start list their rows read-only. A row the pack itself keeps off, by a `disabled: true` row or a `!!js` gate, is locked with the reason, because the user layer only ever denies. A failing row can be switched off; a row another layer already owns has nothing mounted to switch. Switching a row off first asks the Host what other rows inject of what the row provides, with the switch inert meanwhile, and opens a confirmation naming them only when there are any; a row nothing depends on switches off at once, and switching on never asks. A list longer than ten rows gets a filter over the row ids.
 
-### Adding a plugin globally
+### Packages without a bundle
 
-A package with explicit `dsh.plugins` declarations offers **Add globally** for each module. An already-added module has a disabled action. Addition writes the profile's `cordis.patch.yml` without an import precheck. Packages without recognized declarations remain uninstallable and have no Add action.
-
-### Reading a failure
-
-The page separates enablement, running phase and current issues. A missing service reads as *waiting for a dependency*. A row whose old instance remains active after invalid configuration reads *Running; latest update failed* with the actual error. The pack detail also identifies issues in other components its patch affects, preserving their original ownership. Runtime changes refresh the page after Loader and profile recomposition settle. Action refusals retain the Host’s reason.
+Non-bundle dependencies remain installed and appear under **Other installed packages**. Their detail page shows package metadata and uninstall. Modules can be loaded by writing Cordis configuration; this page does not infer module exports.
 
 -----
 
@@ -63,7 +59,7 @@ The browser plugin registers the `plugins` sidebar entry and its `main` panel th
 
 ### The store
 
-`PluginManagerController` owns the package and global-module snapshot, busy keys, notices, install progress and confirmations. It coalesces overlapping reads, refreshes after operations and Host changes, and ignores late results after disposal. Install output is grouped by job id.
+`PluginManagerController` owns the package snapshot, busy keys, notices, install progress and confirmations. It coalesces overlapping reads, refreshes after operations and Host changes, and ignores late results after disposal. Install output is grouped by job id.
 
 ### Confirmation
 
@@ -79,7 +75,7 @@ The browser plugin registers the `plugins` sidebar entry and its `main` panel th
 These pages cover the settings section, the Remote calls, and the Host-side manager.
 
 - [ui-sidebar](../ui-sidebar/README.md) — the panel list the Plugins entry registers into; [ui-layout](../ui-layout/README.md) — the main slot the page occupies.
-- [api-remotes](../../api/remotes/README.md) — the Remote BFF surface behind `plugins.*` and `pluginInventory.list()`.
+- [api-remotes](../../api/remotes/README.md) — the Remote BFF surface behind `plugins.*`.
 - [plugin-manager](../../host/plugin-manager/README.md) — the Host-side manager this page drives.
 
 -----
@@ -100,8 +96,8 @@ None; this package neither assembles nor sends a provider request.
 
 These limits define the reach of the management view; they are current package constraints.
 
-- **Global user-layer rows are not listed per package** — a row added to every session marks that target in the package's **Add to…** menu and shows in its dependents, not on the card; removing it goes through the patch file or a future row list.
-- **Names for harness modules live in this page's dictionary** — a new first-party agent-plane module reads by its short module name until `name.<slug>` and `desc.<slug>` are added here.
+- **User-layer rows are not listed per package** — module references in handwritten patches appear in dependency queries and are cleaned up by uninstall; editing those rows remains a file operation.
+- **Names for harness modules live in this page's dictionary** — a new first-party agent-plane module reads by its short module name until `name.<slug>` are added here.
 - **One install at a time** — the dialog runs one pnpm command; a second spec waits for the first to finish.
 - **No version picker** — the spec is typed as pnpm accepts it; the page neither lists registry versions nor offers upgrades.
 

+ 8 - 12
packages/client/ui-plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用 Web 侧栏的**插件**入口管理已安装的包。可以启停组合包层、检查插件行与失败、把声明的模块添加到全局用户层,并在安装时查看 pnpm 输出。影响依赖的修改与卸载会要求确认。全局配置仍在设置中编辑。
+使用 Web 侧栏的**插件**入口管理已安装的包。可以启停组合包层、检查插件行与失败、并在安装时查看 pnpm 输出。影响依赖的修改与卸载会要求确认。全局配置仍在设置中编辑。
 
 ## 目录
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取包与全局组合。全局配置仍在设置的**插件**分区中编辑。
+在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取包。全局配置仍在设置的**插件**分区中编辑。
 
 ### 安装一个包
 
@@ -39,13 +39,9 @@ kind: "package-reference"
 
 包页面上行的开关以 profile 的全局用户层为目标调用 `plugins.setRowDisabled`:关闭往 profile 的 `cordis.patch.yml` 写入该行 id 的 `disabled: true`,打开再把这个键删掉。树随即重组,该行的宿主半区卸下或挂上,插件包其余部分照常运行;插件包自己的客户端 bundle 为每个组件挂载的浏览器半区要到页面重载才消失。开关只在立即生效的地方出现——已打开的外部插件包,且 profile 在运行中就应用补丁;内置插件包、已关闭的插件包,以及下次启动才应用补丁的 profile,其行只读列出。插件包自己关掉的行,无论是 `disabled: true` 的行还是 `!!js` 门,都带原因锁定,因为用户层只能拒绝。异常的行可以关掉;已被别的层占用 id 的行没有挂载任何东西,无从切换。关闭一行前先向宿主询问有哪些行注入它提供的服务,询问期间开关置灰,有则弹出确认框点名它们;没有依赖方的行直接关闭,打开从不询问。超过十行的列表带一个按行 id 筛选的输入框。
 
-### 将插件添加到全局
+### 没有组合包声明的包
 
-带有显式 `dsh.plugins` 声明的包为各模块提供**加入全局**操作。已加入模块的操作不可点击。添加会写入 profile 的 `cordis.patch.yml`,不预先导入模块检查。没有可识别声明的包仍可卸载,但没有添加操作。
-
-### 阅读失败
-
-页面分别展示启用选择、运行阶段和当前问题。缺少服务显示为*等待依赖*。无效配置后旧实例仍在运行的行显示*运行中,但最近更新失败*及实际错误。组合包详情还标识其 patch 影响的其他组件的问题,保留原归属。运行变化在 Loader 和 profile 重组完成后刷新页面。操作被拒绝时保留宿主给出的原因。
+非组合包依赖保持已安装,列在**其他已安装包**下。详情页展示包元数据和卸载操作。模块可以通过手写 Cordis 配置加载;此页面不推断模块导出。
 
 -----
 
@@ -63,7 +59,7 @@ kind: "package-reference"
 
 ### store
 
-`PluginManagerController` 拥有包与全局模块快照、忙碌键、提示、安装进度和确认状态。它合并重叠读取,在操作或 Host 变化后刷新,并在销毁后忽略晚到结果。安装输出按 job id 分组。
+`PluginManagerController` 拥有包快照、忙碌键、提示、安装进度和确认状态。它合并重叠读取,在操作或 Host 变化后刷新,并在销毁后忽略晚到结果。安装输出按 job id 分组。
 
 ### 确认
 
@@ -79,7 +75,7 @@ kind: "package-reference"
 这些页面覆盖设置分区、Remote 调用与宿主侧管理器。
 
 - [ui-sidebar](../ui-sidebar/README.zh.md)——「插件」入口注册进的面板列表;[ui-layout](../ui-layout/README.zh.md)——页面所占的 main slot。
-- [api-remotes](../../api/remotes/README.zh.md)——`plugins.*` 与 `pluginInventory.list()` 背后的 Remote BFF 表面。
+- [api-remotes](../../api/remotes/README.zh.md)——`plugins.*` 背后的 Remote BFF 表面。
 - [plugin-manager](../../host/plugin-manager/README.zh.md)——本页面驱动的宿主侧管理器。
 
 -----
@@ -100,8 +96,8 @@ kind: "package-reference"
 
 这些限制界定管理视图的范围;它们是本包当前的约束。
 
-- **全局用户层的行不按包列出**——加入所有会话的行会在该包的**加入到…**菜单里标注该目标并出现在其依赖方里,而不在卡片上;移除它要走补丁文件或将来的行列表。
-- **harness 模块的名字放在本页面的字典里**——新的第一方 agent 面模块在补上 `name.<slug>` 与 `desc.<slug>` 之前按短模块名显示。
+- **用户层行不按包列出**——手写 patch 中的模块引用会出现在依赖查询中,并在卸载时清理;编辑这些行仍通过文件完成。
+- **harness 模块的名字放在本页面的字典里**——新的第一方 agent 面模块在补上 `name.<slug>` 之前按短模块名显示。
 - **一次只能安装一个**——对话框运行一条 pnpm 命令;第二个 spec 要等第一个结束。
 - **没有版本选择器**——spec 按 pnpm 接受的写法输入;页面既不列出仓库版本也不提供升级。
 

+ 0 - 17
packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css

@@ -334,14 +334,6 @@
   outline-offset: 1px;
 }
 
-.rowNote {
-  font-size: 12px;
-  color: var(--dsw-alias-label-tertiary);
-  max-width: 180px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
 
 .facts {
   display: grid;
@@ -724,16 +716,7 @@
   color: var(--dsw-alias-state-error-primary);
 }
 
-.rowNote {
-  font-size: 12px;
-  line-height: 18px;
-  color: var(--dsw-alias-label-tertiary);
-  overflow-wrap: anywhere;
-}
 
-.row[data-state='failed'] .rowNote {
-  color: var(--dsw-alias-state-error-primary);
-}
 
 .rowFailure {
   margin: 4px 0 0 44px;

+ 13 - 115
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -1,23 +1,16 @@
 /**
- * The plugin management page behind the sidebar's Plugins entry: the
- * profile's installed packages as cards — a
- * plugin pack with its switch, a plugin with its **Add to…** menu, a built-in
- * pack with a locked switch — each a name, a one-liner, and a tag only when
- * a restart is pending or something is wrong; the install dialog streaming
- * pnpm's output behind a fold; and the confirmation a destructive action
- * waits on, naming what still uses the package. Entry ids, module names,
- * and module names stay in the detail view. A card opens the package's own
- * page: its version and source, its rows — each with its own switch on an
- * external pack composed on a live-reload profile — the built-in rows it
- * changes, the modules it declares addable, and its uninstall.
+ * Global plugin management: installed package cards, bundle component switches,
+ * entry diagnostics, streamed installation output and dependency confirmations.
+ * Package details expose module names and runtime failures; non-bundle packages
+ * retain package information and uninstall without automatic composition actions.
  */
 
 import { useEffect, useId, useState, type ReactNode } from 'react'
 import type { PluginInstallRejection, PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
 import {
   Button, IconChevronDownOutline14, IconCordisPluginOutline14, IconRefreshOutline16,
-  Input, Menu, Modal, StateDot, Switch, Tag, TerminalBlock,
-  type MenuItem, type StateDotState, type TerminalBlockLabels,
+  Input, Modal, StateDot, Switch, Tag, TerminalBlock,
+  type StateDotState, type TerminalBlockLabels,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type { PluginManagerLocaleKey } from './locales.ts'
@@ -253,44 +246,6 @@ function OverridesSection({ overrides, t }: { readonly overrides: readonly strin
   )
 }
 
-/** Declared modules with their global composition state and add action. */
-function ModulesSection({ pkg, t, busy, globalModules, onAddRow }: {
-  readonly pkg: PluginPackageView
-  readonly t: Translate
-  readonly busy: boolean
-  readonly globalModules: readonly string[]
-  readonly onAddRow: (declaredName: string) => void
-}): ReactNode {
-  const title = pkg.title ?? shortName(pkg.name)
-  return (
-    <section className={css.detailSection} data-plugin-modules>
-      <div className={css.sectionHead}>
-        <h4 className={css.sectionTitle}>{t('modulesLabel')}</h4>
-      </div>
-      <ul className={css.rows}>
-        {pkg.addable.map((entry) => {
-          const added = globalModules.includes(entry.moduleName)
-          return (
-            <li key={entry.moduleName} className={css.row} data-plugin-module={entry.moduleName}>
-              <div className={css.rowLine}>
-                <span className={css.rowIcon} aria-hidden="true"><IconCordisPluginOutline14 /></span>
-                <div className={css.rowMain}>
-                  <span className={css.rowId}>{entry.title ?? (entry.declaredName === '.' ? title : entry.declaredName)}</span>
-                  <span className={css.rowModule}>{entry.moduleName}</span>
-                  <span className={css.rowNote}>{t(added ? 'joinedGlobal' : 'moduleNotJoined')}</span>
-                </div>
-                <Button variant="outline" size="sm" disabled={busy || added} onClick={() => { onAddRow(entry.declaredName) }}>
-                  {t(added ? 'addToGlobalAdded' : 'addToGlobal')}
-                </Button>
-              </div>
-            </li>
-          )
-        })}
-      </ul>
-    </section>
-  )
-}
-
 /** A bundle's enable switch on its card and its page: locked for a built-in bundle, off and locked for one the profile cannot enable. */
 function EnableSwitch({ pkg, title, t, busy, onSetEnabled }: {
   readonly pkg: PluginPackageView
@@ -312,27 +267,17 @@ function EnableSwitch({ pkg, title, t, busy, onSetEnabled }: {
   )
 }
 
-/** One installed package as a card that opens its page: its name, its one-liner, its tags, and its switch or its **Add to…** menu. */
-function PackageCard({ pkg, t, busy, globalModules, onOpen, onSetEnabled, onAddRow }: {
+/** One installed package as a card that opens its page: its name, its one-liner, its tags, and its bundle switch. */
+function PackageCard({ pkg, t, busy, onOpen, onSetEnabled }: {
   readonly pkg: PluginPackageView
   readonly t: Translate
   readonly busy: boolean
-  readonly globalModules: readonly string[]
   readonly onOpen: () => void
   readonly onSetEnabled: (enabled: boolean) => void
-  readonly onAddRow: (declaredName: string) => void
 }): ReactNode {
-  const [addMenu, setAddMenu] = useState(false)
   const title = pkg.title ?? shortName(pkg.name)
   const builtin = !pkg.installed
   const status = cardStatus(pkg)
-  const addable = pkg.addable
-  const [single] = addable
-  const menuItems: MenuItem[] = addable.map(entry => ({
-    id: entry.declaredName,
-    label: entry.title ?? entry.declaredName,
-    disabled: globalModules.includes(entry.moduleName),
-  }))
   return (
     <li className={`${css.card} ${css.cardLink}`} data-plugin-package={pkg.name} data-plugin-status={pkg.status}>
       <div className={css.cardHead}>
@@ -346,37 +291,7 @@ function PackageCard({ pkg, t, busy, globalModules, onOpen, onSetEnabled, onAddR
         </div>
         <div className={css.cardEnd}>
           <EnableSwitch pkg={pkg} title={title} t={t} busy={busy} onSetEnabled={onSetEnabled} />
-          {menuItems.length === 0
-            ? null
-            : single !== undefined && addable.length === 1 ? (
-              <Button variant="outline" size="sm" disabled={busy || globalModules.includes(single.moduleName)} onClick={() => { onAddRow(single.declaredName) }}>
-                {t(globalModules.includes(single.moduleName) ? 'addToGlobalAdded' : 'addToGlobal')}
-              </Button>
-            ) : (
-              <Menu
-                open={addMenu}
-                onClose={() => { setAddMenu(false) }}
-                items={menuItems}
-                onSelect={(id) => {
-                  setAddMenu(false)
-                  onAddRow(id)
-                }}
-                align="end"
-                portal
-                anchor={(
-                  <Button
-                    variant="outline"
-                    size="sm"
-                    aria-haspopup="menu"
-                    aria-expanded={addMenu}
-                    disabled={busy}
-                    onClick={() => { setAddMenu(current => !current) }}
-                  >
-                    {t('addToGlobal')}
-                  </Button>
-                )}
-              />
-            )}
+
         </div>
       </div>
     </li>
@@ -386,24 +301,21 @@ function PackageCard({ pkg, t, busy, globalModules, onOpen, onSetEnabled, onAddR
 /**
  * One package's page: the crumb back to the list; its name, tags, one-liner,
  * and its switch; the reason when one applies; its version and source; its
- * rows with their switches; the built-in rows it changes; the modules it
- * declares addable with where each is composed; and retry and uninstall.
+ * rows with their switches; the built-in rows it changes; and retry and uninstall.
  */
 function PackageDetail({
-  pkg, t, busy, rowBusy, globalModules,
-  onBack, onSetEnabled, onRetry, onUninstall, onAddRow, onSetRowDisabled,
+  pkg, t, busy, rowBusy,
+  onBack, onSetEnabled, onRetry, onUninstall, onSetRowDisabled,
 }: {
   readonly pkg: PluginPackageView
   readonly t: Translate
   readonly busy: boolean
   /** Whether a row has a write in flight. */
   readonly rowBusy: (rowId: string) => boolean
-  readonly globalModules: readonly string[]
   readonly onBack: () => void
   readonly onSetEnabled: (enabled: boolean) => void
   readonly onRetry: () => void
   readonly onUninstall: () => void
-  readonly onAddRow: (declaredName: string) => void
   readonly onSetRowDisabled: (row: RowView, disabled: boolean) => void
 }): ReactNode {
   const title = pkg.title ?? shortName(pkg.name)
@@ -486,17 +398,7 @@ function PackageDetail({
           )
           : null}
         {pkg.overrides.length === 0 ? null : <OverridesSection overrides={pkg.overrides} t={t} />}
-        {pkg.addable.length === 0
-          ? null
-          : (
-            <ModulesSection
-              pkg={pkg}
-              t={t}
-              busy={busy}
-              globalModules={globalModules}
-              onAddRow={onAddRow}
-            />
-          )}
+
       </div>
     </div>
   )
@@ -742,12 +644,10 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
             t={t}
             busy={state.busy.includes(openPkg.name)}
             rowBusy={rowId => state.busy.includes(rowKey(rowId))}
-            globalModules={state.globalModules}
             onBack={() => { setOpenPackage(null) }}
             onSetEnabled={(enabled) => { props.setEnabled(openPkg.name, enabled) }}
             onRetry={() => { props.retry(openPkg.name) }}
             onUninstall={() => { props.uninstall(openPkg.name) }}
-            onAddRow={(declaredName) => { props.addRow(openPkg.name, declaredName) }}
             onSetRowDisabled={(row, disabled) => {
               // Off asks first when other rows inject what the row provides; on has nothing to ask.
               if (disabled) props.disableRow(openPkg.name, row.entryId, row.rowId)
@@ -776,10 +676,8 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
                         pkg={pkg}
                         t={t}
                         busy={state.busy.includes(pkg.name)}
-                        globalModules={state.globalModules}
                         onOpen={() => { setOpenPackage(pkg.name) }}
                         onSetEnabled={(enabled) => { props.setEnabled(pkg.name, enabled) }}
-                        onAddRow={(declaredName) => { props.addRow(pkg.name, declaredName) }}
                       />
                     ))}
                   </ul>

+ 2 - 2
packages/client/ui-plugin-manager/src/client/index.ts

@@ -43,8 +43,8 @@ export const NS = 'pluginManager'
 /** The id shared by the sidebar entry and the main panel it opens. */
 export const PANEL_ID = 'plugins' as MainPanelId
 
-/** Services required by the Settings registration and the two Remote faces. */
-export const inject = ['slots', 'locale', 'remote', 'remote.plugins', 'remote.pluginInventory']
+/** Services required by the Settings registration and the Remote methods. */
+export const inject = ['slots', 'locale', 'remote', 'remote.plugins']
 
 /**
  * Contribute the Plugins entry to the sidebar with the management page it

+ 6 - 24
packages/client/ui-plugin-manager/src/client/locales.ts

@@ -16,7 +16,7 @@ export const zh = {
   restartNotice: '更改会在下次启动生效。',
   dismiss: '知道了',
   bundlesTitle: '插件包',
-  pluginsTitle: '插件',
+  pluginsTitle: '其他已安装包',
   countUnit: '个',
   builtinTag: '内置',
   statusRestart: '需重启',
@@ -38,7 +38,7 @@ export const zh = {
   partsCountFailed: '{count} 异常',
   partsCountUpdateFailed: '{count} 更新失败',
   rowUpdateFailed: '运行中,但最近更新失败',
-  unknownPackage: '未识别到可添加的插件入口。',
+  unknownPackage: '此包未提供组合包 patch,可通过 Cordis 配置手动加载模块。',
   partsFilter: '筛选组件',
   partsFilterEmpty: '没有匹配的组件。',
   partDisabledByUser: '已停用',
@@ -52,19 +52,11 @@ export const zh = {
   rowPhaseUnloading: '卸载中',
   enableToggle: '启用 {name}',
   builtinLocked: '内置功能随产品启动,不能在这里停用。',
-  addTo: '加入到…',
   openDetail: '查看 {name}',
   backToList: '返回插件列表',
   crumbRoot: '插件管理',
   rowStateIdle: '未运行',
   overridesLabel: '改动了这些内置组件',
-  modulesLabel: '可加入的模块',
-  moduleJoined: '已加入:{targets}',
-  moduleNotJoined: '尚未加入',
-  joinedGlobal: '所有会话',
-  joinedSeparator: '、',
-  addToGlobal: '加入全局',
-  addToGlobalAdded: '已加入全局',
   retryPackage: '重试',
   uninstall: '卸载',
   uninstallLabel: '卸载 {name}',
@@ -95,7 +87,7 @@ export const zh = {
   terminalNoExitCode: '未正常退出',
   installDoneEnabled: '已安装并启用 {name}。',
   installDoneBundle: '已安装 {name},打开它的开关即可启用。',
-  installDonePlugin: '已安装 {name},可以添加到全局。',
+  installDonePlugin: '已安装 {name},可通过 Cordis 配置手动加载。',
   installDoneOther: '已安装 {name}。',
   installDoneNothing: '安装完成,没有新增依赖。',
   installRemovedInvalid: '{name} 的插件包声明无效,已自动移除:{reason}',
@@ -122,7 +114,6 @@ export const zh = {
   actionFailed: '操作失败:{reason}',
   notEnableable: '无法启用:{reason}',
   enableFailed: '无法应用插件包:{reason}',
-  rowConflict: '已经有同名的插件行:{row}。',
   notInstalled: '{name} 没有安装在当前 profile。',
   'name.persona': '人设',
   'name.agent-instructions': '项目指令',
@@ -181,7 +172,7 @@ export const en = {
   restartNotice: 'The change takes effect at the next start.',
   dismiss: 'Got it',
   bundlesTitle: 'Plugin packs',
-  pluginsTitle: 'Plugins',
+  pluginsTitle: 'Other installed packages',
   countUnit: 'packages',
   builtinTag: 'Built-in',
   statusRestart: 'Restart needed',
@@ -203,7 +194,7 @@ export const en = {
   partsCountFailed: '{count} failed',
   partsCountUpdateFailed: '{count} update failed',
   rowUpdateFailed: 'Running; latest update failed',
-  unknownPackage: 'No addable plugin entry was declared.',
+  unknownPackage: 'This package has no bundle patch. Load its modules through Cordis configuration.',
   partsFilter: 'Filter components',
   partsFilterEmpty: 'No component matches.',
   partDisabledByUser: 'Off',
@@ -217,19 +208,11 @@ export const en = {
   rowPhaseUnloading: 'Unloading',
   enableToggle: 'Enable {name}',
   builtinLocked: 'A built-in feature starts with the product and cannot be switched off here.',
-  addTo: 'Add to…',
   openDetail: 'View {name}',
   backToList: 'Back to plugins',
   crumbRoot: 'Manage plugins',
   rowStateIdle: 'Not running',
   overridesLabel: 'Changes these built-in components',
-  modulesLabel: 'Modules to add',
-  moduleJoined: 'Added to: {targets}',
-  moduleNotJoined: 'Not added yet',
-  joinedGlobal: 'Every session',
-  joinedSeparator: ', ',
-  addToGlobal: 'Add globally',
-  addToGlobalAdded: 'Added globally',
   retryPackage: 'Retry',
   uninstall: 'Uninstall',
   uninstallLabel: 'Uninstall {name}',
@@ -260,7 +243,7 @@ export const en = {
   terminalNoExitCode: 'no exit code',
   installDoneEnabled: 'Installed and enabled {name}.',
   installDoneBundle: 'Installed {name}; turn its switch on to enable it.',
-  installDonePlugin: 'Installed {name}; it can be added globally.',
+  installDonePlugin: 'Installed {name}; load it through Cordis configuration.',
   installDoneOther: 'Installed {name}.',
   installDoneNothing: 'Install finished with no new dependency.',
   installRemovedInvalid: '{name} has invalid plugin pack declarations and was removed: {reason}',
@@ -287,7 +270,6 @@ export const en = {
   actionFailed: 'The action failed: {reason}',
   notEnableable: 'Cannot be enabled: {reason}',
   enableFailed: 'Could not apply this plugin pack: {reason}',
-  rowConflict: 'A plugin row with this name already exists: {row}.',
   notInstalled: '{name} is not installed in the current profile.',
   'name.persona': 'Persona',
   'name.agent-instructions': 'Project instructions',

+ 6 - 26
packages/client/ui-plugin-manager/src/client/manager-store.ts

@@ -1,6 +1,6 @@
 /**
  * The plugin manager's state: the Host's package views
- * and global composition, the action in flight, the install run, and the
+ * the action in flight, the install run, and the
  * confirmation a destructive action waits on. Every fact comes from the Host — the store
  * re-reads after each action and after every `plugins/changed` event, so a
  * change made on another surface shows here without a manual refresh.
@@ -59,7 +59,7 @@ export interface InstallState {
   readonly enabled: readonly string[]
   /** Bundles the run installed and left off. */
   readonly installedOnly: readonly string[]
-  /** Plugin modules the run installed, which join a composition per row. */
+  /** Packages installed without a bundle patch. */
   readonly plain: readonly string[]
   /** Packages pnpm added that the Host removed again, each with its reason. */
   readonly removed: readonly PluginInstallRejection[]
@@ -89,8 +89,6 @@ export interface PluginManagerState {
   /** `unavailable` when the Host runs without a profile runtime; `error` keeps the last packages. */
   readonly status: 'idle' | 'loading' | 'ready' | 'error' | 'unavailable'
   readonly packages: readonly PluginPackageView[]
-  /** Module names of the rows the host tree carries, for the **Add to…** menu's "added" marks. */
-  readonly globalModules: readonly string[]
   /** Package names and row keys with an action crossing the wire. */
   readonly busy: readonly string[]
   readonly notice: ManagerNotice | null
@@ -121,9 +119,6 @@ export interface PluginManagerFace {
   uninstall: (packageName: string) => void
   confirm: () => void
   cancelConfirm: () => void
-  /** Add a row naming one of a package's modules — by its declared name, `.` for the main export — to a user layer. */
-  addRow: (packageName: string, declaredName: string) => void
-  removeRow: (rowId: string) => void
   setRowDisabled: (rowId: string, disabled: boolean) => void
   /** Switch one of a package's rows off in the global layer, after asking when other rows inject what it provides. */
   disableRow: (packageName: string, entryId: string, rowId: string) => void
@@ -178,7 +173,7 @@ const IDLE_INSTALL: InstallState = {
   open: false, spec: '', enable: true, phase: 'idle', runs: [], installed: [], enabled: [], installedOnly: [], plain: [], removed: [], failure: null,
 }
 
-/** Reads and mutates the profile's plugins through the `plugins` and `pluginInventory` Remotes. */
+/** Reads and mutates the profile's plugins through the `plugins` Remote. */
 export class PluginManagerController {
   private readonly store: SnapshotStore<PluginManagerState>
   private inFlight: Promise<void> | undefined
@@ -188,14 +183,13 @@ export class PluginManagerController {
   private pendingConfirm: (() => Promise<void>) | undefined
 
   /**
-   * @param ctx - the tab plugin's context, whose `remote.plugins` and
-   * `remote.pluginInventory` namespaces answer.
+   * @param ctx - the tab plugin's context, whose `remote.plugins` namespace answers.
    */
   constructor(
     private readonly ctx: ClientContext,
   ) {
     this.store = createSnapshotStore<PluginManagerState>({
-      status: 'idle', packages: [], globalModules: [], busy: [], notice: null,
+      status: 'idle', packages: [], busy: [], notice: null,
       install: IDLE_INSTALL, confirm: null,
     })
   }
@@ -246,16 +240,6 @@ export class PluginManagerController {
       uninstall: (packageName) => { void this.askConfirm('uninstall', packageName) },
       confirm: () => { void this.confirm() },
       cancelConfirm: () => { this.pendingConfirm = undefined; this.patch({ confirm: null }) },
-      addRow: (packageName, declaredName) => {
-        void this.run(packageName, { packageName }, async () => {
-          this.answer(await this.ctx.remote.plugins.addRow(packageName, { module: declaredName }))
-        })
-      },
-      removeRow: (rowId) => {
-        void this.run(rowKey(rowId), { rowId }, async () => {
-          this.answer(await this.ctx.remote.plugins.removeRow(rowId))
-        })
-      },
       setRowDisabled: (rowId, disabled) => {
         void this.run(rowKey(rowId), { rowId }, async () => {
           this.answer(await this.ctx.remote.plugins.setRowDisabled(rowId, disabled))
@@ -308,10 +292,7 @@ export class PluginManagerController {
         this.rerun = false
         const generation = ++this.generation
         if (this.getSnapshot().status === 'idle') this.patch({ status: 'loading' })
-        const [packages, inventory] = await Promise.all([
-          this.ctx.remote.plugins.list(),
-          this.ctx.remote.pluginInventory.list(),
-        ])
+        const packages = await this.ctx.remote.plugins.list()
         if (generation !== this.generation) return
         if (!packages.ok) {
           this.patch({ status: packages.error.code === 'plugins/unavailable' ? 'unavailable' : 'error' })
@@ -320,7 +301,6 @@ export class PluginManagerController {
         this.patch({
           status: 'ready',
           packages: packages.value,
-          globalModules: inventory.ok ? inventory.value.entries.map(entry => entry.moduleName) : this.getSnapshot().globalModules,
         })
       } while (this.shouldRerun())
     } finally {

+ 0 - 1
packages/client/ui-plugin-manager/src/client/presentation.ts

@@ -73,7 +73,6 @@ export function noticeText(notice: ManagerNotice, t: Translate): string {
       switch (notice.code) {
         case 'plugins/not-enableable': return t('notEnableable', { reason: notice.reason })
         case 'plugins/enable-failed': return t('enableFailed', { reason: notice.reason })
-        case 'plugins/row-conflict': return t('rowConflict', { row: notice.rowId ?? '' })
         case 'plugins/not-installed': return t('notInstalled', { name: notice.packageName ?? '' })
         default: return refusalText(notice, t)
       }

+ 3 - 5
packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx

@@ -27,12 +27,10 @@ async function bench() {
   }
   new LocaleHolder(ctx)
   const list = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))
-  const inventory = vi.fn(() => Promise.resolve({ ok: true as const, value: { entries: [] } }))
   const remote = new TestRemote(ctx, {
     plugins: { list },
-    pluginInventory: { list: inventory },
   })
-  return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list, inventory, remote }
+  return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list, remote }
 }
 
 function declare(slots: SlotRegistry): () => void {
@@ -50,8 +48,8 @@ describe('ui-plugin-manager browser plugin', () => {
     expect(hostApply).not.toThrow()
   })
 
-  it('declares only the services the page and its two Remote faces use', () => {
-    expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.plugins', 'remote.pluginInventory'])
+  it('declares only the services the page and its Remote methods use', () => {
+    expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.plugins'])
   })
 
   it('registers the sidebar entry and its page, which reads the Host only once rendered and follows Host changes', async () => {

+ 11 - 49
packages/client/ui-plugin-manager/tests/components.client.spec.tsx

@@ -28,7 +28,6 @@ function pkg(overrides: Partial<PluginPackageView> = {}): PluginPackageView {
     cordisSameCopy: true,
     rows: [],
     overrides: [],
-    addable: [],
     liveReload: true,
     ...overrides,
   }
@@ -41,7 +40,6 @@ const IDLE_INSTALL: InstallState = {
 const READY: PluginManagerState = {
   status: 'ready',
   packages: [],
-  globalModules: [],
   busy: [],
   notice: null,
   install: IDLE_INSTALL,
@@ -63,8 +61,6 @@ function renderTab(state: Partial<PluginManagerState> = {}) {
     uninstall: vi.fn(),
     confirm: vi.fn(),
     cancelConfirm: vi.fn(),
-    addRow: vi.fn(),
-    removeRow: vi.fn(),
     setRowDisabled: vi.fn(),
     disableRow: vi.fn(),
     dismissNotice: vi.fn(),
@@ -134,7 +130,7 @@ describe('PluginManagerPage', () => {
   })
 
   it('shows a problem for a plugin with an invalid declaration', () => {
-    renderTab({ packages: [pkg({ kind: 'plugin', status: 'plain', reason: 'invalid declaration' })] })
+    renderTab({ packages: [pkg({ kind: 'unknown', status: 'plain', reason: 'invalid declaration' })] })
     expect(screen.getByText(en.statusProblem)).toBeTruthy()
   })
 
@@ -146,10 +142,10 @@ describe('PluginManagerPage', () => {
       packages: [
         pkg({ description: 'A sidebar.' }),
         bundledPackage,
-        pkg({ name: 'unknown', installed: false, kind: 'plugin', status: 'plain' }),
+        pkg({ name: 'unknown', installed: false, kind: 'unknown', status: 'plain' }),
         pkg({ name: 'builtin-lib', installed: false, kind: 'unknown', status: 'plain' }),
         pkg({ name: 'broken-bundle', enabled: false, status: 'not-enableable', reason: 'foreign cordis' }),
-        pkg({ name: 'dsh-tool-foo', kind: 'plugin', status: 'plain' }),
+        pkg({ name: 'dsh-tool-foo', kind: 'unknown', status: 'plain' }),
         pkg({ name: 'some-lib', kind: 'unknown', status: 'plain' }),
         pkg({ name: 'pending-bundle', title: 'Pending', enabled: true, status: 'restart-required' }),
         pkg({ name: 'dsh-untitled', enabled: false, status: 'restart-required', installed: false }),
@@ -220,7 +216,7 @@ describe('PluginManagerPage', () => {
           rows: [{ entryId: 'include:w1', rowId: 'w1', moduleName: 'waiting-pack', enabled: true, phase: null, failure: { stage: 'inject-pending', message: 'pending (waiting for service: authorization)' } }],
         }),
         pkg({ name: 'no-rows', enabled: false, status: 'disabled' }),
-        pkg({ name: 'dsh-tool-foo', kind: 'plugin', status: 'plain' }),
+        pkg({ name: 'dsh-tool-foo', kind: 'unknown', status: 'plain' }),
         pkg({
           name: '@deepseek-ai/dsh-core-broken', title: 'Core', installed: false, status: 'failed',
           rows: [{ entryId: 'include:core', rowId: 'core', moduleName: '@deepseek-ai/dsh-core-broken', enabled: true, phase: 'active' }],
@@ -258,7 +254,7 @@ describe('PluginManagerPage', () => {
     // A short list has no filter; the built-in rows the pack changes are named.
     expect(screen.queryByRole('searchbox', { name: en.partsFilter })).toBeNull()
     expect(document.querySelector('[data-plugin-overrides]')?.textContent).toContain('directory-picker')
-    expect(screen.queryByText(en.modulesLabel)).toBeNull()
+    expect(document.querySelector('[data-plugin-modules]')).toBeNull()
 
     fireEvent.click(screen.getByRole('button', { name: en.retryPackage }))
     expect(actions.retry).toHaveBeenCalledWith('dsh-better-sidebar')
@@ -367,43 +363,12 @@ describe('PluginManagerPage', () => {
     }
   })
 
-  it('adds a declared module globally and disables the action once it is present', () => {
-    const mod = { moduleName: 'example/search', declaredName: './search', title: 'Search' }
-    const { actions, set } = renderTab({ packages: [pkg({ name: 'example', kind: 'plugin', status: 'plain', addable: [mod] })] })
-    fireEvent.click(screen.getByRole('button', { name: en.addToGlobal }))
-    expect(actions.addRow).toHaveBeenCalledWith('example', './search')
-    set({ globalModules: ['example/search'] })
-    expect(screen.getByRole('button', { name: en.addToGlobalAdded })).toHaveProperty('disabled', true)
-    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'example') }))
-    expect(screen.getByText(en.joinedGlobal)).toBeTruthy()
-    expect(screen.getByRole('button', { name: en.addToGlobalAdded })).toHaveProperty('disabled', true)
-  })
-
-  it('selects among multiple declared modules and adds one directly to the global layer', () => {
-    const { actions } = renderTab({ packages: [pkg({ name: 'example', kind: 'plugin', status: 'plain', addable: [
-      { moduleName: 'example/search', declaredName: './search', title: 'Search' },
-      { moduleName: 'example/fetch', declaredName: './fetch', title: 'Fetch' },
-    ] })] })
-    fireEvent.click(screen.getByRole('button', { name: en.addToGlobal }))
-    fireEvent.click(screen.getByRole('menuitem', { name: 'Fetch' }))
-    expect(actions.addRow).toHaveBeenCalledWith('example', './fetch')
-  })
-
-  it('adds untitled main and subpath modules from a package detail page', () => {
-    const { actions, set } = renderTab({ packages: [pkg({ name: '@fixture/modules', kind: 'plugin', status: 'plain', addable: [
-      { moduleName: '@fixture/modules', declaredName: '.' },
-      { moduleName: '@fixture/modules/search', declaredName: './search' },
-    ] })] })
-    fireEvent.click(screen.getByRole('button', { name: en.addToGlobal }))
-    expect(screen.getByRole('menuitem', { name: './search' })).toBeTruthy()
-    fireEvent.keyDown(document, { key: 'Escape' })
-    expect(screen.queryByRole('menu')).toBeNull()
-    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'modules') }))
-    const buttons = screen.getAllByRole('button', { name: en.addToGlobal })
-    fireEvent.click(buttons[1]!)
-    expect(actions.addRow).toHaveBeenCalledWith('@fixture/modules', './search')
-    set({ busy: ['@fixture/modules'] })
-    expect(screen.getAllByRole('button', { name: en.addToGlobal }).every(button => (button as HTMLButtonElement).disabled)).toBe(true)
+  it('shows a scoped non-bundle package with package details and uninstall', () => {
+    renderTab({ packages: [pkg({ name: '@acme/example', kind: 'unknown', status: 'plain' })] })
+    fireEvent.click(screen.getByRole('button', { name: 'View example' }))
+    expect(screen.getByText(en.unknownPackage)).toBeTruthy()
+    expect(screen.getByRole('button', { name: 'Uninstall example' })).toBeTruthy()
+    expect(screen.queryByRole('switch')).toBeNull()
   })
 
   it('drives the install dialog through its phases and words each outcome', () => {
@@ -563,7 +528,6 @@ describe('PluginManagerPage', () => {
     const failures: [string, string][] = [
       ['plugins/not-enableable', en.notEnableable.replace('{reason}', 'r')],
       ['plugins/enable-failed', en.enableFailed.replace('{reason}', 'r')],
-      ['plugins/row-conflict', en.rowConflict.replace('{row}', 'row-1')],
       ['plugins/not-installed', en.notInstalled.replace('{name}', 'pkg-1')],
       ['plugins/busy', en.busy.replace('{reason}', 'r')],
       ['plugins/agents-running', en.agentsRunning.replace('{reason}', 'r')],
@@ -574,8 +538,6 @@ describe('PluginManagerPage', () => {
       set({ notice: { kind: 'failed', code, reason: 'r', packageName: 'pkg-1', rowId: 'row-1' } })
       expect(screen.getByRole('alert').textContent).toContain(text)
     }
-    set({ notice: { kind: 'failed', code: 'plugins/row-conflict', reason: 'r' } })
-    expect(screen.getByRole('alert').textContent).toContain(en.rowConflict.replace('{row}', ''))
     set({ notice: { kind: 'failed', code: 'plugins/not-installed', reason: 'r' } })
     expect(screen.getByRole('alert').textContent).toContain(en.notInstalled.replace('{name}', ''))
     fireEvent.click(screen.getByRole('button', { name: en.dismiss }))

+ 9 - 41
packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts

@@ -18,15 +18,9 @@ const BUNDLE: PluginPackageView = {
   cordisSameCopy: true,
   rows: [],
   overrides: [],
-  addable: [],
   liveReload: true,
 }
 
-/** One host-tree row the inventory lists, as far as the store reads it. */
-const GLOBAL_ENTRY = {
-  entryId: 'include:ui-settings', moduleName: '@deepseek-ai/dsh-client-ui-settings', enabled: true, fiberPhase: 'active', trust: 'builtin',
-} as const
-
 /** What one install run answers. */
 type InstallValue = {
   installed: string[]
@@ -60,21 +54,18 @@ function bench(overrides: Partial<Record<string, ReturnType<typeof vi.fn>>> = {}
     enable: vi.fn(() => Promise.resolve(ok({ changed: true, effect: 'live' }))),
     disable: vi.fn(() => Promise.resolve(ok({ changed: true, effect: 'restart' }))),
     retry: vi.fn(() => Promise.resolve(ok({ changed: true, effect: 'live' }))),
-    addRow: vi.fn(() => Promise.resolve(ok({ rowId: 'r', file: '/f' }))),
-    removeRow: vi.fn(() => Promise.resolve(ok(undefined))),
     setRowDisabled: vi.fn(() => Promise.resolve(ok(undefined))),
     dependents: vi.fn(() => Promise.resolve(ok({ services: [], references: [] }))),
     ...overrides,
   }
-  const inventory = { list: vi.fn(() => Promise.resolve(ok({ entries: [GLOBAL_ENTRY] }))) }
-  const ctx = { remote: { plugins, pluginInventory: inventory } } as never
+  const ctx = { remote: { plugins } } as never
   const controller = new PluginManagerController(ctx)
   const face = controller.inject()
-  return { plugins, inventory, controller, face, state: () => controller.getSnapshot() }
+  return { plugins, controller, face, state: () => controller.getSnapshot() }
 }
 
 describe('PluginManagerController', () => {
-  it('starts idle, reads packages and global modules on first use, and folds concurrent loads', async () => {
+  it('starts idle, reads packages on first use, and folds concurrent loads', async () => {
     const gate = deferred<ReturnType<typeof ok<PluginPackageView[]>>>()
     const { plugins, face, state, controller } = bench({ list: vi.fn().mockReturnValueOnce(gate.promise).mockResolvedValue(ok([BUNDLE])) })
     expect(state().status).toBe('idle')
@@ -87,7 +78,7 @@ describe('PluginManagerController', () => {
     await mid
     // The in-flight read reran once for the load that landed mid-read.
     expect(plugins.list).toHaveBeenCalledTimes(2)
-    expect(state()).toMatchObject({ status: 'ready', packages: [BUNDLE], globalModules: [GLOBAL_ENTRY.moduleName] })
+    expect(state()).toMatchObject({ status: 'ready', packages: [BUNDLE] })
     face.ensure()
     expect(plugins.list).toHaveBeenCalledTimes(2)
     face.refresh()
@@ -111,14 +102,6 @@ describe('PluginManagerController', () => {
     expect(plugins.list).toHaveBeenCalledTimes(3)
   })
 
-  it('keeps the held host-tree modules when the inventory read is refused', async () => {
-    const { inventory, controller, state } = bench()
-    await controller.load()
-    inventory.list.mockResolvedValueOnce(refused('gateway/internal', 'boom') as never)
-    await controller.load()
-    expect(state().globalModules).toEqual([GLOBAL_ENTRY.moduleName])
-  })
-
   it('enables a bundle, marks it busy meanwhile, and says when a restart is needed', async () => {
     const gate = deferred<ReturnType<typeof ok<{ changed: boolean; effect: 'live' | 'restart' }>>>()
     const { plugins, face, state, controller } = bench({ enable: vi.fn().mockReturnValueOnce(gate.promise) })
@@ -263,7 +246,7 @@ describe('PluginManagerController', () => {
     await vi.waitFor(() => { expect(state().confirm?.dependents).toEqual({ services: [], references: [] }) })
   })
 
-  it('retries, adds rows, removes rows, and switches rows under their own busy keys', async () => {
+  it('retries and switches rows under their own busy keys', async () => {
     const { plugins, face, state, controller } = bench()
     await controller.load()
     face.retry(BUNDLE.name)
@@ -271,34 +254,19 @@ describe('PluginManagerController', () => {
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
     expect(state().notice).toBeNull()
 
-    face.addRow('@fixture/tool', '.')
-    await vi.waitFor(() => {
-      expect(plugins.addRow).toHaveBeenCalledWith('@fixture/tool', { module: '.' })
-    })
-    await vi.waitFor(() => { expect(state().busy).toEqual([]) })
-    expect(state().notice).toBeNull()
-
     face.setRowDisabled('bash', true)
     expect(state().busy).toEqual([rowKey('bash')])
     await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith('bash', true) })
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
-    face.removeRow('extra')
-    await vi.waitFor(() => { expect(plugins.removeRow).toHaveBeenCalledWith('extra') })
-    await vi.waitFor(() => { expect(state().busy).toEqual([]) })
+
   })
 
-  it('reports a row conflict with the row id and a thrown transport failure generically', async () => {
+  it('reports thrown row transport failures using the row id', async () => {
     const { face, state, controller } = bench({
-      addRow: vi.fn(() => Promise.resolve(refused('plugins/row-conflict', 'taken', { rowId: 'r' }))),
-      removeRow: vi.fn(() => Promise.reject(new Error('offline'))),
-      // A rejection that is not an Error reaches the notice by its string form.
-      setRowDisabled: vi.fn().mockRejectedValueOnce('plain text'),
+      setRowDisabled: vi.fn().mockRejectedValueOnce(new Error('offline')).mockRejectedValueOnce('plain text'),
     })
     await controller.load()
-    face.addRow('p', 'p')
-    await vi.waitFor(() => { expect(state().notice).toMatchObject({ kind: 'failed' }) })
-    expect(state().notice).toEqual({ kind: 'failed', code: 'plugins/row-conflict', reason: 'taken', packageName: 'p' })
-    face.removeRow('r')
+    face.setRowDisabled('r', true)
     await vi.waitFor(() => {
       expect(state().notice).toEqual({ kind: 'failed', code: 'gateway/internal', reason: 'offline', rowId: 'r' })
     })

+ 2 - 21
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1423,17 +1423,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         parameters: [{ name: 'packageName', description: 'the enabled bundle.' }],
         returns: 'the enable outcome of the second step.',
       },
-      {
-        signature: '@Remote(\'addRow\') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>',
-        description: 'Add a row naming one of the package\'s modules to the profile user layer.',
-        parameters: [{ name: 'packageName', description: 'the installed package.' }, { name: 'options', description: '`module` selects a declared `dsh.plugins[]` name (default `.`), `id` overrides the derived row id, `config` overrides the declared default.' }],
-        returns: 'where the row landed.',
-      },
-      {
-        signature: '@Remote(\'removeRow\') async removeRow(rowId: string): Promise<void>',
-        description: 'Remove a row a user layer inserted.',
-        parameters: [{ name: 'rowId', description: 'the inserted row\'s id.' }],
-      },
       {
         signature: '@Remote(\'setRowDisabled\') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>',
         description: 'Switch one row off or on in a user layer; deny-only.',
@@ -4951,13 +4940,9 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'PluginInstallResult',
     declaration: 'export interface PluginInstallResult {\n    readonly installed: readonly string[];\n    readonly removed: readonly PluginInstallRejection[];\n    readonly enabled: readonly string[];\n    readonly installedOnly: readonly string[];\n    readonly plain: readonly string[];\n    readonly jobId: string;\n}',
   },
-  {
-    name: 'PluginPackageAddableView',
-    declaration: 'export interface PluginPackageAddableView {\n    readonly moduleName: string;\n    readonly declaredName: string;\n    readonly title?: string;\n    readonly config?: JsonValue;\n}',
-  },
   {
     name: 'PluginPackageKind',
-    declaration: 'export type PluginPackageKind = \'bundle\' | \'plugin\' | \'unknown\';',
+    declaration: 'export type PluginPackageKind = \'bundle\' | \'unknown\';',
   },
   {
     name: 'PluginPackageRowView',
@@ -4969,11 +4954,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'PluginPackageView',
-    declaration: 'export interface PluginPackageView {\n    readonly name: string;\n    readonly version?: string;\n    readonly title?: string;\n    readonly description?: string;\n    readonly kind: PluginPackageKind;\n    readonly installed: boolean;\n    readonly enabled: boolean;\n    readonly status: PluginPackageStatus;\n    readonly reason?: string;\n    readonly enginesDsh?: string;\n    readonly cordisSameCopy: boolean | null;\n    readonly rows: readonly PluginPackageRowView[];\n    readonly issues?: readonly PluginRowIssue[];\n    readonly overrides: readonly string[];\n    readonly addable: readonly PluginPackageAddableView[];\n    readonly liveReload: boolean;\n}',
-  },
-  {
-    name: 'PluginRowAddition',
-    declaration: 'export interface PluginRowAddition {\n    readonly rowId: string;\n    readonly file: string;\n}',
+    declaration: 'export interface PluginPackageView {\n    readonly name: string;\n    readonly version?: string;\n    readonly title?: string;\n    readonly description?: string;\n    readonly kind: PluginPackageKind;\n    readonly installed: boolean;\n    readonly enabled: boolean;\n    readonly status: PluginPackageStatus;\n    readonly reason?: string;\n    readonly enginesDsh?: string;\n    readonly cordisSameCopy: boolean | null;\n    readonly rows: readonly PluginPackageRowView[];\n    readonly issues?: readonly PluginRowIssue[];\n    readonly overrides: readonly string[];\n    readonly liveReload: boolean;\n}',
   },
   {
     name: 'PluginRowIssue',

+ 2 - 2
packages/host/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/host/plugin-manager/README.md
-README.md: 1eea9fe8e3a0d785a52e4109716ea8d4bb698c34
-README.zh.md: afbdc28c8021c4e5260f7d1ab8f510d02c6299d2
+README.md: b1cab6b904de1cc3c46884db2683abf1c1d6482d
+README.zh.md: af9338467c015239b1e4363c07ff13bc874b65d6

+ 4 - 4
packages/host/plugin-manager/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-host-plugin-manager` mounts `pluginManager` as a Typert service and exposes the `plugins` Remote — `list`, `add`, `uninstall`, `enable`, `disable`, `retry`, `addRow`, `removeRow`, `setRowDisabled`, `dependents` — each relaying to the [`dsh-plugin-manager`](../../boot/plugin-manager/README.md) `PluginManager` built over this context, with every `plugins/*` failure crossing the wire as a `RemoteError` of the same code. What the operations do, and the `plugins/changed` and `plugins/install-log` events that follow them, belong to that package; this one reads the profile runtime and the agent registry off the context and hands them over. Clients consume the Remote through the [`api-remotes`](../../api/remotes/README.md) assembly.
+`dsh-host-plugin-manager` mounts `pluginManager` as a Typert service and exposes the `plugins` Remote — `list`, `add`, `uninstall`, `enable`, `disable`, `retry`, `setRowDisabled`, `dependents` — each relaying to the [`dsh-plugin-manager`](../../boot/plugin-manager/README.md) `PluginManager` built over this context, with every `plugins/*` failure crossing the wire as a `RemoteError` of the same code. What the operations do, and the `plugins/changed` and `plugins/install-log` events that follow them, belong to that package; this one reads the profile runtime and the agent registry off the context and hands them over. Clients consume the Remote through the [`api-remotes`](../../api/remotes/README.md) assembly.
 
 ## Table of Contents
 
@@ -29,11 +29,11 @@ Mount the row in a host composition beside the plugin inventory; the web bundle
 
 ### The Remote
 
-`plugins/list`, `plugins/add`, `plugins/uninstall`, `plugins/enable`, `plugins/disable`, `plugins/retry`, `plugins/addRow`, `plugins/removeRow`, `plugins/setRowDisabled`, and `plugins/dependents` carry the arguments and answers the manager's methods of the same names define; the [manager README](../../boot/plugin-manager/README.md#use-this-package) documents each. The `./types` export re-exports the manager's payload types unchanged, so a client imports one vocabulary.
+`plugins/list`, `plugins/add`, `plugins/uninstall`, `plugins/enable`, `plugins/disable`, `plugins/retry`, `plugins/setRowDisabled`, and `plugins/dependents` carry the arguments and answers the manager's methods of the same names define; the [manager README](../../boot/plugin-manager/README.md#use-this-package) documents each. The `./types` export re-exports the manager's payload types unchanged, so a client imports one vocabulary.
 
 ### Failure codes
 
-A manager failure reaches the client as a `RemoteError` with the same `code` and `details`: `plugins/unavailable`, `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, and `plugins/agents-running`, each declared in the Remote failure map with the manager's details type. The manager's generic refusal, `plugins/bad-request`, crosses as the Gateway's `gateway/bad-request`. Any other error the manager throws propagates untouched, which the Gateway reports as `gateway/internal`.
+A manager failure reaches the client as a `RemoteError` with the same `code` and `details`: `plugins/unavailable`, `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/busy`, and `plugins/agents-running`, each declared in the Remote failure map with the manager's details type. The manager's generic refusal, `plugins/bad-request`, crosses as the Gateway's `gateway/bad-request`. Any other error the manager throws propagates untouched, which the Gateway reports as `gateway/internal`.
 
 ### Configuration
 
@@ -59,7 +59,7 @@ The adapter supplies per-call profile and agent readers to one `PluginManager`,
 
 | File | Role |
 |---|---|
-| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`: the `pluginManager` service, the ten `plugins` Remote methods, and `remoteErrorOf` |
+| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`: the `pluginManager` service, the eight `plugins` Remote methods, and `remoteErrorOf` |
 | [`src/types.ts`](src/types.ts) | The manager's types re-exported, and the `plugins/*` codes declared in the Remote failure map |
 | — | No runtime invariant companion is published; the row holds no state of its own. |
 

+ 4 - 4
packages/host/plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-host-plugin-manager` 把 `pluginManager` 挂载为 Typert 服务并暴露 `plugins` Remote——`list`、`add`、`uninstall`、`enable`、`disable`、`retry`、`addRow`、`removeRow`、`setRowDisabled`、`dependents`——每个方法都转给建立在本上下文之上的 [`dsh-plugin-manager`](../../boot/plugin-manager/README.zh.md) `PluginManager`,每个 `plugins/*` 失败以同码的 `RemoteError` 过线。操作做什么、以及随之而来的 `plugins/changed` 与 `plugins/install-log` 事件属于那个包;本包只是从上下文读取 profile runtime 与 agent 注册表并交给它。客户端经 [`api-remotes`](../../api/remotes/README.zh.md) 装配消费该 Remote。
+`dsh-host-plugin-manager` 把 `pluginManager` 挂载为 Typert 服务并暴露 `plugins` Remote——`list`、`add`、`uninstall`、`enable`、`disable`、`retry`、`setRowDisabled`、`dependents`——每个方法都转给建立在本上下文之上的 [`dsh-plugin-manager`](../../boot/plugin-manager/README.zh.md) `PluginManager`,每个 `plugins/*` 失败以同码的 `RemoteError` 过线。操作做什么、以及随之而来的 `plugins/changed` 与 `plugins/install-log` 事件属于那个包;本包只是从上下文读取 profile runtime 与 agent 注册表并交给它。客户端经 [`api-remotes`](../../api/remotes/README.zh.md) 装配消费该 Remote。
 
 ## 目录
 
@@ -29,11 +29,11 @@ kind: "package-reference"
 
 ### Remote
 
-`plugins/list`、`plugins/add`、`plugins/uninstall`、`plugins/enable`、`plugins/disable`、`plugins/retry`、`plugins/addRow`、`plugins/removeRow`、`plugins/setRowDisabled` 与 `plugins/dependents` 携带管理器同名方法定义的参数与答复;[管理器 README](../../boot/plugin-manager/README.zh.md#use-this-package) 逐一说明。`./types` 导出原样 re-export 管理器的载荷类型,客户端只需导入一套词汇。
+`plugins/list`、`plugins/add`、`plugins/uninstall`、`plugins/enable`、`plugins/disable`、`plugins/retry`、`plugins/setRowDisabled` 与 `plugins/dependents` 携带管理器同名方法定义的参数与答复;[管理器 README](../../boot/plugin-manager/README.zh.md#use-this-package) 逐一说明。`./types` 导出原样 re-export 管理器的载荷类型,客户端只需导入一套词汇。
 
 ### 失败码
 
-管理器的失败以同样的 `code` 与 `details` 作为 `RemoteError` 到达客户端:`plugins/unavailable`、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy` 与 `plugins/agents-running`,各自以管理器的 details 类型声明在 Remote 失败表里。管理器的通用拒绝 `plugins/bad-request` 以 Gateway 的 `gateway/bad-request` 过线。管理器抛出的其他错误原样传播,由 Gateway 报为 `gateway/internal`。
+管理器的失败以同样的 `code` 与 `details` 作为 `RemoteError` 到达客户端:`plugins/unavailable`、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/busy` 与 `plugins/agents-running`,各自以管理器的 details 类型声明在 Remote 失败表里。管理器的通用拒绝 `plugins/bad-request` 以 Gateway 的 `gateway/bad-request` 过线。管理器抛出的其他错误原样传播,由 Gateway 报为 `gateway/internal`。
 
 ### 配置
 
@@ -59,7 +59,7 @@ kind: "package-reference"
 
 | 文件 | 职责 |
 |---|---|
-| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`:`pluginManager` 服务、十个 `plugins` Remote 方法与 `remoteErrorOf` |
+| [`src/index.ts`](src/index.ts) | `PluginManagerRemote`:`pluginManager` 服务、八个 `plugins` Remote 方法与 `remoteErrorOf` |
 | [`src/types.ts`](src/types.ts) | re-export 管理器的类型,并把 `plugins/*` 码声明进 Remote 失败表 |
 | — | 不发布运行时不变量伴随件;该行不持有自己的状态。 |
 

+ 1 - 27
packages/host/plugin-manager/src/index.ts

@@ -20,11 +20,10 @@ import {
   type PluginInstallResult,
   type PluginOperationFailure,
   type PluginPackageView,
-  type PluginRowAddition,
   type SpawnLike,
 } from '@deepseek-ai/dsh-plugin-manager'
 import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
-import { assertNever, type JsonValue } from '@deepseek-ai/dsh-util-values'
+import { assertNever } from '@deepseek-ai/dsh-util-values'
 import type {} from './types.ts'
 
 export type * from './types.ts'
@@ -180,30 +179,6 @@ export class PluginManagerRemote extends TypertRemoteService {
     return relay(() => this.manager.retry(packageName))
   }
 
-  /**
-   * Add a row naming one of the package's modules to the profile user layer.
-   * @param packageName - the installed package.
-   * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
-   * `id` overrides the derived row id, `config` overrides the declared default.
-   * @returns where the row landed.
-   */
-  @Remote('addRow')
-  async addRow(
-    packageName: string,
-    options?: { module?: string; id?: string; config?: JsonValue },
-  ): Promise<PluginRowAddition> {
-    return relay(() => this.manager.addRow(packageName, options))
-  }
-
-  /**
-   * Remove a row a user layer inserted.
-   * @param rowId - the inserted row's id.
-   */
-  @Remote('removeRow')
-  async removeRow(rowId: string): Promise<void> {
-    return relay(() => this.manager.removeRow(rowId))
-  }
-
   /**
    * Switch one row off or on in a user layer; deny-only.
    * @param rowId - the row's id as the composition declares it.
@@ -253,7 +228,6 @@ export function remoteErrorOf(failure: PluginOperationFailure): RemoteError {
     case 'plugins/not-enableable': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/enable-failed': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/install-failed': return new RemoteError(failure.code, failure.message, failure.details, options)
-    case 'plugins/row-conflict': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/busy': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/agents-running': return new RemoteError(failure.code, failure.message, failure.details, options)
     case 'plugins/bad-request': return new RemoteError('gateway/bad-request', failure.message, {}, options)

+ 1 - 3
packages/host/plugin-manager/src/types.ts

@@ -16,14 +16,12 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
     'plugins/unavailable': PluginOperationDetailsMap['plugins/unavailable']
     /** The package is not a profile dependency. */
     'plugins/not-installed': PluginOperationDetailsMap['plugins/not-installed']
-    /** The package cannot be enabled or added, with the declaration or request error. */
+    /** The package cannot be enabled, with the declaration or request error. */
     'plugins/not-enableable': PluginOperationDetailsMap['plugins/not-enableable']
     /** Preparation or the root Include rejected enablement; the layer selection was reverted. */
     'plugins/enable-failed': PluginOperationDetailsMap['plugins/enable-failed']
     /** pnpm exited non-zero, could not be spawned, or timed out. */
     'plugins/install-failed': PluginOperationDetailsMap['plugins/install-failed']
-    /** The row id is already taken in the target user layer. */
-    'plugins/row-conflict': PluginOperationDetailsMap['plugins/row-conflict']
     /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
     'plugins/busy': PluginOperationDetailsMap['plugins/busy']
     /** `node_modules` cannot change while a session runs; `running` counts the agents in `running` status. */

+ 5 - 10
packages/host/plugin-manager/tests/plugin-manager.spec.ts

@@ -249,7 +249,7 @@ describe('PluginManagerRemote', () => {
     const remote = await mount()
     expect(remote.typertRemote).toMatchObject({ serviceKey: 'pluginManager', namespace: 'plugins' })
     expect(remoteMethods(remote).map(marker => marker.method)).toEqual([
-      'list', 'add', 'uninstall', 'enable', 'disable', 'retry', 'addRow', 'removeRow', 'setRowDisabled', 'dependents',
+      'list', 'add', 'uninstall', 'enable', 'disable', 'retry', 'setRowDisabled', 'dependents',
     ])
   })
 
@@ -274,8 +274,6 @@ describe('PluginManagerRemote', () => {
     await remote.enable('pkg')
     await remote.disable('pkg')
     await remote.retry('pkg')
-    await remote.addRow('pkg', { module: './x.js', id: 'x', config: { a: 1 } })
-    await remote.removeRow('x')
     await remote.setRowDisabled('x', true)
     await remote.dependents('pkg')
 
@@ -286,8 +284,6 @@ describe('PluginManagerRemote', () => {
       ['enable', 'pkg'],
       ['disable', 'pkg'],
       ['retry', 'pkg'],
-      ['addRow', 'pkg', { module: './x.js', id: 'x', config: { a: 1 } }],
-      ['removeRow', 'x'],
       ['setRowDisabled', 'x', true],
       ['dependents', 'pkg'],
     ])
@@ -306,7 +302,7 @@ describe('PluginManagerRemote', () => {
       whenIdle: async () => {},
     } as never)
     const metadata: typeof readPackageMetadata = options => ({
-      packageName: options.packageName, kind: 'plugin', cordisSameCopy: null, rows: [], overrides: [], addable: [{ name: '.' }],
+      packageName: options.packageName, kind: 'unknown', cordisSameCopy: null, rows: [], overrides: [],
     })
     const spawn: SpawnLike = () => { throw new Error('this test spawns nothing') }
     class SeamedRemote extends PluginManagerRemote {
@@ -323,7 +319,7 @@ describe('PluginManagerRemote', () => {
       // The agent-count reader: a running session refuses an install before pnpm runs.
       ctx.provide('agents', { list: () => [{ status: 'running' }, { status: 'idle' }] } as never)
       await expect(remote.add('anything')).rejects.toMatchObject({ code: 'plugins/agents-running', details: { operation: 'add', running: 1 } })
-      await expect(remote.addRow('pkg', { module: './unknown' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
+      await expect(remote.enable('pkg')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
     } finally {
       rmSync(profileDir, { recursive: true, force: true })
     }
@@ -352,7 +348,6 @@ describe('remoteErrorOf', () => {
       new PluginOperationError('plugins/not-enableable', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/enable-failed', 'm', { packageName: 'p', reason: 'r' }),
       new PluginOperationError('plugins/install-failed', 'm', { spec: 's', exitCode: 1, log: 'l' }),
-      new PluginOperationError('plugins/row-conflict', 'm', { rowId: 'x' }),
       new PluginOperationError('plugins/busy', 'm', { operation: 'add', subject: 'y', active: { operation: 'add', subject: 'x' } }),
       new PluginOperationError('plugins/agents-running', 'm', { operation: 'add', running: 1 }),
     ]
@@ -368,7 +363,7 @@ describe('remoteErrorOf', () => {
 
 describe('RemoteError codes', () => {
   it('declare their details', () => {
-    const error = new RemoteError('plugins/row-conflict', 'taken', { rowId: 'x' })
-    expect(error.details).toEqual({ rowId: 'x' })
+    const error = new RemoteError('plugins/not-installed', 'absent', { packageName: 'x' })
+    expect(error.details).toEqual({ packageName: 'x' })
   })
 })

+ 2 - 2
packages/util/package-manifest/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/util/package-manifest/README.md
-README.md: 081b07d9a9123d688b288f1ad30dc9465b7dcbe9
-README.zh.md: d9edc50e6979a1d6664076c977ca0ad62b27dc6b
+README.md: ca14a4bd1929faa3d2009ba6c6d2c0bb331a5b37
+README.zh.md: e52f0699f225925eb99dcbfad8b4c00e72a52e72

+ 0 - 1
packages/util/package-manifest/README.md

@@ -38,7 +38,6 @@ const manifest: DshPackageManifest = {
   dsh: {
     manifestVersion: 1,
     title: 'Example plugin',
-    plugins: [{ name: './tool.js', config: { enabled: true } }],
     bundle: { patch: './cordis.patch.yml' },
     client,
   },

+ 0 - 1
packages/util/package-manifest/README.zh.md

@@ -38,7 +38,6 @@ const manifest: DshPackageManifest = {
   dsh: {
     manifestVersion: 1,
     title: 'Example plugin',
-    plugins: [{ name: './tool.js', config: { enabled: true } }],
     bundle: { patch: './cordis.patch.yml' },
     client,
   },

+ 0 - 1
packages/util/package-manifest/src/index.ts

@@ -9,7 +9,6 @@ export type {
   DshEnginesManifest,
   DshManifest,
   DshPackageManifest,
-  DshPluginDeclaration,
   DshProfileManifest,
   ProfilePatchReload,
 } from './types.ts'

+ 0 - 12
packages/util/package-manifest/src/types.ts

@@ -36,8 +36,6 @@ export interface DshManifest {
   client?: DshClientManifest
   /** Display title of the package, read without executing package code. */
   title?: string
-  /** Agent-plane modules the package declares addable to a composition, read without executing package code. */
-  plugins?: DshPluginDeclaration[]
 }
 
 /** Runtime version requirements under `package.json.engines`. */
@@ -52,16 +50,6 @@ export interface DshEnginesManifest {
   [engine: string]: string | undefined
 }
 
-/** One agent-plane module a package declares addable to a composition. */
-export interface DshPluginDeclaration {
-  /** The package's export subpath, with `.` naming its main entry and `./tools` a subpath. */
-  name: string
-  /** Display title. */
-  title?: string
-  /** Default row config. */
-  config?: unknown
-}
-
 /** The configuration layer exported by a bundle package. */
 export interface DshBundleManifest {
   /** Patch file path relative to the declaring package root. */

+ 4 - 3
pnpm-lock.yaml

@@ -1358,6 +1358,10 @@ importers:
         version: 15.0.0
 
   packages/boot/plugin-manager:
+    dependencies:
+      '@deepseek-ai/dsh-util-values':
+        specifier: workspace:^
+        version: link:../../util/values
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1374,9 +1378,6 @@ importers:
       '@deepseek-ai/dsh-package-manifest':
         specifier: workspace:^
         version: link:../../util/package-manifest
-      '@deepseek-ai/dsh-util-values':
-        specifier: workspace:^
-        version: link:../../util/values
 
   packages/bundle/acp-app:
     dependencies:

+ 0 - 1
scripts/gen-cordis-catalog.ts

@@ -721,7 +721,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
   PluginPackageView: 'plugin manager package views are owned by packages/host/plugin-manager/README.md',
   PluginInstallResult: 'plugin manager install results are owned by packages/host/plugin-manager/README.md',
   PluginEnableResult: 'plugin manager enable results are owned by packages/host/plugin-manager/README.md',
-  PluginRowAddition: 'plugin manager row additions are owned by packages/host/plugin-manager/README.md',
   PluginDependents: 'plugin manager dependents are owned by packages/host/plugin-manager/README.md',
   AgentPresetRoster: 'path-free preset roster is owned by packages/preset/agent-presets/README.md',
   AgentPresetDocument: 'preset composition view is owned by packages/preset/agent-presets/README.md',