Forráskód Böngészése

fix(plugin-manager): address approval review and remove agent notices

Turtle 2 napja
szülő
commit
a5dea502a6
32 módosított fájl, 100 hozzáadás és 197 törlés
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.i18n.yaml
  2. 2 2
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.zh.md
  4. 2 2
      docs/config-catalog.i18n.yaml
  5. 2 4
      docs/config-catalog.md
  6. 1 3
      docs/config-catalog.zh.md
  7. 2 2
      docs/subsystems/boot.i18n.yaml
  8. 2 2
      docs/subsystems/boot.md
  9. 2 2
      docs/subsystems/boot.zh.md
  10. 2 2
      docs/tool-catalog.i18n.yaml
  11. 1 1
      docs/tool-catalog.md
  12. 1 1
      docs/tool-catalog.zh.md
  13. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  14. 3 17
      packages/boot/plugin-manager/README.md
  15. 3 17
      packages/boot/plugin-manager/README.zh.md
  16. 0 8
      packages/boot/plugin-manager/package.json
  17. 9 3
      packages/boot/plugin-manager/src/build-approval.ts
  18. 10 0
      packages/boot/plugin-manager/src/failure.ts
  19. 7 68
      packages/boot/plugin-manager/src/index.ts
  20. 1 1
      packages/boot/plugin-manager/src/tools.ts
  21. 1 1
      packages/boot/plugin-manager/src/types.ts
  22. 12 2
      packages/boot/plugin-manager/tests/build-approval.spec.ts
  23. 14 38
      packages/boot/plugin-manager/tests/manager.spec.ts
  24. 2 2
      packages/client/ui-settings-plugin-inventory/README.i18n.yaml
  25. 1 1
      packages/client/ui-settings-plugin-inventory/README.md
  26. 1 1
      packages/client/ui-settings-plugin-inventory/README.zh.md
  27. 2 0
      packages/client/ui-settings-plugin-inventory/src/client/locales.ts
  28. 9 0
      packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx
  29. 1 1
      packages/extensions/tool-cordis/src/api-catalog.ts
  30. 0 9
      pnpm-lock.yaml
  31. 0 0
      snapshots/session/plugin-manager/session.v3.jsonl
  32. 1 1
      snapshots/session/plugin-manager/tool-schemas.expected.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-14-current-profile-plugin-management.md
-2026-09-14-current-profile-plugin-management.md: 3f1fca65201385b01cdccbdca34f5b45b896fc3e
-2026-09-14-current-profile-plugin-management.zh.md: 125b0012ddf9d24fda34e82722e551588ed75761
+2026-09-14-current-profile-plugin-management.md: eef121bd7968547ebec83bbb52c4776f84082278
+2026-09-14-current-profile-plugin-management.zh.md: 009d1df124bfee31385c25b5c1e80696556448a4

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

@@ -16,11 +16,11 @@ Configuration watches use Chokidar write stabilization by default. Its ordinary
 
 Profile files remain the persisted state: entry toggles edit only `disabled` in the last override matching the entry id and any module-name assertion, appending when none matches, and bundle toggles edit the ordered string list. Dependency updates do not reactivate retained disabled bundles. A service removal first applies the composition without the bundle and waits for old fibers to finish before deleting the dependency. Saved configuration, pnpm completion and runtime activation have separate outcomes; failure preserves the actual partial state and a diagnostic path.
 
-This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service, whose batched durable notices inform live Agents without waking them. The agent tool is disabled by default in the base bundle and shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
+This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Profiles without HMR keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service. Management operations return results to callers without adding messages to live Agents. The agent tool is disabled by default in the base bundle and shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
 
 CLI calls inherit the terminal and authentication environment; service calls retain the subprocess credential scrub and bounded diagnostics. Management records carry error codes and parameters for locale-owned Web presentation. Reconciliation compares entry identity, fiber identity, configuration and diagnostics before and after updating: unchanged inactive entries remain warnings, while newly affected failures reject the operation. Explicit enablement targets must activate.
 
-Build approvals update pnpm 11's unresolved `allowBuilds` entries under the same profile lock and preserve unrelated YAML. They persist by exact package name rather than applying an unrestricted script policy. The retry accepts only names still pending, so stale requests cannot override a subsequent denial. Package cleanup leaves the approval settings intact; a later retry can use them without retaining partially installed dependencies. The service reports policy-only changes and injects them through the existing management notices.
+Build approvals update pnpm 11's unresolved `allowBuilds` entries under the same profile lock and preserve unrelated YAML. They persist by exact package name rather than applying an unrestricted script policy. The retry accepts only names still pending, so stale requests cannot override a subsequent denial. Package cleanup leaves the approval settings intact; a later retry can use them without retaining partially installed dependencies. The service reports policy-only changes. Agent tools may grant approval on the user's behalf; their instructions require explicit conversational consent, while the service validates only pending package names. Approval rejects anchors and aliases inside `allowBuilds` to prevent shared YAML nodes from changing unrequested permissions.
 
 ## Alternatives considered
 

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

@@ -16,11 +16,11 @@ Web 和 Agent 控件需要修改运行中的 profile,同时避免另建包安
 
 profile 文件保持为持久状态:条目开关只修改最后一条符合条目 id 及模块名称断言的覆盖项中的 `disabled`,没有匹配项时追加,组合包开关修改有序字符串列表。更新依赖不会重新激活保留的已停用组合包。service 删除组合包时,先应用去掉该组合包的配置,等待旧 fiber 完成卸载后再删除依赖。已保存配置、pnpm 完成状态与运行时激活分别报告;失败保留实际的部分状态与诊断路径。
 
-这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service;service 合并持久通知,告知存活 Agent 而不唤醒它们。base 组合包和内置预设默认禁用该 Agent 工具。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
+这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service。管理操作向调用方返回结果,不向存活 Agent 添加消息。base 组合包和内置预设默认禁用该 Agent 工具。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
 
 CLI 调用继承终端和认证环境;service 调用保留子进程凭据清理与有界诊断。管理结果提供错误码和参数,由 Web 词典呈现文案。重载前后比较 entry、fiber、配置与诊断:未变化的已有故障保留为警告,本次影响到的新故障使操作失败。显式启用的目标必须成功激活。
 
-构建审批在同一个 profile 写锁内更新 pnpm 11 尚未决定的 `allowBuilds` 条目,并保留无关 YAML。授权按准确包名持久化,不采用无条件允许脚本的策略。重试只接受仍在待审批列表中的包名,因此过期请求不能覆盖后续拒绝。包清理保留审批设置,后续重试可以复用授权而不必保留部分安装的依赖。service 报告仅涉及策略的变化,并通过现有管理通知注入
+构建审批在同一个 profile 写锁内更新 pnpm 11 尚未决定的 `allowBuilds` 条目,并保留无关 YAML。授权按准确包名持久化,不采用无条件允许脚本的策略。重试只接受仍在待审批列表中的包名,因此过期请求不能覆盖后续拒绝。包清理保留审批设置,后续重试可以复用授权而不必保留部分安装的依赖。service 报告仅涉及策略的变化。Agent 工具可以代表用户授权;工具说明要求用户在对话中明确同意,而 service 仅验证待审批包名。审批拒绝 `allowBuilds` 内的锚点和别名,避免共享 YAML 节点改变未请求的权限
 
 ## 考虑过的替代方案
 

+ 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: 6349bdb010910086a82d7d47c5a300f2157a7436
-config-catalog.zh.md: a9a07b97da99ee5ce6b5666255dd07ea0080f5ae
+config-catalog.md: b2a1177bc58e71fcff318613fb4781ff66b355a7
+config-catalog.zh.md: fd40eb46ed9103a5a10688f1bf75215c4ab084b2

+ 2 - 4
docs/config-catalog.md

@@ -1810,18 +1810,16 @@ Source: [`packages/plan/plan-mode/src/index.ts:64`](../packages/plan/plan-mode/s
 Requires: `loader` · `profileContext`
 
 ```ts config-catalog
-/** Limits for package diagnostics and change notifications. */
+/** Limits for package diagnostics and lock acquisition. */
 export interface Config {
   /** Maximum retained pnpm diagnostic bytes per operation. */
   outputBytes?: number
-  /** Delay for combining consecutive management notices in one durable injection. */
-  notificationDelayMs?: number
   /** Maximum time to wait for another process's profile package operation. */
   lockWaitMs?: number
 }
 ```
 
-Source: [`packages/boot/plugin-manager/src/index.ts:22`](../packages/boot/plugin-manager/src/index.ts)
+Source: [`packages/boot/plugin-manager/src/index.ts:21`](../packages/boot/plugin-manager/src/index.ts)
 
 <a id="deepseek-aidsh-plugin-package-inventory-deepseek"></a>
 

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

@@ -1812,12 +1812,10 @@ export interface PlanModeConfig {
 依赖: `loader` · `profileContext`
 
 ```ts config-catalog
-/** Limits for package diagnostics and change notifications. */
+/** Limits for package diagnostics and lock acquisition. */
 export interface Config {
   /** Maximum retained pnpm diagnostic bytes per operation. */
   outputBytes?: number
-  /** Delay for combining consecutive management notices in one durable injection. */
-  notificationDelayMs?: number
   /** Maximum time to wait for another process's profile package operation. */
   lockWaitMs?: number
 }

+ 2 - 2
docs/subsystems/boot.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/boot.md
-boot.md: bf47e32055f921579c0e4e34f91a25dbef28f97d
-boot.zh.md: ac0a8b2ba5bb1222967d4b1a5136aaa8eb2a5b4e
+boot.md: 18f06789f450b0694329feb1fb8b7a2df7a9905a
+boot.zh.md: afe4c6f79c3cbe2d8ef8570e512fa4669c10b1ff

+ 2 - 2
docs/subsystems/boot.md

@@ -12,9 +12,9 @@ The [boot package group](../../packages/boot/README.md) owns launcher-provided p
 
 `BundleInfo` carries the package name, optional installed version, selected enablement, removal availability and optional resolution error.
 
-`InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer.
+`InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation.
 
-`ChangeResult.changed` reports a disk edit independently of `application`: `applied`, `restart-required`, `overridden` or `failed`. `message` describes the result. Optional `packageResult` records the pnpm exit code, bounded output, truncation flag and complete diagnostic log path.
+`ChangeResult.changed` reports a disk edit independently of `application`: `applied`, `restart-required`, `overridden` or `failed`. Optional `error` carries a localizable code and external diagnostic. `packageResult` records the pnpm exit code, bounded output, truncation flag and complete diagnostic log path. `pendingBuilds` lists undecided packages across the profile; `approvedBuilds` records the names granted permission by this operation.
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/subsystems/boot.zh.md

@@ -12,9 +12,9 @@
 
 `BundleInfo` 包含包名、可选的安装版本、组合层选择状态、删除可用性及可选的解析错误。
 
-`InstallBundleOptions.enabled` 默认为 true。False 表示安装但不选择该组合包层
+`InstallBundleOptions.enabled` 默认为 true,false 表示安装但不选择组合包层。`approvedBuilds` 在安装前向指定的待审批包名授予持久脚本权限
 
-`ChangeResult.changed` 独立报告磁盘修改,`application` 为 `applied`、`restart-required`、`overridden` 或 `failed`。`message` 描述结果。可选的 `packageResult` 记录 pnpm 退出码、有界输出、截断标记与完整诊断日志路径
+`ChangeResult.changed` 报告磁盘修改,独立于 `application`:`applied`、`restart-required`、`overridden` 或 `failed`。可选的 `error` 包含可本地化的错误码和外部诊断。`packageResult` 记录 pnpm 退出码、有界输出、截断标志及完整诊断日志路径。`pendingBuilds` 列出整个 profile 尚未决定的包;`approvedBuilds` 记录本次操作授予权限的包名
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/tool-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/tool-catalog.md
-tool-catalog.md: 14fff202b77c45afe5ec25276032b6e6a589da33
-tool-catalog.zh.md: f9965bfb320ff61b9cef333287954898e628336b
+tool-catalog.md: 0166c7dd9829f0774c277508299726aac8426969
+tool-catalog.zh.md: 7722301c04febbd7b8d1c5509bfdb9e3881d0b76

+ 1 - 1
docs/tool-catalog.md

@@ -79,7 +79,7 @@ List plugins or bundles in the current profile, enable or disable them, install
     },
     "approvedBuilds": {
       "type": "array",
-      "description": "For install_bundle: package names from pendingBuilds whose install scripts the user explicitly approved. Permission persists for this profile.",
+      "description": "For install_bundle: pass names from pendingBuilds only after the user explicitly approves running their install scripts in the conversation. This grants persistent permission for this profile.",
       "items": {
         "type": "string"
       }

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

@@ -83,7 +83,7 @@
     },
     "approvedBuilds": {
       "type": "array",
-      "description": "For install_bundle: package names from pendingBuilds whose install scripts the user explicitly approved. Permission persists for this profile.",
+      "description": "For install_bundle: pass names from pendingBuilds only after the user explicitly approves running their install scripts in the conversation. This grants persistent permission for this profile.",
       "items": {
         "type": "string"
       }

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

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

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

@@ -37,7 +37,7 @@ Enable the tool explicitly in the profile patch; agents using a preset also need
 
 A plugin toggle updates only `disabled` in the last matching override in the profile's `cordis.patch.yml`, or appends an override when none matches. Matching uses the entry id and any module-name assertion. A bundle toggle changes `package.json`'s ordered `dsh.profile.bundles` list. Disabling retains the dependency; enabling appends the bundle at the end, which can change configuration precedence. Installation enables a new bundle by default. Home and invocation patches retain their higher priority.
 
-When pnpm 11 blocks dependency scripts, the failed installation reports pending package names. Web offers **Allow these scripts and retry**; the tool accepts the same names through `approvedBuilds` on `install_bundle`. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Installation cleanup retains these decisions. Retry preserves the original activation choice.
+When pnpm 11 blocks dependency scripts, the failed installation reports all pending package names in the profile, including earlier attempts. Web offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Installation cleanup retains these decisions. Retry preserves the original activation choice.
 
 ### Configuration
 
@@ -45,7 +45,6 @@ When pnpm 11 blocks dependency scripts, the failed installation reports pending
 |---|---|---|
 | `outputBytes` | `16384` | Maximum pnpm diagnostic bytes returned per operation; the full output remains in the returned log path. |
 | `lockWaitMs` | `120000` | Maximum time in milliseconds to acquire the profile write lock. |
-| `notificationDelayMs` | `250` | Delay in milliseconds for combining operation notices. |
 
 -----
 
@@ -77,7 +76,7 @@ Results contain the last attempted stage, target, saved-state change, applicatio
 
 #### What the model sees
 
-The [`plugin_manager` tool](../../../docs/tool-catalog.md#deepseek-aidsh-plugin-manager) lists plugin entries and bundles and performs profile-wide changes. Its results include saved-state changes, application status and package diagnostics.
+The [`plugin_manager` tool](../../../docs/tool-catalog.md#deepseek-aidsh-plugin-manager) lists plugin entries and bundles and performs profile-wide changes. Its results include saved-state changes, application status and package diagnostics. Management operations do not inject messages into Agents.
 
 #### Token effect
 
@@ -87,24 +86,11 @@ The tool declaration is present when its consumer is mounted; each invocation ad
 
 Tool results append to the transcript. Enabling or disabling other tools can change subsequent tool declarations and their cache reuse.
 
-### Configuration change notices
-
-#### What the model sees
-
-Consecutive operation results are combined within `notificationDelayMs` and injected into each affected live Agent. Notices include the application outcome, disclose omitted results when the configured output bound is reached, and do not wake an idle Agent.
-
-#### Token effect
-
-Notices add conditional user-message context to each affected Agent.
-
-#### KV Cache effect
-
-Notices append context; they do not rewrite earlier messages.
-
 ## Known Limitations and Deferred Work
 
 <a id="known-limitations-and-deferred-work"></a>
 
+- Web approves the entire displayed pending group; it has no per-package selection.
 - Package replacements require restarting the process to load a fresh JavaScript module generation.
 - Startup-only profiles cannot remove packages used to start the current process; stop it and use `dsh plugin`.
 - The manager cannot disable its own management components, change another profile, or edit an agent preset's composition.

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

@@ -37,7 +37,7 @@ kind: "package-reference"
 
 插件开关只更新 profile 的 `cordis.patch.yml` 中最后一条匹配覆盖项的 `disabled`;没有匹配项时追加。匹配依据是条目 id,以及覆盖项声明的模块名称。组合包开关修改 `package.json` 的有序 `dsh.profile.bundles` 列表。关闭保留依赖;开启追加到列表末尾,可能改变配置优先级。安装新组合包默认启用。home 和单次启动 patch 保留更高优先级。
 
-pnpm 11 阻止依赖脚本时,安装失败结果列出待审批包名。Web 提供“允许这些脚本并重试”;工具通过 `install_bundle` 的 `approvedBuilds` 接收相同包名。授权按包名保存在当前 profile,允许脚本以宿主用户权限执行命令,并在再次安装失败后保留。该操作只能批准当前尚未决定的包名,不能覆盖已有拒绝或通配符规则。安装清理保留这些决定,重试沿用原来的启用选项。
+pnpm 11 阻止依赖脚本时,安装失败结果列出 profile 内所有待审批包名,包括此前安装留下的条目。Web 提供“允许这些脚本并重试”;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代表用户授权。service 验证包名仍在待审批列表中,不验证对话审批。授权按包名保存在当前 profile,允许脚本以宿主用户权限执行命令,并在再次安装失败后保留。该操作只能批准当前尚未决定的包名,不能覆盖已有拒绝或通配符规则。审批拒绝 `allowBuilds` 内的 YAML 锚点或别名。安装清理保留这些决定,重试沿用原来的启用选项。
 
 ### 配置
 
@@ -45,7 +45,6 @@ pnpm 11 阻止依赖脚本时,安装失败结果列出待审批包名。Web 
 |---|---|---|
 | `outputBytes` | `16384` | 每次操作返回的 pnpm 诊断字节上限;完整输出保留在返回的日志路径中。 |
 | `lockWaitMs` | `120000` | 获取 profile 写锁的最长等待毫秒数。 |
-| `notificationDelayMs` | `250` | 合并操作通知的延迟毫秒数。 |
 
 -----
 
@@ -77,7 +76,7 @@ pnpm 11 阻止依赖脚本时,安装失败结果列出待审批包名。Web 
 
 #### 模型看到什么
 
-[`plugin_manager` 工具](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plugin-manager) 列出插件条目和组合包,并执行影响整个 profile 的改动。结果包含保存状态变化、应用状态和包管理诊断。
+[`plugin_manager` 工具](../../../docs/tool-catalog.zh.md#deepseek-aidsh-plugin-manager) 列出插件条目和组合包,并执行影响整个 profile 的改动。结果包含保存状态变化、应用状态和包管理诊断。管理操作不会向 Agent 注入消息。
 
 #### Token 影响
 
@@ -87,24 +86,11 @@ pnpm 11 阻止依赖脚本时,安装失败结果列出待审批包名。Web 
 
 工具结果追加到对话中。启停其他工具可能改变后续工具声明及其缓存复用。
 
-### 配置变更通知
-
-#### 模型看到什么
-
-连续操作结果在 `notificationDelayMs` 内合并后注入每个受影响的存活 Agent。通知包含应用结果,达到配置的输出上限时标明省略的结果数,不会唤醒空闲 Agent。
-
-#### Token 影响
-
-通知按需向每个受影响 Agent 追加用户消息上下文。
-
-#### KV Cache 影响
-
-通知追加上下文,不改写先前消息。
-
 ## 已知限制与延期工作
 
 <a id="known-limitations-and-deferred-work"></a>
 
+- Web 一次批准整个待审批列表,暂不支持逐包选择。
 - 替换已有包后需要重启进程,以加载新的 JavaScript 模块版本。
 - 仅启动时加载的 profile 不能删除当前进程启动时使用的包;停止进程后使用 `dsh plugin`。
 - 管理器不能关闭自身所需的管理组件、修改其他 profile 或编辑 agent 预设组合。

+ 0 - 8
packages/boot/plugin-manager/package.json

@@ -63,18 +63,13 @@
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
-    "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-subprocess": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-hmr": "workspace:^"
   },
   "peerDependenciesMeta": {
-    "@deepseek-ai/dsh-agent": {
-      "optional": true
-    },
     "@deepseek-ai/dsh-hmr": {
       "optional": true
     }
@@ -82,14 +77,11 @@
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
-    "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-subprocess": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-hmr": "workspace:^",
     "@deepseek-ai/cordis-plugin-timer": "workspace:^"
   }

+ 9 - 3
packages/boot/plugin-manager/src/build-approval.ts

@@ -1,7 +1,8 @@
 /** Approve pnpm's pending dependency scripts in the current profile's workspace settings. */
 import { readFile } from 'node:fs/promises'
 import { join } from 'node:path'
-import { isMap, isScalar, parseDocument } from 'yaml'
+import { isAlias, isMap, isNode, isScalar, parseDocument, visit } from 'yaml'
+import { ManagementFailure } from './failure.ts'
 import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
 
 async function readPolicy(dir: string) {
@@ -16,6 +17,11 @@ async function readPolicy(dir: string) {
   if (!isMap(document.contents)) throw new Error('pnpm-workspace.yaml must be a YAML mapping')
   const builds = document.get('allowBuilds')
   if (builds !== undefined && !isMap(builds)) throw new Error('allowBuilds must be a YAML mapping')
+  visit(builds ?? null, (_key, node) => {
+    if (isAlias(node) || (isNode(node) && 'anchor' in node && node.anchor)) {
+      throw new Error('allowBuilds must not contain YAML anchors or aliases')
+    }
+  })
   const pending = isMap(builds) ? builds.items.flatMap(({ key, value }) =>
     isScalar(key) && typeof key.value === 'string' && !/[*?]/.test(key.value)
       && isScalar(value) && value.value === 'set this to true or false' ? [key.value] : []) : []
@@ -33,11 +39,11 @@ export async function readPendingBuilds(dir: string): Promise<string[]> {
 /** Persist approval without running scripts; the caller holds the profile manifest lock.
  * @param dir Current profile directory.
  * @param names Explicit package names from the pending build list.
- * @throws If any requested name is no longer pending; no approvals are written.
+ * @throws If a name is no longer pending or allowBuilds contains YAML anchors or aliases; no approvals are written.
  */
 export async function approveBuilds(dir: string, names: readonly string[]): Promise<void> {
   const { document, pending } = await readPolicy(dir)
-  if (names.some(name => !pending.includes(name))) throw new Error('Build approval changed; retry installation to refresh the pending packages')
+  if (names.some(name => !pending.includes(name))) throw new ManagementFailure('stale-approval')
   if (names.length === 0) return
   for (const name of names) document.setIn(['allowBuilds', name], true)
   await writeFileAtomic(join(dir, 'pnpm-workspace.yaml'), String(document), { mode: 0o600 })

+ 10 - 0
packages/boot/plugin-manager/src/failure.ts

@@ -0,0 +1,10 @@
+/** Localizable rejections shared by profile management operations. */
+import type { ManagementError } from './types.ts'
+
+/** Expected management rejection; presentation belongs to the caller's locale. */
+export class ManagementFailure extends Error {
+  /** Code rendered by the caller's locale dictionary. */
+  readonly code: ManagementError['code']
+  /** @param code Localizable management rejection. */
+  constructor(code: ManagementError['code']) { super(code); this.code = code }
+}

+ 7 - 68
packages/boot/plugin-manager/src/index.ts

@@ -10,20 +10,17 @@ import { readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory'
 import { readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches } from '@deepseek-ai/dsh-app-boot'
 import type {} from '@deepseek-ai/dsh-hmr'
 import type { ProfileContext } from '@deepseek-ai/dsh-app-boot'
-import type { Agent } from '@deepseek-ai/dsh-agent'
-import { createUserMessage } from '@deepseek-ai/dsh-llm'
 import { bundleManifest, runProfilePnpm, saveManifest } from './operations.ts'
 import { writePluginEnabled } from './patch.ts'
+import { ManagementFailure } from './failure.ts'
 import { approveBuilds, readPendingBuilds } from './build-approval.ts'
 import type { BundleInfo, ChangeResult, InstallBundleOptions, ManagementError, PackageResult, PluginEntryId, PluginInfo } from './types.ts'
 export type * from './types.ts'
 
-/** Limits for package diagnostics and change notifications. */
+/** Limits for package diagnostics and lock acquisition. */
 export interface Config {
   /** Maximum retained pnpm diagnostic bytes per operation. */
   outputBytes?: number
-  /** Delay for combining consecutive management notices in one durable injection. */
-  notificationDelayMs?: number
   /** Maximum time to wait for another process's profile package operation. */
   lockWaitMs?: number
 }
@@ -51,12 +48,6 @@ function managementError(error: unknown): ManagementError {
     : { code: 'operation-error', diagnostic: error instanceof Error ? error.message : String(error) }
 }
 
-/** Expected management rejection; presentation belongs to the caller's locale. */
-class ManagementFailure extends Error {
-  readonly code: ManagementError['code']
-  constructor(code: ManagementError['code']) { super(code); this.code = code }
-}
-
 declare module '@deepseek-ai/cordis' {
   interface Context {
     /** Persistent management of the current profile's composition and packages. */
@@ -69,21 +60,14 @@ export class PluginManager extends TypertRemoteService {
   static inject = ['loader', 'profileContext']
   static Config: z<Config> = z.object({
     outputBytes: z.number().step(1).min(1).default(16384),
-    notificationDelayMs: z.number().step(1).min(0).default(250),
     lockWaitMs: z.number().step(1).min(0).default(120000),
   })
   private readonly ownerEntryId: string | undefined
   private readonly packageOperations = new Set<Promise<PackageResult>>()
   private readonly profile: ProfileContext
   private readonly outputBytes: number
-  private readonly notificationDelayMs: number
   private readonly lockWaitMs: number
   private readonly ownerContext: Context
-  private pendingNotice = ''
-  private omittedNotices = 0
-  private noticeTimer: ReturnType<typeof setTimeout> | undefined
-  private noticeDelivered: PromiseWithResolvers<void> | undefined
-  private readonly noticeAgents = new Set<Agent>()
   private readonly abort = new AbortController()
 
   constructor(ctx: Context, config: Config) {
@@ -92,13 +76,10 @@ export class PluginManager extends TypertRemoteService {
     this.ownerContext = ctx
     this.profile = ctx.profileContext
     this.outputBytes = (config as Required<Config>).outputBytes
-    this.notificationDelayMs = (config as Required<Config>).notificationDelayMs
     this.lockWaitMs = (config as Required<Config>).lockWaitMs
     ctx.effect(() => async () => {
       this.abort.abort()
       await Promise.allSettled([...this.packageOperations])
-      clearTimeout(this.noticeTimer)
-      this.flushNotice()
     }, 'plugin-manager: package cancellation')
   }
 
@@ -207,7 +188,10 @@ export class PluginManager extends TypertRemoteService {
       try {
         result.packageResult = await this.runPnpm(['add', spec])
         if (result.packageResult.exitCode !== 0) {
-          result.pendingBuilds = await readPendingBuilds(this.profile.dir)
+          try { result.pendingBuilds = await readPendingBuilds(this.profile.dir) }
+          catch (error) {
+            this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error)
+          }
           throw new Error(result.packageResult.output)
         }
         const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {}
@@ -337,8 +321,7 @@ export class PluginManager extends TypertRemoteService {
     operation: (result: ChangeResult) => Promise<ChangeResult['application'] | void>,
     request: Pick<ChangeResult, 'stage' | 'target' | 'enabled'>,
   ): Promise<ChangeResult> {
-    let notice: Promise<void> | undefined
-    const locked = () => withFileLock(join(this.profile.dir, 'package.json'), async () => {
+    return withFileLock(join(this.profile.dir, 'package.json'), async () => {
       this.abort.signal.throwIfAborted()
       const before = this.diskState()
       const result: ChangeResult = { ...request, changed: false,
@@ -350,12 +333,8 @@ export class PluginManager extends TypertRemoteService {
         result.error = managementError(error)
       }
       result.changed = before !== this.diskState()
-      notice = this.notify(result)
       return result
     }, { waitMs: this.lockWaitMs })
-    const result = await locked()
-    await notice
-    return result
   }
 
   private diskState(): string {
@@ -367,46 +346,6 @@ export class PluginManager extends TypertRemoteService {
       }
     }).join('\u0000')
   }
-
-  private notify(result: ChangeResult): Promise<void> {
-    const agents = this.ownerContext.get('agents')?.list() ?? []
-    if (agents.length === 0) return Promise.resolve()
-    for (const agent of agents) this.noticeAgents.add(agent)
-    this.noticeDelivered ??= Promise.withResolvers<void>()
-    const delivered = this.noticeDelivered.promise
-    const notice = JSON.stringify({ profile: this.profile.name, ...result })
-    if (Buffer.byteLength(this.pendingNotice + notice) > this.outputBytes) {
-      this.omittedNotices += 1
-    } else {
-      this.pendingNotice += `${notice}\n`
-    }
-    if (this.noticeTimer !== undefined) return delivered
-    this.noticeTimer = setTimeout(() => { this.flushNotice() }, this.notificationDelayMs)
-    return delivered
-  }
-
-  private flushNotice(): void {
-    this.noticeTimer = undefined
-    const text = this.pendingNotice + (this.omittedNotices === 0 ? '' : JSON.stringify({ omitted: this.omittedNotices, refresh: 'plugin_manager' }) + '\n')
-    this.pendingNotice = ''
-    this.omittedNotices = 0
-    const delivered = this.noticeDelivered
-    this.noticeDelivered = undefined
-    if (delivered === undefined) return
-    const agents = [...this.noticeAgents]
-    this.noticeAgents.clear()
-    for (const agent of agents) {
-      try {
-        agent.inject(createUserMessage({
-          content: [{ type: 'text', text }],
-          source: { kind: 'plugin', plugin: 'plugin-manager' },
-        }))
-      } catch (error) {
-        this.ownerContext.logger.warn('Plugin management notification could not reach an Agent', error)
-      }
-    }
-    delivered.resolve()
-  }
 }
 
 export default PluginManager

+ 1 - 1
packages/boot/plugin-manager/src/tools.ts

@@ -19,7 +19,7 @@ export function apply(ctx: Context): void {
       action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle'], description: 'Management operation.' },
       target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
       enabled: { type: 'boolean', description: 'Required for set operations; defaults to true for installation.' },
-      approvedBuilds: { type: 'array', items: { type: 'string' }, description: 'For install_bundle: package names from pendingBuilds whose install scripts the user explicitly approved. Permission persists for this profile.' },
+      approvedBuilds: { type: 'array', items: { type: 'string' }, description: 'For install_bundle: pass names from pendingBuilds only after the user explicitly approves running their install scripts in the conversation. This grants persistent permission for this profile.' },
       offset: { type: 'number', description: 'Zero-based list offset; defaults to 0.' },
       limit: { type: 'number', description: 'List page size, from 1 to 100; defaults to 25.' },
     },

+ 1 - 1
packages/boot/plugin-manager/src/types.ts

@@ -7,7 +7,7 @@ export type ReadOnlyReason = 'management-required' | 'unaddressable'
 
 /** Localizable management failure and optional external diagnostic. */
 export interface ManagementError {
-  code: ReadOnlyReason | 'unknown-plugin' | 'invalid-spec' | 'ambiguous-install' | 'not-bundle' | 'not-removable' | 'stop-profile' | 'bundle-in-use' | 'operation-error'
+  code: ReadOnlyReason | 'unknown-plugin' | 'invalid-spec' | 'ambiguous-install' | 'not-bundle' | 'not-removable' | 'stop-profile' | 'bundle-in-use' | 'stale-approval' | 'operation-error'
   diagnostic?: string
 }
 

+ 12 - 2
packages/boot/plugin-manager/tests/build-approval.spec.ts

@@ -15,7 +15,7 @@ function fixture(text?: string) {
 }
 
 it('approves only named pending packages and preserves comments, decisions and settings', async () => {
-  const { dir, filename } = fixture('# profile settings\nnodeLinker: hoisted\nallowBuilds:\n  native: set this to true or false\n  "@scope/other": set this to true or false\n  trusted: true\n  denied: false\n  "@scope/*": set this to true or false\n')
+  const { dir, filename } = fixture('# profile settings\nother: &unrelated value\ncopy: *unrelated\nnodeLinker: hoisted\nallowBuilds:\n  native: set this to true or false\n  "@scope/other": set this to true or false\n  trusted: true\n  denied: false\n  "@scope/*": set this to true or false\n')
   expect(await readPendingBuilds(dir)).toEqual(['native', '@scope/other'])
   await approveBuilds(dir, ['native'])
   const text = readFileSync(filename, 'utf8')
@@ -27,7 +27,7 @@ it('approves only named pending packages and preserves comments, decisions and s
 it.each(['missing', 'denied', '*', '--all'])('rejects an unlisted approval atomically: %s', async (name) => {
   const original = 'allowBuilds:\n  native: set this to true or false\n  denied: false\n'
   const { dir, filename } = fixture(original)
-  await expect(approveBuilds(dir, ['native', name])).rejects.toThrow('Build approval changed')
+  await expect(approveBuilds(dir, ['native', name])).rejects.toThrow('stale-approval')
   expect(readFileSync(filename, 'utf8')).toBe(original)
 })
 
@@ -57,3 +57,13 @@ it('reports unreadable workspace settings', async () => {
   mkdirSync(filename)
   await expect(readPendingBuilds(dir)).rejects.toThrow()
 })
+
+it.each([
+  'allowBuilds:\n  native: &pending set this to true or false\n  other: *pending\n',
+  'allowBuilds: &builds\n  native: set this to true or false\nshared: *builds\n',
+  'shared: &pending set this to true or false\nallowBuilds:\n  native: *pending\n',
+])('rejects shared YAML approval nodes without changing permissions: %s', async (original) => {
+  const { dir, filename } = fixture(original)
+  await expect(approveBuilds(dir, ['native'])).rejects.toThrow()
+  expect(readFileSync(filename, 'utf8')).toBe(original)
+})

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

@@ -4,9 +4,6 @@ import { realpath } from 'node:fs/promises'
 import { join } from 'node:path'
 import { tmpdir } from 'node:os'
 import type { Context } from '@deepseek-ai/cordis'
-import type AgentRegistry from '@deepseek-ai/dsh-agent'
-import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import type { UserMessage } from '@deepseek-ai/dsh-llm'
 import { expect, it, onTestFinished, vi } from 'vitest'
 import {
   boot, composeEntries, initProfile, readProfilePatches, readProfileManifest, reconcileProfilePatches,
@@ -18,7 +15,7 @@ import Timer from '@deepseek-ai/cordis-plugin-timer'
 import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import { Group } from '@deepseek-ai/cordis-plugin-loader'
 import * as operations from '../src/operations.ts'
-import { parse } from 'yaml'
+import { parse, parseDocument } from 'yaml'
 
 async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}) {
   // pnpm resolves workspace roots through native realpath, including Windows 8.3 aliases.
@@ -170,7 +167,7 @@ it('cleans a blocked installation and retries only after explicit profile build
   })
   expect(readProfileManifest('test', dir).dependencies).not.toHaveProperty('addon')
   expect(parse(readFileSync(policy, 'utf8'))).toMatchObject({ allowBuilds: { native: 'set this to true or false' } })
-  expect(await manager.installBundle('addon', { approvedBuilds: ['denied'] })).toMatchObject({ application: 'failed', changed: false })
+  expect(await manager.installBundle('addon', { approvedBuilds: ['denied'] })).toMatchObject({ application: 'failed', changed: false, error: { code: 'stale-approval' } })
   expect(run).toHaveBeenCalledTimes(2)
   expect(await manager.installBundle('addon', { approvedBuilds: ['native'], enabled: false })).toMatchObject({ application: 'applied', changed: true })
   expect(readProfileManifest('test', dir).dsh?.profile?.bundles).not.toContain('addon')
@@ -196,7 +193,10 @@ it('runs a real pnpm dependency script only after approval and cleanup retry', a
   writeFileSync(join(addon, 'build.cjs'), 'require("node:fs").writeFileSync("built.txt", "built")\n')
   writeFileSync(join(addon, 'cordis.patch.yml'), '[]\n')
   writeFileSync(join(dir, 'package.json'), '{"name":"approval-fixture","private":true}\n')
-  writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'packages:\n  - .\nnodeLinker: hoisted\nautoInstallPeers: false\nstrictDepBuilds: true\n')
+  const policy = parseDocument(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8'))
+  policy.set('offline', true)
+  policy.set('storeDir', join(profile.cwd, 'store'))
+  writeFileSync(join(dir, 'pnpm-workspace.yaml'), String(policy))
   const blocked = await manager.installBundle('file:./addon', { enabled: false })
   expect(blocked, JSON.stringify(blocked)).toMatchObject({ application: 'failed', cleanup: { name: 'approval-fixture-addon' } })
   expect(blocked.pendingBuilds).toHaveLength(1)
@@ -257,29 +257,6 @@ it('keeps saved changes after activation failure and allows a corrected configur
 })
 
 
-it('combines concurrent changes into durable notices without waking Agents', async () => {
-  const session = Session.create(SessionId('manager-notices'))
-  const wake = vi.fn()
-  const notices: UserMessage[] = []
-  const liveAgents = [{
-    inject(message: UserMessage) {
-      notices.push(message)
-      session.append('user/message', message, { surfaceOp: 'append' })
-    }, followup: wake, steer: wake,
-  }, { inject() { throw new Error('already disposed') } }]
-  const agents = { list: () => liveAgents }
-  const { manager } = await fixture('live', false, (ctx) => { ctx.provide('agents', agents as unknown as AgentRegistry) })
-  const id = (await manager.listPlugins()).find(row => row.patchId === 'managed')!.entryId
-  await Promise.all([manager.setPluginEnabled(id, false), manager.setPluginEnabled(id, true)])
-  expect(notices).toHaveLength(1)
-  const noticeText = notices[0]?.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')
-  expect(noticeText).toContain('"enabled":false')
-  expect(noticeText).toContain('"enabled":true')
-  expect(wake).not.toHaveBeenCalled()
-  expect(session.snapshotEvents().filter(row => row.type === 'user/message')).toHaveLength(1)
-})
-
-
 it('reports plain dependencies, missing versions and invalid selected bundles distinctly', async () => {
   const { manager, dir, profile } = await fixture()
   writeFileSync(profile.installAnchor, '{}')
@@ -465,15 +442,6 @@ it('handles missing patch files and retains non-Error package diagnostics', asyn
   await expect(manager.setPluginEnabled(id, true)).rejects.toThrow()
 })
 
-it('bounds batched notices and discloses omitted operation results', async () => {
-  const messages: UserMessage[] = []
-  const { manager } = await fixture('live', false, (ctx) => {
-    ctx.provide('agents', { list: () => [{ inject: (message: UserMessage) => { messages.push(message) } }] } as unknown as AgentRegistry)
-  }, { outputBytes: 1, notificationDelayMs: 0 })
-  await manager.setBundleEnabled('extra', false)
-  expect(messages[0]?.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('')).toContain('"omitted":1')
-})
-
 it('applies a manager change through the active HMR service', async () => {
   const { manager } = await fixture()
   const id = (await manager.listPlugins()).find(row => row.patchId === 'managed')!.entryId
@@ -582,3 +550,11 @@ it('applies watched configuration while pnpm installation is still running', asy
   expect(readProfileManifest('test', dir).dsh?.profile?.bundles).toEqual(['core', 'extra', 'new-bundle'])
   expect(ctx.get('managedProbe')).toBeUndefined()
 })
+
+it.each(['[', 'allowBuilds: false\n'])('preserves pnpm diagnostics when pending approvals cannot be read: %s', async (policy) => {
+  const { manager, dir } = await fixture()
+  writeFileSync(join(dir, 'pnpm-workspace.yaml'), policy)
+  const run = vi.spyOn(operations, 'runProfilePnpm').mockResolvedValue({ exitCode: 1, output: 'original pnpm failure', truncated: false, logPath: '/log' })
+  onTestFinished(() => { run.mockRestore() })
+  expect(await manager.installBundle('addon')).toMatchObject({ application: 'failed', error: { diagnostic: 'original pnpm failure' } })
+})

+ 2 - 2
packages/client/ui-settings-plugin-inventory/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-settings-plugin-inventory/README.md
-README.md: ed0fe35984958486528322520c2b6c50b2985051
-README.zh.md: 5c5368a26fff78b6ec6c3c0923587e7577a8431d
+README.md: dd8c778341feb3969186f1d916362516fa3dccf4
+README.zh.md: e16760937ff003c7cfff95129846a8249849f2c6

+ 1 - 1
packages/client/ui-settings-plugin-inventory/README.md

@@ -27,7 +27,7 @@ The **Plugin list** tab lets Web users inspect plugins without changing their co
 
 Open the Plugins section in Settings and select the **Plugin list** tab to inspect the Host's plugin inventory. The tab reads no Remote during plugin activation — selecting it for the first time mounts the component and lazily calls `ctx.remote.pluginInventory.list()` through `api-remotes`.
 
-A failed installation with pending pnpm build permissions offers **Allow these scripts and retry**. The action displays the exact package names and persistent permission scope, then retries the original package spec and activation choice. Closing the page grants no permission.
+A failed installation with pending pnpm build permissions offers **Allow these scripts and retry**. The action displays the exact package names and persistent permission scope, then retries the original package spec and activation choice. One click approves the entire displayed group, which can include pending packages from earlier attempts. Closing the page grants no permission.
 
 ### Reading a card
 

+ 1 - 1
packages/client/ui-settings-plugin-inventory/README.zh.md

@@ -27,7 +27,7 @@ kind: "package-reference"
 
 打开设置中的「插件」分区并选择**插件列表**标签页,即可查看宿主的插件清单。插件激活期间不会读取 Remote——首次选择该标签页时才挂载组件,并通过 `api-remotes` 懒调用 `ctx.remote.pluginInventory.list()`。
 
-安装失败且存在待审批的 pnpm 构建权限时,页面提供“允许这些脚本并重试”。该操作展示准确包名和持久授权范围,再使用原来的包规格及启用选项重试。关闭页面不会授予权限。
+安装失败且存在待审批的 pnpm 构建权限时,页面提供“允许这些脚本并重试”。该操作展示准确包名和持久授权范围,再使用原来的包规格及启用选项重试。一次点击批准整个列表,其中可能包括此前安装留下的待审批包。关闭页面不会授予权限。
 
 ### 阅读卡片
 

+ 2 - 0
packages/client/ui-settings-plugin-inventory/src/client/locales.ts

@@ -10,6 +10,7 @@ export const zh = {
   'not-removable': '该依赖不属于当前 profile,或是管理所需的组件。',
   'bundle-in-use': '其他配置仍在使用该组合包,请先停用相应插件。',
   'stop-profile': '请停止当前 profile,再通过 dsh plugin 删除仍在使用的依赖。',
+  'stale-approval': '构建审批已变化,请重新安装以刷新待审批的包。',
   'operation-error': '请查看诊断信息。',
   installFailed: '安装失败', enableFailed: '启停失败', removeFailed: '删除失败',
   cleanup: '清理新增依赖', remainingDependencies: '剩余新增依赖', existingFailures: '其他诊断',
@@ -74,6 +75,7 @@ export const en = {
   'not-removable': 'This dependency is not owned by the current profile or is required for management.',
   'bundle-in-use': 'Other configuration still uses this bundle. Disable those plugins first.',
   'stop-profile': 'Stop this profile, then remove the dependency still in use with dsh plugin.',
+  'stale-approval': 'Build approvals changed. Retry installation to refresh the pending packages.',
   'operation-error': 'See the diagnostics for details.',
   installFailed: 'Installation failed', enableFailed: 'Enablement failed', removeFailed: 'Removal failed',
   cleanup: 'New dependency cleanup', remainingDependencies: 'Remaining new dependencies', existingFailures: 'Other diagnostics',

+ 9 - 0
packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx

@@ -570,3 +570,12 @@ it('shows current-page sync errors and retries without re-reading Host inventory
   act(() => { sync.set({ syncing: false, failures: [] }) })
   expect(screen.queryByRole('alert')).toBeNull()
 })
+
+it.each([en, zh])('renders stale build approval in the selected locale', (dictionary) => {
+  const localized = ((key: PluginInventoryLocaleKey) => dictionary[key]) as PluginInventorySettingsTabProps['t']
+  render(<BundleManager manager={managementFixture()} t={localized} state={{
+    plugins: [], bundles: [], busy: false, refresh: 0, error: undefined, run: async () => {},
+    result: { stage: 'install', target: 'addon', changed: false, application: 'failed', error: { code: 'stale-approval' } },
+  }} />)
+  expect(screen.getByRole('alert').textContent).toContain(dictionary['stale-approval'])
+})

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

@@ -4871,7 +4871,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'ManagementError',
-    declaration: 'export interface ManagementError {\n    code: ReadOnlyReason | \'unknown-plugin\' | \'invalid-spec\' | \'ambiguous-install\' | \'not-bundle\' | \'not-removable\' | \'stop-profile\' | \'bundle-in-use\' | \'operation-error\';\n    diagnostic?: string;\n}',
+    declaration: 'export interface ManagementError {\n    code: ReadOnlyReason | \'unknown-plugin\' | \'invalid-spec\' | \'ambiguous-install\' | \'not-bundle\' | \'not-removable\' | \'stop-profile\' | \'bundle-in-use\' | \'stale-approval\' | \'operation-error\';\n    diagnostic?: string;\n}',
   },
   {
     name: 'ManualCompactAgentContext',

+ 0 - 9
pnpm-lock.yaml

@@ -1521,21 +1521,12 @@ importers:
       '@deepseek-ai/cordis-plugin-timer':
         specifier: workspace:^
         version: link:../../../vendor/timer
-      '@deepseek-ai/dsh-agent':
-        specifier: workspace:^
-        version: link:../../core/agent
       '@deepseek-ai/dsh-hmr':
         specifier: workspace:^
         version: link:../hmr
       '@deepseek-ai/dsh-host-plugin-inventory':
         specifier: workspace:^
         version: link:../../host/plugin-inventory
-      '@deepseek-ai/dsh-llm':
-        specifier: workspace:^
-        version: link:../../llm/llm
-      '@deepseek-ai/dsh-session':
-        specifier: workspace:^
-        version: link:../../core/session
       '@deepseek-ai/dsh-subprocess':
         specifier: workspace:^
         version: link:../../subprocess/subprocess

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
snapshots/session/plugin-manager/session.v3.jsonl


+ 1 - 1
snapshots/session/plugin-manager/tool-schemas.expected.json

@@ -288,7 +288,7 @@
           },
           "approvedBuilds": {
             "type": "array",
-            "description": "For install_bundle: package names from pendingBuilds whose install scripts the user explicitly approved. Permission persists for this profile.",
+            "description": "For install_bundle: pass names from pendingBuilds only after the user explicitly approves running their install scripts in the conversation. This grants persistent permission for this profile.",
             "items": {
               "type": "string"
             }

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott