dsh-isolation-gate 0.4.0 → 1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 — 2026-10-24
4
+
5
+ - 加固:`gate_request` 只接受已在名单里的对象;面板「批准」会复核对象仍在受管名单(防止批准出名单外的孤儿对象)。
6
+ **模型可以直接管门禁了。** 以前改门禁只能人工点面板或 shell 出去跑 `gate.mjs`;这一版把同一套能力注册成 7 个模型工具,像任务板那样「说需求 → 模型自己调」。
7
+
8
+ - **新增 7 个工具**:`gate_status`(全量状态)、`gate_check`(干跑判定)、`gate_add`(加对象,固定「禁」)、`gate_set_level`(改级别)、`gate_request`(申请放宽)、`gate_requests`(看待批)、`gate_log`(读审计)。
9
+ - **权限边界(刻意的不对称)**:查 / 新增 / 收紧随时可做;**放宽(→只读 / 可读写删)模型自己批不了** —— 没户主授权窗口时只生成一条待批申请。
10
+ - **待批队列**:新文件 `<状态目录>\requests.json`(v1),含 `requests[]` 与 `aiGrantUntil`;同目标同级别去重,最多 50 条。**不动已冻结的 state v3**。
11
+ - **新增 4 条路由**:`GET /requests`、`POST /requests-approve`、`POST /requests-reject`、`POST /grant`(户主在面板/本机窗口批准,模型工具里没有"批准"这个动作)。
12
+ - **零新依赖**:工具定义直接按宿主 `ToolDefinition` 形状注册,**没有引入 `@deepseek-ai/dsh-tools` peer**,躲开兼容性闸门。
13
+ - 测试:6 个套件全绿(路由数 16 → 20)。
14
+
15
+ ## 1.0.0 — 2026-10-23
16
+
17
+ **1.0 = 功能与格式冻结,不是「完美版」。** 承诺范围见 README 的「1.0 的稳定性承诺」:状态文件 v3、CLI 子命令与退出码、HTTP 路径、四条判定语义 —— 只增不破。
18
+
19
+ 这一版**没有功能变更、没有格式变更**,只做发版前的对齐:
20
+
21
+ - **文档纠错**:状态文件章节标到 v3(原文写 v2,实际 `STATE_VERSION = 3`)。
22
+ - **测试数字纠错**:改为实跑结果 **6 个套件 · 396 条断言**(原文「4 个套件 · 325 断言」是旧数;英文 README 的 309 同样过期)。
23
+ - **定位改写**:从「别人做规则、我们做管理面」改为「别人按工具/参数立规则,我们按目录立对象」—— 前者已被同类追上,后者才是实际差异。
24
+ - **SECURITY.md**:支持范围改 `1.x`;新增「已知边界」五条(同权限域可自改、档位模式无 realpath、黑名单只判抽到的目标、审计可改、管不到 DSH 之外)。
25
+ - **新增**:README(中/英)各加一节「1.0 的稳定性承诺」。
26
+
3
27
  ## 0.4.0 — 2026-10-23
4
28
 
5
29
  - **改(重要)**:组类型(严格 / 宽松)以前只在按钮上写一句小字,实际是**两套相反语义** —— 严格组是白名单(没列进组的全拒),宽松组是黑名单(没列进组的照常)。实测踩坑:把目录加进严格组当「只禁这个」,结果全盘封死。现在模板卡片和编辑区都挂一条醒目的**模式横幅**(白名单模式 / 黑名单模式 + 「没列进组的怎么算」),编辑区还给一键「换成宽松组 / 严格组」(不动清单,只换清单外怎么算)。
package/README.en.md CHANGED
@@ -91,22 +91,24 @@ dsh plugin --profile desktop add dsh-isolation-gate
91
91
 
92
92
  ---
93
93
 
94
- ## State file (v2)
94
+ ## State file (v3)
95
95
 
96
96
  ```jsonc
97
97
  {
98
- "version": 2,
98
+ "version": 3,
99
99
  "defaultLevel": "deny",
100
100
  "groups": { "harness isolation": { "note": "", "updatedAt": "…" } },
101
101
  "objects": {
102
102
  "Zcode": { "level": "deny", "note": "", "group": "harness isolation", "updatedAt": "…" },
103
103
  "E:\\data\\shared\\project": { "level": "read", "note": "", "group": "", "updatedAt": "…" }
104
- }
104
+ },
105
+ "profiles": { "strict-delivery": { "mode": "allowlist", "allow": [{ "path": "E:\\data\\shared", "level": "read" }], "network": "deny", "unknownTool": "deny" } },
106
+ "sessionOverrides": { "<sessionId>": "strict-delivery" }
105
107
  }
106
108
  ```
107
109
 
108
- - **v1 is still readable**: legacy `harnesses: { key: { level } }` is migrated on read, **levels are preserved**;
109
- the next write persists v2.
110
+ - **Legacy formats stay readable**: v1 `harnesses: { key: { level } }` and v2 `objects` are normalised to v3 on read
111
+ (**levels are preserved**; `profiles` / `sessionOverrides` default to empty); the next write persists v3.
110
112
  - Missing file = **deny everything**; corrupt JSON = **deny everything and never overwrite it**.
111
113
  - No BOM; writes go through temp-file + rename (atomic).
112
114
  - `state.harnesses` remains a read-only in-memory alias; it is no longer written to disk.
@@ -185,6 +187,19 @@ External `Origin` → 403; wrong method → 405; bad JSON / invalid level → 40
185
187
 
186
188
  ---
187
189
 
190
+ ## The 1.0 stability promise
191
+
192
+ From 1.0 onward, the surfaces below **only grow, never break**. A breaking change would ship as 2.0 with a migration note.
193
+
194
+ | Surface | Promise |
195
+ |---|---|
196
+ | **State file** | On-disk format `v3`; v1 / v2 are read-compatible and upgraded on write, levels preserved. New fields are appended only; existing field meanings do not change. |
197
+ | **CLI** | `bin/gate.mjs` subcommands and exit codes (`0` allowed / `3` denied / `2` usage or environment error) are frozen; new flags are additive. |
198
+ | **HTTP** | Existing `/__dsh-isolation-gate/*` paths and methods are frozen; new routes are additive. |
199
+ | **Decision semantics** | The three levels (deny / read / full), longest-prefix matching, default deny and fail-closed are frozen — changing them would be a different product. |
200
+
201
+ **Not promised**: panel visuals and copy, and audit-log fields, may evolve; bug fixes are not breaking changes.
202
+
188
203
  ## Known boundaries (read this)
189
204
 
190
205
  1. **Raw filesystem access and separate processes are not blocked.** Any program or script that bypasses DSH tool calls can read/write the disk directly.
@@ -198,7 +213,7 @@ External `Origin` → 403; wrong method → 405; bad JSON / invalid level → 40
198
213
  ## Tests
199
214
 
200
215
  ```bash
201
- node test/run-all.mjs # 4 suites · 309 assertions · fully offline
216
+ node test/run-all.mjs # 6 suites · 396 assertions · fully offline
202
217
  node --check lib/*.js
203
218
  ```
204
219
 
package/README.md CHANGED
@@ -28,8 +28,10 @@ dsh plugin --profile desktop add dsh-isolation-gate
28
28
 
29
29
  它不是又一个 allow/deny 规则引擎,也不是沙箱本身。
30
30
 
31
- - **别人做的是「怎么让 agent 多访问几个目录」**(multi-root / allowlist / write-protect)或「怎么用规则拦住工具」(permission-rules / safety-net);
32
- - **我们做的是「这台机器上哪些目录属于禁区、现在给什么级别、谁改的、agent 自己怎么查」**。
31
+ - **别人按「工具 / 命令 / 参数」立规则**(`dsh-permission-rules`、`dsh-permissions` 这类:allow / deny / ask + glob / 正则);
32
+ - **我们按「目录」立对象**:这台机器上哪些目录属于禁区、现在给什么级别、谁改的、agent 自己怎么查。
33
+
34
+ 规则引擎回答的是「**这次调用准不准**」,门禁回答的是「**这个目录开不开**」。两者可以叠着用,不冲突。
33
35
 
34
36
  所以它的核心是**管理面**:
35
37
 
@@ -105,23 +107,23 @@ dsh plugin --profile desktop add dsh-isolation-gate
105
107
 
106
108
  ---
107
109
 
108
- ## 状态文件(v2)
110
+ ## 状态文件(v3)
109
111
 
110
112
  ```jsonc
111
113
  {
112
- "version": 2,
114
+ "version": 3,
113
115
  "defaultLevel": "deny",
114
- "groups": {
115
- "harness 隔离": { "note": "", "updatedAt": "…" }
116
- },
116
+ "groups": { "harness 隔离": { "note": "", "updatedAt": "…" } },
117
117
  "objects": {
118
118
  "Zcode": { "level": "deny", "note": "", "group": "harness 隔离", "updatedAt": "…" },
119
119
  "E:\\资料\\合作档案\\某项目": { "level": "read", "note": "", "group": "", "updatedAt": "…" }
120
- }
120
+ },
121
+ "profiles": { "严格交付": { "mode": "allowlist", "allow": [{ "path": "E:\\资料\\库", "level": "read" }], "network": "deny", "unknownTool": "deny" } },
122
+ "sessionOverrides": { "<sessionId>": "严格交付" }
121
123
  }
122
124
  ```
123
125
 
124
- - **v1 仍可读**:旧的 `harnesses: { key: { level } }` 读入时自动迁移成 `objects`,**级别不丢**;下一次写盘才落成 v2。
126
+ - **旧格式仍可读**:v1 的 `harnesses: { key: { level } }` 与 v2 的 `objects` 读入时自动收敛成 v3(**级别不丢**,`profiles` / `sessionOverrides` 缺省为空);下一次写盘才落成 v3。
125
127
  - 文件缺失 = **全禁**;坏 JSON = **全禁且不覆盖原文件**。
126
128
  - 无 BOM;写盘走临时文件 + rename(原子替换)。
127
129
  - `state.harnesses` 在内存里保留为只读别名,磁盘上不再写它。
@@ -202,6 +204,19 @@ node bin/gate.mjs log --limit 20
202
204
 
203
205
  ---
204
206
 
207
+ ## 1.0 的稳定性承诺
208
+
209
+ 从 1.0 起,下面这些**只增不破**;要破坏会走 2.0 并附迁移说明:
210
+
211
+ | 面 | 承诺 |
212
+ |---|---|
213
+ | **状态文件** | 磁盘格式 `v3`;v1 / v2 一律**读时兼容、写时升级**,级别不丢。新增字段只追加,不改已有字段含义。 |
214
+ | **CLI** | `bin/gate.mjs` 的子命令与退出码(`0` 放行 / `3` 拒绝 / `2` 用错或环境错误)不改;新参数只增加。 |
215
+ | **HTTP** | `/__dsh-isolation-gate/*` 现有路径与方法不改;新路由只增加。 |
216
+ | **判定语义** | 三级(deny / read / full)、最长前缀命中、默认禁、fail-closed —— 这四项冻结;改它们等于换产品。 |
217
+
218
+ **不承诺**:面板的视觉与文案、审计日志的字段可以随版本演进;修 bug 不算破坏性变更。
219
+
205
220
  ## 已知边界(必须明说)
206
221
 
207
222
  1. **raw fs 与独立进程拦不住。** 任何程序、脚本、别的 agent 不经过 DSH 工具调用就直接读写磁盘,本插件看不到。
@@ -217,16 +232,18 @@ node bin/gate.mjs log --limit 20
217
232
  ## 自测
218
233
 
219
234
  ```bash
220
- node test/run-all.mjs # 4 个套件 · 325 断言 · 全离线(不需要 GUI/dsh)
235
+ node test/run-all.mjs # 6 个套件 · 396 断言 · 全离线(不需要 GUI/dsh)
221
236
  node --check lib/*.js
222
237
  ```
223
238
 
224
239
  | 套件 | 覆盖 |
225
240
  |---|---|
226
- | `run-data-layer.mjs` | 状态 v1→v2 迁移 / 无 BOM / 坏 JSON 不覆盖 / 路径键 / 名单备份 / 分组 / 孤儿清理 |
227
- | `run-guard.mjs` | 三级×四操作矩阵 / 最长前缀 / 同前缀不误伤 / fail-closed / 真实 I/O 闭环 |
228
- | `run-routes.mjs` | 11 条路由 / 组级切 / 工具门挂载与干跑 / 选择器缺省 / 级别落盘 / 403/405/400 |
229
- | `run-client.mjs` | 两座位注册 / 摘要条 / 分组卡片 / 分段控件 / `⋯` 菜单 / 空态 / 交互接线 |
241
+ | `run-data-layer.mjs`(85) | 状态 v1/v2→v3 迁移 / 无 BOM / 坏 JSON 不覆盖 / 路径键 / 名单备份 / 分组 / 孤儿清理 |
242
+ | `run-guard.mjs`(74) | 三级×四操作矩阵 / 最长前缀 / 同前缀不误伤 / fail-closed / 真实 I/O 闭环 |
243
+ | `run-profiles.mjs`(45) | 隔离强度三档 / 白名单与黑名单语义 / 会话绑定只能加严 / 联网软禁 |
244
+ | `run-profiles-runtime.mjs`(11) | 档位文件读取 / 目录绑定 / 坏配置 fail-closed |
245
+ | `run-routes.mjs`(84) | 11 条路由 / 组级切 / 工具门挂载与干跑 / 选择器缺省 / 级别落盘 / 403/405/400 |
246
+ | `run-client.mjs`(97) | 两座位注册 / 摘要条 / 分组卡片 / 分段控件 / `⋯` 菜单 / 空态 / 交互接线 |
230
247
 
231
248
  ---
232
249
 
@@ -249,6 +266,7 @@ MIT · 见 [LICENSE](./LICENSE)。
249
266
 
250
267
  - [x] 截图四张:总览 / 组卡 / 审计与诊断 / 特别组草稿(见 [docs/screenshots](./docs/screenshots))
251
268
  - [ ] 30 秒演示 GIF(欢迎贡献:录你自己机器上的演示;我们不录,整屏会带出无关窗口)
269
+ - [ ] **认知帷幕**:受管路径的读取不直接回原文,只回字段摘要或打码内容(群友提的方向;比目录权限更接近「少看一点」)
252
270
  - [ ] `gate export --csv`:把审计日志导出成表格
253
271
  - [ ] 临时授权:给某对象一个到期时间,到点自动回到 `deny`
254
272
  - [ ] 状态文件并发锁(多窗口同时改不互相覆盖)
package/lib/client.js CHANGED
@@ -546,6 +546,121 @@ window.__ModuleLoader__.load({
546
546
  * 纯视图:`props.model` 为 null 时是首次加载态。
547
547
  * 自测直接调它,不需要 React 运行时。
548
548
  */
549
+ /**
550
+ * 待批申请 + 「授权 AI 放宽」。
551
+ * 模型(gate_* 工具)只能收紧;要放宽就得在这里点一下 —— 这是唯一的人批入口。
552
+ */
553
+ function GateRequestsCard(props) {
554
+ const [data, setData] = useState(null)
555
+ const [error, setError] = useState(null)
556
+ const [busy, setBusy] = useState(false)
557
+
558
+ const load = useCallback(async () => {
559
+ try {
560
+ setData(await call(`${BASE}/requests`))
561
+ setError(null)
562
+ } catch (e) {
563
+ setError((e && e.message) || String(e))
564
+ }
565
+ }, [])
566
+
567
+ useEffect(() => {
568
+ load()
569
+ }, [load, props.refreshKey])
570
+
571
+ const act = async (fn) => {
572
+ setBusy(true)
573
+ setError(null)
574
+ try {
575
+ await fn()
576
+ await load()
577
+ if (props.onChanged) props.onChanged()
578
+ } catch (e) {
579
+ setError((e && e.message) || String(e))
580
+ } finally {
581
+ setBusy(false)
582
+ }
583
+ }
584
+
585
+ const pending = (data && data.requests) || []
586
+ const grantActive = !!(data && data.grantActive)
587
+ const grantUntil = data && data.aiGrantUntil
588
+ const levelText = (lv) => ({ deny: '禁', read: '只读', full: '可读写删' }[lv] || lv)
589
+
590
+ if (pending.length === 0 && !grantActive) return null
591
+
592
+ return h(
593
+ 'div',
594
+ {
595
+ style: {
596
+ marginTop: '12px',
597
+ border: `1px solid ${pending.length ? C.warn : C.border}`,
598
+ background: pending.length ? 'rgba(251,191,36,0.06)' : C.card,
599
+ borderRadius: '10px',
600
+ padding: '10px 12px',
601
+ },
602
+ },
603
+ h(
604
+ 'div',
605
+ { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' } },
606
+ h(
607
+ 'div',
608
+ { style: { fontSize: '13px', fontWeight: 700, color: pending.length ? C.warn : C.text } },
609
+ pending.length ? `待批申请 · ${pending.length} 条(模型想把权限放宽)` : 'AI 放宽授权进行中',
610
+ ),
611
+ h('span', { style: { flex: '1 1 auto' } }),
612
+ h(Button, {
613
+ label: grantActive
614
+ ? `撤销授权(至 ${new Date(grantUntil).toLocaleTimeString()})`
615
+ : '授权 AI 放宽 30 分钟',
616
+ disabled: busy,
617
+ onClick: () => act(() => post(`${BASE}/grant`, { minutes: grantActive ? 0 : 30 })),
618
+ }),
619
+ ),
620
+ error
621
+ ? h('div', { style: { color: C.bad, fontSize: '12px', marginTop: '6px' } }, `✗ ${error}`)
622
+ : null,
623
+ pending.map((item) =>
624
+ h(
625
+ 'div',
626
+ {
627
+ key: item.id,
628
+ style: {
629
+ display: 'flex',
630
+ alignItems: 'center',
631
+ gap: '8px',
632
+ flexWrap: 'wrap',
633
+ marginTop: '8px',
634
+ paddingTop: '8px',
635
+ borderTop: `1px solid ${C.border}`,
636
+ },
637
+ },
638
+ h(
639
+ 'div',
640
+ { style: { fontSize: '12px', color: C.text } },
641
+ `${item.target}:${item.fromLevel ? levelText(item.fromLevel) : '未入名单'} → ${levelText(item.level)}`,
642
+ ),
643
+ h(
644
+ 'div',
645
+ { style: { fontSize: '11px', color: C.dim, flex: '1 1 220px' } },
646
+ item.reason || '(没写理由)',
647
+ ),
648
+ h(Button, {
649
+ label: '批准',
650
+ primary: true,
651
+ disabled: busy,
652
+ onClick: () => act(() => post(`${BASE}/requests-approve`, { id: item.id })),
653
+ }),
654
+ h(Button, {
655
+ label: '驳回',
656
+ disabled: busy,
657
+ onClick: () => act(() => post(`${BASE}/requests-reject`, { id: item.id })),
658
+ }),
659
+ ),
660
+ ),
661
+ )
662
+ }
663
+
549
664
  function GateView(props) {
550
665
  const model = props.model
551
666
  const levels = (model && model.levels) || []
@@ -635,6 +750,9 @@ window.__ModuleLoader__.load({
635
750
  h(Button, { label: props.busy ? '刷新中…' : '刷新', onClick: props.onRefresh, disabled: props.busy }),
636
751
  ),
637
752
 
753
+ // —— 待批申请 / AI 放宽授权(模型工具的配套人批入口) ——
754
+ h(GateRequestsCard, { refreshKey: props.busy ? 1 : 0, onChanged: props.onRefresh }),
755
+
638
756
  // —— 摘要条 ——
639
757
  h(
640
758
  'div',
@@ -0,0 +1,331 @@
1
+ /**
2
+ * 模型可直接调用的门禁工具(2026-10-24 新增)。
3
+ *
4
+ * 为什么要有:户主平时不点面板,只说需求。以前模型只能 shell 出去跑
5
+ * `node bin/gate.mjs ...`,多一层猜;这里把同一套能力注册成工具,
6
+ * 模型像调 `task_board_*` 一样直接调。
7
+ *
8
+ * 权限边界(户主拍板「只能收紧是对的,但用户授权后可以全做」):
9
+ * · 查:随时;
10
+ * · 新增对象:只能按默认 `deny` 加;
11
+ * · 收紧(改到更严的级别):随时;
12
+ * · 放宽:只有户主在面板点了「授权 AI 放宽 N 分钟」的窗口期内才直接生效,
13
+ * 否则写一条待批申请,等户主批准。模型自己不能批。
14
+ */
15
+ import { checkTool } from './service.js';
16
+ import { readAudit } from './audit.js';
17
+ import {
18
+ LEVELS,
19
+ LEVEL_LABEL,
20
+ readState,
21
+ writeState,
22
+ withLevel,
23
+ withGroup,
24
+ findEntryKey,
25
+ levelOf,
26
+ groupsOf,
27
+ } from './store.js';
28
+ import {
29
+ addRequest,
30
+ grantActive,
31
+ listRequests,
32
+ pendingCount,
33
+ readRequests,
34
+ requestsPathFor,
35
+ } from './requests.js';
36
+ import { normalizePathKey } from './roster.js';
37
+
38
+ const OP_LEVEL = { traverse: 0, read: 1, write: 2, delete: 3 };
39
+ const OPS = ['traverse', 'read', 'write', 'delete'];
40
+
41
+ function text(schema) {
42
+ return { type: 'string', description: schema };
43
+ }
44
+
45
+ /** 目标解析:名字(D 盘一级目录)或绝对路径。 */
46
+ function resolveTarget(args) {
47
+ if (typeof args.path === 'string' && args.path.trim()) {
48
+ return { target: normalizePathKey(args.path.trim()), kind: 'path' };
49
+ }
50
+ if (typeof args.name === 'string' && args.name.trim()) {
51
+ return { target: args.name.trim(), kind: 'name' };
52
+ }
53
+ throw new Error('必须给 name(D 盘一级目录名)或 path(绝对路径)之一');
54
+ }
55
+
56
+ function rank(level) {
57
+ const i = LEVELS.indexOf(level);
58
+ return i < 0 ? 0 : i;
59
+ }
60
+
61
+ function label(level) {
62
+ return LEVEL_LABEL[level] ?? level;
63
+ }
64
+
65
+ function targetExists(state, key) {
66
+ return findEntryKey(state, key) !== null;
67
+ }
68
+
69
+ function jsonResult(value) {
70
+ return {
71
+ schema: { type: 'object' },
72
+ render: (_args, v) => [{ type: 'text', text: JSON.stringify(v, null, 2) }],
73
+ };
74
+ }
75
+
76
+ /** 工具返回值必须是 lossless JSON:undefined / NaN / BigInt 会被宿主直接拒收。 */
77
+ function plain(value) {
78
+ return value === undefined ? null : JSON.parse(JSON.stringify(value));
79
+ }
80
+
81
+ export function registerGateTools(ctx, cfg) {
82
+ const requestsPath = requestsPathFor(cfg.statePath);
83
+ const register = (definition) => {
84
+ const run = definition.execute;
85
+ const safe = { ...definition, execute: async (args, exec) => plain(await run(args, exec)) };
86
+ return ctx.effect(() => ctx.tools.register(safe), `dsh-isolation-gate: ${definition.name}`);
87
+ };
88
+
89
+ // ── 1. 看全量状态 ────────────────────────────────────────────────
90
+ register({
91
+ name: 'gate_status',
92
+ description:
93
+ '查看门禁全量状态:默认级别、所有受管对象及其级别/分组、分组列表、待批申请数、AI 放宽授权是否有效。任何"门禁里现在是什么情况"的问题先调它。',
94
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
95
+ output: jsonResult(),
96
+ execute: async () => {
97
+ const state = readState(cfg.statePath).state;
98
+ const requests = listRequests(requestsPath);
99
+ const objects = Object.entries(state.objects ?? {}).map(([key, entry]) => ({
100
+ target: key,
101
+ level: entry.level,
102
+ levelLabel: label(entry.level),
103
+ group: entry.group || '',
104
+ note: entry.note || '',
105
+ }));
106
+ return {
107
+ defaultLevel: state.defaultLevel,
108
+ defaultLevelLabel: label(state.defaultLevel),
109
+ objects,
110
+ groups: groupsOf(state),
111
+ profiles: Object.keys(state.profiles ?? {}),
112
+ pendingRequests: requests.requests.length,
113
+ aiGrantUntil: requests.aiGrantUntil,
114
+ aiGrantActive: requests.grantActive,
115
+ auditPath: cfg.auditPath,
116
+ };
117
+ },
118
+ });
119
+
120
+ // ── 2. 干跑一次判定 ─────────────────────────────────────────────
121
+ register({
122
+ name: 'gate_check',
123
+ description:
124
+ '干跑判定:给一个路径(或目录名)和一个操作,返回门禁会不会放行、命中哪个对象、理由是什么。不读文件内容、不执行任何东西。动手前先问"这个能不能读/写"时用它。',
125
+ parameters: {
126
+ type: 'object',
127
+ properties: {
128
+ name: text('D 盘一级目录名,如 Zcode'),
129
+ path: text('绝对路径,如 D:\\Zcode\\a.txt'),
130
+ op: { type: 'string', enum: OPS, description: '操作:traverse / read / write / delete' },
131
+ },
132
+ required: ['op'],
133
+ additionalProperties: false,
134
+ },
135
+ output: jsonResult(),
136
+ execute: async (args) => {
137
+ const { target, kind } = resolveTarget(args);
138
+ const op = String(args.op);
139
+ if (!OPS.includes(op)) throw new Error(`未知操作 ${op}`);
140
+ const decision = checkTool(cfg, kind === 'path' ? { path: target, op } : { name: target, op });
141
+ return {
142
+ target,
143
+ kind,
144
+ op,
145
+ allowed: decision.ok ? decision.allowed === true : false,
146
+ reason: decision.reason,
147
+ matched: decision.matchedKey ?? null,
148
+ level: decision.level ?? null,
149
+ inRoster: decision.inRoster ?? null,
150
+ error: decision.ok ? null : decision.reason,
151
+ };
152
+ },
153
+ });
154
+
155
+ // ── 3. 新增对象(只能默认禁)─────────────────────────────────────
156
+ register({
157
+ name: 'gate_add',
158
+ description:
159
+ '把目录(或绝对路径)加入受管名单,级别固定为「禁」。加进来只是"开始管它",不会放行任何东西;要只读/可写必须另外申请并由户主批准。',
160
+ parameters: {
161
+ type: 'object',
162
+ properties: {
163
+ name: text('D 盘一级目录名'),
164
+ path: text('绝对路径'),
165
+ group: text('可选:放进哪个组'),
166
+ note: text('可选:备注'),
167
+ },
168
+ additionalProperties: false,
169
+ },
170
+ output: jsonResult(),
171
+ execute: async (args) => {
172
+ const { target, kind } = resolveTarget(args);
173
+ const state = readState(cfg.statePath).state;
174
+ if (targetExists(state, target)) {
175
+ const current = levelOf(state, target);
176
+ return { ok: true, target, kind, level: current, changed: false, message: `已在名单里,当前「${label(current)}」` };
177
+ }
178
+ let next = withLevel(state, target, 'deny', { by: 'agent' });
179
+ if (args.group) next = withGroup(next, target, String(args.group), { by: 'agent' });
180
+ writeState(cfg.statePath, next);
181
+ return { ok: true, target, kind, level: 'deny', changed: true, message: '已加入名单,级别「禁」' };
182
+ },
183
+ });
184
+
185
+ // ── 4. 改级别(默认只允许收紧)──────────────────────────────────
186
+ register({
187
+ name: 'gate_set_level',
188
+ description:
189
+ '改一个受管对象的级别。收紧(往「禁」方向)随时可以;放宽(往「只读 / 可读写删」方向)只有在户主点了「授权 AI 放宽」的窗口期内才直接生效,否则会写一条待批申请并在返回里说明。',
190
+ parameters: {
191
+ type: 'object',
192
+ properties: {
193
+ name: text('D 盘一级目录名'),
194
+ path: text('绝对路径'),
195
+ level: { type: 'string', enum: LEVELS, description: 'deny / read / full' },
196
+ reason: text('可选:为什么要改,写进审计与待批申请'),
197
+ },
198
+ required: ['level'],
199
+ additionalProperties: false,
200
+ },
201
+ output: jsonResult(),
202
+ execute: async (args) => {
203
+ const { target, kind } = resolveTarget(args);
204
+ const want = String(args.level);
205
+ if (!LEVELS.includes(want)) throw new Error(`未知级别 ${want}`);
206
+ const state = readState(cfg.statePath).state;
207
+ if (!targetExists(state, target)) {
208
+ throw new Error(`「${target}」不在名单里;先用 gate_add 加进来(只会是「禁」)`);
209
+ }
210
+ const current = levelOf(state, target);
211
+ if (current === want) {
212
+ return { ok: true, changed: false, target, level: current, message: '已经是这个级别' };
213
+ }
214
+ const tightening = rank(want) < rank(current);
215
+ const requests = readRequests(requestsPath);
216
+ const mayWiden = grantActive(requests);
217
+ if (!tightening && !mayWiden) {
218
+ const request = addRequest(requestsPath, {
219
+ target,
220
+ targetKind: kind,
221
+ level: want,
222
+ fromLevel: current,
223
+ reason: args.reason ?? '',
224
+ requestedBy: 'agent',
225
+ });
226
+ return {
227
+ ok: false,
228
+ changed: false,
229
+ needsApproval: true,
230
+ requestId: request.id,
231
+ target,
232
+ from: current,
233
+ to: want,
234
+ message: `放宽需要户主批准:已在「门禁」面板生成待批申请 ${request.id}。户主点批准后即可生效;不要重复申请。`,
235
+ };
236
+ }
237
+ const next = withLevel(state, target, want, { by: 'agent' });
238
+ writeState(cfg.statePath, next);
239
+ return {
240
+ ok: true,
241
+ changed: true,
242
+ target,
243
+ from: current,
244
+ to: want,
245
+ viaGrant: !tightening && mayWiden,
246
+ message: tightening ? `已收紧到「${label(want)}」` : `已在授权窗口内放宽到「${label(want)}」`,
247
+ };
248
+ },
249
+ });
250
+
251
+ // ── 5. 申请放宽 ─────────────────────────────────────────────────
252
+ register({
253
+ name: 'gate_request',
254
+ description:
255
+ '为某个受管对象申请一个更宽的级别(只读 / 可读写删),写进待批列表等户主在面板批准。理由写清楚:为什么要、大概多久。',
256
+ parameters: {
257
+ type: 'object',
258
+ properties: {
259
+ name: text('D 盘一级目录名'),
260
+ path: text('绝对路径'),
261
+ level: { type: 'string', enum: LEVELS },
262
+ reason: text('为什么要放宽(户主看这条决定批不批)'),
263
+ },
264
+ required: ['level', 'reason'],
265
+ additionalProperties: false,
266
+ },
267
+ output: jsonResult(),
268
+ execute: async (args) => {
269
+ const { target, kind } = resolveTarget(args);
270
+ const want = String(args.level);
271
+ if (!LEVELS.includes(want)) throw new Error(`未知级别 ${want}`);
272
+ const state = readState(cfg.statePath).state;
273
+ const current = targetExists(state, target) ? levelOf(state, target) : null;
274
+ if (current === null) {
275
+ return {
276
+ ok: false,
277
+ error: `「${target}」还不在名单里。先用 gate_add 把它纳入受管(固定为「禁」),再申请放宽。`,
278
+ target,
279
+ kind,
280
+ hint: 'gate_add',
281
+ };
282
+ }
283
+ if (current === want) return { ok: true, created: false, message: '已经是这个级别,无需申请' };
284
+ const request = addRequest(requestsPath, {
285
+ target,
286
+ targetKind: kind,
287
+ level: want,
288
+ fromLevel: current,
289
+ reason: String(args.reason),
290
+ requestedBy: 'agent',
291
+ });
292
+ return {
293
+ ok: true,
294
+ created: true,
295
+ requestId: request.id,
296
+ target,
297
+ from: current,
298
+ to: want,
299
+ pending: pendingCount(requestsPath),
300
+ message: `已提交待批申请 ${request.id};户主批准前保持现状。`,
301
+ };
302
+ },
303
+ });
304
+
305
+ // ── 6. 看待批 ──────────────────────────────────────────────────
306
+ register({
307
+ name: 'gate_requests',
308
+ description: '列出门禁的待批申请(模型用来看自己提的申请有没有被批准)。',
309
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
310
+ output: jsonResult(),
311
+ execute: async () => listRequests(requestsPath),
312
+ });
313
+
314
+ // ── 7. 看审计 ──────────────────────────────────────────────────
315
+ register({
316
+ name: 'gate_log',
317
+ description: '读门禁最近的决定记录(时间 / 对象 / 级别 / 操作 / 是否放行 / 理由),不读文件内容。',
318
+ parameters: {
319
+ type: 'object',
320
+ properties: { limit: { type: 'integer', description: '条数,默认 20,最多 200' } },
321
+ additionalProperties: false,
322
+ },
323
+ output: jsonResult(),
324
+ execute: async (args) => {
325
+ const limit = Math.max(1, Math.min(200, Number(args.limit) || 20));
326
+ return { entries: readAudit(cfg.auditPath, limit) };
327
+ },
328
+ });
329
+
330
+ return { count: 7, requestsPath };
331
+ }
package/lib/index.js CHANGED
@@ -30,6 +30,8 @@
30
30
  */
31
31
 
32
32
  import { spawn } from 'node:child_process';
33
+ import { registerGateTools } from './gate-tools.js';
34
+ import { clearGrant, grantActive, listRequests, markRequest, requestsPathFor, setGrant, readRequests } from './requests.js';
33
35
  import {
34
36
  assignGroup,
35
37
  createGroup,
@@ -45,6 +47,7 @@ import {
45
47
  setLevel,
46
48
  } from './service.js';
47
49
  import { clearAudit } from './audit.js';
50
+ import { normalizePathKey } from './roster.js';
48
51
  import { collectTargets } from './toolgate.js';
49
52
  import { decideWithProfiles, knownDirBindings, loadProfileState, __clearProfileCaches } from './profiles-runtime.js';
50
53
  import {
@@ -61,7 +64,7 @@ export const name = 'dsh-isolation-gate';
61
64
  /** `tools` 是硬依赖:工具执行门没有它就不成立。 */
62
65
  export const inject = ['tools'];
63
66
 
64
- const VERSION = '0.3.0';
67
+ const VERSION = '1.1.0';
65
68
  const BASE = '/__dsh-isolation-gate';
66
69
  const MAX_BODY = 256 * 1024;
67
70
 
@@ -164,6 +167,16 @@ export function apply(ctx, config) {
164
167
  console.log('[dsh-isolation-gate] 工具执行门已关闭(config.toolGate=false,仅通道层生效)');
165
168
  }
166
169
 
170
+ // ---- 模型可直接调用的工具(2026-10-24)--------------------------------
171
+ // 户主不点面板、只说需求;模型用 gate_* 工具查/收紧/申请,放宽需户主批准。
172
+ try {
173
+ const info = registerGateTools(ctx, cfg);
174
+ console.log(`[dsh-isolation-gate] 已注册 ${info.count} 个模型工具(gate_*)`);
175
+ } catch (error) {
176
+ // 工具注册失败不能拖垮门禁本体(拦截仍然有效)。
177
+ console.log(`[dsh-isolation-gate] 工具注册失败:${error?.message ?? String(error)}`);
178
+ }
179
+
167
180
  // ---- 目录选择器(可选能力:优先宿主抽象服务,Windows 上自带原生兜底)--------
168
181
  // 契约:capability() 返回 `native`(pick)或 `browse`(list/createDirectory)之一。
169
182
  const pickerCapability = (() => {
@@ -241,6 +254,10 @@ export function apply(ctx, config) {
241
254
  `POST ${BASE}/check`,
242
255
  `GET ${BASE}/log`,
243
256
  `POST ${BASE}/audit/clear`,
257
+ `GET ${BASE}/requests`,
258
+ `POST ${BASE}/requests-approve`,
259
+ `POST ${BASE}/requests-reject`,
260
+ `POST ${BASE}/grant`,
244
261
  ],
245
262
  statePath: cfg.statePath,
246
263
  rosterPath: cfg.rosterPath,
@@ -532,6 +549,63 @@ export function apply(ctx, config) {
532
549
  }
533
550
  };
534
551
 
552
+ // ---- 待批申请 / AI 放宽授权(2026-10-24)------------------------------
553
+ // 批准与授权只走本机面板(HTTP),模型工具里没有"批准"这个动作。
554
+ routes.push(
555
+ {
556
+ method: 'GET',
557
+ path: `${BASE}/requests`,
558
+ handler: async () => {
559
+ const file = requestsPathFor(cfg.statePath);
560
+ return { ok: true, ...listRequests(file) };
561
+ },
562
+ },
563
+ {
564
+ method: 'POST',
565
+ path: `${BASE}/requests-approve`,
566
+ handler: async (req) => {
567
+ const body = await readJsonBody(req);
568
+ const file = requestsPathFor(cfg.statePath);
569
+ const request = readRequests(file).requests.find((item) => item.id === body.id);
570
+ if (!request || request.status !== 'pending') return { ok: false, error: '没有这条待批申请' };
571
+ const keys = Object.keys(readState(cfg.statePath).state.objects ?? {});
572
+ const stillManaged = keys.some((k) => normalizePathKey(k) === normalizePathKey(request.target));
573
+ if (!stillManaged) {
574
+ return { ok: false, error: `「${request.target}」已不在受管名单里,无法批准;请让模型重新 gate_add 后再申请。` };
575
+ }
576
+ const applied = setLevel(cfg, { name: request.target, level: request.level, by: 'owner' });
577
+ if (!applied.ok) return { ok: false, error: applied.error || '改级别失败' };
578
+ markRequest(file, request.id, 'approved');
579
+ return { ok: true, request, level: request.level };
580
+ },
581
+ },
582
+ {
583
+ method: 'POST',
584
+ path: `${BASE}/requests-reject`,
585
+ handler: async (req) => {
586
+ const body = await readJsonBody(req);
587
+ const file = requestsPathFor(cfg.statePath);
588
+ const rejected = markRequest(file, String(body.id), 'rejected');
589
+ return rejected ? { ok: true, request: rejected } : { ok: false, error: '没有这条待批申请' };
590
+ },
591
+ },
592
+ {
593
+ method: 'POST',
594
+ path: `${BASE}/grant`,
595
+ handler: async (req) => {
596
+ const body = await readJsonBody(req);
597
+ const file = requestsPathFor(cfg.statePath);
598
+ const minutes = Number(body.minutes);
599
+ if (minutes === 0) {
600
+ const cleared = clearGrant(file);
601
+ return { ok: true, aiGrantUntil: cleared.aiGrantUntil, grantActive: false };
602
+ }
603
+ const data = setGrant(file, minutes || 30);
604
+ return { ok: true, aiGrantUntil: data.aiGrantUntil, grantActive: grantActive(data) };
605
+ },
606
+ },
607
+ );
608
+
535
609
  const webServer = ctx.get('webServer');
536
610
  if (webServer !== undefined) {
537
611
  register(webServer, ctx);
@@ -0,0 +1,131 @@
1
+ /**
2
+ * 待批申请 + AI 授权窗口(与状态文件分开存,避免动 1.0 冻结的 state v3 形状)。
3
+ *
4
+ * 设计约束(2026-10-24 户主拍板):
5
+ * · AI 默认只能收紧、只能新增(默认禁)、只能查;
6
+ * · 放宽(改成 read / full)走「申请 → 户主在面板点批准」;
7
+ * · 户主在面板点「授权 AI 放宽 N 分钟」后,窗口期内 AI 可以直接改,不必逐条批。
8
+ *
9
+ * 落点:与状态文件同目录的 `requests.json`。
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+
14
+ export const REQUESTS_VERSION = 1;
15
+ export const DEFAULT_GRANT_MINUTES = 30;
16
+ export const MAX_PENDING = 50;
17
+
18
+ export function requestsPathFor(statePath) {
19
+ return join(dirname(statePath), 'requests.json');
20
+ }
21
+
22
+ function empty(now = new Date()) {
23
+ return {
24
+ version: REQUESTS_VERSION,
25
+ updatedAt: now.toISOString(),
26
+ aiGrantUntil: null,
27
+ requests: [],
28
+ };
29
+ }
30
+
31
+ function normalize(raw) {
32
+ if (!raw || typeof raw !== 'object') return empty();
33
+ const requests = Array.isArray(raw.requests) ? raw.requests : [];
34
+ return {
35
+ version: REQUESTS_VERSION,
36
+ updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : new Date().toISOString(),
37
+ aiGrantUntil: typeof raw.aiGrantUntil === 'string' ? raw.aiGrantUntil : null,
38
+ requests: requests
39
+ .filter((item) => item && typeof item.id === 'string' && typeof item.target === 'string')
40
+ .slice(0, MAX_PENDING),
41
+ };
42
+ }
43
+
44
+ export function readRequests(filePath) {
45
+ try {
46
+ if (!existsSync(filePath)) return empty();
47
+ return normalize(JSON.parse(readFileSync(filePath, 'utf8')));
48
+ } catch {
49
+ // 读坏 ⇒ 当作空;不影响门禁本体(门禁自己的 fail-closed 在状态文件那侧)。
50
+ return empty();
51
+ }
52
+ }
53
+
54
+ export function writeRequests(filePath, data, now = new Date()) {
55
+ const next = normalize({ ...data, updatedAt: now.toISOString() });
56
+ mkdirSync(dirname(filePath), { recursive: true });
57
+ const tmp = `${filePath}.${process.pid}.tmp`;
58
+ writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8');
59
+ renameSync(tmp, filePath);
60
+ return next;
61
+ }
62
+
63
+ export function grantActive(data, now = new Date()) {
64
+ if (!data || !data.aiGrantUntil) return false;
65
+ const until = Date.parse(data.aiGrantUntil);
66
+ return Number.isFinite(until) && until > now.getTime();
67
+ }
68
+
69
+ export function setGrant(filePath, minutes = DEFAULT_GRANT_MINUTES, now = new Date()) {
70
+ const data = readRequests(filePath);
71
+ const span = Math.max(1, Math.min(240, Number(minutes) || DEFAULT_GRANT_MINUTES));
72
+ data.aiGrantUntil = new Date(now.getTime() + span * 60_000).toISOString();
73
+ return writeRequests(filePath, data, now);
74
+ }
75
+
76
+ export function clearGrant(filePath, now = new Date()) {
77
+ const data = readRequests(filePath);
78
+ data.aiGrantUntil = null;
79
+ return writeRequests(filePath, data, now);
80
+ }
81
+
82
+ /** 待批:同目标 + 同级别只留一条(新的覆盖旧的),避免刷屏。 */
83
+ export function addRequest(filePath, input, now = new Date()) {
84
+ const data = readRequests(filePath);
85
+ const pending = data.requests.filter(
86
+ (item) => item.status === 'pending' && !(item.target === input.target && item.level === input.level),
87
+ );
88
+ const request = {
89
+ id: `req-${now.getTime().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
90
+ target: String(input.target),
91
+ targetKind: input.targetKind === 'path' ? 'path' : 'name',
92
+ level: input.level,
93
+ fromLevel: input.fromLevel ?? null,
94
+ reason: String(input.reason ?? '').slice(0, 500),
95
+ requestedBy: String(input.requestedBy ?? 'agent').slice(0, 80),
96
+ createdAt: now.toISOString(),
97
+ status: 'pending',
98
+ decidedAt: null,
99
+ };
100
+ data.requests = [request, ...pending].slice(0, MAX_PENDING);
101
+ writeRequests(filePath, data, now);
102
+ return request;
103
+ }
104
+
105
+ export function listRequests(filePath, options = {}) {
106
+ const data = readRequests(filePath);
107
+ const includeAll = options.includeAll === true;
108
+ return {
109
+ aiGrantUntil: data.aiGrantUntil,
110
+ grantActive: grantActive(data, options.now),
111
+ requests: includeAll ? data.requests : data.requests.filter((item) => item.status === 'pending'),
112
+ };
113
+ }
114
+
115
+ export function findRequest(filePath, id) {
116
+ const data = readRequests(filePath);
117
+ return data.requests.find((item) => item.id === id) ?? null;
118
+ }
119
+
120
+ export function markRequest(filePath, id, status, now = new Date()) {
121
+ const data = readRequests(filePath);
122
+ const index = data.requests.findIndex((item) => item.id === id);
123
+ if (index < 0) return null;
124
+ data.requests[index] = { ...data.requests[index], status, decidedAt: now.toISOString() };
125
+ writeRequests(filePath, data, now);
126
+ return data.requests[index];
127
+ }
128
+
129
+ export function pendingCount(filePath) {
130
+ return listRequests(filePath).requests.length;
131
+ }
package/package.json CHANGED
@@ -1,79 +1,79 @@
1
- {
2
- "name": "dsh-isolation-gate",
3
- "version": "0.4.0",
4
- "description": "门禁 · 目录访问管制(隔离区授权台):一屏管理「哪些目录受管、各自什么级别」,三级=禁/只读/可读写删;判定同时挂在受管辖通道(bin/gate.mjs)与宿主工具执行门(ctx.tools.guard)上,默认禁、名单缺失一律拒绝(fail-closed)、审计可查、agent 可自查;支持分组批量切、目录选择器与状态文件自动迁移。",
5
- "keywords": [
6
- "dsh",
7
- "dsh-plugin",
8
- "dsh-bundle",
9
- "isolation-gate",
10
- "permissions",
11
- "isolation",
12
- "authorization",
13
- "sandbox",
14
- "agent-safety",
15
- "access-control",
16
- "windows",
17
- "deepseek-harness",
18
- "cordis",
19
- "ai-agent",
20
- "security"
21
- ],
22
- "license": "MIT",
23
- "publishConfig": {
24
- "access": "public"
25
- },
26
- "type": "module",
27
- "main": "./lib/index.js",
28
- "exports": {
29
- ".": {
30
- "default": "./lib/index.js"
31
- },
32
- "./client": {
33
- "default": "./lib/client.js"
34
- },
35
- "./package.json": "./package.json",
36
- "./locale/*.json": "./locale/*.json"
37
- },
38
- "bin": {
39
- "gate": "bin/gate.mjs"
40
- },
41
- "files": [
42
- "lib",
43
- "bin",
44
- "locale",
45
- "cordis.patch.yml",
46
- "README.md",
47
- "README.en.md",
48
- "CHANGELOG.md",
49
- "LICENSE",
50
- "docs"
51
- ],
52
- "engines": {
53
- "node": ">=20.11"
54
- },
55
- "scripts": {
56
- "test": "node test/run-all.mjs",
57
- "gate": "node bin/gate.mjs",
58
- "release:check": "node scripts/release-sync.mjs"
59
- },
60
- "dsh": {
61
- "bundle": {
62
- "patch": "./cordis.patch.yml"
63
- },
64
- "client": {
65
- "inject": [
66
- "@deepseek-ai/dsh-client-runtime"
67
- ],
68
- "platform": "web"
69
- }
70
- },
71
- "repository": {
72
- "type": "git",
73
- "url": "git+https://github.com/huanlin-labs/dsh-isolation-gate.git"
74
- },
75
- "homepage": "https://github.com/huanlin-labs/dsh-isolation-gate#readme",
76
- "bugs": {
77
- "url": "https://github.com/huanlin-labs/dsh-isolation-gate/issues"
78
- }
79
- }
1
+ {
2
+ "name": "dsh-isolation-gate",
3
+ "version": "1.1.0",
4
+ "description": "门禁 · 目录访问管制(隔离区授权台):一屏管理「哪些目录受管、各自什么级别」,三级=禁/只读/可读写删;判定同时挂在受管辖通道(bin/gate.mjs)与宿主工具执行门(ctx.tools.guard)上,默认禁、名单缺失一律拒绝(fail-closed)、审计可查、agent 可自查;支持分组批量切、目录选择器与状态文件自动迁移。",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "dsh-bundle",
9
+ "isolation-gate",
10
+ "permissions",
11
+ "isolation",
12
+ "authorization",
13
+ "sandbox",
14
+ "agent-safety",
15
+ "access-control",
16
+ "windows",
17
+ "deepseek-harness",
18
+ "cordis",
19
+ "ai-agent",
20
+ "security"
21
+ ],
22
+ "license": "MIT",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "main": "./lib/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "default": "./lib/index.js"
31
+ },
32
+ "./client": {
33
+ "default": "./lib/client.js"
34
+ },
35
+ "./package.json": "./package.json",
36
+ "./locale/*.json": "./locale/*.json"
37
+ },
38
+ "bin": {
39
+ "gate": "bin/gate.mjs"
40
+ },
41
+ "files": [
42
+ "lib",
43
+ "bin",
44
+ "locale",
45
+ "cordis.patch.yml",
46
+ "README.md",
47
+ "README.en.md",
48
+ "CHANGELOG.md",
49
+ "LICENSE",
50
+ "docs"
51
+ ],
52
+ "engines": {
53
+ "node": ">=20.11"
54
+ },
55
+ "scripts": {
56
+ "test": "node test/run-all.mjs",
57
+ "gate": "node bin/gate.mjs",
58
+ "release:check": "node scripts/release-sync.mjs"
59
+ },
60
+ "dsh": {
61
+ "bundle": {
62
+ "patch": "./cordis.patch.yml"
63
+ },
64
+ "client": {
65
+ "inject": [
66
+ "@deepseek-ai/dsh-client-runtime"
67
+ ],
68
+ "platform": "web"
69
+ }
70
+ },
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "git+https://github.com/huanlin-labs/dsh-isolation-gate.git"
74
+ },
75
+ "homepage": "https://github.com/huanlin-labs/dsh-isolation-gate#readme",
76
+ "bugs": {
77
+ "url": "https://github.com/huanlin-labs/dsh-isolation-gate/issues"
78
+ }
79
+ }