dsh-sandbox-param-normalizer 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rengy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,131 @@
1
- # Temporary Holding Version
1
+ # dsh-sandbox-param-normalizer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 按会话实时裁剪 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)沙箱提权参数的宿主插件:模型看不到当前会话里根本不可能生效的 `sandbox_permissions` / `justification`,仍然塞进来的无效提权参数也在执行前被清理掉。
4
+
5
+ **支持的 dsh 版本**:`0.2.0-rc.2` 起的全部 `0.2.x`(含 `0.2.1-alpha.1`、`0.2.1-alpha.2`)。范围由 `engines.dsh` 与全部 `@deepseek-ai/dsh-*` 的 peer 依赖共同声明。
6
+
7
+ ## 它解决什么问题
8
+
9
+ dsh 的 `bash`、`pwsh`、`write`、`edit` 在**注册期**静态声明 `sandbox_permissions` 与 `justification` 两个提权字段——只要挂载了 confining 后端,所有模型都能在 schema 里看到它们。但这两个字段能否生效,取决于**每个会话运行期的两个事实**:当前沙箱模式(`read-only` / `workspace-write` / `danger-full-access`)与审批策略(`never` / `ask`)。
10
+
11
+ 于是出现三类必然失败的调用,模型收到报错后换参数重试,又撞上同样的报错,陷入"验证失败 → 重试 → 再失败"的循环:
12
+
13
+ ```
14
+ Error: invalid justification: expected a non-empty sentence
15
+ Error: sandbox escalation to "danger-full-access" is not strictly wider than this call's current "danger-full-access" mode
16
+ Error: sandbox escalation to "workspace-write" is not strictly wider than this call's current "danger-full-access" mode
17
+ ```
18
+
19
+ All Access 会话(`danger-full-access` + `never`)是最典型的场景:当前模式已经最宽,不存在任何"更宽"的合法目标;审批又被禁用,任何提权请求都只会被拒。模型却仍然看得见字段。
20
+
21
+ ## 功能说明
22
+
23
+ | 能力 | 说明 |
24
+ | --- | --- |
25
+ | 会话感知的 schema 投影 | 按每个 Agent 当前的沙箱模式与审批策略,实时裁剪模型可见的提权字段:整组隐藏,或把 `sandbox_permissions` 的 `enum` 收窄到真正可用的模式 |
26
+ | 执行前参数归一化 | 对仍然抵达执行层的参数做最小处理:摘除同级/降级/畸形的提权字段、清理孤立 `justification`、为确实合法的提权补齐缺失理由 |
27
+ | 按 Agent 隔离 | 同一进程内不同会话可以各自处于不同沙箱模式与审批策略,互不影响;投影在包装器读取 schema 时实时计算,用户中途切换权限立即生效 |
28
+ | 不侵入宿主 | 只在各 Agent 自己的作用域层注册包装定义,不修改 dsh 安装目录、不改任何核心包、不就地改写全局工具定义 |
29
+ | 协作式包装协议 | 通过 `Symbol.for('dsh.tool-wrapper.v1')` 识别同进程内已存在的包装器,优先贡献一层而不是重复包裹 |
30
+ | 装配期契约校验 | 启动与重同步时校验目标工具的提权 schema 形状;契约漂移只记一条按 Agent + 工具去重的 `warn`,并让该工具保持原状——插件最差退化成"没装",既不会在提示词装配路径上抛错,也不会阻断会话创建 |
31
+ | 运行日志 | 启动一行 `info` 汇总;契约不符或重同步失败按 Agent + 工具去重后记 `warn` |
32
+
33
+ ### 权限真值表
34
+
35
+ 插件把 dsh 的两个运行期事实收敛成一份"可行提权目标"集合,schema 投影与参数归一化共用同一个答案:
36
+
37
+ | 会话沙箱模式 | 审批策略 | 模型可见的提权目标 |
38
+ | --- | --- | --- |
39
+ | `read-only` | `ask` | `workspace-write`、`danger-full-access` |
40
+ | `read-only` | `never` | 无(两个字段整组隐藏) |
41
+ | `workspace-write` | `ask` | `danger-full-access` |
42
+ | `workspace-write` | `never` | 无 |
43
+ | `danger-full-access` | `ask` | 无(已是最宽,没有更宽目标) |
44
+ | `danger-full-access` | `never` | 无 |
45
+
46
+ ### 参数归一化规则
47
+
48
+ | 模型提交的提权参数 | 处理方式 |
49
+ | --- | --- |
50
+ | 目标不在可行集合内(同级、降级、非字符串) | 摘除两个提权字段,该次调用按会话标准模式执行 |
51
+ | 只带了孤立的 `justification` | 只摘除 `justification`,其余参数原样保留 |
52
+ | 目标可行,但理由缺失、空白或不是字符串 | 补一句如实说明"理由缺失"的兜底理由,仍走 dsh 原有审批流程 |
53
+ | 目标可行且理由完整 | 原样通过,不做任何改写 |
54
+
55
+ **不做什么**:不新增、不放大任何提权能力(第二层只会**摘除**或**补齐理由**,绝不把无效请求变成有效请求);不绕过、不代替 dsh 的审批——合法的真正提权仍完整经过 dsh 的 `approveEscalation` 与用户审批;不处理 `read`、`grep` 等不含提权字段的工具;不覆盖 PTC 模式下的 `run_code`(该工具自带独立的提权参数与执行面,不在本插件的作用域内);不提供客户端 UI。
56
+
57
+ ## 安装部署
58
+
59
+ ```sh
60
+ # 从 npm 装进 web profile;声明了 dsh.bundle 的包会被自动登记进 dsh.profile.bundles
61
+ dsh plugin --profile web add dsh-sandbox-param-normalizer
62
+
63
+ # 或本地 tarball
64
+ pnpm build && pnpm pack
65
+ dsh plugin --profile web add ./dsh-sandbox-param-normalizer-0.1.0.tgz
66
+
67
+ # 开发检出直接装目录
68
+ dsh plugin --profile web add link:/path/to/dsh-sandbox-param-normalizer
69
+
70
+ # 卸载
71
+ dsh plugin --profile web remove dsh-sandbox-param-normalizer
72
+ ```
73
+
74
+ 包内 [`cordis.patch.yml`](./cordis.patch.yml) 是 `dsh.bundle.patch` 指向的补丁层,装配时自动应用,**无需手工编辑 profile 文件**。安装或卸载后需重启 dsh 使补丁层生效。
75
+
76
+ ### 配置项
77
+
78
+ | 字段 | 类型 | 默认 | 说明 |
79
+ | --- | --- | --- | --- |
80
+ | `logLevel` | `'silent' \| 'info'` | `'info'` | `silent` 关闭启动提示;契约失败与重同步失败的 `warn` 不受影响 |
81
+
82
+ ## 开发说明
83
+
84
+ ```sh
85
+ pnpm install
86
+ pnpm typecheck # 类型检查(源码与测试)
87
+ pnpm test # 单元与集成测试
88
+ pnpm test:coverage # 覆盖率报告
89
+ pnpm build # 构建到 lib/(先 typecheck,再 tsdown)
90
+ ```
91
+
92
+ 真机验证脚本(真机验证一律在开发容器 `dsh-sandbox-param-normalizer` 中进行):
93
+
94
+ ```sh
95
+ # 宿主机编排:打包 → 建镜像 → 起容器 → 容器内驱动一轮 GPT 对话 → 判定 → 还原插件状态
96
+ pnpm verify:host
97
+ # 对照组:同上,但先把插件写进用户补丁层临时禁用,应当判定不通过
98
+ pnpm verify:host:without-plugin
99
+ ```
100
+
101
+ 判据取自会话日志而非界面:`request/header.data.header.tools` 是**这一轮真正发给模型的工具清单**,其中 `bash` / `write` / `edit` 是否带提权字段就是插件的直接效果;工具结果里是否出现那三条原始报错则是"关插件会炸、开插件不炸"的直接证据。脚本用强制字面量的提示词让模型把注定失败的降级目标发出来,因此不依赖模型自觉。
102
+
103
+ ```
104
+ src/
105
+ index.ts 插件入口:name / inject / Config / apply
106
+ policy.ts 由沙箱模式 + 审批策略推出"可行提权目标"集合
107
+ schema-projection.ts 第一层:按会话投影模型可见的工具参数 schema
108
+ argument-normalization.ts 第二层:执行前清理/补齐提权参数
109
+ escalation-fields.ts 提权字段名与参数 schema 的低层读取工具
110
+ compatibility.ts 目标工具集合与装配期契约校验
111
+ wrapper.ts 协作式工具包装绑定(schema 投影 + 执行链)
112
+ wrapper-protocol.ts 包装协议 v1(Symbol.for 全局键)
113
+ supervisor.ts 按 Agent 装配/重同步/卸载包装器
114
+ scripts/
115
+ verify.mjs 真机验证:驱动容器内 GUI + 解析会话日志判定
116
+ tests/
117
+ pure.spec.ts 真值表、投影、归一化、契约校验
118
+ regression.spec.ts 三条原始报错的回归:钉住上游报错原文,并验证归一化后能通过 dsh 的真实校验
119
+ wrapper.spec.ts 包装链顺序、委托替换、元数据转发、协议
120
+ integration.spec.ts 真实 cordis + ToolRuntime + 沙箱策略 + 审批服务的端到端验证
121
+ ```
122
+
123
+ `regression.spec.ts` 直接调用 `@deepseek-ai/dsh-sandbox` 导出的 `validateEscalationArgs` 与 `approveEscalation`:既断言上游此刻仍会抛出那三条原文(同级那条已改为直接放行),也断言经插件归一化的参数能吃下真实校验。上游改措辞或改触发条件时,这里会先失败。
124
+
125
+ 集成测试用真实的 `ToolRuntime`、`SandboxPolicyService`、`ApprovalService`、`SessionStore` 与作用域系统,配合最简的工具替身,覆盖:All Access 会话字段整组隐藏、模式切换后 `enum` 收窄、合法提权原样通过、缺失理由补兜底、同级/孤立/降级参数被摘除、工具被替换或被作用域屏蔽后投影重算、契约漂移时只记 `warn` 且不波及其余工具。
126
+
127
+ **版本与兼容范围**:版本号唯一来源是 `package.json` 的 `version`;宿主兼容范围由 `engines.dsh` 与全部 `@deepseek-ai/dsh-*` 的 `peerDependencies` 声明,两处必须同源。
128
+
129
+ ## 许可说明
130
+
131
+ [MIT](./LICENSE)
@@ -0,0 +1,12 @@
1
+ # 本插件的 bundle 补丁层。声明 dsh.bundle 后,安装命令会把本包写进
2
+ # $DSH_HOME/profiles/<profile>/package.json 的 dsh.profile.bundles,本文件即作为
3
+ # bundle 层自动应用,不需要再手工编辑 profile 的 cordis.patch.yml。
4
+ #
5
+ # 安装:dsh plugin --profile web add dsh-sandbox-param-normalizer
6
+ # (开发检出装目录时换成 link:<本仓库绝对路径>)
7
+ #
8
+ # 行 id = 包名,重复 id 会被 Loader 静默丢弃,请勿在 preset 与 profile 两处同时挂载。
9
+ - insert:
10
+ - id: dsh-sandbox-param-normalizer
11
+ name: dsh-sandbox-param-normalizer
12
+ config: {}
package/lib/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+
4
+ //#region src/index.d.ts
5
+ /** 插件配置。 */
6
+ interface Config {
7
+ /** 启动日志级别;`silent` 关闭本插件的启动提示。 */
8
+ logLevel?: 'silent' | 'info';
9
+ }
10
+ /** 插件配置 schema(静态声明,供 DSH 配置目录与校验使用)。 */
11
+ declare const Config: z<Config>;
12
+ /** 插件名,也是包装协议里的所有者标识来源。 */
13
+ declare const name = "sandbox-param-normalizer";
14
+ /** 依赖的宿主能力:Agent 注册表、工具注册表、沙箱策略解析与审批策略读取。 */
15
+ declare const inject: string[];
16
+ /**
17
+ * 启动插件。
18
+ * @param ctx - 已注入全部依赖的插件上下文。
19
+ * @param config - 插件配置。
20
+ */
21
+ declare function apply(ctx: Context, config?: Config): void;
22
+ declare const _default: {
23
+ name: string;
24
+ inject: string[];
25
+ Config: z<Config>;
26
+ apply: typeof apply;
27
+ };
28
+ //#endregion
29
+ export { Config, apply, _default as default, inject, name };
package/lib/index.js ADDED
@@ -0,0 +1,638 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { WIDER_MODES } from "@deepseek-ai/dsh-sandbox";
3
+
4
+ //#region src/escalation-fields.ts
5
+ /** DSH 提权请求的目标模式字段名。 */
6
+ const SANDBOX_PERMISSIONS = "sandbox_permissions";
7
+ /** DSH 提权请求的一句理由字段名,必须与 {@link SANDBOX_PERMISSIONS} 成对出现。 */
8
+ const JUSTIFICATION = "justification";
9
+ /** 两个提权字段的固定顺序,用于成对校验与整组摘除。 */
10
+ const ESCALATION_FIELDS = [SANDBOX_PERMISSIONS, JUSTIFICATION];
11
+ /**
12
+ * 判断一个值是不是可枚举属性的普通对象记录(排除 `null` 与数组)。
13
+ * @param value - 待判断的值。
14
+ * @returns 是普通对象记录时为 `true`。
15
+ */
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ /**
20
+ * 读取工具参数 schema 的 `properties` 映射。
21
+ * @param parameters - 工具参数 schema。
22
+ * @returns `properties` 记录,形状不符时为 `undefined`。
23
+ */
24
+ function parameterProperties(parameters) {
25
+ if (parameters.type !== "object" || !isRecord(parameters.properties)) return void 0;
26
+ return parameters.properties;
27
+ }
28
+
29
+ //#endregion
30
+ //#region src/argument-normalization.ts
31
+ /**
32
+ * 模型提交了合法提权请求却漏写理由时补上的兜底句子。
33
+ *
34
+ * 这句会原样进入审批提示(`escalate sandbox to <mode>: <justification>`),
35
+ * 因此必须如实说明"理由缺失",不能伪造一个看起来像模型判断的理由。
36
+ */
37
+ const FALLBACK_JUSTIFICATION = "The assistant requested a one-shot sandbox escalation without a reason; approve only if this single operation should run with wider access.";
38
+ /**
39
+ * 执行前的提权参数归一化——插件第二层,也是最小回退。
40
+ *
41
+ * 第一层投影只改变模型**看到**的 schema;DSH 的参数校验是开放根对象,
42
+ * 模型仍可能(例如沿用上一轮的参数、或从旧的工具清单里抄)把提权字段塞进来,
43
+ * 于是这些参数会穿过 schema 直接进入工具执行前的 `validateEscalationArgs` /
44
+ * `approveEscalation`,触发三类必然失败的报错:
45
+ * - `invalid justification: expected a non-empty sentence`
46
+ * - `sandbox escalation to "<x>" is not strictly wider than this call's current "<y>" mode`
47
+ * - `the user rejected escalating ...`(`never` 策略下审批必然被拒)
48
+ *
49
+ * 归一化只处理**注定失败**的请求,且只动提权字段本身,其余参数原样保留:
50
+ * - 请求不是"当前模式下可行的严格更宽目标"(同级、降级、非字符串、审批被禁用)→ 整组摘除;
51
+ * - 只带了孤立的 `justification` → 只摘除该字段;
52
+ * - 请求确实合法但理由缺失或空白 → 补上 {@link FALLBACK_JUSTIFICATION},
53
+ * 让真正需要的提权仍走 DSH 原有审批流程,只是提示里如实标注理由缺失。
54
+ *
55
+ * 入参是注册表深冻结的调用参数,因此任何改动都返回新的浅拷贝对象;
56
+ * 无需改动时原样返回入参本身,便于调用方与测试判定"未触碰"。
57
+ * @param args - 模型提交的工具参数(任意形状)。
58
+ * @param viableTargets - 该调用所属会话真正可行的提权目标集合。
59
+ * @returns 归一化后的参数;无需改动时与入参同一引用。
60
+ */
61
+ function normalizeEscalationArguments(args, viableTargets) {
62
+ if (!isRecord(args)) return args;
63
+ const hasPermissions = Object.hasOwn(args, SANDBOX_PERMISSIONS);
64
+ if (!hasPermissions && !Object.hasOwn(args, JUSTIFICATION)) return args;
65
+ const requested = args[SANDBOX_PERMISSIONS];
66
+ if (!(hasPermissions && typeof requested === "string" && viableTargets.includes(requested))) {
67
+ const normalized = { ...args };
68
+ if (!hasPermissions) delete normalized[JUSTIFICATION];
69
+ else {
70
+ delete normalized[SANDBOX_PERMISSIONS];
71
+ delete normalized[JUSTIFICATION];
72
+ }
73
+ return normalized;
74
+ }
75
+ const justification = args[JUSTIFICATION];
76
+ if (typeof justification === "string" && justification.trim().length > 0) return args;
77
+ return {
78
+ ...args,
79
+ [JUSTIFICATION]: FALLBACK_JUSTIFICATION
80
+ };
81
+ }
82
+
83
+ //#endregion
84
+ //#region src/compatibility.ts
85
+ /**
86
+ * 本插件投影与归一化的目标工具集合。
87
+ *
88
+ * 只有真正**声明了提权字段**的工具才需要处理:DSH 的 shell 家族(`bash` / `pwsh`)
89
+ * 与文件家族(`write` / `edit`)在挂载了 confining 后端时都会声明
90
+ * `sandbox_permissions` + `justification`。
91
+ */
92
+ const TARGET_TOOL_NAMES = [
93
+ "bash",
94
+ "pwsh",
95
+ "write",
96
+ "edit"
97
+ ];
98
+ /** PTC 模式的保留传输工具名;它带自己的提权参数,且禁止被任何插件遮蔽。 */
99
+ const RESERVED_PTC_TOOL_NAME = "run_code";
100
+ /**
101
+ * 判断工具名是否在本插件的目标集合内。
102
+ * @param name - 工具名。
103
+ * @returns 是目标工具时为 `true`。
104
+ */
105
+ function isTargetToolName(name$1) {
106
+ return TARGET_TOOL_NAMES.includes(name$1);
107
+ }
108
+ function incompatibility(message) {
109
+ return /* @__PURE__ */ new Error(`sandbox-param-normalizer: ${message}`);
110
+ }
111
+ function requireRecord(value, label) {
112
+ if (!isRecord(value)) throw incompatibility(`${label} must be an object`);
113
+ return value;
114
+ }
115
+ /**
116
+ * 校验一个目标工具的运行时契约是否仍是本插件投影所假设的形状。
117
+ *
118
+ * 这一步在**装配期**执行(而不是模型消息装配期),把契约漂移转成一条可见的启动/重同步
119
+ * 失败,避免投影函数在提示词装配路径上抛错。校验覆盖三类事实:
120
+ * 1. 工具身份:目标集合内、且不是 PTC 保留名;
121
+ * 2. 提权字段:两个字段要么都在、要么都不在;都在时 `sandbox_permissions` 必须是
122
+ * 非空字符串枚举、`justification` 必须是字符串;
123
+ * 3. `execute` 与 `output` 契约:包装后的定义要重新注册,必须能通过注册表校验。
124
+ * @param definition - 目标工具当前的可见定义。
125
+ * @throws 任一契约不满足时抛出带插件前缀的错误。
126
+ */
127
+ function validateTargetTool(definition) {
128
+ const name$1 = definition.name;
129
+ if (name$1 === RESERVED_PTC_TOOL_NAME) throw incompatibility(`reserved tool "${RESERVED_PTC_TOOL_NAME}" cannot be wrapped`);
130
+ if (!isTargetToolName(name$1)) throw incompatibility(`unsupported target tool "${name$1}"`);
131
+ if (typeof definition.description !== "string" || typeof definition.execute !== "function") throw incompatibility(`tool "${name$1}" has an incompatible definition`);
132
+ const properties = parameterProperties(requireRecord(definition.parameters, `tool "${name$1}" parameters`));
133
+ if (properties === void 0) throw incompatibility(`tool "${name$1}" parameters must be an object schema with properties`);
134
+ const declared = ESCALATION_FIELDS.filter((field) => Object.hasOwn(properties, field));
135
+ if (declared.length === 1) throw incompatibility(`tool "${name$1}" must expose sandbox_permissions and justification together or omit both`);
136
+ if (declared.length === 2) validateEscalationSchema(name$1, properties);
137
+ const output = requireRecord(definition.output, `tool "${name$1}" output`);
138
+ if (typeof output.render !== "function" || !isRecord(output.schema)) throw incompatibility(`tool "${name$1}" output contract is incompatible`);
139
+ }
140
+ function validateEscalationSchema(name$1, properties) {
141
+ const permissions = requireRecord(properties[SANDBOX_PERMISSIONS], `tool "${name$1}" sandbox_permissions schema`);
142
+ const targets = permissions.enum;
143
+ if (permissions.type !== "string" || !Array.isArray(targets) || targets.length === 0 || !targets.every((target) => typeof target === "string")) throw incompatibility(`tool "${name$1}" sandbox_permissions must be a non-empty string enum`);
144
+ if (requireRecord(properties[JUSTIFICATION], `tool "${name$1}" justification schema`).type !== "string") throw incompatibility(`tool "${name$1}" justification must be a string`);
145
+ }
146
+
147
+ //#endregion
148
+ //#region src/policy.ts
149
+ /**
150
+ * 计算当前模式下真正可行的提权目标集合。
151
+ *
152
+ * 两个约束必须同时满足,缺一即不可行:
153
+ * 1. 目标模式必须**严格宽于**当前模式(DSH 执行期校验此点,同级或降级一律抛错);
154
+ * 2. 审批策略不能是 `never`(该策略下审批请求在服务内部直接判 `rejected`,没有渠道可走)。
155
+ * @param effectiveMode - 会话当前生效的沙箱模式。
156
+ * @param approvalPolicy - 会话当前生效的审批策略。
157
+ * @returns 可行目标;不可行时为空数组。
158
+ */
159
+ function viableEscalationTargets(effectiveMode, approvalPolicy) {
160
+ if (approvalPolicy === "never") return [];
161
+ return WIDER_MODES[effectiveMode] ?? [];
162
+ }
163
+ /**
164
+ * 解析某个 Agent 所在会话的提权可行性视图。
165
+ * @param ctx - 插件上下文,需已装配 `sandboxPolicy` 与 `approval`。
166
+ * @param agent - 目标会话所属的 Agent。
167
+ * @returns 该会话当前的沙箱模式、审批策略与可行提权目标。
168
+ */
169
+ function escalationPolicyFor(ctx, agent) {
170
+ const effectiveMode = ctx.sandboxPolicy.resolve({ session: agent.session }).mode;
171
+ const approvalPolicy = ctx.approval.overrideOf(agent.session) ?? ctx.approval.config.policy ?? "ask";
172
+ return {
173
+ effectiveMode,
174
+ approvalPolicy,
175
+ viableTargets: viableEscalationTargets(effectiveMode, approvalPolicy)
176
+ };
177
+ }
178
+
179
+ //#endregion
180
+ //#region src/schema-projection.ts
181
+ /**
182
+ * 按会话当前的可行提权目标,投影一份工具参数 schema 给模型看。
183
+ *
184
+ * 这是插件的第一层机制:DSH 的工具 schema 是注册期静态声明、跨会话共享的,而
185
+ * "能提权到哪" 是会话运行期事实。投影在**模型消息装配期**按 Agent 生效,因此:
186
+ * - 没有可行目标(`danger-full-access` 会话,或审批策略为 `never`)时整组摘除两个字段,
187
+ * 模型从源头看不到提权入口,不会再发起必然失败的调用;
188
+ * - 有可行目标时把 `sandbox_permissions` 的 `enum` 收窄到真正可用的模式,
189
+ * 避免模型选到同级或降级目标后触发 DSH 的严格变宽校验错误。
190
+ *
191
+ * 本函数是**全函数**:形状不像 DSH 工具 schema 时原样返回,绝不在模型装配期抛错
192
+ * (形状问题由装配期的 {@link validateTargetTool} 提前拦截)。
193
+ * @param parameters - 目标工具当前的参数 schema。
194
+ * @param viableTargets - 该会话真正可行的提权目标集合。
195
+ * @returns 投影后的参数 schema;无可投影内容时原样返回入参。
196
+ */
197
+ function projectEscalationParameters(parameters, viableTargets) {
198
+ const properties = parameterProperties(parameters);
199
+ if (properties === void 0) return parameters;
200
+ const hasPermissions = Object.hasOwn(properties, SANDBOX_PERMISSIONS);
201
+ const hasJustification = Object.hasOwn(properties, JUSTIFICATION);
202
+ if (!hasPermissions && !hasJustification) return parameters;
203
+ if (!hasPermissions || !hasJustification) return parameters;
204
+ const projected = structuredClone(parameters);
205
+ const projectedProperties = parameterProperties(projected);
206
+ /* v8 ignore next -- projected is a clone of a parameters value whose properties were readable above. */
207
+ if (projectedProperties === void 0) return parameters;
208
+ if (viableTargets.length === 0) {
209
+ delete projectedProperties[SANDBOX_PERMISSIONS];
210
+ delete projectedProperties[JUSTIFICATION];
211
+ stripRequired(projected);
212
+ return projected;
213
+ }
214
+ const permissions = projectedProperties[SANDBOX_PERMISSIONS];
215
+ if (!isRecord(permissions)) return parameters;
216
+ permissions.enum = [...viableTargets];
217
+ return projected;
218
+ }
219
+ /** 把提权字段从 `required` 中摘掉;摘空后连同 `required` 一起删除。 */
220
+ function stripRequired(parameters) {
221
+ const required = parameters.required;
222
+ if (!Array.isArray(required)) return;
223
+ const kept = required.filter((value) => value !== SANDBOX_PERMISSIONS && value !== JUSTIFICATION);
224
+ if (kept.length === 0) delete parameters.required;
225
+ else parameters.required = kept;
226
+ }
227
+
228
+ //#endregion
229
+ //#region src/wrapper-protocol.ts
230
+ /**
231
+ * 协作式工具包装协议的注册键。
232
+ *
233
+ * 用 `Symbol.for` 而不是模块私有 `Symbol`:同一进程内可能加载同一插件的多个副本,
234
+ * 协议必须靠全局键互相识别,否则会出现"包装器套包装器"的重复投影与重复归一化。
235
+ */
236
+ const TOOL_WRAPPER_PROTOCOL = Symbol.for("dsh.tool-wrapper.v1");
237
+ /**
238
+ * 读取一个工具定义上的包装协议。
239
+ * @param definition - 待检查的工具定义。
240
+ * @returns 协议对象;未携带时为 `undefined`。
241
+ * @throws 协议存在但形状非法时抛错,避免静默退化成重复包装。
242
+ */
243
+ function protocolOf(definition) {
244
+ const protocol = definition[TOOL_WRAPPER_PROTOCOL];
245
+ if (protocol === void 0) return void 0;
246
+ if (protocol.version !== 1 || typeof protocol.owner !== "string" || typeof protocol.name !== "string" || typeof protocol.contribute !== "function") throw new Error(`sandbox-param-normalizer: tool "${definition.name}" exposes an invalid wrapper protocol`);
247
+ return protocol;
248
+ }
249
+
250
+ //#endregion
251
+ //#region src/wrapper.ts
252
+ /** 本插件在包装协议里的所有者标识,也是 `WrapperLayer.owner`。 */
253
+ const PLUGIN_OWNER = "dsh-sandbox-param-normalizer";
254
+ /**
255
+ * 需要从当前委托原样转发给注册表的可选**数据**成员。
256
+ *
257
+ * DSH 的 `ToolDefinition` 除 schema 与 `execute` 外,还带执行/展示元数据。包装器必须把
258
+ * 它们一并转发,否则包装后的工具会丢失超时保护与延迟加载声明。数据成员用取值器实时读取
259
+ * 当前委托,因此替换委托后新声明或变更的元数据都能生效。
260
+ */
261
+ const FORWARDED_DATA_MEMBERS = ["deferLoading", "timeoutMs"];
262
+ /**
263
+ * 需要转发给注册表的可选**方法**成员。
264
+ *
265
+ * 无条件定义为转发函数:调用时取当前委托的同名方法(绑定委托自身为接收者),
266
+ * 当前委托未声明该成员时返回 `undefined`。这与"未声明"在 DSH 注册表侧的语义一致——
267
+ * 并发分类只认精确的 `true`,展示与内容收尾回调返回 `undefined` 即保持原行为。
268
+ */
269
+ const FORWARDED_METHOD_MEMBERS = [
270
+ "isConcurrencySafe",
271
+ "presentCall",
272
+ "presentResult",
273
+ "projectContent",
274
+ "finalizeContent"
275
+ ];
276
+ function sortedLayers(layers) {
277
+ return [...layers.values()].sort((left, right) => left.priority - right.priority || left.owner.localeCompare(right.owner));
278
+ }
279
+ /**
280
+ * 建立一个工具包装绑定。
281
+ *
282
+ * 包装器只替换 `description` / `parameters` / `execute` 三处语义,其余成员一律转发
283
+ * 给当前委托,因此它对 DSH 而言仍是一个完全正常的工具定义——模型可见 schema 由层链
284
+ * 实时投影,执行时参数由层链依次归一化后交给真实工具。
285
+ * @param initialDelegate - 首次包装的真实工具定义。
286
+ * @param ownLayer - 本绑定自带的层。
287
+ * @returns 可注册、可增量贡献层、可切换委托的绑定。
288
+ */
289
+ function createWrapperBinding(initialDelegate, ownLayer$1) {
290
+ let currentDelegate = initialDelegate;
291
+ const layers = new Map([[ownLayer$1.owner, ownLayer$1]]);
292
+ const context = { toolName: initialDelegate.name };
293
+ const delegate = () => currentDelegate;
294
+ const projectParameters = () => sortedLayers(layers).reduce((value, layer) => layer.projectParameters?.(value, context) ?? value, delegate().parameters);
295
+ const execute = (args, exec) => {
296
+ const target = delegate();
297
+ const active = sortedLayers(layers).filter((layer) => layer.execute !== void 0);
298
+ const dispatch = (index, current) => {
299
+ const layer = active[index];
300
+ if (layer === void 0) return Reflect.apply(target.execute, target, [current, exec]);
301
+ let called = false;
302
+ return layer.execute(current, exec, (nextArgs) => {
303
+ if (called) throw new Error(`${PLUGIN_OWNER}: wrapper "${layer.owner}" called next() more than once for tool "${context.toolName}"`);
304
+ called = true;
305
+ return dispatch(index + 1, nextArgs);
306
+ });
307
+ };
308
+ return dispatch(0, args);
309
+ };
310
+ const protocol = {
311
+ version: 1,
312
+ owner: PLUGIN_OWNER,
313
+ name: initialDelegate.name,
314
+ contribute(layer) {
315
+ if (layers.has(layer.owner)) throw new Error(`${PLUGIN_OWNER}: wrapper owner "${layer.owner}" is already registered for "${initialDelegate.name}"`);
316
+ layers.set(layer.owner, layer);
317
+ let active = true;
318
+ return () => {
319
+ if (!active) return;
320
+ active = false;
321
+ layers.delete(layer.owner);
322
+ };
323
+ }
324
+ };
325
+ const definition = {};
326
+ Object.defineProperties(definition, {
327
+ name: {
328
+ value: initialDelegate.name,
329
+ enumerable: true
330
+ },
331
+ description: {
332
+ enumerable: true,
333
+ get: () => delegate().description
334
+ },
335
+ parameters: {
336
+ enumerable: true,
337
+ get: projectParameters
338
+ },
339
+ output: {
340
+ enumerable: true,
341
+ get: () => delegate().output
342
+ },
343
+ execute: {
344
+ value: execute,
345
+ enumerable: true
346
+ }
347
+ });
348
+ Object.defineProperty(definition, TOOL_WRAPPER_PROTOCOL, { value: protocol });
349
+ for (const member of FORWARDED_DATA_MEMBERS) Object.defineProperty(definition, member, {
350
+ enumerable: true,
351
+ configurable: true,
352
+ get: () => propertyOf(delegate(), member)
353
+ });
354
+ for (const member of FORWARDED_METHOD_MEMBERS) Object.defineProperty(definition, member, {
355
+ enumerable: true,
356
+ configurable: true,
357
+ value: (...args) => {
358
+ const owner = delegate();
359
+ const current = propertyOf(owner, member);
360
+ return typeof current === "function" ? Reflect.apply(current, owner, args) : void 0;
361
+ }
362
+ });
363
+ return {
364
+ definition,
365
+ updateDelegate(next) {
366
+ if (next.name !== initialDelegate.name) throw new Error(`${PLUGIN_OWNER}: delegate name changed from "${initialDelegate.name}" to "${next.name}"`);
367
+ currentDelegate = next;
368
+ },
369
+ contribute(layer) {
370
+ return protocol.contribute(layer);
371
+ },
372
+ releaseOwnLayer() {
373
+ layers.delete(ownLayer$1.owner);
374
+ }
375
+ };
376
+ }
377
+ function propertyOf(definition, member) {
378
+ return definition[member];
379
+ }
380
+
381
+ //#endregion
382
+ //#region src/supervisor.ts
383
+ /** 本插件自带层的优先级(数值小者先执行)。 */
384
+ const OWN_LAYER_PRIORITY = 100;
385
+ function describeError(error) {
386
+ return error instanceof Error ? error.message : String(error);
387
+ }
388
+ /**
389
+ * 本插件自带的投影层:模型消息装配期收窄 schema,工具派发期归一化参数。
390
+ *
391
+ * `execute` 用**本次调用**的 Agent 解析策略(复合工具可能代别的 Agent 派发),
392
+ * 投影则固定用被包装的那个 Agent——因为作用域注册本身就绑定了它。
393
+ */
394
+ function ownLayer(ctx, agent) {
395
+ return {
396
+ owner: PLUGIN_OWNER,
397
+ priority: OWN_LAYER_PRIORITY,
398
+ projectParameters(value) {
399
+ return projectEscalationParameters(value, escalationPolicyFor(ctx, agent).viableTargets);
400
+ },
401
+ execute(args, exec, next) {
402
+ const { viableTargets } = escalationPolicyFor(ctx, exec.agent ?? agent);
403
+ return next(normalizeEscalationArguments(args, viableTargets));
404
+ }
405
+ };
406
+ }
407
+ /**
408
+ * 按 Agent 调度工具包装:为每个存活的会话维护它自己的目标工具投影视图。
409
+ *
410
+ * DSH 的工具定义是**注册期静态、跨会话共享**的(模型可见 schema 通过 `ctx.tools.schemas(scope)`
411
+ * 按作用域解析),而提权可行性是**每会话事实**。因此插件不能就地改全局定义,只能在
412
+ * 各 Agent 自己的作用域层里注册一份包装定义去遮蔽继承来的原定义。
413
+ *
414
+ * 生命周期与 DSH 的真实时序对齐:Agent 创建时装配,Agent 销毁时撤下;preset 在创建后
415
+ * 才注册工具、或工具集合在运行期变化(`tools/change`)时重新对账。
416
+ */
417
+ var Supervisor = class {
418
+ states = /* @__PURE__ */ new Map();
419
+ /** 重入计数:非 0 时表示正在对账,期间到达的 `tools/change` 合并成一次补偿对账。 */
420
+ reconciling = 0;
421
+ /** 本插件自身注册/注销引发的 `tools/change` 配额,避免自触发无限对账。 */
422
+ expectedToolChanges = 0;
423
+ reconcilePending = false;
424
+ /**
425
+ * @param ctx - 已装配 `agents` / `tools` / `sandboxPolicy` / `approval` 的插件上下文。
426
+ */
427
+ constructor(ctx) {
428
+ this.ctx = ctx;
429
+ }
430
+ /**
431
+ * 订阅生命周期事件并对账当前已有的全部 Agent。
432
+ * @returns 供 `ctx.effect` 使用的清理函数。
433
+ */
434
+ start() {
435
+ const stopCreated = this.ctx.on("agent/created", ({ agent }) => {
436
+ this.install(agent);
437
+ });
438
+ const stopDisposed = this.ctx.on("agent/disposed", ({ agent }) => {
439
+ try {
440
+ this.remove(agent);
441
+ } catch (error) {
442
+ this.warn(agent, `cleanup failed: ${describeError(error)}`);
443
+ }
444
+ });
445
+ const stopPreset = this.ctx.on("agent-preset/selected", (sessionId) => {
446
+ const agent = this.ctx.agents.get(sessionId);
447
+ if (agent !== void 0) this.reconcileAgent(agent);
448
+ });
449
+ const stopTools = this.ctx.on("tools/change", () => {
450
+ if (this.expectedToolChanges > 0) {
451
+ this.expectedToolChanges -= 1;
452
+ return;
453
+ }
454
+ if (this.reconciling > 0) {
455
+ this.reconcilePending = true;
456
+ return;
457
+ }
458
+ this.reconcileAll();
459
+ });
460
+ for (const agent of this.ctx.agents.list()) this.install(agent);
461
+ return () => {
462
+ stopTools();
463
+ stopPreset();
464
+ stopDisposed();
465
+ stopCreated();
466
+ const states = [...this.states.values()];
467
+ this.states.clear();
468
+ for (const state of states) try {
469
+ this.coordinate(() => this.disposeState(state));
470
+ } catch (error) {
471
+ this.warn(state.agent, `cleanup failed: ${describeError(error)}`);
472
+ }
473
+ return Promise.resolve();
474
+ };
475
+ }
476
+ install(agent) {
477
+ if (this.states.has(agent)) return;
478
+ const state = {
479
+ agent,
480
+ targets: new Map(TARGET_TOOL_NAMES.map((name$1) => [name$1, {
481
+ name: name$1,
482
+ attachment: { kind: "dormant" }
483
+ }]))
484
+ };
485
+ this.states.set(agent, state);
486
+ this.coordinate(() => this.reconcileState(state));
487
+ }
488
+ remove(agent) {
489
+ const state = this.states.get(agent);
490
+ if (state === void 0) return;
491
+ this.states.delete(agent);
492
+ this.coordinate(() => this.disposeState(state));
493
+ }
494
+ reconcileAgent(agent) {
495
+ if (this.reconciling > 0) return;
496
+ const state = this.states.get(agent);
497
+ if (state === void 0) return;
498
+ this.coordinate(() => this.reconcileState(state));
499
+ }
500
+ reconcileAll() {
501
+ if (this.reconciling > 0) return;
502
+ this.coordinate(() => {
503
+ for (const state of this.states.values()) this.reconcileState(state);
504
+ });
505
+ }
506
+ /** 逐个目标工具对账;失败按 Agent + 工具去重上报,不影响其余工具。 */
507
+ reconcileState(state) {
508
+ for (const target of state.targets.values()) try {
509
+ this.reconcileTarget(state.agent, target);
510
+ } catch (error) {
511
+ this.reportFailure(state.agent, target, error);
512
+ }
513
+ }
514
+ /**
515
+ * 对齐一个目标工具:先撤下上次的挂载,再按**当前可见的**定义重新挂载。
516
+ *
517
+ * 关键在于先撤后读:本插件的包装定义注册在同一作用域层里,`ctx.tools.get(name, agent)`
518
+ * 会优先返回它。撤下之后读到的才是继承来的真实定义。
519
+ */
520
+ reconcileTarget(agent, target) {
521
+ this.detachTarget(target);
522
+ const delegate = this.ctx.tools.get(target.name, agent);
523
+ if (delegate === void 0) {
524
+ delete target.lastReportedError;
525
+ return;
526
+ }
527
+ try {
528
+ validateTargetTool(delegate);
529
+ const protocol = protocolOf(delegate);
530
+ if (protocol !== void 0) target.attachment = {
531
+ kind: "cooperative",
532
+ release: protocol.contribute(ownLayer(this.ctx, agent))
533
+ };
534
+ else {
535
+ const binding = target.binding ?? createWrapperBinding(delegate, ownLayer(this.ctx, agent));
536
+ if (target.binding === void 0) target.binding = binding;
537
+ else binding.updateDelegate(delegate);
538
+ const registrationCtx = agent.ctx.extend({ fiber: this.ctx.fiber });
539
+ target.attachment = {
540
+ kind: "owned",
541
+ unregister: this.mutateTools(() => registrationCtx.tools.register(binding.definition))
542
+ };
543
+ }
544
+ delete target.lastReportedError;
545
+ } catch (error) {
546
+ target.attachment = {
547
+ kind: "incompatible",
548
+ reason: describeError(error)
549
+ };
550
+ throw error;
551
+ }
552
+ }
553
+ detachTarget(target) {
554
+ const attachment = target.attachment;
555
+ if (attachment.kind === "dormant") return;
556
+ if (attachment.kind === "incompatible") {
557
+ target.attachment = { kind: "dormant" };
558
+ return;
559
+ }
560
+ if (attachment.kind === "owned") this.mutateTools(attachment.unregister);
561
+ else attachment.release();
562
+ target.attachment = { kind: "dormant" };
563
+ }
564
+ disposeState(state) {
565
+ const errors = [];
566
+ for (const target of [...state.targets.values()].reverse()) try {
567
+ this.detachTarget(target);
568
+ } catch (error) {
569
+ errors.push(error);
570
+ }
571
+ if (errors.length > 0) throw new AggregateError(errors, `agent "${state.agent.id}" cleanup failed`);
572
+ }
573
+ reportFailure(agent, target, error) {
574
+ const message = describeError(error);
575
+ if (target.lastReportedError === message) return;
576
+ target.lastReportedError = message;
577
+ this.warn(agent, `tool "${target.name}" dynamic reconciliation failed: ${message}`);
578
+ }
579
+ warn(agent, message) {
580
+ this.ctx.logger.warn(`sandbox-param-normalizer: agent "${agent.id}" ${message}`);
581
+ }
582
+ /** 执行一段会触发 `tools/change` 的对账动作,并合并期间挂起的补偿对账。 */
583
+ coordinate(action) {
584
+ this.reconciling += 1;
585
+ try {
586
+ action();
587
+ } finally {
588
+ this.reconciling -= 1;
589
+ if (this.reconciling === 0 && this.reconcilePending) {
590
+ this.reconcilePending = false;
591
+ this.reconcileAll();
592
+ }
593
+ }
594
+ }
595
+ /** 标记紧随其后的 `tools/change` 是本插件自己造成的,消费掉这一配额。 */
596
+ mutateTools(action) {
597
+ this.expectedToolChanges += 1;
598
+ const expected = this.expectedToolChanges;
599
+ try {
600
+ return action();
601
+ } finally {
602
+ if (this.expectedToolChanges === expected) this.expectedToolChanges -= 1;
603
+ }
604
+ }
605
+ };
606
+
607
+ //#endregion
608
+ //#region src/index.ts
609
+ /** 插件配置 schema(静态声明,供 DSH 配置目录与校验使用)。 */
610
+ const Config = z.object({ logLevel: z.union(["silent", "info"]).default("info") });
611
+ /** 插件名,也是包装协议里的所有者标识来源。 */
612
+ const name = "sandbox-param-normalizer";
613
+ /** 依赖的宿主能力:Agent 注册表、工具注册表、沙箱策略解析与审批策略读取。 */
614
+ const inject = [
615
+ "agents",
616
+ "tools",
617
+ "sandboxPolicy",
618
+ "approval"
619
+ ];
620
+ /**
621
+ * 启动插件。
622
+ * @param ctx - 已注入全部依赖的插件上下文。
623
+ * @param config - 插件配置。
624
+ */
625
+ function apply(ctx, config = {}) {
626
+ const supervisor = new Supervisor(ctx);
627
+ ctx.effect(() => supervisor.start(), "sandbox-param-normalizer.lifecycle()");
628
+ if ((config.logLevel ?? "info") !== "silent") ctx.logger.info("sandbox-param-normalizer: session-aware escalation schema projection enabled");
629
+ }
630
+ var src_default = {
631
+ name,
632
+ inject,
633
+ Config,
634
+ apply
635
+ };
636
+
637
+ //#endregion
638
+ export { Config, apply, src_default as default, inject, name };
package/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
2
  "name": "dsh-sandbox-param-normalizer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "会话感知的沙箱提权参数归一化插件:按会话的沙箱模式与审批策略裁剪模型可见的 sandbox_permissions/justification,并在执行前清理无法生效的提权参数",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json",
14
+ "./cordis.patch.yml": "./cordis.patch.yml"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/rengy/dsh-sandbox-param-normalizer.git"
26
+ },
27
+ "homepage": "https://github.com/rengy/dsh-sandbox-param-normalizer",
28
+ "bugs": {
29
+ "url": "https://github.com/rengy/dsh-sandbox-param-normalizer/issues"
30
+ },
31
+ "keywords": [
32
+ "deepseek",
33
+ "harness",
34
+ "dsh",
35
+ "sandbox",
36
+ "escalation",
37
+ "plugin"
38
+ ],
39
+ "engines": {
40
+ "node": "^22.19.0 || >=24.0.0",
41
+ "dsh": "^0.2.0-rc.2 || ^0.2.1-alpha.1"
42
+ },
43
+ "dsh": {
44
+ "manifestVersion": 1,
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ }
48
+ },
49
+ "peerDependencies": {
50
+ "@deepseek-ai/cordis": "^4.0.4 || ^4.0.5-alpha.1",
51
+ "@deepseek-ai/dsh-agent": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
52
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
53
+ "@deepseek-ai/dsh-sandbox": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
54
+ "@deepseek-ai/dsh-sandbox-policy": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
55
+ "@deepseek-ai/dsh-scope": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
56
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
57
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
58
+ "@deepseek-ai/dsh-user-approval": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
59
+ "@deepseek-ai/schemastery": "^3.18.4"
60
+ },
61
+ "devDependencies": {
62
+ "@deepseek-ai/cordis": "4.0.5-alpha.1",
63
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
64
+ "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
65
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
66
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
67
+ "@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
68
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
69
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
70
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
71
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
72
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2",
73
+ "@deepseek-ai/schemastery": "^3.18.4",
74
+ "@types/node": "^22.20.0",
75
+ "tsdown": "^0.15.0",
76
+ "typescript": "^5.8.0",
77
+ "vitest": "^3.2.0"
78
+ },
79
+ "publishConfig": {
80
+ "access": "public",
81
+ "registry": "https://registry.npmjs.org/"
82
+ },
83
+ "scripts": {
84
+ "build": "tsc -p tsconfig.json --noEmit && tsdown",
85
+ "typecheck": "tsc -p tsconfig.json --noEmit",
86
+ "test": "vitest run",
87
+ "verify:host": "node scripts/verify.mjs --host",
88
+ "verify:host:without-plugin": "node scripts/verify.mjs --host --without-plugin",
89
+ "verify:schema": "docker cp scripts/schema-probe.mjs dsh-sandbox-param-normalizer:/tmp/schema-probe.mjs && docker exec dsh-sandbox-param-normalizer node /tmp/schema-probe.mjs"
90
+ }
6
91
  }