Ver Fonte

feat(plugins): ship optional bundles switched off and guide the install dialog

The dsh installation lists under `dsh.optionalBundles` the bundles it
ships for a person to switch on. The plugin manager reports them as
`optional`, and the Web plugin page shows them first, in a built-in
group with an official tag, off until selected and never removable.
Agent Teams ships this way, so a blank profile offers both of its
layers. Default-product isolation, the workspace constraints, and the
packed-install check accept that one dependency edge and nothing else
experimental.

A bundle's title and one-liner come from `dsh.title` and the new
`dsh.description`, else from the package name and `description`. A
dependency without a bundle patch is no longer listed unless the
profile selects it. The install dialog asks for a package name or
address; a guide under the field shows the three accepted forms with
an example each and ends in a security warning, and the copy no longer
speaks of a plugin ID.
Yichen Jiang há 6 dias atrás
pai
commit
bc0ecae21f
52 ficheiros alterados com 703 adições e 154 exclusões
  1. 2 2
      .agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.i18n.yaml
  2. 1 1
      .agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md
  3. 1 1
      .agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.zh.md
  4. 6 0
      .agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.i18n.yaml
  5. 27 0
      .agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md
  6. 27 0
      .agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md
  7. 6 1
      apps/cli/package.json
  8. 7 5
      apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md
  9. 11 0
      apps/web/tests/expected/plugin-manager/live-enabled.expected.md
  10. 11 0
      apps/web/tests/expected/plugin-manager/manager.expected.md
  11. 6 4
      apps/web/tests/plugin-manager.e2e.ts
  12. 2 2
      docs/event-producer-consumer.i18n.yaml
  13. 3 3
      docs/event-producer-consumer.md
  14. 3 3
      docs/event-producer-consumer.zh.md
  15. 2 2
      docs/subsystems/boot.i18n.yaml
  16. 4 2
      docs/subsystems/boot.md
  17. 4 2
      docs/subsystems/boot.zh.md
  18. 2 2
      docs/user/develop/basic/publish.i18n.yaml
  19. 7 1
      docs/user/develop/basic/publish.md
  20. 7 1
      docs/user/develop/basic/publish.zh.md
  21. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  22. 1 1
      packages/boot/plugin-manager/README.md
  23. 1 1
      packages/boot/plugin-manager/README.zh.md
  24. 21 10
      packages/boot/plugin-manager/src/index.ts
  25. 8 1
      packages/boot/plugin-manager/src/types.ts
  26. 28 7
      packages/boot/plugin-manager/tests/manager.spec.ts
  27. 2 2
      packages/client/ui-plugin-manager/README.i18n.yaml
  28. 4 4
      packages/client/ui-plugin-manager/README.md
  29. 4 4
      packages/client/ui-plugin-manager/README.zh.md
  30. 119 3
      packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
  31. 121 43
      packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
  32. 46 12
      packages/client/ui-plugin-manager/src/client/locales.ts
  33. 3 0
      packages/client/ui-plugin-manager/src/client/manager-store.ts
  34. 42 6
      packages/client/ui-plugin-manager/tests/components.client.spec.tsx
  35. 5 4
      packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
  36. 2 2
      packages/experimental/agent-team-profile/README.i18n.yaml
  37. 2 2
      packages/experimental/agent-team-profile/README.md
  38. 2 2
      packages/experimental/agent-team-profile/README.zh.md
  39. 2 0
      packages/experimental/agent-team-profile/package.json
  40. 2 2
      packages/experimental/agent-team-web-profile/README.i18n.yaml
  41. 2 2
      packages/experimental/agent-team-web-profile/README.md
  42. 2 2
      packages/experimental/agent-team-web-profile/README.zh.md
  43. 2 0
      packages/experimental/agent-team-web-profile/package.json
  44. 3 3
      packages/extensions/tool-cordis/src/api-catalog.ts
  45. 2 0
      packages/util/package-manifest/src/types.ts
  46. 6 3
      pnpm-lock.yaml
  47. 19 0
      scripts/check-workspace-constraints.spec.ts
  48. 9 1
      scripts/check-workspace-constraints.ts
  49. 21 0
      scripts/release/installed-product-isolation.spec.ts
  50. 17 0
      scripts/release/installed-product-isolation.ts
  51. 36 0
      scripts/verify-default-product-isolation.spec.ts
  52. 28 3
      scripts/verify-default-product-isolation.ts

+ 2 - 2
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.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/process/2026-09-12-default-product-experimental-isolation.md
-2026-09-12-default-product-experimental-isolation.md: d2312d6061f372e910bfd625512230ccd48a586a
-2026-09-12-default-product-experimental-isolation.zh.md: 6ea86f1f3e859719ffd8287e66ebe63df77d64c2
+2026-09-12-default-product-experimental-isolation.md: 5c0c0c1c3d194f792591c6eb54a56cea65519bbe
+2026-09-12-default-product-experimental-isolation.zh.md: b8f3d64cb0db41f645ea8ca70bfc4c2a098c904f

+ 1 - 1
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.md

@@ -10,7 +10,7 @@ Public npm availability does not make an experimental package part of the defaul
 
 ## Decision
 
-[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) runs in static CI and package hygiene. It follows runtime dependencies, optional dependencies, and peers from every app and the Python runtime, resolves workspace and npm aliases, and identifies experimental packages by their npm prefix or repository directory. Publication denylist membership has no effect on this classification.
+[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) runs in static CI and package hygiene. It follows runtime dependencies, optional dependencies, and peers from every app and the Python runtime, resolves workspace and npm aliases, and identifies experimental packages by their npm prefix or repository directory. Publication denylist membership has no effect on this classification. The bundles the installation lists under `dsh.optionalBundles` are the one declared exception ([shipped optional bundles](2026-09-15-shipped-optional-bundles.md)).
 
 The source check also reads runtime imports in the selected packages, installation-owned profile bundle lists, bundle patches, shipped agent presets, and declared configuration trees. It loads the default Web layers with the production patch parser and composes them with the same patch engine used at boot. The effective rows and patched Include trees are checked, so an id-only patch cannot hide a replacement group's plugins. Disabled plugin rows remain checked; ordinary plugin configuration data is not interpreted as another Loader entry list. Missing default roots fail the check.
 

+ 1 - 1
.agents/notes/implemented/process/2026-09-12-default-product-experimental-isolation.zh.md

@@ -10,7 +10,7 @@ Status: implemented
 
 ## Decision
 
-[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) 在静态 CI 和包 hygiene 中运行。它从所有应用与 Python runtime 出发,遍历运行时依赖、可选依赖和 peer,解析 workspace 与 npm 别名,并按 npm 前缀或仓库目录识别实验包。发布 denylist 的成员关系不影响此分类。
+[`verify-default-product-isolation`](../../../../scripts/verify-default-product-isolation.ts) 在静态 CI 和包 hygiene 中运行。它从所有应用与 Python runtime 出发,遍历运行时依赖、可选依赖和 peer,解析 workspace 与 npm 别名,并按 npm 前缀或仓库目录识别实验包。发布 denylist 的成员关系不影响此分类。安装在 `dsh.optionalBundles` 下列出的组合包是唯一声明的例外([随安装提供的可选组合包](2026-09-15-shipped-optional-bundles.zh.md))。
 
 源码检查还读取所选包的运行时导入、安装自带的 profile bundle 列表、bundle patch、随产品提供的 Agent preset,以及声明的配置树。它使用生产 patch 解析器加载默认 Web 各层,并使用启动时的同一个 patch 引擎完成组合。检查对象包括最终 entry 和应用 patch 后的 Include 树,因此仅按 id 覆盖 group 的 patch 也无法隐藏替换后的插件。禁用的插件行仍纳入检查;普通插件配置数据不会被解释为另一个 Loader entry 列表。默认入口缺失会使检查失败。
 

+ 6 - 0
.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.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/process/2026-09-15-shipped-optional-bundles.md
+2026-09-15-shipped-optional-bundles.md: cbacff975118f2b8e6187cb5c80d1ec702d64fde
+2026-09-15-shipped-optional-bundles.zh.md: d0d2dc24f27697883d96934f473ff35db2e2ace7

+ 27 - 0
.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md

@@ -0,0 +1,27 @@
+# Agent Note: Ship optional bundles with the installation
+
+Status: implemented
+
+English | [中文](2026-09-15-shipped-optional-bundles.zh.md)
+
+## Problem
+
+The Web plugin page manages only the bundles a person installed into the profile. An official experimental layer such as Agent Teams had to be found on npm and installed by name before it could be switched on, and [default-product isolation](2026-09-12-default-product-experimental-isolation.md) kept every experimental package out of the installation's runtime dependencies, so nothing shipped with dsh could offer it.
+
+## Decision
+
+The installation's manifest, `apps/cli/package.json`, lists under `dsh.optionalBundles` the bundles it ships for a person to switch on. Each must be a runtime dependency that declares `dsh.bundle.patch`, and no shipped profile template selects it. The plugin manager's `listBundles` reports such a bundle as `optional`: switched off until selected, never removable, resolved from the installation like any installation-supplied bundle. The Web plugin page lists optional bundles in a built-in group with an official tag beside the profile's own installed bundles.
+
+Default-product isolation keeps its rules with one declared exception: an optional bundle's dependency graph is outside the default product. The static gate skips the `dependencies` edge from `@deepseek-ai/dsh` to a listed bundle and still rejects a runtime import, a shipped composition, a preset, or a default template that names it, an experimental dependency the list does not name, and a listed name that is not a runtime dependency or not a bundle. The workspace-constraints check accepts the same `dependencies` edges and no other runtime section, and the packed-install release check skips them from the installed entry package while requiring each listed bundle to be installed.
+
+Agent Teams ships this way first, as `@deepseek-ai/dsh-experimental-agent-team-profile` and `@deepseek-ai/dsh-experimental-agent-team-web-profile`.
+
+## Alternatives considered
+
+**A catalog of installable official bundles.** The page would offer names to install from the registry on demand. That keeps the installation unchanged but needs network access at the moment of switching on and a version pin per release.
+
+**A flag on the bundle package.** A `dsh.bundle.optional` declaration would let any published bundle claim a place in the installation; the installation's own list keeps the choice with the product.
+
+## Consequences
+
+Optional bundles are downloaded with the product and stay inactive until selected; the runtime isolation smokes still observe no experimental module in a default composition. Switching on an optional Web layer loads its client plugin through the live client module graph. A bundle that needs a companion layer, such as the Agent Teams Web layer over its Host layer, says so in its description; the manager does not select companions automatically.

+ 27 - 0
.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md

@@ -0,0 +1,27 @@
+# Agent Note: Ship optional bundles with the installation
+
+Status: implemented
+
+[English](2026-09-15-shipped-optional-bundles.md) | 中文
+
+## 问题
+
+Web 插件页只管理用户装进 profile 的组合包。像 Agent Teams 这样的官方实验层,用户得先去 npm 找到包名并按名安装才能开启;而[默认产品隔离](2026-09-12-default-product-experimental-isolation.zh.md)又把所有实验包挡在安装的运行时依赖之外,所以随 dsh 一起交付的东西没有办法把它提供出来。
+
+## 决策
+
+安装的 manifest,即 `apps/cli/package.json`,在 `dsh.optionalBundles` 下列出它随附、供用户开启的组合包。每一个都必须是声明了 `dsh.bundle.patch` 的运行时依赖,且不被任何随附 profile 模板选中。插件管理器的 `listBundles` 把这类组合包报告为 `optional`:选中前保持关闭、永不可卸载、像其他安装提供的组合包一样从安装目录解析。Web 插件页把可选组合包放在带官方标签的内置分组里,与 profile 自己安装的组合包并列。
+
+默认产品隔离的规则保持不变,只声明一个例外:可选组合包的依赖图在默认产品之外。静态门禁跳过从 `@deepseek-ai/dsh` 到列表中组合包的 `dependencies` 边,仍然拒绝运行时 import、随附组合、preset 或默认模板对它的引用,拒绝列表没有点名的实验依赖,也拒绝不是运行时依赖或不是组合包的列表项。workspace 约束检查接受同样的 `dependencies` 边而不接受其他运行时依赖段;发布时的 packed-install 检查对已安装入口包跳过这些边,并要求列表中的每个组合包都已安装。
+
+Agent Teams 首先以这种方式交付,即 `@deepseek-ai/dsh-experimental-agent-team-profile` 与 `@deepseek-ai/dsh-experimental-agent-team-web-profile`。
+
+## 考虑过的替代方案
+
+**可安装官方组合包目录。** 页面按需提供从注册表安装的包名。安装本身不变,但开启那一刻需要网络,并且每次发布都要钉一个版本。
+
+**组合包自身的标记。** `dsh.bundle.optional` 声明会让任何已发布的组合包都能自称随安装提供;由安装自己的列表来决定,选择权留在产品手里。
+
+## 影响
+
+可选组合包随产品一起下载,选中前保持不活动;运行时隔离 smoke 在默认组合中仍观察不到任何实验模块。开启一个可选 Web 层会通过实时客户端模块图加载它的客户端插件。需要配套层的组合包,例如 Agent Teams Web 层依赖其 Host 层,会在描述里说明;管理器不会自动选中配套层。

+ 6 - 1
apps/cli/package.json

@@ -25,6 +25,10 @@
         "path": "../../packages/preset/agent-presets/presets",
         "scanRoster": true
       }
+    ],
+    "optionalBundles": [
+      "@deepseek-ai/dsh-experimental-agent-team-profile",
+      "@deepseek-ai/dsh-experimental-agent-team-web-profile"
     ]
   },
   "license": "MIT",
@@ -47,6 +51,8 @@
     "@deepseek-ai/dsh-compaction-basic": "workspace:^",
     "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:^",
     "@deepseek-ai/dsh-cordis-client-runner": "workspace:^",
+    "@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:^",
+    "@deepseek-ai/dsh-experimental-agent-team-web-profile": "workspace:^",
     "@deepseek-ai/dsh-fs-local": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-goal-round-driver": "workspace:^",
@@ -116,7 +122,6 @@
     "@deepseek-ai/dsh-credentials-local": "workspace:^",
     "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
     "@deepseek-ai/dsh-experimental-agent-team": "workspace:^",
-    "@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:^",
     "@deepseek-ai/dsh-experimental-ptc-runtime-python": "workspace:^",
     "@deepseek-ai/dsh-experimental-tool-agent-team": "workspace:^",
     "@deepseek-ai/dsh-fs-observation-policy": "workspace:^",

+ 7 - 5
apps/web/tests/expected/plugin-install-cancel/cancelled.expected.md

@@ -2,10 +2,12 @@
   - heading "添加插件" [level=2]
   - button "关闭":
     - img
-  - paragraph: 粘贴插件作者提供的插件 ID、Git 仓库地址(如 GitHub 链接)或本地绝对路径。
-  - text: 插件 ID 或地址
-  - textbox "插件 ID 或地址":
-    - /placeholder: dsh-xxx、github:作者/仓库 或 /绝对路径
+  - paragraph: 输入插件的包名、GitHub 仓库地址或本地目录路径。
+  - text: 包名或地址
+  - textbox "包名或地址":
+    - /placeholder: 例如 dsh-better-sidebar
     - text: slow-package
-  - paragraph: 插件 ID 是安装时使用的包名,不是插件显示的名称,常见形式是 dsh-xxx 或 @作者/插件名;加 @版本 可指定版本。
+  - button "不知道该填什么?":
+    - img
+    - text: 不知道该填什么?
   - button "安装"

+ 11 - 0
apps/web/tests/expected/plugin-manager/live-enabled.expected.md

@@ -4,6 +4,17 @@
 - button "添加插件":
   - img
   - text: 添加插件
+- heading "内置" [level=3]
+- text: "2"
+- list:
+  - listitem:
+    - button "查看 Agent Teams": Agent Teams
+    - text: 官方 实验功能:把子代理委派换成 Agent Teams 的团队协作工具,这是宿主层,需与 Agent Teams Web 面板一起开启。
+    - switch "启用 Agent Teams"
+  - listitem:
+    - button "查看 Agent Teams Web 面板": Agent Teams Web 面板
+    - text: 官方 实验功能:在浏览器里显示 Agent Teams 的团队名单、任务板和成员导航,需先开启 Agent Teams。
+    - switch "启用 Agent Teams Web 面板"
 - heading "已安装" [level=3]
 - text: "1"
 - list:

+ 11 - 0
apps/web/tests/expected/plugin-manager/manager.expected.md

@@ -4,6 +4,17 @@
 - button "添加插件":
   - img
   - text: 添加插件
+- heading "内置" [level=3]
+- text: "2"
+- list:
+  - listitem:
+    - button "查看 Agent Teams": Agent Teams
+    - text: 官方 实验功能:把子代理委派换成 Agent Teams 的团队协作工具,这是宿主层,需与 Agent Teams Web 面板一起开启。
+    - switch "启用 Agent Teams"
+  - listitem:
+    - button "查看 Agent Teams Web 面板": Agent Teams Web 面板
+    - text: 官方 实验功能:在浏览器里显示 Agent Teams 的团队名单、任务板和成员导航,需先开启 Agent Teams。
+    - switch "启用 Agent Teams Web 面板"
 - heading "已安装" [level=3]
 - text: "1"
 - list:

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

@@ -71,8 +71,10 @@ describe('web e2e: plugin manager', () => {
     await panel.getByText('示例组合包', { exact: true }).waitFor({ timeout: 20_000 })
     const toggle = panel.getByRole('switch', { name: '启用 示例组合包' })
     expect(await toggle.getAttribute('aria-checked')).toBe('false')
-    // The shipped bundles are not the person's to manage here.
-    expect(await panel.locator('[data-plugin-package]').count()).toBe(1)
+    // The profile's own group holds its one bundle; the installation's optional bundles form the built-in
+    // group, and its other bundles stay off the page.
+    expect(await panel.locator('[data-plugin-group="bundles"] [data-plugin-package]').count()).toBe(1)
+    expect(await panel.locator('[data-plugin-group="builtin"] [data-plugin-package]').count()).toBe(2)
     // A bundle that is off still shows the rows its patch declares, without switches.
     await panel.getByRole('button', { name: '查看 示例组合包' }).click()
     await panel.locator('[data-plugin-row]', { hasText: 'fixture-row' }).waitFor({ timeout: 10_000 })
@@ -92,7 +94,7 @@ describe('web e2e: plugin manager', () => {
     await panel.getByRole('button', { name: '添加插件', exact: true }).click()
     const dialog = page.getByRole('dialog', { name: '添加插件' })
     await dialog.waitFor({ timeout: 10_000 })
-    const field = dialog.getByRole('textbox', { name: '插件 ID 或地址' })
+    const field = dialog.getByRole('textbox', { name: '包名或地址' })
     const install = dialog.getByRole('button', { name: '安装', exact: true })
     expect(await install.isDisabled()).toBe(true)
     // A name the list already shows is refused without asking the Host.
@@ -108,7 +110,7 @@ describe('web e2e: plugin manager', () => {
     // A name the registry would refuse never reaches it.
     await field.fill('Not A Package')
     await install.click()
-    await expect.poll(() => dialog.getByRole('alert').textContent(), { timeout: 10_000 }).toContain('无法识别这个插件 ID 或地址')
+    await expect.poll(() => dialog.getByRole('alert').textContent(), { timeout: 10_000 }).toContain('无法识别这个包名或地址')
     await dialog.getByRole('button', { name: '关闭' }).click()
     await expect.poll(() => page.getByRole('dialog', { name: '添加插件' }).count(), { timeout: 5_000 }).toBe(0)
     expect(tripwire.pageErrors).toEqual([])

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: 96f739d0a5c5c2c948e05f49bc5998514f7fa0c9
-event-producer-consumer.zh.md: 1d1022b7ba4ef26e7e67d78071562dc8f348be32
+event-producer-consumer.md: 40ba4a648b58358829e049b4cbd28e79359229a4
+event-producer-consumer.zh.md: aa12b07e575b68b338ca08a811c6a6565935d31f

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

@@ -50,9 +50,9 @@ 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` |
-| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:178`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:184`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:190`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:185`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:191`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:197`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `profile/reconciled` | `emit` | [`packages/boot/app-boot/src/index.ts:255`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`plugin-manager`](../packages/boot/plugin-manager) |
 | `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) |

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

@@ -52,9 +52,9 @@
 | `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` |
-| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:178`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:184`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:190`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:185`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:191`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:197`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `profile/reconciled` | `emit` | [`packages/boot/app-boot/src/index.ts:255`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`plugin-manager`](../packages/boot/plugin-manager) |
 | `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) |

+ 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: 054e2f0586d5c4627bcef17639e02b48b797100d
-boot.zh.md: 4d5dda814da4086e862d98efcad2ab5f99479b81
+boot.md: 079fd9d705d43a85fb84cb3bd0983cd49169a4d6
+boot.zh.md: bc2f847be044670c726989c107664cf88206e9fa

+ 4 - 2
docs/subsystems/boot.md

@@ -69,8 +69,10 @@ Manage profile files and apply their declared reload lifecycle.
  */
 @Remote async listPlugins(): Promise<PluginInfo[]>
 
-/** Read installed bundles and bundles supplied by this dsh installation.
- * @returns Package versions, titles, rows, activation selections and removal availability.
+/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
+ * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
+ * @returns Package versions, titles, one-liners, rows, activation selections, whether the installation offers the
+ * bundle, and removal availability.
  */
 @Remote listBundles(): Promise<BundleInfo[]>
 

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

@@ -69,8 +69,10 @@ Manage profile files and apply their declared reload lifecycle.
  */
 @Remote async listPlugins(): Promise<PluginInfo[]>
 
-/** Read installed bundles and bundles supplied by this dsh installation.
- * @returns Package versions, titles, rows, activation selections and removal availability.
+/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
+ * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
+ * @returns Package versions, titles, one-liners, rows, activation selections, whether the installation offers the
+ * bundle, and removal availability.
  */
 @Remote listBundles(): Promise<BundleInfo[]>
 

+ 2 - 2
docs/user/develop/basic/publish.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/user/develop/basic/publish.md
-publish.md: 17b0cb80f9cf385773ae9e1f4d7061ec1df9c1a2
-publish.zh.md: 3cc65af58d34028a6bff03455451153e2c7f439b
+publish.md: da3a80a809a2e01b5f5914178a8d34e8c4453fc1
+publish.zh.md: 3baa952d42c3a223b8ce1aeea326799e10cbe1f5

+ 7 - 1
docs/user/develop/basic/publish.md

@@ -39,10 +39,16 @@ Create `hello-plugin/package.json`:
   "type": "module",
   "main": "index.js",
   "files": ["index.js", "cordis.patch.yml"],
-  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
+  "dsh": {
+    "title": "Hello plugin",
+    "description": "Logs a greeting when it loads.",
+    "bundle": { "patch": "./cordis.patch.yml" }
+  }
 }
 ```
 
+`dsh.title` and `dsh.description` are what users see where bundles are listed, such as the Web sidebar's Plugins page; without them the page shows the package name stripped of its scope and `dsh-` prefix, and the package `description`.
+
 Create `hello-plugin/index.js` with the plugin entry point:
 
 ```js

+ 7 - 1
docs/user/develop/basic/publish.zh.md

@@ -39,10 +39,16 @@ hello-plugin/
   "type": "module",
   "main": "index.js",
   "files": ["index.js", "cordis.patch.yml"],
-  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
+  "dsh": {
+    "title": "Hello plugin",
+    "description": "Logs a greeting when it loads.",
+    "bundle": { "patch": "./cordis.patch.yml" }
+  }
 }
 ```
 
+`dsh.title` 与 `dsh.description` 是用户在组合包列表(例如 Web 侧栏的插件页)里看到的名字和简介;没有它们时,页面显示去掉 scope 和 `dsh-` 前缀的包名,以及包的 `description`。
+
 创建 `hello-plugin/index.js`,写入插件入口:
 
 ```js

+ 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: 38d0737f1e3629ff1f33c90845ad1993eaabbeb0
-README.zh.md: e533542586aba80c80bbfa526fd2a3abdf9811bf
+README.md: b9c9942c947557ccc4142504affb8a8f68508013
+README.zh.md: 7d8f78af5c23a6dd2b2e0e3316296dc1913d29c4

+ 1 - 1
packages/boot/plugin-manager/README.md

@@ -39,7 +39,7 @@ A plugin toggle updates only `disabled` in the last matching override in the pro
 
 `inspect(spec)` reads what a spec names before anything installs: a registry name is asked of the registry through `pnpm view`, run in the profile directory so the same registry and proxy settings apply as to the install; an absolute path has its `package.json` read; a git address or tarball answers only its form. The answer carries the name, version, description, `dsh.title`, and whether the package declares a bundle, or a `problem`: `invalid-spec`, `already-installed`, `not-found`, `not-a-package`, `not-a-bundle`, `network`, or `unknown`. A caller's `signal` or `inspectTimeoutMs` ends the lookup.
 
-`installBundle` accepts a caller-generated `requestId`, under which `plugin-manager/install-log` streams each pnpm run's output and `plugin-manager/install-state` announces `installing`, `cancelling`, and `applying`. `cancelInstall(requestId)` stops the run and answers `cancelled` only after pnpm exited and the files are back, `too-late` once the bundle is being applied, and `not-running` for any other id; the install call then reports `application: 'cancelled'`. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; `packageResult.kind` classifies a failed run from its exit and output, and `bundle` names the package a finished run added. `listBundles` carries each bundle's title, one-liner, the rows its patch declares with their live entries, and the built-in rows it overrides. Every completed operation, and every patch generation applied outside the manager, emits `plugin-manager/changed`.
+`installBundle` accepts a caller-generated `requestId`, under which `plugin-manager/install-log` streams each pnpm run's output and `plugin-manager/install-state` announces `installing`, `cancelling`, and `applying`. `cancelInstall(requestId)` stops the run and answers `cancelled` only after pnpm exited and the files are back, `too-late` once the bundle is being applied, and `not-running` for any other id; the install call then reports `application: 'cancelled'`. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; `packageResult.kind` classifies a failed run from its exit and output, and `bundle` names the package a finished run added. `listBundles` carries each bundle's title and one-liner (`dsh.title` and `dsh.description`, else the package `description`), the rows its patch declares with their live entries, and the built-in rows it overrides; it lists the profile's own bundles, the bundles the installation supplies, and a selected name without a bundle patch as a `not-bundle` problem, while an unselected plain dependency is left out. A bundle the installation's manifest names under `dsh.optionalBundles` is `optional`: shipped switched off for the person to turn on, never removable, and selected by no shipped template ([rationale](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)). Every completed operation, and every patch generation applied outside the manager, emits `plugin-manager/changed`.
 
 ### Configuration
 

+ 1 - 1
packages/boot/plugin-manager/README.zh.md

@@ -39,7 +39,7 @@ kind: "package-reference"
 
 `inspect(spec)` 在任何东西安装之前读出 spec 指向什么:注册表包名通过 `pnpm view` 询问注册表,在 profile 目录中运行,因而与安装使用同样的注册表与代理设置;绝对路径读取其 `package.json`;git 地址或 tarball 只答复自己的形式。答复携带名称、版本、描述、`dsh.title` 以及该包是否声明组合包,否则给出 `problem`:`invalid-spec`、`already-installed`、`not-found`、`not-a-package`、`not-a-bundle`、`network` 或 `unknown`。调用方的 `signal` 或 `inspectTimeoutMs` 会结束查询。
 
-`installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`。`cancelInstall(requestId)` 停止运行,只在 pnpm 退出且文件恢复后答复 `cancelled`,组合包已在应用时答复 `too-late`,其他 id 答复 `not-running`;安装调用随后报告 `application: 'cancelled'`。失败、被取消或装入了没有组合包 patch 的包的运行,会把 `package.json` 与 `pnpm-lock.yaml` 恢复原样;`packageResult.kind` 按退出方式与输出对失败运行分类,`bundle` 给出完成的运行新增的包。`listBundles` 携带每个组合包的标题、一句话简介、其 patch 声明的行及其存活条目,以及它覆盖的内置行。每个完成的操作,以及在管理器之外应用的每一代 patch,都会发出 `plugin-manager/changed`。
+`installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`。`cancelInstall(requestId)` 停止运行,只在 pnpm 退出且文件恢复后答复 `cancelled`,组合包已在应用时答复 `too-late`,其他 id 答复 `not-running`;安装调用随后报告 `application: 'cancelled'`。失败、被取消或装入了没有组合包 patch 的包的运行,会把 `package.json` 与 `pnpm-lock.yaml` 恢复原样;`packageResult.kind` 按退出方式与输出对失败运行分类,`bundle` 给出完成的运行新增的包。`listBundles` 携带每个组合包的标题与一句话简介(`dsh.title` 与 `dsh.description`,没有则用包的 `description`)、其 patch 声明的行及其存活条目,以及它覆盖的内置行;它列出 profile 自己的组合包、安装提供的组合包,以及被选中却没有组合包 patch 的名字(作为 `not-bundle` 问题),未选中的普通依赖不列出。安装的 manifest 在 `dsh.optionalBundles` 下点名的组合包是 `optional`:随安装提供、默认关闭、由用户开启,永不可卸载,也不被任何随附模板选中([理由](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md))。每个完成的操作,以及在管理器之外应用的每一代 patch,都会发出 `plugin-manager/changed`。
 
 ### 配置
 

+ 21 - 10
packages/boot/plugin-manager/src/index.ts

@@ -101,15 +101,21 @@ function stringField(manifest: object, field: string): string | undefined {
   return typeof value === 'string' ? value : undefined
 }
 
+/** The fields of the dsh installation's own manifest the manager reads. */
+interface InstallationManifest {
+  dependencies?: Record<string, string>
+  dsh?: { optionalBundles?: string[] }
+}
+
 /** What a package manifest says about the package: identity, one-liner, and whether it is a bundle. */
 function inspectionOf(kind: 'registry' | 'path', manifest: object): Extract<PluginSpecInspection, { status: 'accepted' }> {
   const dsh = (manifest as { dsh?: unknown }).dsh
-  const declared = typeof dsh === 'object' && dsh !== null ? dsh as { title?: unknown; bundle?: unknown } : undefined
+  const declared = typeof dsh === 'object' && dsh !== null ? dsh as { title?: unknown; description?: unknown; bundle?: unknown } : undefined
   const title = declared === undefined ? undefined : stringField(declared, 'title')
   const bundle = declared !== undefined && typeof declared.bundle === 'object' && declared.bundle !== null
   const name = stringField(manifest, 'name')
   const version = stringField(manifest, 'version')
-  const description = stringField(manifest, 'description')
+  const description = (declared === undefined ? undefined : stringField(declared, 'description')) ?? stringField(manifest, 'description')
   return {
     status: 'accepted', kind, bundle,
     ...name === undefined ? {} : { name },
@@ -205,38 +211,43 @@ export class PluginManager extends TypertRemoteService {
     })
   }
 
-  /** Read installed bundles and bundles supplied by this dsh installation.
-   * @returns Package versions, titles, rows, activation selections and removal availability.
+  /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
+   * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
+   * @returns Package versions, titles, one-liners, rows, activation selections, whether the installation offers the
+   * bundle, and removal availability.
    */
   @Remote
   listBundles(): Promise<BundleInfo[]> {
     const manifest = readProfileManifest('dsh', this.profile.dir)
     const selected = manifest.dsh?.profile?.bundles ?? []
     const dependencies = Object.keys(manifest.dependencies ?? {})
-    const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as { dependencies?: Record<string, string> }
+    const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest
+    const offered = installation.dsh?.optionalBundles ?? []
     const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])]
     const bundles: BundleInfo[] = []
     for (const name of names) {
       const installed = dependencies.includes(name)
+      const optional = offered.includes(name)
       const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name)
       const enabled = selected.includes(name)
       try {
         const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor)
         if (info === undefined) {
-          if (enabled || installed) bundles.push({ name, enabled, installed, removable, error: { code: 'not-bundle' }, rows: [], overrides: [] })
+          if (enabled) bundles.push({ name, enabled, installed, optional, removable, error: { code: 'not-bundle' }, rows: [], overrides: [] })
           continue
         }
         const readOnlyReason = this.protectsManager(name) ? 'management-required' as const : undefined
         const title = info.dsh?.title
+        const description = info.dsh?.description ?? info.description
         bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }),
           ...(title === undefined ? {} : { title }),
-          ...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
-          enabled, installed, removable: removable && readOnlyReason === undefined,
+          ...(description === undefined || description === '' ? {} : { description }),
+          enabled, installed, optional, removable: removable && readOnlyReason === undefined,
           ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
           ...this.declaredRows(name, info) })
       } catch (error) {
         if (enabled || installed) {
-          bundles.push({ name, enabled, installed, removable, error: managementError(error), rows: [], overrides: [] })
+          bundles.push({ name, enabled, installed, optional, removable, error: managementError(error), rows: [], overrides: [] })
         }
       }
     }
@@ -259,7 +270,7 @@ export class PluginManager extends TypertRemoteService {
       return refused('invalid-spec', error.reason)
     }
     const manifest = readProfileManifest('dsh', this.profile.dir)
-    const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as { dependencies?: Record<string, string> }
+    const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest
     const known = new Set([
       ...manifest.dsh?.profile?.bundles ?? [], ...Object.keys(manifest.dependencies ?? {}), ...Object.keys(installation.dependencies ?? {}),
     ])

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

@@ -35,11 +35,16 @@ export interface BundleInfo {
   version?: string
   /** `dsh.title` of the package manifest. */
   title?: string
-  /** `description` of the package manifest. */
+  /** `dsh.description` of the package manifest, else its `description`. */
   description?: string
   enabled: boolean
   /** Whether the profile's own dependencies hold the package; false for a bundle the dsh installation supplies. */
   installed: boolean
+  /**
+   * Whether the installation ships the bundle for the person to switch on: named under `dsh.optionalBundles` in
+   * the installation's manifest, selected by no shipped template, and never removable.
+   */
+  optional: boolean
   removable: boolean
   readOnlyReason?: ReadOnlyReason
   error?: ManagementError
@@ -122,7 +127,9 @@ export type PluginSpecInspection =
     readonly kind: InstallSpecKind
     readonly name?: string
     readonly version?: string
+    /** `dsh.description` of the package manifest, else its `description`. */
     readonly description?: string
+    /** `dsh.title` of the package manifest. */
     readonly title?: string
     /** Whether the package declares a bundle patch; null when the spec's form does not say. */
     readonly bundle: boolean | null

+ 28 - 7
packages/boot/plugin-manager/tests/manager.spec.ts

@@ -69,11 +69,11 @@ it('lists bundle versions and current-profile plugin targets', async () => {
   expect(plugins.find(row => row.entryId === 'include:manager')?.readOnlyReason).toBe('management-required')
   expect(await manager.listBundles()).toEqual([
     {
-      name: 'core', version: '1.0.0', enabled: true, installed: false, removable: false, readOnlyReason: 'management-required',
+      name: 'core', version: '1.0.0', enabled: true, installed: false, optional: false, removable: false, readOnlyReason: 'management-required',
       rows: [{ rowId: 'manager', moduleName: 'cordis:manager', entryId: 'include:manager' }], overrides: [],
     },
     {
-      name: 'extra', version: '1.0.0', enabled: true, installed: true, removable: true,
+      name: 'extra', version: '1.0.0', enabled: true, installed: true, optional: false, removable: true,
       rows: [{ rowId: 'managed', moduleName: pathToFileURL(join(dir, 'node_modules', 'extra', 'plugin.mjs')).href, entryId: 'include:managed' }], overrides: [],
     },
   ])
@@ -94,7 +94,7 @@ it('describes a bundle by its manifest and patch: title, one-liner, rows without
   writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest))
   const moduleName = pathToFileURL(join(dir, 'node_modules', 'described', 'plugin.mjs')).href
   expect((await manager.listBundles()).find(row => row.name === 'described')).toEqual({
-    name: 'described', version: '2.0.0', title: 'Described', description: 'Describes itself.', enabled: false, installed: true, removable: true,
+    name: 'described', version: '2.0.0', title: 'Described', description: 'Describes itself.', enabled: false, installed: true, optional: false, removable: true,
     rows: [{ rowId: 'described-row', moduleName }], overrides: ['managed'],
   })
   await manager.setBundleEnabled('described', true)
@@ -261,13 +261,14 @@ it('combines concurrent changes into durable notices without waking Agents', asy
 })
 
 
-it('reports plain dependencies, missing versions and invalid selected bundles distinctly', async () => {
+it('reports a selected plain dependency as a problem, omits an unselected one, and reports missing versions', async () => {
   const { manager, dir, profile } = await fixture()
   writeFileSync(profile.installAnchor, '{}')
   writeFileSync(join(dir, 'node_modules', 'extra', 'package.json'), '{"name":"extra"}')
   expect((await manager.listBundles()).find(row => row.name === 'extra')).toMatchObject({ enabled: true, error: { code: 'not-bundle' } })
   expect(await manager.setBundleEnabled('extra', false)).toMatchObject({ application: 'applied' })
-  expect((await manager.listBundles()).find(row => row.name === 'extra')).toMatchObject({ enabled: false, removable: true, error: { code: 'not-bundle' } })
+  // Switched off, a dependency without a bundle patch is a library the page has no business with.
+  expect((await manager.listBundles()).some(row => row.name === 'extra')).toBe(false)
   expect(await manager.setBundleEnabled('extra', true)).toMatchObject({ changed: false, application: 'failed' })
   writeFileSync(join(dir, 'node_modules', 'core', 'package.json'), '{"name":"core","dsh":{"bundle":{"patch":"./cordis.patch.yml"}}}')
   expect((await manager.listBundles())[0]?.version).toBeUndefined()
@@ -512,9 +513,11 @@ it('reads what a spec names before installing it', async () => {
   const view = vi.spyOn(operations, 'viewProfilePackage')
   onTestFinished(() => { view.mockRestore() })
   const answers = (stdout: string) => view.mockResolvedValueOnce({ exitCode: 0, stdout, stderr: '', timedOut: false })
-  answers(JSON.stringify({ name: 'dsh-x', version: '1.4.2', description: 'A sidebar.', dsh: { title: 'Sidebar', bundle: { patch: './cordis.patch.yml' } } }))
+  answers(JSON.stringify({
+    name: 'dsh-x', version: '1.4.2', description: 'A sidebar.', dsh: { title: 'Sidebar', description: 'A better sidebar.', bundle: { patch: './cordis.patch.yml' } },
+  }))
   expect(await manager.inspect('dsh-x')).toEqual({
-    status: 'accepted', kind: 'registry', name: 'dsh-x', version: '1.4.2', description: 'A sidebar.', title: 'Sidebar', bundle: true,
+    status: 'accepted', kind: 'registry', name: 'dsh-x', version: '1.4.2', description: 'A better sidebar.', title: 'Sidebar', bundle: true,
   })
   expect(view).toHaveBeenCalledWith(dir, 'dsh-x', { command: 'pnpm-test', timeoutMs: 1000 })
   const signal = AbortSignal.abort()
@@ -631,6 +634,24 @@ it('refuses removal of a hot-installed bundle after HMR is disabled', async () =
   expect(await manager.removeBundle('later')).toMatchObject({ changed: false, application: 'failed' })
 })
 
+it('offers the installation\'s optional bundles switched off and never removable, preferring dsh.description', async () => {
+  const { manager, dir, profile, bundle } = await fixture()
+  bundle('offered', [{ id: 'offered-row', name: './plugin.mjs', config: { service: 'offeredProbe' } }])
+  writeFileSync(join(dir, 'node_modules', 'offered', 'package.json'), JSON.stringify({
+    name: 'offered', version: '3.0.0', description: 'Package one-liner.',
+    dsh: { title: 'Offered', description: 'Display one-liner.', bundle: { patch: './cordis.patch.yml' } },
+  }))
+  writeFileSync(profile.installAnchor, JSON.stringify({ name: 'installation', dependencies: { offered: '3.0.0' }, dsh: { optionalBundles: ['offered'] } }))
+  expect((await manager.listBundles()).find(row => row.name === 'offered')).toEqual({
+    name: 'offered', version: '3.0.0', title: 'Offered', description: 'Display one-liner.',
+    enabled: false, installed: false, optional: true, removable: false,
+    rows: [{ rowId: 'offered-row', moduleName: pathToFileURL(join(dir, 'node_modules', 'offered', 'plugin.mjs')).href }], overrides: [],
+  })
+  expect(await manager.setBundleEnabled('offered', true)).toMatchObject({ application: 'applied' })
+  expect((await manager.listBundles()).find(row => row.name === 'offered')).toMatchObject({ enabled: true, optional: true, removable: false })
+  expect(await manager.removeBundle('offered')).toMatchObject({ changed: false, application: 'failed' })
+})
+
 it('omits installation-owned plain packages from the bundle inventory', async () => {
   const { manager, dir, profile, bundle } = await fixture()
   bundle('installation-plain', [])

+ 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: 070ad1cf31ca649317c84b15f742b4d45768e3f6
-README.zh.md: a317be188efa6c8cc294a5c687740ef5de047623
+README.md: 10d67d405d7731112f3671dbce1a1a37e6476178
+README.zh.md: a26165d23f8b4567aa1289770d91006ed7565e2f

+ 4 - 4
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 the profile's installed bundles. Switch bundles and their rows on and off, install a bundle after the Host has read what the spec names, watch pnpm's output, stop a run, and enable what it added. Uninstalling asks for confirmation. Global configuration remains in Settings.
+Use the **Plugins** entry in the Web sidebar to manage the profile's installed bundles and the official bundles the installation ships switched off. Switch bundles and their rows on and off, install a bundle after the Host has read what the spec names, watch pnpm's output, stop a run, and enable what it added. Uninstalling asks for confirmation. Global configuration remains in Settings.
 
 ## Table of Contents
 
@@ -25,11 +25,11 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b
 <a id="use-this-package"></a>
 ## Use this package
 
-Select **Plugins** in the sidebar. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. Cards are listed by title, so switching a bundle on or off does not move its card. Global configuration remains in the Settings **Plugins** section.
+Select **Plugins** in the sidebar. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. **Built in** comes first and lists the bundles the installation ships for switching on, each tagged official, off until switched on and without an uninstall; **Installed** lists the bundles the profile holds. Cards are listed by title, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Plugins** section.
 
 ### Installing a bundle
 
-**Add plugin** takes a plugin ID, which is the registry package name with an optional version, a Git address, a tarball, or an absolute local path; a hint under the field says what an ID looks like. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`): a name the list already shows, a name the registry does not have, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing. An accepted spec opens the installing screen, which shows the package's title, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. A failed install says what went wrong in one line — the registry or network could not be reached, the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand; the Host has already put the profile files back. A successful installation does not certify that a module can activate.
+**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. **Not sure what to enter?** under the field opens a guide that shows the three common forms with an example each; **Use example** drops one into the field. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`): a name the list already shows, a name the registry does not have, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing. An accepted spec opens the installing screen, which shows the package's title, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. A failed install says what went wrong in one line — the registry or network could not be reached, the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand; the Host has already put the profile files back. A successful installation does not certify that a module can activate.
 
 During installation, **Cancel install** asks the Host to stop the run and shows **Stopping installation…** until the Host confirms. Loading the bundle cannot be cancelled. Once confirmed, the dialog returns to the spec, ready to install again, and a toast says the installation was cancelled; the manifest and lockfile are back as they were, while downloaded files can remain. Closing the dialog is blocked while the Host owns the operation. A connection error does not confirm cancellation: the running screen says so and cancelling can be tried again.
 
@@ -90,7 +90,7 @@ None; this package neither assembles nor sends a provider request.
 
 These limits define the reach of the management view; they are current package constraints.
 
-- **Only bundles are managed** — a dependency without a bundle patch is refused before it installs; one the profile already holds is listed as a problem that can only be uninstalled, and loading plain plugin modules stays a file operation.
+- **Only bundles are managed** — a dependency without a bundle patch is refused before it installs; one the profile already holds is left off the page unless the profile selects it, and loading plain plugin modules stays a file operation.
 - **Rows show a phase, not a reason** — a failed row reads as failed without the Host's error text; the Host log has it.
 - **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.

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

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用 Web 侧栏的**插件**入口管理 profile 已安装的组合包。可以启停组合包及其行、在 Host 读出 spec 指向什么之后安装组合包、查看 pnpm 输出、停止一次运行,并启用它新增的包。卸载会要求确认。全局配置仍在设置中编辑。
+使用 Web 侧栏的**插件**入口管理 profile 已安装的组合包,以及安装随附、默认关闭的官方组合包。可以启停组合包及其行、在 Host 读出 spec 指向什么之后安装组合包、查看 pnpm 输出、停止一次运行,并启用它新增的包。卸载会要求确认。全局配置仍在设置中编辑。
 
 ## 目录
 
@@ -25,11 +25,11 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。卡片按标题排序,启停组合包不会挪动它的卡片。全局配置仍在设置的**插件**分区中编辑。
+在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**内置**排在前面,列出安装随附、供开启的组合包,每个带官方标签,开启前保持关闭,且没有卸载;**已安装**列出 profile 持有的组合包。卡片按标题排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**插件**分区中编辑。
 
 ### 安装一个组合包
 
-**添加插件**接受插件 ID(即注册表包名,可带版本)、Git 地址、压缩包或本地绝对路径,输入框下方提示插件 ID 的常见形式。**安装**先让 Host 读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、注册表没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑。通过检查的 spec 打开安装中界面,展示 Host 读到的包标题、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。安装失败时用一行话说明原因——注册表或网络不可达、包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边;Host 已经把 profile 文件放回原样。安装成功不代表模块一定能够激活。
+**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。输入框下方的**不知道该填什么?**展开一段引导,给出三种常见形式各一个示例;**填入示例**把示例填进输入框。**安装**先让 Host 读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、注册表没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑。通过检查的 spec 打开安装中界面,展示 Host 读到的包标题、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。安装失败时用一行话说明原因——注册表或网络不可达、包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边;Host 已经把 profile 文件放回原样。安装成功不代表模块一定能够激活。
 
 安装期间可点击**取消安装**,对话框显示**正在停止安装…**,直到 Host 确认。加载组合包的阶段不可取消。确认后对话框回到 spec 输入界面,可再次安装,并用 toast 说明安装已取消;manifest 与 lockfile 已恢复原样,已下载文件可能保留。Host 仍在处理操作时不能关闭对话框。连接错误不代表取消成功:安装中界面会如此说明,可以再次尝试取消。
 
@@ -90,7 +90,7 @@ kind: "package-reference"
 
 这些限制界定了管理视图的范围;它们是当前包的约束。
 
-- **只管理组合包**——没有组合包 patch 的依赖在安装前就被拒绝;profile 里已有的这类依赖作为异常列出,只能卸载;加载普通插件模块仍是文件操作。
+- **只管理组合包**——没有组合包 patch 的依赖在安装前就被拒绝;profile 里已有的这类依赖不上页面,除非 profile 选中了它;加载普通插件模块仍是文件操作。
 - **行只显示阶段,不显示原因**——失败的行只显示为失败,没有 Host 的错误文本;Host 日志里有。
 - **一次只能安装一个**——对话框一次运行一个 pnpm 命令;第二个 spec 要等前一个完成。
 - **没有版本选择器**——spec 按 pnpm 接受的写法输入;页面不列出注册表版本,也不提供升级。

+ 119 - 3
packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css

@@ -418,14 +418,130 @@
   border-color: var(--dsw-alias-state-error-primary);
 }
 
-/* What an ID is, under the field, for someone who only has a plugin's display name. */
-.installHint {
-  margin: -4px 0 0;
+/* The guide under the field: opened by a text button, it says what an ID is and shows the three forms with examples. */
+.guideToggle {
+  display: inline-flex;
+  align-items: center;
+  align-self: flex-start;
+  gap: 4px;
+  padding: 0;
+  border: 0;
+  background: none;
+  font: inherit;
+  font-size: 12.5px;
+  color: var(--dsw-alias-label-secondary);
+  cursor: pointer;
+}
+
+.guideToggle:hover {
+  color: var(--dsw-alias-label-primary);
+}
+
+.guideChevron {
+  transition: transform 160ms ease;
+}
+
+.guideToggle[aria-expanded='true'] .guideChevron {
+  transform: rotate(180deg);
+}
+
+.guide {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  padding: 12px 14px;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 12px;
+  background: var(--dsw-alias-bg-layer-1);
+}
+
+.guideIntro,
+.guideHint {
+  margin: 0;
   font-size: 12px;
   line-height: 18px;
   color: var(--dsw-alias-label-tertiary);
 }
 
+/* The security warning that closes the guide: plugins run with the person's own permissions. */
+.guideSafety {
+  display: flex;
+  align-items: flex-start;
+  gap: 6px;
+  margin: 0;
+  padding: 8px 10px;
+  border-radius: 8px;
+  background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent);
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary));
+}
+
+.guideSafety > svg {
+  flex: none;
+  margin-top: 2px;
+}
+
+.guideNote {
+  margin: 0;
+  padding: 8px 10px;
+  border-radius: 8px;
+  background: var(--dsw-alias-bg-layer-3);
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--dsw-alias-label-secondary);
+}
+
+.guideList {
+  display: flex;
+  flex-direction: column;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.guideItem {
+  display: flex;
+  align-items: flex-start;
+  gap: 10px;
+  padding: 8px 0;
+  border-top: 0.5px solid var(--dsw-alias-border-l4);
+}
+
+.guideIndex {
+  flex: none;
+  width: 20px;
+  height: 20px;
+  border-radius: 999px;
+  corner-shape: round;
+  background: var(--dsw-alias-bg-layer-3);
+  font-size: 11px;
+  line-height: 20px;
+  text-align: center;
+  color: var(--dsw-alias-label-tertiary);
+}
+
+.guideMain {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  gap: 2px;
+  min-width: 0;
+}
+
+.guideTitle {
+  font-size: 12.5px;
+  font-weight: 600;
+  color: var(--dsw-alias-label-primary);
+}
+
+.guideExample {
+  font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
+  font-size: 12px;
+  color: var(--dsw-alias-label-secondary);
+  overflow-wrap: anywhere;
+}
+
 /* What the check refused, under the field. */
 .inputError {
   margin: -4px 0 0;

+ 121 - 43
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -1,8 +1,9 @@
 /**
- * Global plugin management: installed bundle cards, their row switches, the
- * guided install dialog with its folded pnpm output, the uninstall
- * confirmation, and the toasts an action's outcome becomes. A bundle's page
- * lists the rows it contributes as the Host runs them.
+ * Global plugin management: cards for the profile's installed bundles and for
+ * the official bundles the installation ships switched off, their row
+ * switches, the install dialog with its guide and folded pnpm output, the
+ * uninstall confirmation, and the toasts an action's outcome becomes. A
+ * bundle's page lists the rows it contributes as the Host runs them.
  */
 
 import { useEffect, useId, useState, type ReactNode } from 'react'
@@ -203,7 +204,7 @@ function packageStatus(pkg: PackageView): 'running' | 'disabled' | 'problem' {
   return pkg.enabled ? 'running' : 'disabled'
 }
 
-/** One installed package as a card that opens its page: its name, its one-liner, its tags, and its bundle switch. */
+/** One package as a card that opens its page: its name, its one-liner, its tags, and its bundle switch. */
 function PackageCard({ pkg, t, busy, highlighted, onOpen, onSetEnabled }: {
   readonly pkg: PackageView
   readonly t: Translate
@@ -226,6 +227,7 @@ function PackageCard({ pkg, t, busy, highlighted, onOpen, onSetEnabled }: {
         <div className={css.cardMain}>
           <div className={css.titleRow}>
             <button type="button" className={`${css.cardTitle} ${css.cardOpen}`} aria-label={t('openDetail', { name: title })} onClick={onOpen}>{title}</button>
+            {pkg.optional ? <Tag className={css.statusTag} tone="info">{t('statusOfficial')}</Tag> : null}
             {status === 'problem' ? <Tag className={css.statusTag} tone="danger">{t('statusProblem')}</Tag> : null}
           </div>
           {pkg.description === undefined ? null : <span className={css.cardDesc}>{pkg.description}</span>}
@@ -239,8 +241,9 @@ function PackageCard({ pkg, t, busy, highlighted, onOpen, onSetEnabled }: {
 }
 
 /**
- * One package's page: the crumb back to the list; its icon with uninstall
- * and its switch; its title beside its version tag and problem tag; the
+ * One package's page: the crumb back to the list; its icon with its switch
+ * and, for a package the profile installed, uninstall; its title beside its
+ * version tag, its official tag, and its problem tag; the
  * package name the title stands for, which is what installs it elsewhere;
  * its one-liner; the Host's problem when it reports one; and its rows with
  * their switches.
@@ -270,17 +273,21 @@ function PackageDetail({
       <div className={css.detailHead}>
         <span className={css.cardIcon} aria-hidden="true"><IconPluginPinwheelOutline16 size={20} /></span>
         <div className={css.detailActions}>
-          <Button
-            variant="outline"
-            size="sm"
-            className={css.danger}
-            icon={<IconTrashOutline16 size={13} />}
-            aria-label={t('uninstallLabel', { name: title })}
-            disabled={busy || pkg.readOnlyReason !== undefined}
-            onClick={onUninstall}
-          >
-            {t('uninstall')}
-          </Button>
+          {pkg.installed
+            ? (
+              <Button
+                variant="outline"
+                size="sm"
+                className={css.danger}
+                icon={<IconTrashOutline16 size={13} />}
+                aria-label={t('uninstallLabel', { name: title })}
+                disabled={busy || pkg.readOnlyReason !== undefined}
+                onClick={onUninstall}
+              >
+                {t('uninstall')}
+              </Button>
+            )
+            : null}
           <EnableSwitch pkg={pkg} title={title} t={t} busy={busy} onSetEnabled={onSetEnabled} />
         </div>
       </div>
@@ -288,6 +295,7 @@ function PackageDetail({
         <div className={css.titleRow}>
           <h3 className={css.detailTitle}>{title}</h3>
           {pkg.version === undefined ? null : <span className={css.versionTag} data-plugin-version>{t('versionTag', { version: pkg.version })}</span>}
+          {pkg.optional ? <Tag className={css.statusTag} tone="info">{t('statusOfficial')}</Tag> : null}
           {status === 'problem' ? <Tag className={css.statusTag} tone="danger">{t('statusProblem')}</Tag> : null}
         </div>
         <p className={css.detailName}><code data-plugin-name>{pkg.name}</code></p>
@@ -340,6 +348,21 @@ const INPUT_PROBLEM_KEYS = {
   'unknown': 'installProblemUnknown',
 } satisfies Record<InstallInputError['problem'], PluginManagerLocaleKey>
 
+/** One row of the install guide: a spec form's title, its example, and where the person finds it. */
+interface GuideExample {
+  readonly key: string
+  readonly titleKey: PluginManagerLocaleKey
+  readonly exampleKey: PluginManagerLocaleKey
+  readonly hintKey: PluginManagerLocaleKey
+}
+
+/** The spec forms the install guide shows, each with an example the person can drop into the field. */
+const GUIDE_EXAMPLES = [
+  { key: 'id', titleKey: 'installGuideIdTitle', exampleKey: 'installGuideIdExample', hintKey: 'installGuideIdHint' },
+  { key: 'git', titleKey: 'installGuideGitTitle', exampleKey: 'installGuideGitExample', hintKey: 'installGuideGitHint' },
+  { key: 'path', titleKey: 'installGuidePathTitle', exampleKey: 'installGuidePathExample', hintKey: 'installGuidePathHint' },
+] as const satisfies readonly GuideExample[]
+
 /** The one-line reading of a classified pnpm failure. */
 const FAILURE_KIND_KEYS = {
   'pnpm-missing': 'installFailurePnpmMissing',
@@ -409,7 +432,8 @@ function InstallDialog({ install, t, onClose, onEditSpec, onRun, onCancel, onTog
   readonly onEnableNow: () => void
 }): ReactNode {
   const errorId = useId()
-  const hintId = useId()
+  const guideId = useId()
+  const [guideOpen, setGuideOpen] = useState(false)
   const { phase } = install
   if (phase === 'idle' || phase === 'checking') {
     const checking = phase === 'checking'
@@ -438,7 +462,7 @@ function InstallDialog({ install, t, onClose, onEditSpec, onRun, onCancel, onTog
               placeholder={t('installSpecPlaceholder')}
               disabled={checking}
               aria-invalid={install.inputError !== null}
-              aria-describedby={install.inputError === null ? hintId : `${errorId} ${hintId}`}
+              aria-describedby={install.inputError === null ? undefined : errorId}
               onChange={(event) => { onEditSpec(event.currentTarget.value) }}
               onKeyDown={(event) => { if (event.key === 'Enter' && !empty && !checking) onRun() }}
             />
@@ -446,7 +470,49 @@ function InstallDialog({ install, t, onClose, onEditSpec, onRun, onCancel, onTog
           {install.inputError === null
             ? null
             : <p id={errorId} className={css.inputError} role="alert">{t(INPUT_PROBLEM_KEYS[install.inputError.problem], { reason: install.inputError.reason })}</p>}
-          <p id={hintId} className={css.installHint}>{t('installSpecHint')}</p>
+          <button
+            type="button"
+            className={css.guideToggle}
+            aria-expanded={guideOpen}
+            aria-controls={guideId}
+            onClick={() => { setGuideOpen(open => !open) }}
+          >
+            <IconChevronDownOutline14 className={css.guideChevron} aria-hidden="true" />
+            <span>{t(guideOpen ? 'installGuideHide' : 'installGuideToggle')}</span>
+          </button>
+          {guideOpen
+            ? (
+              <div id={guideId} className={css.guide} data-install-guide>
+                <p className={css.guideIntro}>{t('installGuideIntro')}</p>
+                <p className={css.guideNote}>{t('installGuideIdNote')}</p>
+                <ol className={css.guideList}>
+                  {GUIDE_EXAMPLES.map(({ key, titleKey, exampleKey, hintKey }, index) => (
+                    <li key={key} className={css.guideItem}>
+                      <span className={css.guideIndex} aria-hidden="true">{index + 1}</span>
+                      <div className={css.guideMain}>
+                        <span className={css.guideTitle}>{t(titleKey)}</span>
+                        <code className={css.guideExample}>{t(exampleKey)}</code>
+                        <span className={css.guideHint}>{t(hintKey)}</span>
+                      </div>
+                      <Button
+                        variant="outline"
+                        size="sm"
+                        aria-label={t('installGuideFillAria', { example: t(exampleKey) })}
+                        disabled={checking}
+                        onClick={() => { onEditSpec(t(exampleKey)) }}
+                      >
+                        {t('installGuideFill')}
+                      </Button>
+                    </li>
+                  ))}
+                </ol>
+                <p className={css.guideSafety} role="note">
+                  <IconWarningOutline16 size={14} aria-hidden="true" />
+                  <span>{t('installGuideSafety')}</span>
+                </p>
+              </div>
+            )
+            : null}
         </div>
       </Modal>
     )
@@ -580,15 +646,42 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
   }, [highlight, clearHighlight])
   const noticeLine = state.notice === null ? null : noticeText(state.notice, t)
 
-  // The page manages what the person installed; the bundles the dsh
-  // installation supplies are inspected in the Settings Plugins section's Plugin list tab.
-  const listed = state.packages.filter(pkg => pkg.installed)
+  // The page manages what the person installed, what the installation ships for them to switch on, and a
+  // selected name the Host cannot read; the installation's other bundles are inspected in the Settings
+  // Plugins section's Plugin list tab.
+  const listed = state.packages.filter(pkg => pkg.installed || pkg.optional || pkg.error !== undefined)
+  const mine = listed.filter(pkg => pkg.installed || !pkg.optional)
+  const builtin = listed.filter(pkg => pkg.optional && !pkg.installed)
   const loaded = state.status === 'ready' || state.status === 'error'
   const openPkg = openPackage === null ? undefined : listed.find(pkg => pkg.name === openPackage)
   const setRowEnabled = (row: PackageRow, enabled: boolean): void => {
     /* v8 ignore next -- a row without a live entry has its switch disabled */
     if (row.entryId !== undefined) props.setRowEnabled(row.entryId, enabled)
   }
+  // One group of cards under its heading and count; the built-in group comes first, and a group with nothing in it takes no room.
+  const renderGroup = (id: 'bundles' | 'builtin', heading: string, packages: readonly PackageView[]): ReactNode => packages.length === 0
+    ? null
+    : (
+      <section className={css.group} data-plugin-scope="global" data-plugin-group={id}>
+        <div className={css.groupHead}>
+          <h3 className={css.groupTitle}>{heading}</h3>
+          <span className={css.count} data-plugin-count={packages.length}>{packages.length}</span>
+        </div>
+        <ul className={css.cards}>
+          {packages.map(pkg => (
+            <PackageCard
+              key={pkg.name}
+              pkg={pkg}
+              t={t}
+              busy={state.busy.includes(pkg.name)}
+              highlighted={state.highlight === pkg.name}
+              onOpen={() => { setOpenPackage(pkg.name) }}
+              onSetEnabled={(enabled) => { props.setEnabled(pkg.name, enabled) }}
+            />
+          ))}
+        </ul>
+      </section>
+    )
 
   return (
     <section className={css.page} data-plugin-panel aria-busy={state.status === 'loading'}>
@@ -647,25 +740,10 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
         ? listed.length === 0
           ? <p className={css.empty}>{t('empty')}</p>
           : (
-            <section className={css.group} data-plugin-scope="global" data-plugin-group="bundles">
-              <div className={css.groupHead}>
-                <h3 className={css.groupTitle}>{t('bundlesTitle')}</h3>
-                <span className={css.count} data-plugin-count={listed.length}>{listed.length}</span>
-              </div>
-              <ul className={css.cards}>
-                {listed.map(pkg => (
-                  <PackageCard
-                    key={pkg.name}
-                    pkg={pkg}
-                    t={t}
-                    busy={state.busy.includes(pkg.name)}
-                    highlighted={state.highlight === pkg.name}
-                    onOpen={() => { setOpenPackage(pkg.name) }}
-                    onSetEnabled={(enabled) => { props.setEnabled(pkg.name, enabled) }}
-                  />
-                ))}
-              </ul>
-            </section>
+            <>
+              {renderGroup('builtin', t('builtinTitle'), builtin)}
+              {renderGroup('bundles', t('bundlesTitle'), mine)}
+            </>
           )
         : null}
       <InstallDialog

+ 46 - 12
packages/client/ui-plugin-manager/src/client/locales.ts

@@ -15,7 +15,9 @@ export const zh = {
   restartNotice: '更改将在下次启动生效',
   overriddenNotice: '{name} 已保存,但被更高优先级的配置覆盖,当前未生效',
   bundlesTitle: '已安装',
+  builtinTitle: '内置',
   statusProblem: '异常',
+  statusOfficial: '官方',
   reasonLabel: '原因',
   versionTag: 'v{version}',
   noDescription: '暂无描述。',
@@ -42,13 +44,28 @@ export const zh = {
   uninstall: '卸载',
   uninstallLabel: '卸载 {name}',
   installTitle: '添加插件',
-  installDescription: '粘贴插件作者提供的插件 ID、Git 仓库地址(如 GitHub 链接)或本地绝对路径。',
-  installSpecLabel: '插件 ID 或地址',
-  installSpecPlaceholder: 'dsh-xxx、github:作者/仓库 或 /绝对路径',
-  installSpecHint: '插件 ID 是安装时使用的包名,不是插件显示的名称,常见形式是 dsh-xxx 或 @作者/插件名;加 @版本 可指定版本。',
+  installDescription: '输入插件的包名、GitHub 仓库地址或本地目录路径。',
+  installSpecLabel: '包名或地址',
+  installSpecPlaceholder: '例如 dsh-better-sidebar',
+  installGuideToggle: '不知道该填什么?',
+  installGuideHide: '收起引导',
+  installGuideIntro: '从插件的 README 或发布页面复制以下任意一项。',
+  installGuideIdNote: '包名即 npm 包名,常见形式为 dsh-xxx 或 @作者/插件名;插件的显示名称不是包名。',
+  installGuideIdTitle: '包名',
+  installGuideIdExample: 'dsh-better-sidebar',
+  installGuideIdHint: 'README 安装命令中 dsh plugin add 或 pnpm add 之后的部分。',
+  installGuideGitTitle: 'GitHub 仓库地址',
+  installGuideGitExample: 'https://github.com/author/dsh-plugin',
+  installGuideGitHint: '插件在 GitHub 上的开源仓库地址,也支持其他 Git 仓库。',
+  installGuidePathTitle: '本地插件目录',
+  installGuidePathExample: '/Users/name/my-plugin',
+  installGuidePathHint: '本机上插件目录的绝对路径,适用于自行开发或已下载的插件。',
+  installGuideFill: '填入示例',
+  installGuideFillAria: '填入示例 {example}',
+  installGuideSafety: '请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。',
   installRun: '安装',
   installChecking: '正在检查…',
-  installProblemInvalid: '无法识别这个插件 ID 或地址:{reason}',
+  installProblemInvalid: '无法识别这个包名或地址:{reason}',
   installProblemInstalled: '该插件已安装',
   installProblemNotFound: '未找到相关插件',
   installProblemNotPackage: '该路径不存在或不是有效的插件包',
@@ -115,7 +132,7 @@ export const zh = {
   reasonManagementRequired: '插件管理所需,不能停用或卸载',
   reasonUnaddressable: '当前 profile 的 patch 无法唯一定位这一项',
   reasonUnknownPlugin: '找不到该插件',
-  reasonInvalidSpec: '请输入有效的插件 ID 或地址',
+  reasonInvalidSpec: '请输入有效的包名或地址',
   reasonAmbiguousInstall: '无法从依赖变更中确定安装了哪一个包',
   reasonNotBundle: '这个包没有声明组合包,不能作为插件管理',
   reasonNotRemovable: '这个包不属于当前 profile,或者是插件管理所需的组件',
@@ -142,7 +159,9 @@ export const en = {
   restartNotice: 'The change takes effect at the next start',
   overriddenNotice: '{name} was saved, but a higher-priority configuration overrides it, so it is not in effect',
   bundlesTitle: 'Installed',
+  builtinTitle: 'Built in',
   statusProblem: 'Problem',
+  statusOfficial: 'Official',
   reasonLabel: 'Reason',
   versionTag: 'v{version}',
   noDescription: 'No description.',
@@ -169,13 +188,28 @@ export const en = {
   uninstall: 'Uninstall',
   uninstallLabel: 'Uninstall {name}',
   installTitle: 'Add plugin',
-  installDescription: 'Paste the plugin ID the author provides, a Git address such as a GitHub link, or an absolute local path.',
-  installSpecLabel: 'Plugin ID or address',
-  installSpecPlaceholder: 'dsh-xxx, github:author/repo, or /absolute/path',
-  installSpecHint: 'The plugin ID is the package name, not the plugin\'s display name: dsh-xxx or @author/plugin; @version pins one.',
+  installDescription: 'Enter the plugin\'s package name, GitHub repository address, or local directory path.',
+  installSpecLabel: 'Package name or address',
+  installSpecPlaceholder: 'for example dsh-better-sidebar',
+  installGuideToggle: 'Not sure what to enter?',
+  installGuideHide: 'Hide the guide',
+  installGuideIntro: 'Copy any one of these from the plugin\'s README or release page.',
+  installGuideIdNote: 'The package name is the npm package name, usually dsh-xxx or @author/plugin; the display name is not one.',
+  installGuideIdTitle: 'Package name',
+  installGuideIdExample: 'dsh-better-sidebar',
+  installGuideIdHint: 'The part after dsh plugin add or pnpm add in the README\'s install command.',
+  installGuideGitTitle: 'GitHub repository address',
+  installGuideGitExample: 'https://github.com/author/dsh-plugin',
+  installGuideGitHint: 'The address of the plugin\'s open-source repository on GitHub; other Git hosts work too.',
+  installGuidePathTitle: 'Local plugin directory',
+  installGuidePathExample: '/Users/name/my-plugin',
+  installGuidePathHint: 'The absolute path of a plugin directory on this machine, developed here or downloaded.',
+  installGuideFill: 'Use example',
+  installGuideFillAria: 'Use the example {example}',
+  installGuideSafety: 'Install only plugins you trust: they run with your permissions and can damage DeepSeek Harness or leak your data.',
   installRun: 'Install',
   installChecking: 'Checking…',
-  installProblemInvalid: 'This is not a plugin ID or address that can be installed: {reason}',
+  installProblemInvalid: 'This is not a package name or address that can be installed: {reason}',
   installProblemInstalled: 'This plugin is already installed',
   installProblemNotFound: 'No such plugin was found',
   installProblemNotPackage: 'The path does not exist or is not a valid plugin package',
@@ -242,7 +276,7 @@ export const en = {
   reasonManagementRequired: 'Plugin management needs it; it cannot be switched off or uninstalled.',
   reasonUnaddressable: 'The profile patch cannot address this one uniquely.',
   reasonUnknownPlugin: 'No such plugin.',
-  reasonInvalidSpec: 'Enter a valid plugin ID or address.',
+  reasonInvalidSpec: 'Enter a valid package name or address.',
   reasonAmbiguousInstall: 'Which package was installed cannot be told from the dependency change.',
   reasonNotBundle: 'This package declares no bundle, so it cannot be managed as a plugin.',
   reasonNotRemovable: 'This package is not owned by the profile, or plugin management needs it.',

+ 3 - 0
packages/client/ui-plugin-manager/src/client/manager-store.ts

@@ -69,6 +69,8 @@ export interface PackageView {
   readonly description?: string
   /** Whether the profile's own dependencies hold the package; false for a bundle the installation supplies. */
   readonly installed: boolean
+  /** Whether the installation ships the bundle for the person to switch on: official, off until selected, never removable. */
+  readonly optional: boolean
   /** Whether the bundle is in the profile's layer list. */
   readonly enabled: boolean
   /** Why the Host refuses to switch the bundle off or remove it, when it does. */
@@ -269,6 +271,7 @@ export function packageView(bundle: BundleInfo, plugins: readonly PluginInfo[]):
   return {
     name: bundle.name,
     installed: bundle.installed,
+    optional: bundle.optional,
     enabled: bundle.enabled,
     rows,
     ...bundle.version === undefined ? {} : { version: bundle.version },

+ 42 - 6
packages/client/ui-plugin-manager/tests/components.client.spec.tsx

@@ -22,6 +22,7 @@ function pkg(overrides: Partial<PackageView> = {}): PackageView {
     name: 'dsh-better-sidebar',
     version: '0.16.0',
     installed: true,
+    optional: false,
     enabled: true,
     rows: [],
     ...overrides,
@@ -96,7 +97,7 @@ describe('PluginManagerPage', () => {
     expect(actions.openInstall).toHaveBeenCalledTimes(1)
   })
 
-  it('lists the installed bundles as cards, leaves the installation\'s own to Settings, and tags a problem the Host reports', () => {
+  it('lists the installed bundles as cards, the installation\'s offered ones as built in, and tags a problem the Host reports', () => {
     const { actions } = renderTab({
       packages: [
         pkg({ title: 'Better sidebar', description: 'A sidebar.' }),
@@ -104,19 +105,27 @@ describe('PluginManagerPage', () => {
         pkg({ name: '@deepseek-ai/dsh-web-app', installed: false }),
         pkg({ name: 'dsh-protected', readOnlyReason: 'management-required' }),
         pkg({ name: '@acme/dsh-tool', enabled: false }),
+        // Selected by the profile but not a bundle: a problem the person can switch off, in the profile's own group.
+        pkg({ name: 'dsh-selected', installed: false, error: { code: 'not-bundle' } }),
+        pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', title: 'Agent Teams', installed: false, optional: true, enabled: false }),
       ],
       busy: ['dsh-protected'],
     })
     const cards = screen.getAllByRole('listitem')
-    expect(cards.map(card => card.getAttribute('data-plugin-package'))).toEqual(['dsh-better-sidebar', 'dsh-broken', 'dsh-protected', '@acme/dsh-tool'])
-    expect(cards.map(card => card.getAttribute('data-plugin-status'))).toEqual(['running', 'problem', 'running', 'disabled'])
-    // The group heads with its title and its bare count.
+    // The built-in group comes first.
+    expect(cards.map(card => card.getAttribute('data-plugin-package'))).toEqual([
+      '@deepseek-ai/dsh-experimental-agent-team-profile', 'dsh-better-sidebar', 'dsh-broken', 'dsh-protected', '@acme/dsh-tool', 'dsh-selected',
+    ])
+    expect(cards.map(card => card.getAttribute('data-plugin-status'))).toEqual(['disabled', 'running', 'problem', 'running', 'disabled', 'problem'])
+    // Each group heads with its title and its bare count; the built-in card carries the official tag.
     expect(screen.getByRole('heading', { name: en.bundlesTitle })).toBeTruthy()
-    expect(document.querySelector('[data-plugin-count]')?.textContent).toBe('4')
+    expect(screen.getByRole('heading', { name: en.builtinTitle })).toBeTruthy()
+    expect([...document.querySelectorAll('[data-plugin-count]')].map(count => count.textContent)).toEqual(['1', '5'])
+    expect(screen.getAllByText(en.statusOfficial)).toHaveLength(1)
     // A scoped name reads without its scope and harness prefix.
     expect(screen.getByRole('switch', { name: en.enableToggle.replace('{name}', 'tool') })).toHaveProperty('disabled', false)
     expect(screen.getByText('A sidebar.')).toBeTruthy()
-    expect(screen.getAllByText(en.statusProblem)).toHaveLength(1)
+    expect(screen.getAllByText(en.statusProblem)).toHaveLength(2)
     // The switch acts on the bundle; a bundle the Host cannot read stays off, a protected one stays as it is.
     fireEvent.click(screen.getByRole('switch', { name: en.enableToggle.replace('{name}', 'Better sidebar') }))
     expect(actions.setEnabled).toHaveBeenCalledWith('dsh-better-sidebar', false)
@@ -126,6 +135,33 @@ describe('PluginManagerPage', () => {
     expect(locked.getAttribute('title')).toBe(en.reasonManagementRequired)
   })
 
+  it('opens a built-in bundle\'s page with its official tag and no uninstall, and switches it on', () => {
+    const { actions } = renderTab({
+      packages: [pkg({ name: '@deepseek-ai/dsh-experimental-agent-team-profile', title: 'Agent Teams', installed: false, optional: true, enabled: false })],
+    })
+    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'Agent Teams') }))
+    const detail = document.querySelector('[data-plugin-detail]') as HTMLElement
+    expect(within(detail).getByText(en.statusOfficial)).toBeTruthy()
+    expect(within(detail).queryByRole('button', { name: en.uninstallLabel.replace('{name}', 'Agent Teams') })).toBeNull()
+    fireEvent.click(within(detail).getByRole('switch', { name: en.enableToggle.replace('{name}', 'Agent Teams') }))
+    expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith('@deepseek-ai/dsh-experimental-agent-team-profile', true)
+  })
+
+  it('opens a guide under the field and drops an example into it', () => {
+    const { actions } = renderTab({ install: { ...IDLE_INSTALL, open: true } })
+    expect(screen.queryByText(en.installGuideIntro)).toBeNull()
+    const toggle = screen.getByRole('button', { name: en.installGuideToggle })
+    expect(toggle.getAttribute('aria-expanded')).toBe('false')
+    fireEvent.click(toggle)
+    expect(screen.getByRole('button', { name: en.installGuideHide }).getAttribute('aria-expanded')).toBe('true')
+    expect(screen.getByText(en.installGuideIdNote)).toBeTruthy()
+    expect(screen.getByText(en.installGuideGitExample)).toBeTruthy()
+    fireEvent.click(screen.getByRole('button', { name: en.installGuideFillAria.replace('{example}', en.installGuideIdExample) }))
+    expect(actions.editInstallSpec).toHaveBeenCalledExactlyOnceWith(en.installGuideIdExample)
+    fireEvent.click(screen.getByRole('button', { name: en.installGuideHide }))
+    expect(screen.queryByText(en.installGuideIntro)).toBeNull()
+  })
+
   it('opens a bundle\'s page with its facts and rows, and uninstalls from it', () => {
     const { actions, set } = renderTab({
       packages: [pkg({

+ 5 - 4
packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts

@@ -17,6 +17,7 @@ const BUNDLE: BundleInfo = {
   description: 'A sidebar.',
   enabled: false,
   installed: true,
+  optional: false,
   removable: true,
   rows: [{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY }, { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme' }],
   overrides: [],
@@ -84,7 +85,7 @@ describe('packageView', () => {
   it('joins a bundle with the entries its rows run as', () => {
     expect(packageView(BUNDLE, PLUGINS)).toEqual({
       name: 'dsh-better-sidebar', version: '0.16.0', title: 'Better sidebar', description: 'A sidebar.',
-      installed: true, enabled: false,
+      installed: true, optional: false, enabled: false,
       rows: [
         { rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY, enabled: true, phase: 'active' },
         { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme', enabled: false, phase: null },
@@ -92,13 +93,13 @@ describe('packageView', () => {
     })
     // A row the inventory no longer lists, a protected row, and a bundle the Host cannot read.
     const protectedBundle: BundleInfo = {
-      name: '@deepseek-ai/dsh-base', enabled: true, installed: false, removable: false, readOnlyReason: 'management-required',
+      name: '@deepseek-ai/dsh-base', enabled: true, installed: false, optional: false, removable: false, readOnlyReason: 'management-required',
       error: { code: 'operation-error', diagnostic: 'broken' },
       rows: [{ rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core' as PluginEntryId }, { rowId: 'gone', moduleName: 'x', entryId: 'include:gone' as PluginEntryId }],
       overrides: [],
     }
     expect(packageView(protectedBundle, PLUGINS)).toEqual({
-      name: '@deepseek-ai/dsh-base', installed: false, enabled: true, readOnlyReason: 'management-required',
+      name: '@deepseek-ai/dsh-base', installed: false, optional: false, enabled: true, readOnlyReason: 'management-required',
       error: { code: 'operation-error', diagnostic: 'broken' },
       rows: [
         { rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core', enabled: true, phase: 'active', readOnlyReason: 'management-required' },
@@ -110,7 +111,7 @@ describe('packageView', () => {
 
 describe('sortPackages', () => {
   it('orders packages by the title a person reads, not by the Host order or enablement', async () => {
-    const plain = { enabled: true, installed: true, removable: true, rows: [], overrides: [] }
+    const plain = { enabled: true, installed: true, optional: false, removable: true, rows: [], overrides: [] }
     const zeta: BundleInfo = { ...plain, name: 'dsh-zeta' }
     const alpha: BundleInfo = { ...plain, name: '@acme/dsh-alpha', title: 'Alpha tools', enabled: false }
     const views = [zeta, BUNDLE, alpha].map(bundle => packageView(bundle, PLUGINS))

+ 2 - 2
packages/experimental/agent-team-profile/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/experimental/agent-team-profile/README.md
-README.md: 86ac08c5d2278013ecbc371301bbb6a3a01a65d9
-README.zh.md: 973f5f2342b32109d26291d762f4ad7b9b4c3306
+README.md: 9aa294991d76d63a596c1d9afed2f9991f424f20
+README.zh.md: 98db275bbf583b02d1f370008345fa68882dbba1

+ 2 - 2
packages/experimental/agent-team-profile/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-experimental-agent-team-profile` is a published experimental profile layer that enables [Agent Teams](../agent-team/README.md) over `@deepseek-ai/dsh-base`. Its patch inserts the Team domain and Team-scoped tools and disables ordinary subagent delegation and the overlapping global continuable-child controls. Workflow remains available with fresh children. Add it explicitly to an initialized profile; no shipped profile enables it by default.
+`dsh-experimental-agent-team-profile` is a published experimental profile layer that enables [Agent Teams](../agent-team/README.md) over `@deepseek-ai/dsh-base`. Its patch inserts the Team domain and Team-scoped tools and disables ordinary subagent delegation and the overlapping global continuable-child controls. Workflow remains available with fresh children. The dsh installation ships it as an optional bundle that no shipped profile enables; switch it on from the Web sidebar's Plugins page, or add it explicitly to an initialized profile.
 
 ## Table of Contents
 
@@ -91,7 +91,7 @@ The bundle's composition is prefix-stable while its patch, Team identity, and co
 
 <a id="known-limitations-and-deferred-work"></a>
 
-- **Opt-in only** — the package is public, but no shipped CLI, Web, SDK, ACP, or Python profile enables it.
+- **Opt-in only** — the package ships with the installation switched off; no shipped CLI, Web, SDK, ACP, or Python profile enables it.
 - **Workflow child tools** — the [Team tool visibility limitation](../tool-agent-team/README.md#known-limitations-and-deferred-work) also applies to workflow children.
 - **Shared checkout** — every teammate observes the same working directory; this bundle adds no worktree isolation or filesystem locking.
 - **Base profile required** — the patch depends on row ids and Subagent providers supplied by `dsh-base`; it is not a standalone profile.

+ 2 - 2
packages/experimental/agent-team-profile/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-bundle"
 
 ## 概述
 
-`dsh-experimental-agent-team-profile` 是在 `@deepseek-ai/dsh-base` 之上启用 [Agent Teams](../agent-team/README.zh.md) 的公开实验性 profile 层。它的 patch 会插入 Team domain 与 Team-scoped 工具,并禁用普通 subagent 委派和名称重叠的全局 continuable-child control。Workflow 仍可创建 fresh 子代理。必须将本包显式添加到已初始化的 profile;随附 profile 默认都不会启用它
+`dsh-experimental-agent-team-profile` 是在 `@deepseek-ai/dsh-base` 之上启用 [Agent Teams](../agent-team/README.zh.md) 的公开实验性 profile 层。它的 patch 会插入 Team domain 与 Team-scoped 工具,并禁用普通 subagent 委派和名称重叠的全局 continuable-child control。Workflow 仍可创建 fresh 子代理。dsh 安装随附本包作为可选组合包,随附 profile 都不会启用它;可在 Web 侧栏的插件页开启,或显式添加到已初始化的 profile
 
 ## 目录
 
@@ -91,7 +91,7 @@ Team 策略与 schema 由 [`@deepseek-ai/dsh-experimental-tool-agent-team`](../t
 
 <a id="known-limitations-and-deferred-work"></a>
 
-- **仅显式启用**——本包公开发布,但随附 CLI、Web、SDK、ACP 与 Python profile 都不会启用它。
+- **仅显式启用**——本包随安装提供但默认关闭;随附 CLI、Web、SDK、ACP 与 Python profile 都不会启用它。
 - **Workflow 子代理工具**——[Team 工具可见性限制](../tool-agent-team/README.zh.md#known-limitations-and-deferred-work)也适用于 workflow 子代理。
 - **共享 checkout**——所有 teammate 都观察同一个工作目录;本 bundle 不提供 worktree 隔离或文件系统锁。
 - **需要 base profile**——本 patch 依赖 `dsh-base` 提供的配置行 id 与 Subagent 提供方;它不是独立 profile。

+ 2 - 0
packages/experimental/agent-team-profile/package.json

@@ -29,6 +29,8 @@
   ],
   "license": "MIT",
   "dsh": {
+    "title": "Agent Teams",
+    "description": "实验功能:把子代理委派换成 Agent Teams 的团队协作工具,这是宿主层,需与 Agent Teams Web 面板一起开启。",
     "bundle": {
       "patch": "./cordis.patch.yml"
     }

+ 2 - 2
packages/experimental/agent-team-web-profile/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/experimental/agent-team-web-profile/README.md
-README.md: 16d71b5b30f5b18cb35e3eb289757d48a5271385
-README.zh.md: dd6c99453f9dacde19c299ab30c3a89d9aca02a3
+README.md: e21a7eeebc83a97d3e4630c95a9aeaa4213bc227
+README.zh.md: 9966697cb7225083a6fff515675b9dd9ecc51c82

+ 2 - 2
packages/experimental/agent-team-web-profile/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-experimental-agent-team-web-profile` is the published experimental Web layer for [Agent Teams](../agent-team/README.md). Add it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md) to show the Team roster, task board, and teammate navigation in the browser. Removing either experimental layer leaves the stable base and Web composition unchanged. No shipped Web profile enables it by default.
+`dsh-experimental-agent-team-web-profile` is the published experimental Web layer for [Agent Teams](../agent-team/README.md). Add it after `@deepseek-ai/dsh-web-app` and [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.md) to show the Team roster, task board, and teammate navigation in the browser. Removing either experimental layer leaves the stable base and Web composition unchanged. The dsh installation ships it as an optional bundle that no shipped Web profile enables; switch it on from the Web sidebar's Plugins page after the Host layer.
 
 ## Table of Contents
 
@@ -85,7 +85,7 @@ This Web bundle adds no model request content; the Host-side Team tools own prom
 
 - **Ordered composition** — `dsh-base`, `dsh-web-app`, `dsh-experimental-agent-team-profile`, and this package must remain in that order.
 - **Preset-scoped legacy controls** — stable Web presets still mount continuable Subagent controls inside the preset scope. Top-level Host profile overrides do not replace those scoped registrations, so the Team roster and legacy child controls can both appear until Web has a Team-aware preset. The [Web Agent Teams decision](../../../.agents/notes/archived/feature/2026-08-06-agent-teams-web.md) records this deferred composition work.
-- **Opt-in only** — the package is public, but no shipped Web profile enables either Agent Teams layer.
+- **Opt-in only** — the package ships with the installation switched off; no shipped Web profile enables either Agent Teams layer.
 
 <a id="dev-note"></a>
 ### Dev Note

+ 2 - 2
packages/experimental/agent-team-web-profile/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-bundle"
 
 ## 概述
 
-`dsh-experimental-agent-team-web-profile` 是 [Agent Teams](../agent-team/README.zh.md) 公开发布的实验性 Web 层。把它放在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 之后,即可在浏览器中显示 Team roster、任务板与 teammate 导航。移除任一实验层都会让稳定的 base 与 Web composition 保持不变。随附 Web profile 默认不会启用它。
+`dsh-experimental-agent-team-web-profile` 是 [Agent Teams](../agent-team/README.zh.md) 公开发布的实验性 Web 层。把它放在 `@deepseek-ai/dsh-web-app` 与 [`@deepseek-ai/dsh-experimental-agent-team-profile`](../agent-team-profile/README.zh.md) 之后,即可在浏览器中显示 Team roster、任务板与 teammate 导航。移除任一实验层都会让稳定的 base 与 Web composition 保持不变。dsh 安装随附本包作为可选组合包,随附 Web profile 不会启用它;在开启 Host 层后可在 Web 侧栏的插件页开启
 
 ## 目录
 
@@ -85,7 +85,7 @@ dsh plugin --profile web add @deepseek-ai/dsh-experimental-agent-team-web-profil
 
 - **有序组合**——`dsh-base`、`dsh-web-app`、`dsh-experimental-agent-team-profile` 与本包必须保持这个顺序。
 - **Preset-scoped 旧控制项**——稳定 Web preset 仍会在 preset scope 内挂载 continuable Subagent 控制项。顶层 Host profile override 不会替换这些 scoped registration,因此在 Web 获得 Team-aware preset 前,Team roster 与旧 child 控制项可能同时出现。[Web Agent Teams 决策](../../../.agents/notes/archived/feature/2026-08-06-agent-teams-web.md)记录了这项暂缓的 composition 工作。
-- **仅显式启用**——本包公开发布,但随附 Web profile 默认不会启用任何 Agent Teams 层。
+- **仅显式启用**——本包随安装提供但默认关闭;随附 Web profile 不会启用任何 Agent Teams 层。
 
 <a id="dev-note"></a>
 ### 开发备注

+ 2 - 0
packages/experimental/agent-team-web-profile/package.json

@@ -29,6 +29,8 @@
   ],
   "license": "MIT",
   "dsh": {
+    "title": "Agent Teams Web 面板",
+    "description": "实验功能:在浏览器里显示 Agent Teams 的团队名单、任务板和成员导航,需先开启 Agent Teams。",
     "bundle": {
       "patch": "./cordis.patch.yml"
     }

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

@@ -1446,9 +1446,9 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
       },
       {
         signature: '@Remote listBundles(): Promise<BundleInfo[]>',
-        description: 'Read installed bundles and bundles supplied by this dsh installation.',
+        description: 'Read the profile\'s installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles. A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.',
         parameters: [],
-        returns: 'Package versions, titles, rows, activation selections and removal availability.',
+        returns: 'Package versions, titles, one-liners, rows, activation selections, whether the installation offers the bundle, and removal availability.',
       },
       {
         signature: '@Remote async inspect(spec: string, signal?: AbortSignal): Promise<PluginSpecInspection>',
@@ -4135,7 +4135,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'BundleInfo',
-    declaration: 'export interface BundleInfo {\n    name: string;\n    version?: string;\n    title?: string;\n    description?: string;\n    enabled: boolean;\n    installed: boolean;\n    removable: boolean;\n    readOnlyReason?: ReadOnlyReason;\n    error?: ManagementError;\n    rows: BundleRowInfo[];\n    overrides: string[];\n}',
+    declaration: 'export interface BundleInfo {\n    name: string;\n    version?: string;\n    title?: string;\n    description?: string;\n    enabled: boolean;\n    installed: boolean;\n    optional: boolean;\n    removable: boolean;\n    readOnlyReason?: ReadOnlyReason;\n    error?: ManagementError;\n    rows: BundleRowInfo[];\n    overrides: string[];\n}',
   },
   {
     name: 'BundleRowInfo',

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

@@ -30,6 +30,8 @@ export interface DshManifest {
   manifestVersion?: 1
   /** Display title of the package, read without executing package code. */
   title?: string
+  /** Display one-liner of the package, read without executing package code; shown instead of the package `description`. */
+  description?: string
   /** Bundle metadata consumed by the profile launcher. */
   bundle?: DshBundleManifest
   /** Profile metadata consumed by the profile launcher. */

+ 6 - 3
pnpm-lock.yaml

@@ -212,6 +212,12 @@ importers:
       '@deepseek-ai/dsh-cordis-client-runner':
         specifier: workspace:^
         version: link:../../packages/extensions/cordis-client-runner
+      '@deepseek-ai/dsh-experimental-agent-team-profile':
+        specifier: workspace:^
+        version: link:../../packages/experimental/agent-team-profile
+      '@deepseek-ai/dsh-experimental-agent-team-web-profile':
+        specifier: workspace:^
+        version: link:../../packages/experimental/agent-team-web-profile
       '@deepseek-ai/dsh-fs-local':
         specifier: workspace:^
         version: link:../../packages/fs/fs-local
@@ -414,9 +420,6 @@ importers:
       '@deepseek-ai/dsh-experimental-agent-team':
         specifier: workspace:^
         version: link:../../packages/experimental/agent-team
-      '@deepseek-ai/dsh-experimental-agent-team-profile':
-        specifier: workspace:^
-        version: link:../../packages/experimental/agent-team-profile
       '@deepseek-ai/dsh-experimental-ptc-runtime-python':
         specifier: workspace:^
         version: link:../../packages/experimental/ptc-runtime-python

+ 19 - 0
scripts/check-workspace-constraints.spec.ts

@@ -88,6 +88,25 @@ describe('experimental workspace constraints', () => {
     },
   )
 
+  it('allows the dsh installation to ship the optional bundles it lists, and nothing else experimental', () => {
+    const installation = (dependencies: Record<string, string>, optionalBundles: unknown) => ({
+      dir: 'apps/cli',
+      manifest: { name: '@deepseek-ai/dsh', dependencies, dsh: { optionalBundles } },
+    }) satisfies WorkspaceManifest
+    const listed = { '@deepseek-ai/dsh-experimental-prototype': 'workspace:^' }
+    expect(checkExperimentalDependencyIsolation([experimental, installation(listed, ['@deepseek-ai/dsh-experimental-prototype'])])).toEqual([])
+    expect(checkExperimentalDependencyIsolation([experimental, installation(listed, [])])).toEqual([
+      '@deepseek-ai/dsh: dependencies.@deepseek-ai/dsh-experimental-prototype must not reference an experimental package',
+    ])
+    // Only a plain dependency edge is offered; a peer would make the bundle a requirement of every consumer.
+    expect(checkExperimentalDependencyIsolation([experimental, {
+      dir: 'apps/cli',
+      manifest: { name: '@deepseek-ai/dsh', peerDependencies: listed, dsh: { optionalBundles: ['@deepseek-ai/dsh-experimental-prototype'] } },
+    }])).toEqual([
+      '@deepseek-ai/dsh: peerDependencies.@deepseek-ai/dsh-experimental-prototype must not reference an experimental package',
+    ])
+  })
+
   it('allows development and experimental consumers but rejects the Python release runtime', () => {
     const manifests: WorkspaceManifest[] = [experimental, {
       dir: 'packages/core/test-only',

+ 9 - 1
scripts/check-workspace-constraints.ts

@@ -99,6 +99,8 @@ export interface PackageManifest {
     bundle?: {
       patch?: string
     }
+    /** The bundles the dsh installation ships switched off; the one runtime edge to experimental packages it may hold. */
+    optionalBundles?: unknown
   }
 }
 
@@ -500,7 +502,9 @@ const dependencySections = ['dependencies', 'devDependencies', 'peerDependencies
 const runtimeDependencySections = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
 
 /**
- * Prevent an official runtime from requiring a package its release omits.
+ * Prevent an official runtime from requiring an experimental package. The dsh installation's `dependencies`
+ * may hold the bundles its `dsh.optionalBundles` lists: shipped switched off, they are not a requirement
+ * ([rationale](../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)).
  * @param manifests - release, private experimental, and deployment-root manifests.
  * @returns One error for each forbidden runtime dependency.
  */
@@ -512,9 +516,13 @@ export function checkExperimentalDependencyIsolation(manifests: readonly Workspa
   const errors: string[] = []
   for (const { dir, manifest } of manifests) {
     if (!standardReleaseMemberDirectory.test(dir) && dir !== 'python/sdk-runtime') continue
+    const offered = manifest.name === '@deepseek-ai/dsh' && Array.isArray(manifest.dsh?.optionalBundles)
+      ? new Set(manifest.dsh.optionalBundles.filter((name): name is string => typeof name === 'string'))
+      : new Set<string>()
     for (const section of runtimeDependencySections) {
       for (const name of Object.keys(manifest[section] ?? {})) {
         if (!experimentalNames.has(name)) continue
+        if (section === 'dependencies' && offered.has(name)) continue
         errors.push(`${manifest.name ?? dir}: ${section}.${name} must not reference an experimental package`)
       }
     }

+ 21 - 0
scripts/release/installed-product-isolation.spec.ts

@@ -52,6 +52,27 @@ describe('installed default-product isolation', () => {
     },
   )
 
+  it('skips the entry package\'s optional bundles, which must be installed, and still rejects other experimental edges', () => {
+    const root = fixture()
+    const entry = writePackage(root, '@deepseek-ai/dsh', {
+      dependencies: { core: '1.0.0', [experimental]: '1.0.0' },
+      dsh: { optionalBundles: [experimental] },
+    })
+    writePackage(root, 'core', {})
+    writePackage(root, experimental, { dependencies: { '@deepseek-ai/dsh-experimental-inner': '1.0.0' } })
+    expect(verifyInstalledProductIsolation(entry)).toBe(2)
+
+    writePackage(root, 'core', { dependencies: { [experimental]: '1.0.0' } })
+    expect(() => verifyInstalledProductIsolation(entry)).toThrow(`core -> ${experimental}`)
+
+    const missing = writePackage(root, '@deepseek-ai/dsh-missing', {
+      dependencies: { absent: '1.0.0' }, dsh: { optionalBundles: ['absent'] },
+    })
+    expect(() => verifyInstalledProductIsolation(missing)).toThrow('optional bundle is missing: @deepseek-ai/dsh-missing -> absent')
+    const malformed = writePackage(root, '@deepseek-ai/dsh-malformed', { dsh: { optionalBundles: 'absent' } })
+    expect(() => verifyInstalledProductIsolation(malformed)).toThrow('dsh.optionalBundles must be a list of package names')
+  })
+
   it('rejects experimental identities hidden behind an installed alias', () => {
     const root = fixture()
     const entry = writePackage(root, '@deepseek-ai/dsh', { dependencies: { safeName: 'file:../prototype' } })

+ 17 - 0
scripts/release/installed-product-isolation.ts

@@ -9,6 +9,7 @@ interface InstalledManifest {
   dependencies?: Record<string, string>
   optionalDependencies?: Record<string, string>
   peerDependencies?: Record<string, string>
+  dsh?: { optionalBundles?: unknown }
   peerDependenciesMeta?: Record<string, { optional?: boolean }>
 }
 
@@ -28,8 +29,14 @@ export function verifyInstalledProductIsolation(directory: string): number {
     const manifest = JSON.parse(readFileSync(join(canonical, 'package.json'), 'utf8')) as InstalledManifest
     const chain = [...item.chain, manifest.name]
     rejectExperimental(manifest.name, chain)
+    // The bundles the entry package ships switched off are installed beside the product, not required by it.
+    const optionalBundles = item.chain.length === 0 ? optionalBundlesOf(manifest) : new Set<string>()
     for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies'] as const) {
       for (const [name, range] of Object.entries(manifest[section] ?? {})) {
+        if (section === 'dependencies' && optionalBundles.has(name)) {
+          if (installedPackage(canonical, name) === undefined) throw new Error(`optional bundle is missing: ${[...chain, name].join(' -> ')}`)
+          continue
+        }
         rejectExperimental(name, [...chain, name])
         if (range.startsWith('npm:')) {
           rejectExperimental(range.slice(4), [...chain, `${name} (${range})`])
@@ -48,6 +55,16 @@ export function verifyInstalledProductIsolation(directory: string): number {
   return visited.size
 }
 
+/** The names under the entry package's `dsh.optionalBundles`, the shipped bundles no default composition selects. */
+function optionalBundlesOf(manifest: InstalledManifest): Set<string> {
+  const offered = manifest.dsh?.optionalBundles
+  if (offered === undefined) return new Set()
+  if (!Array.isArray(offered) || !offered.every(name => typeof name === 'string')) {
+    throw new Error(`${manifest.name}: dsh.optionalBundles must be a list of package names`)
+  }
+  return new Set(offered)
+}
+
 function rejectExperimental(name: string, chain: readonly string[]): void {
   if (name.startsWith('@deepseek-ai/dsh-experimental-')) {
     throw new Error(`default product includes an experimental package: ${chain.join(' -> ')}`)

+ 36 - 0
scripts/verify-default-product-isolation.spec.ts

@@ -61,6 +61,42 @@ describe('default product isolation', () => {
     expect(verifyDefaultProductIsolation(root)).toMatchObject({ failures: [], packageCount: 5, configCount: 2 })
   })
 
+  it('ships an optional bundle switched off: its graph is outside the product, its name stays out of imports and defaults', () => {
+    const root = fixture()
+    const layer = '@deepseek-ai/dsh-experimental-layer'
+    write(root, 'packages/experimental/layer/package.json', {
+      name: layer, dependencies: { [experimental]: 'workspace:^' }, dsh: { bundle: { patch: './cordis.patch.yml' } },
+    })
+    write(root, 'packages/experimental/layer/cordis.patch.yml', [{ insert: [{ name: experimental }] }])
+    manifest(root, 'apps/cli/package.json', {
+      dependencies: { [core]: 'workspace:^', [layer]: 'workspace:^' }, dsh: { optionalBundles: [layer] },
+    })
+    expect(verifyDefaultProductIsolation(root)).toMatchObject({ failures: [], packageCount: 5 })
+
+    // The exception covers the dependency edge alone: a runtime import or a default template still names the product.
+    write(root, 'apps/cli/src/bin.ts', `import '${layer}'\n`)
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`apps/cli/src/bin.ts -> ${layer}`)
+    write(root, 'apps/cli/src/bin.ts', 'export {}\n')
+    write(root, profile, `export const PROFILE_TEMPLATES = { web: { bundles: ['${base}', '${layer}'] } }\n`
+      + `export const DEFAULT_PROFILE_BUNDLES = ['${base}']\n`)
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`optional bundle ${layer} must not be a default bundle`)
+  })
+
+  it('requires each optional bundle to be a runtime dependency that declares a bundle patch', () => {
+    const root = fixture()
+    manifest(root, 'apps/cli/package.json', { dsh: { optionalBundles: [experimental] } })
+    const failures = verifyDefaultProductIsolation(root).failures.join('\n')
+    expect(failures).toContain(`optional bundle ${experimental} must be a runtime dependency`)
+    expect(failures).toContain(`optional bundle ${experimental} must declare dsh.bundle.patch`)
+
+    manifest(root, 'apps/cli/package.json', { dsh: { optionalBundles: 'not-a-list' } })
+    expect(verifyDefaultProductIsolation(root).failures).toContain('apps/cli/package.json: dsh.optionalBundles must be a list of package names')
+
+    // An experimental runtime dependency the list does not name is still a product requirement.
+    manifest(root, 'apps/cli/package.json', { dependencies: { [core]: 'workspace:^', [experimental]: 'workspace:^' }, dsh: {} })
+    expect(verifyDefaultProductIsolation(root).failures.join('\n')).toContain(`@deepseek-ai/dsh dependencies -> ${experimental}`)
+  })
+
   it.each(['dependencies', 'optionalDependencies', 'peerDependencies'])(
     'rejects transitive experimental %s',
     (section) => {

+ 28 - 3
scripts/verify-default-product-isolation.ts

@@ -1,4 +1,8 @@
-/** Keep experimental packages outside default installations, runtime imports, and shipped compositions. */
+/**
+ * Keep experimental packages outside default installations, runtime imports, and shipped compositions.
+ * The one declared exception is a bundle the installation lists under `dsh.optionalBundles`: shipped for the
+ * person to switch on, selected by no shipped template, its own dependency graph outside the default product's.
+ */
 
 import { existsSync, globSync, readFileSync, statSync } from 'node:fs'
 import { basename, dirname, extname, relative, resolve } from 'node:path'
@@ -26,7 +30,7 @@ interface Manifest {
   optionalDependencies?: Record<string, string>
   peerDependencies?: Record<string, string>
   devDependencies?: Record<string, string>
-  dsh?: { bundle?: { patch?: string }; configTrees?: Array<{ path: string }> }
+  dsh?: { bundle?: { patch?: string }; configTrees?: Array<{ path: string }>; optionalBundles?: unknown }
 }
 
 interface Package {
@@ -68,9 +72,28 @@ export function verifyDefaultProductIsolation(root: string): ProductIsolationRes
   for (const path of ['apps/cli/package.json', 'apps/web/package.json', 'python/sdk-runtime/package.json']) {
     if (!existsSync(resolve(root, path))) failures.push(`missing default product root ${path}`)
   }
-  if (directories.get(resolve(root, 'apps/cli'))?.manifest.name !== '@deepseek-ai/dsh') {
+  const cli = directories.get(resolve(root, 'apps/cli'))
+  if (cli?.manifest.name !== '@deepseek-ai/dsh') {
     failures.push('apps/cli/package.json must identify @deepseek-ai/dsh')
   }
+  // The bundles the installation ships switched off: each a runtime dependency that is a bundle, none a default.
+  const optionalBundles = new Set<string>()
+  const offered = cli?.manifest.dsh?.optionalBundles
+  if (offered !== undefined) {
+    if (!Array.isArray(offered) || !offered.every(name => typeof name === 'string')) {
+      failures.push('apps/cli/package.json: dsh.optionalBundles must be a list of package names')
+    } else {
+      for (const name of offered) {
+        if (cli?.manifest.dependencies?.[name] === undefined) {
+          failures.push(`apps/cli/package.json: optional bundle ${name} must be a runtime dependency`)
+        }
+        if (packages.get(name)?.manifest.dsh?.bundle?.patch === undefined) {
+          failures.push(`apps/cli/package.json: optional bundle ${name} must declare dsh.bundle.patch`)
+        }
+        optionalBundles.add(name)
+      }
+    }
+  }
 
   const queue: Package[] = []
   const visited = new Set<string>()
@@ -216,6 +239,7 @@ export function verifyDefaultProductIsolation(root: string): ProductIsolationRes
       if (packages.get(name)?.manifest.dsh?.bundle?.patch === undefined) {
         failures.push(`${PROFILE_SOURCE}: default bundle ${name} must declare dsh.bundle.patch`)
       }
+      if (optionalBundles.has(name)) failures.push(`${PROFILE_SOURCE}: optional bundle ${name} must not be a default bundle`)
     }
     const webLayers = selection.webBundles.flatMap((name) => {
       const pkg = packages.get(name)
@@ -262,6 +286,7 @@ export function verifyDefaultProductIsolation(root: string): ProductIsolationRes
     const { manifest } = pkg
     for (const section of RUNTIME_SECTIONS) {
       for (const [name, range] of Object.entries(manifest[section] ?? {})) {
+        if (pkg === cli && section === 'dependencies' && optionalBundles.has(name)) continue
         dependency(name, range, pkg, `${manifest.name} ${section}`)
       }
     }