Browse Source

fix(plugin-manager): require full access for agent tool calls

turtle1999 3 weeks ago
parent
commit
470d4aae11

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-creator-persistent-plugin-management.md
-2026-09-16-creator-persistent-plugin-management.md: 96abb49ef07c8ecae03bde986dce9f1935cdc569
-2026-09-16-creator-persistent-plugin-management.zh.md: d0739e40b7484d8de89f04f1f7ce49537e05756b
+2026-09-16-creator-persistent-plugin-management.md: d2bd3a1a007f55363ed76b058b336201a1ea3e9f
+2026-09-16-creator-persistent-plugin-management.zh.md: 2e8b21cdc5c7f6dc49d0234dc2641dd2c56b3f08

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

@@ -10,7 +10,7 @@ Agents need to install capabilities and use them during the same conversation. G
 
 ## Decision
 
-Creator mode enables the existing `plugin_manager` tool. Agents author packages and Loader YAML patches as workspace files, then install them through `install_bundle`. An MCP connection is a configuration-only bundle inserting the installed `dsh-mcp-client`; a UI bundle includes a Host entry and a Client artifact. Profile locking, package installation, enablement and HMR remain owned by the existing manager.
+Creator mode enables the existing `plugin_manager` tool. Agents author packages and Loader YAML patches as workspace files, then install them through `install_bundle`. An MCP connection is a configuration-only bundle inserting the installed `dsh-mcp-client`; a UI bundle includes a Host entry and a Client artifact. Profile locking, package installation, enablement and HMR remain owned by the existing manager. The entire management tool requires `danger-full-access`: profile changes can load code with host permissions and affect other sessions. Permission is checked on each execution, including inventory reads; human-operated Web and CLI controls retain their existing behavior.
 
 The model sees two read-only Cordis inspection tools. Generated-code define/run/stop/undefine and dynamic self-inspection tool APIs are absent. Existing runtime and Client consumers retain their services; historical session cards remain readable. This supersedes only the model-facing mutation workflow in the [self-referential toolset decision](../feature/2026-07-08-self-referential-cordis-toolset.md); its runtime ownership and sandbox rationale remain independently relevant. The [profile transaction decision](2026-09-14-current-profile-plugin-management.md) continues to govern locking, package installation, and partial failures.
 

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

@@ -10,7 +10,7 @@ agent 需要安装能力并在同一段对话中使用。生成代码工具在
 
 ## Decision
 
-创造模式启用现有的 `plugin_manager` 工具。agent 将包和 Loader YAML patch 写入工作区文件,再通过 `install_bundle` 安装。MCP 连接是插入已安装 `dsh-mcp-client` 的纯配置组合包;UI 组合包包含 Host 入口和 Client 产物。profile 锁、包安装、启停和 HMR 继续由现有管理器负责。
+创造模式启用现有的 `plugin_manager` 工具。agent 将包和 Loader YAML patch 写入工作区文件,再通过 `install_bundle` 安装。MCP 连接是插入已安装 `dsh-mcp-client` 的纯配置组合包;UI 组合包包含 Host 入口和 Client 产物。profile 锁、包安装、启停和 HMR 继续由现有管理器负责。 整个管理工具要求 `danger-full-access`:profile 变更能以宿主权限加载代码,并影响其他会话。每次执行都会检查权限,包括查询列表;由用户直接操作的 Web 和 CLI 控件保持原有行为。
 
 模型可见两个只读 Cordis 检查工具,不再提供生成代码的 define/run/stop/undefine 和动态自省工具 API。现有运行时和 Client 消费者保留其服务;历史会话卡片仍然可读。这仅取代[自引用工具集决策](../feature/2026-07-08-self-referential-cordis-toolset.zh.md)中的模型侧变更流程;其运行时所有权和沙箱依据仍有独立价值。[profile 事务决策](2026-09-14-current-profile-plugin-management.zh.md)继续规定锁、包安装和部分失败行为。
 

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

@@ -10,6 +10,9 @@ import { startHttpMcpFixture } from '../../../../../../packages/mcp/mcp-client/t
 
 interface Observation {
   before: string[]
+  denied: { isError: boolean; content: unknown }
+  afterDenied: string[]
+  bundlesAfterDenied: { name: string }[]
   after: string[]
   other: string[]
   remaining: string[]
@@ -72,6 +75,10 @@ it('configures MCP on a live profile, restores it on restart, and removes its to
     expect(initial.before).not.toContain(retired)
   }
   expect(initial.before).not.toContain('mcp__demo__ping')
+  expect(initial.denied.isError).toBe(true)
+  expect(JSON.stringify(initial.denied.content)).toContain('plugin_manager requires danger-full-access permission')
+  expect(initial.afterDenied).toEqual(initial.before)
+  expect(initial.bundlesAfterDenied.map(bundle => bundle.name)).not.toContain('@test/creator-mcp')
   expect(initial.result).toMatchObject({ application: 'applied', changed: true })
   expect(initial.after).toContain('mcp__demo__ping')
   expect(initial.other).toContain('mcp__demo__ping')

+ 12 - 4
apps/cli/tests/profiles/web/tests/fixtures/creator-plugin-manager.mjs

@@ -1,5 +1,5 @@
 /** Test-only IPC assertions over real Creator presets and profile management. */
-export const inject = ['agents', 'agentPresets', 'tools', 'pluginManager']
+export const inject = ['agents', 'agentPresets', 'tools', 'pluginManager', 'permissionPresets']
 
 export function apply(ctx, config) {
   const receive = message => {
@@ -22,16 +22,24 @@ export function apply(ctx, config) {
     try {
       const first = await make(`${phase}-first`)
       const before = names(first)
+      const manage = args => ctx.tools.execute({ name: 'plugin_manager', arguments: args, agent: first,
+        callId: `${phase}-manage`, signal: new AbortController().signal })
+      ctx.permissionPresets.set(first.session, 'workspace-write')
+      const denied = await manage({ action: 'install_bundle', target: config.bundle })
+      const afterDenied = names(first)
+      const bundlesAfterDenied = await ctx.pluginManager.listBundles()
+      ctx.permissionPresets.set(first.session, 'danger-full-access')
       const result = phase === 'initial'
-        ? await ctx.pluginManager.installBundle(config.bundle)
+        ? JSON.parse((await manage({ action: 'install_bundle', target: config.bundle })).value)
         : ctx.pluginManager.listBundles()
       const second = await make(`${phase}-second`)
       const after = names(first)
       const other = names(second)
       const ping = await ctx.tools.execute({ name: 'mcp__demo__ping', arguments: {}, agent: first,
         callId: `${phase}-ping`, signal: new AbortController().signal })
-      const removed = phase === 'restart' ? await ctx.pluginManager.removeBundle('@test/creator-mcp') : undefined
-      return { before, result, after, other, ping, removed, remaining: names(first) }
+      const removed = phase === 'restart'
+        ? JSON.parse((await manage({ action: 'remove_bundle', target: '@test/creator-mcp' })).value) : undefined
+      return { before, denied, afterDenied, bundlesAfterDenied, result, after, other, ping, removed, remaining: names(first) }
     } finally {
       await Promise.all(handles.map(handle => handle.dispose()))
     }

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 4b86112a9eab5c8bb24de7c434154053cb4eaaeb
-module-graph.zh.md: 6f26a45a18efa66a94ef43eeaa047ad11cfffdea
+module-graph.md: 9c00bd774bd02501f9195e9957b5abd403e0936c
+module-graph.zh.md: c23f07d3f618db54a094d4a6c0920981a9cacb3e

+ 2 - 1
docs/module-graph.md

@@ -1088,6 +1088,7 @@ flowchart TD
   pkg_plugin_manager --> pkg_brand
   pkg_plugin_manager --> pkg_hmr
   pkg_plugin_manager --> pkg_host_plugin_inventory
+  pkg_plugin_manager --> pkg_sandbox_policy
   pkg_plugin_manager --> pkg_subprocess
   pkg_plugin_manager --> pkg_tools
   pkg_plugin_manager --> pkg_typert_protocol
@@ -1546,7 +1547,7 @@ flowchart TD
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
-| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |

+ 2 - 1
docs/module-graph.zh.md

@@ -1090,6 +1090,7 @@ flowchart TD
   pkg_plugin_manager --> pkg_brand
   pkg_plugin_manager --> pkg_hmr
   pkg_plugin_manager --> pkg_host_plugin_inventory
+  pkg_plugin_manager --> pkg_sandbox_policy
   pkg_plugin_manager --> pkg_subprocess
   pkg_plugin_manager --> pkg_tools
   pkg_plugin_manager --> pkg_typert_protocol
@@ -1548,7 +1549,7 @@ flowchart TD
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
-| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |

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

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/tool-catalog.md
-tool-catalog.md: 2d384ee30d5d068f4cbfb51265cfd0202b83deff
-tool-catalog.zh.md: c8949dfeb50cd3843d5c4ea1fbbbc75e7d23ff8c
+tool-catalog.md: 02c552f869ed9ad5894d4bd2883b68bc18a773c3
+tool-catalog.zh.md: 647f389dd79102c48bd6fd29a231cce954bc4fd9

+ 2 - 2
docs/tool-catalog.md

@@ -15,7 +15,7 @@ This table connects model-visible tool names to the plugin package and service s
 
 | Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note |
 | --- | --- | --- | --- | --- | --- |
-| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager` | `tool/call`, `tool/result`, `user/message` | - | - |
+| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager`, `ctx.sandboxPolicy` | `tool/call`, `tool/result`, `user/message` | - | - |
 | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. |
@@ -51,7 +51,7 @@ This table connects model-visible tool names to the plugin package and service s
 
 ### `plugin_manager`
 
-List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.
+List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Requires danger-full-access permission for every action. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.
 
 ```json
 {

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

@@ -19,7 +19,7 @@
 
 | 工具包 | 模型可见名称 | 依赖 | 写入/影响 | 随产品发布的别名 | 部署说明 |
 | --- | --- | --- | --- | --- | --- |
-| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager` | `tool/call`, `tool/result`, `user/message` | - | - |
+| `@deepseek-ai/dsh-plugin-manager` | `plugin_manager` | `ctx.tools`, `ctx.pluginManager`, `ctx.sandboxPolicy` | `tool/call`, `tool/result`, `user/message` | - | - |
 | `@deepseek-ai/dsh-mcp-resources` | `list_mcp_resource_templates`, `list_mcp_resources`, `read_mcp_resource` | `ctx.tools`, `ctx.mcpResources` | `tool/call`, `tool/result` | - | - |
 | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - |
 | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after a UI/provider answers the question` | - | ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 |
@@ -55,7 +55,7 @@
 
 ### `plugin_manager`
 
-管理当前 profile 的持久插件:列出和读取配置、添加已安装插件、替换配置、删除 profile 自有条目、启停条目或组合包,以及安装或移除组合包。变更影响该 profile 的所有会话。先列出条目以获取准确标识。包安装可能运行已获批准的构建脚本。支持热更新的 profile 立即应用变更;仅启动时加载的 profile 需要重启。
+列出当前 profile 中的插件或组合包,启用或禁用它们,安装组合包或移除已安装的组合包。每项操作都要求 danger-full-access 权限。变更影响该 profile 的所有会话。先列出条目以获取准确标识。包安装可能运行已获批准的构建脚本。支持热更新的 profile 立即应用变更;仅启动时加载的 profile 需要重启。
 
 ```json
 {

+ 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: cda59c4d15ee429945e54729e480b11c1aa3b7d9
-README.zh.md: 933a53552f5f68dd02cc8795c9dbb0deb4d7ffb5
+README.md: 750b41b3b0ef2dde8f3358f45a98a4345c9d349b
+README.zh.md: 7755ec7feacd661eb68e8c97934d47ba390cca9a

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

@@ -26,7 +26,7 @@ Manage the current profile's plugins without editing configuration by hand. Enab
 <a id="use-this-package"></a>
 ## Use this package
 
-Base-backed profiles provide the manager. In Web, the sidebar's **Plugins** page ([ui-plugin-manager](../../client/ui-plugin-manager/README.md)) manages the profile's bundles and their uniquely addressable rows; the Settings Plugin list stays read-only. Agent-preset rows remain read-only. The `plugin_manager` tool exposes the same operations and is enabled in Creator mode. Other presets keep it disabled by default.
+Base-backed profiles provide the manager. In Web, the sidebar's **Plugins** page ([ui-plugin-manager](../../client/ui-plugin-manager/README.md)) manages the profile's bundles and their uniquely addressable rows; the Settings Plugin list stays read-only. Agent-preset rows remain read-only. The `plugin_manager` tool exposes the same operations and is enabled in Creator mode. Other presets keep it disabled by default. Every tool action requires the calling session’s `danger-full-access` sandbox mode; lower permissions return an error before accessing the manager.
 
 For a deployment without agent presets, enable the tool in the profile patch. Preset-backed sessions use their preset’s `tool-plugin-manager` entry.
 

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

@@ -26,7 +26,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-基于 base 的 profile 提供管理服务。在 Web 中,侧边栏的**插件**页([ui-plugin-manager](../../client/ui-plugin-manager/README.zh.md))管理 profile 的组合包及其能唯一定位的行;设置页的插件列表保持只读。Agent 预设条目保持只读。`plugin_manager` 工具提供相同操作,在 Creator 模式中启用。其他预设仍默认禁用。
+基于 base 的 profile 提供管理服务。在 Web 中,侧边栏的**插件**页([ui-plugin-manager](../../client/ui-plugin-manager/README.zh.md))管理 profile 的组合包及其能唯一定位的行;设置页的插件列表保持只读。Agent 预设条目保持只读。`plugin_manager` 工具提供相同操作,在 Creator 模式中启用。其他预设仍默认禁用。 每个工具操作都要求调用会话使用 `danger-full-access` 沙箱模式;较低权限会在访问管理器前返回错误。
 
 未使用 Agent 预设的部署在 profile patch 中启用工具;使用预设的会话由其预设中的 `tool-plugin-manager` 条目控制。
 

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

@@ -68,7 +68,8 @@
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-hmr": "workspace:^",
-    "@deepseek-ai/dsh-brand": "workspace:^"
+    "@deepseek-ai/dsh-brand": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-policy": "workspace:^"
   },
   "peerDependenciesMeta": {
     "@deepseek-ai/dsh-hmr": {
@@ -85,6 +86,10 @@
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-hmr": "workspace:^",
     "@deepseek-ai/cordis-plugin-timer": "workspace:^",
-    "@deepseek-ai/dsh-brand": "workspace:^"
+    "@deepseek-ai/dsh-brand": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
+    "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^"
   }
 }

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

@@ -2,11 +2,12 @@
 import { assertNever } from '@deepseek-ai/dsh-util-values'
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from './index.ts'
+import type {} from '@deepseek-ai/dsh-sandbox-policy'
 import type { PluginEntryId } from './types.ts'
 import { defineTool } from '@deepseek-ai/dsh-tools'
 
 /** Required services for the management tool. */
-export const inject = ['tools', 'pluginManager']
+export const inject = ['tools', 'pluginManager', 'sandboxPolicy']
 
 /** Register one management tool for discovery and the four persistent actions.
  * @param ctx Agent-scoped tool registration context.
@@ -14,7 +15,7 @@ export const inject = ['tools', 'pluginManager']
 export function apply(ctx: Context): void {
   ctx.tools.register(defineTool({
     name: 'plugin_manager',
-    description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.',
+    description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Requires danger-full-access permission for every action. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.',
     parameters: {
       action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle'], description: 'Management operation.' },
       target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
@@ -27,7 +28,11 @@ export function apply(ctx: Context): void {
       schema: { type: 'string' },
       render: (_args, value) => [{ type: 'text', text: value }],
     },
-    async execute(args) {
+    async execute(args, exec) {
+      const policy = ctx.sandboxPolicy.resolve(exec.agent === undefined ? {} : { session: exec.agent.session })
+      if (policy.mode !== 'danger-full-access') {
+        throw new Error('plugin_manager requires danger-full-access permission. Ask the user to switch this session to Full access before retrying.')
+      }
       const manager = ctx.pluginManager
       switch (args.action) {
         case 'list_plugins':

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

@@ -3,6 +3,10 @@ import { Context } from '@deepseek-ai/cordis'
 import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
 import ToolRuntime from '@deepseek-ai/dsh-tools'
 import { ToolCallId } from '@deepseek-ai/dsh-llm'
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import SandboxPolicy, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
+import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
+import SessionProjections from '@deepseek-ai/dsh-session-projection'
 import { expect, it, onTestFinished, vi } from 'vitest'
 import type PluginManager from '../src/index.ts'
 import * as tool from '../src/tools.ts'
@@ -12,7 +16,7 @@ function resultText(result: Awaited<ReturnType<ToolRuntime['execute']>>): string
   return result.value
 }
 
-async function fixture() {
+async function fixture(mode: 'read-only' | 'workspace-write' | 'danger-full-access' = 'danger-full-access') {
   const ctx = new Context()
   onTestFinished(() => ctx.fiber.dispose())
   const manager = {
@@ -26,12 +30,40 @@ async function fixture() {
   ctx.provide('pluginManager', manager as unknown as PluginManager)
   await ctx.plugin(SystemPrompt)
   await ctx.plugin(ToolRuntime)
+  await ctx.plugin(SessionProjections)
+  await ctx.plugin(SandboxPolicy, { mode })
   const fiber = await ctx.plugin(tool)
-  const call = (args: unknown) => ctx.tools.execute({ name: 'plugin_manager', arguments: args,
+  const call = (args: unknown, agent?: Agent) => ctx.tools.execute({ name: 'plugin_manager', arguments: args,
+    ...agent === undefined ? {} : { agent },
     callId: ToolCallId('manager-call'), signal: new AbortController().signal })
   return { ctx, manager, call, fiber }
 }
 
+it.each(['read-only', 'workspace-write'] as const)('denies every management action in %s before accessing the manager', async (mode) => {
+  const { call, manager } = await fixture(mode)
+  for (const action of ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle']) {
+    const result = await call({ action, target: 'bundle', enabled: true })
+    expect(result.isError).toBe(true)
+    expect(JSON.stringify(result.content)).toContain('plugin_manager requires danger-full-access permission')
+  }
+  for (const method of Object.values(manager)) expect(method).not.toHaveBeenCalled()
+})
+
+it('checks the calling session on each execution, including after permission is revoked', async () => {
+  const { call, manager } = await fixture()
+  const id = SessionId('manager-permissions')
+  const session = Session.create(id, undefined, { version: SESSION_FORMAT_VERSION, id, createdAt: 0, isSeeded: false })
+  const agent = { session } as unknown as Agent
+  setSandboxMode(session, 'workspace-write')
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(true)
+  expect(manager.listPlugins).not.toHaveBeenCalled()
+  setSandboxMode(session, 'danger-full-access')
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(false)
+  setSandboxMode(session, 'read-only')
+  expect((await call({ action: 'list_plugins' }, agent)).isError).toBe(true)
+  expect(manager.listPlugins).toHaveBeenCalledTimes(1)
+})
+
 it('paginates inventories with an explicit continuation and total', async () => {
   const { call } = await fixture()
   const first = await call({ action: 'list_plugins' })

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

@@ -52,6 +52,9 @@
     },
     {
       "path": "../../util/brand"
+    },
+    {
+      "path": "../../sandbox/sandbox-policy"
     }
   ]
 }

+ 12 - 0
pnpm-lock.yaml

@@ -1537,6 +1537,9 @@ importers:
       '@deepseek-ai/cordis-plugin-timer':
         specifier: workspace:^
         version: link:../../../vendor/timer
+      '@deepseek-ai/dsh-agent':
+        specifier: workspace:^
+        version: link:../../core/agent
       '@deepseek-ai/dsh-brand':
         specifier: workspace:^
         version: link:../../util/brand
@@ -1546,6 +1549,15 @@ importers:
       '@deepseek-ai/dsh-host-plugin-inventory':
         specifier: workspace:^
         version: link:../../host/plugin-inventory
+      '@deepseek-ai/dsh-sandbox-policy':
+        specifier: workspace:^
+        version: link:../../sandbox/sandbox-policy
+      '@deepseek-ai/dsh-session':
+        specifier: workspace:^
+        version: link:../../core/session
+      '@deepseek-ai/dsh-session-projection':
+        specifier: workspace:^
+        version: link:../../session/session-projection
       '@deepseek-ai/dsh-subprocess':
         specifier: workspace:^
         version: link:../../subprocess/subprocess

+ 3 - 1
scripts/gen-tool-catalog.ts

@@ -66,6 +66,7 @@ import * as ToolTeam from '@deepseek-ai/dsh-experimental-tool-agent-team'
 import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
 import type PluginManager from '@deepseek-ai/dsh-plugin-manager'
 import * as PluginManagerTools from '@deepseek-ai/dsh-plugin-manager/tools'
+import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
 import McpResources from '@deepseek-ai/dsh-mcp-resources'
 import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
 import { registerListSubagentModels } from '../packages/subagent/tool-subagent/src/list-models.ts'
@@ -205,11 +206,12 @@ const TOOL_PACKAGES: ToolPackage[] = [
     pkg: '@deepseek-ai/dsh-plugin-manager',
     dir: 'plugin-manager',
     source: 'packages/boot/plugin-manager/src/tools.ts',
-    requires: ['ctx.tools', 'ctx.pluginManager'],
+    requires: ['ctx.tools', 'ctx.pluginManager', 'ctx.sandboxPolicy'],
     writes: ['tool/call', 'tool/result', 'user/message'],
     async mount(ctx) {
       // Schema harvest never executes a management method or opens a profile.
       ctx.provide('pluginManager', {} as PluginManager)
+      await ctx.plugin(SandboxPolicy)
       await ctx.plugin(PluginManagerTools)
     },
   },

+ 1 - 0
snapshots/session/plugin-manager-mcp/snapshot.yml

@@ -3,6 +3,7 @@ scenario: plugin-manager-mcp
 profile: headless
 composition: plugin-manager-mcp
 recording: live
+permission: danger-full-access
 header:
   class: plugin-manager-mcp
   pin: true

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

@@ -262,7 +262,7 @@
     },
     {
       "name": "plugin_manager",
-      "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.",
+      "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Requires danger-full-access permission for every action. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.",
       "parameters": {
         "type": "object",
         "properties": {
@@ -970,7 +970,7 @@
       },
       {
         "name": "plugin_manager",
-        "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.",
+        "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Requires danger-full-access permission for every action. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.",
         "parameters": {
           "type": "object",
           "properties": {

+ 1 - 0
snapshots/session/plugin-manager/snapshot.yml

@@ -3,6 +3,7 @@ scenario: plugin-manager
 profile: headless
 composition: plugin-manager
 recording: live
+permission: danger-full-access
 header:
   class: plugin-manager
   pin: true

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

@@ -262,7 +262,7 @@
     },
     {
       "name": "plugin_manager",
-      "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.",
+      "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Requires danger-full-access permission for every action. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart.",
       "parameters": {
         "type": "object",
         "properties": {