dsh-runtime-kit 0.1.1 → 0.3.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/README.md CHANGED
@@ -31,6 +31,45 @@
31
31
 
32
32
  ---
33
33
 
34
+ ## 🛡️ 治理钩子(0.2.0 起接入请求路径)
35
+
36
+ 0.2.0 之前,上表所有能力都要靠命令手动触发。现在 C7 审计、C6 的脱敏/注入检测已接入
37
+ DSH 的工具调用链;沙箱拦截可以做,但**默认关闭**。
38
+
39
+ | 事件 | 做什么 | 开关 |
40
+ |---|---|---|
41
+ | `tools/result` | 每次工具调用写一条审计 | `governance.audit` |
42
+ | `tools/post-execute` | 返回体里的密钥替换成 `[redacted:kind]`;疑似注入追加警告块或拦截 | `governance.redaction` / `governance.injection` |
43
+ | `tools/pre-execute` | 按文件/命令/网络/工具启停规则拒绝调用 | `governance.sandbox`(默认 `false`) |
44
+
45
+ 三条硬约束:**fail-open**(内部异常一律放行)、**失败结果不改写**、**默认只开零风险项**。
46
+
47
+ ```jsonc
48
+ "governance": {
49
+ "enabled": true,
50
+ "audit": true,
51
+ "redaction": true,
52
+ "injection": "annotate", // off | annotate | block
53
+ "sandbox": false, // 打开后会真的拒绝调用
54
+ "profile": "default"
55
+ }
56
+ ```
57
+
58
+ 细节、`dsh-context` 共存策略与「返回体截断」的方案见 [`docs/GOVERNANCE.md`](docs/GOVERNANCE.md)。
59
+
60
+ ### 页面
61
+
62
+ 插件带一个客户端半(`client.js`),治理状态不用再敲命令看:
63
+
64
+ - **设置页 → 运行时治理**:总览(工具/模板/工作区/审计条数)、四个开关、沙箱规则、最近 20 条审计流水、注册表内容
65
+ - **侧栏 → 运行时**(装了 better-sidebar 时):同一套内容,日常查看更顺手
66
+
67
+ 开关改完点保存即刻生效,不需要重启;选择写进 `<stateDir>/governance.json`,下次启动沿用。
68
+
69
+ 页面和命令读的是同一份状态:`runtime_kit_health`、`runtime_kit_audit_logs`、`runtime_kit_config` 看到的结果与页面一致。
70
+
71
+ ---
72
+
34
73
  ## 🔌 对外契约:Registry(唯一耦合面)
35
74
 
36
75
  ```typescript
package/client.js ADDED
@@ -0,0 +1,522 @@
1
+ /**
2
+ * dsh-runtime-kit — Client half.
3
+ *
4
+ * The page exists because the plugin's abilities used to be reachable only by
5
+ * typing `runtime_kit_*`. Everything shown here comes from `/runtime-kit/api`,
6
+ * so the page and the commands never disagree.
7
+ *
8
+ * Two surfaces, one component: a Settings section (always available) and a
9
+ * better-sidebar tab (when that plugin is installed).
10
+ */
11
+ window.__ModuleLoader__.load({
12
+ id: 'dsh-runtime-kit',
13
+ factory(require) {
14
+ const React = require('react')
15
+ const h = React.createElement
16
+ const { useState, useEffect, useCallback } = React
17
+
18
+ const isZh = /^zh/i.test((typeof navigator !== 'undefined' && navigator.language) || 'zh')
19
+ const L = (zh, en) => (isZh ? zh : en)
20
+
21
+ const API = '/runtime-kit/api/'
22
+
23
+ async function call(method, body) {
24
+ const res = await fetch(API + method, {
25
+ method: 'POST',
26
+ headers: { 'content-type': 'application/json' },
27
+ credentials: 'same-origin',
28
+ body: JSON.stringify(body ?? {}),
29
+ })
30
+ const payload = await res.json().catch(() => null)
31
+ if (!payload || payload.ok !== true) {
32
+ const message = payload?.error?.message || `HTTP ${res.status}`
33
+ throw new Error(message)
34
+ }
35
+ return payload.value
36
+ }
37
+
38
+ // ------------------------------------------------------------ styles ---
39
+ const CSS = `
40
+ .rk-root{max-width:820px;display:flex;flex-direction:column;gap:12px;color:var(--dsw-alias-label-primary,inherit);font-size:13px;line-height:1.5}
41
+ .rk-root,.rk-root *{box-sizing:border-box}
42
+ .rk-hero{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
43
+ .rk-h1{font-size:15px;font-weight:600;letter-spacing:.2px}
44
+ .rk-sub{font-size:12px;color:var(--dsw-alias-label-secondary,#9a9a9a)}
45
+ .rk-card{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.28));border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
46
+ .rk-card-head{display:flex;align-items:center;gap:8px}
47
+ .rk-card-title{font-size:13px;font-weight:600;flex:1;min-width:0}
48
+ .rk-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
49
+ .rk-stat{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));border-radius:10px;padding:9px 11px;display:flex;flex-direction:column;gap:2px;min-width:0}
50
+ .rk-stat-k{font-size:11.5px;color:var(--dsw-alias-label-secondary,#9a9a9a)}
51
+ .rk-stat-v{font-size:18px;font-weight:600;font-variant-numeric:tabular-nums}
52
+ .rk-row{display:flex;align-items:center;gap:10px;justify-content:space-between;padding:7px 0;border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.14))}
53
+ .rk-row:first-child{border-top:0}
54
+ .rk-row-main{display:flex;flex-direction:column;gap:1px;min-width:0}
55
+ .rk-row-t{font-size:12.5px}
56
+ .rk-row-d{font-size:11.5px;color:var(--dsw-alias-label-secondary,#9a9a9a)}
57
+ .rk-badge{font-size:11px;padding:1px 7px;border-radius:20px;border:1px solid currentColor;white-space:nowrap}
58
+ .rk-badge.ok{color:#3fb950}.rk-badge.warn{color:#d29922}.rk-badge.bad{color:#f85149}.rk-badge.muted{color:var(--dsw-alias-label-secondary,#9a9a9a)}
59
+ .rk-btn{height:28px;padding:0 11px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer;white-space:nowrap}
60
+ .rk-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12))}
61
+ .rk-btn:disabled{opacity:.5;cursor:default}
62
+ .rk-btn.primary{border-color:color-mix(in srgb,#4c8dff 55%,transparent);color:var(--dsw-alias-brand-primary,#8ab4ff);background:color-mix(in srgb,#4c8dff 13%,transparent)}
63
+ .rk-seg{display:inline-flex;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:8px;overflow:hidden}
64
+ .rk-seg button{height:26px;padding:0 10px;border:0;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}
65
+ .rk-seg button.on{background:color-mix(in srgb,#4c8dff 18%,transparent);color:var(--dsw-alias-brand-primary,#7fb0ff)}
66
+ .rk-switch{position:relative;width:34px;height:19px;border-radius:20px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:transparent;cursor:pointer;flex:0 0 auto;padding:0}
67
+ .rk-switch.on{background:color-mix(in srgb,#4c8dff 45%,transparent);border-color:transparent}
68
+ .rk-switch::after{content:'';position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:var(--dsw-alias-label-secondary,#9a9a9a);transition:left .12s}
69
+ .rk-switch.on::after{left:17px;background:#fff}
70
+ .rk-mono{font-family:var(--dsw-font-mono,ui-monospace,Consolas,monospace);font-size:11.5px;color:var(--dsw-alias-label-secondary,#9a9a9a);word-break:break-all}
71
+ .rk-table{width:100%;border-collapse:collapse;font-size:11.5px}
72
+ .rk-table th{text-align:left;font-weight:500;color:var(--dsw-alias-label-secondary,#9a9a9a);padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));white-space:nowrap}
73
+ .rk-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.1));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px}
74
+ .rk-chips{display:flex;flex-wrap:wrap;gap:5px}
75
+ .rk-chip{font-family:var(--dsw-font-mono,ui-monospace,monospace);font-size:11px;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12))}
76
+ .rk-empty{font-size:12px;color:var(--dsw-alias-label-secondary,#9a9a9a);padding:2px 0}
77
+ .rk-err{font-size:12px;color:#f85149}
78
+ `
79
+
80
+ let cssReady = false
81
+ function ensureCss() {
82
+ if (cssReady || typeof document === 'undefined') return
83
+ cssReady = true
84
+ const style = document.createElement('style')
85
+ style.id = 'runtime-kit-style'
86
+ style.textContent = CSS
87
+ document.head.appendChild(style)
88
+ }
89
+
90
+ // ------------------------------------------------------------ pieces ---
91
+
92
+ function Card({ title, right, children }) {
93
+ return h(
94
+ 'div',
95
+ { className: 'rk-card' },
96
+ h('div', { className: 'rk-card-head' }, h('div', { className: 'rk-card-title' }, title), right || null),
97
+ children,
98
+ )
99
+ }
100
+
101
+ function Stat({ label, value, tone }) {
102
+ return h(
103
+ 'div',
104
+ { className: 'rk-stat' },
105
+ h('div', { className: 'rk-stat-k' }, label),
106
+ h(
107
+ 'div',
108
+ {
109
+ className: 'rk-stat-v',
110
+ style: {
111
+ ...(tone ? { color: tone } : null),
112
+ // 字符串值(workspace-write / deny)比数字长,缩小一号才不折行
113
+ ...(typeof value === 'string' && value.length > 8 ? { fontSize: '14px' } : null),
114
+ },
115
+ },
116
+ String(value ?? '—'),
117
+ ),
118
+ )
119
+ }
120
+
121
+ function Switch({ on, onChange, disabled }) {
122
+ return h('button', {
123
+ type: 'button',
124
+ className: `rk-switch${on ? ' on' : ''}`,
125
+ 'aria-pressed': on ? 'true' : 'false',
126
+ disabled,
127
+ onClick: () => onChange(!on),
128
+ })
129
+ }
130
+
131
+ function ToggleRow({ title, desc, on, disabled, onChange }) {
132
+ return h(
133
+ 'div',
134
+ { className: 'rk-row' },
135
+ h(
136
+ 'div',
137
+ { className: 'rk-row-main' },
138
+ h('div', { className: 'rk-row-t' }, title),
139
+ desc ? h('div', { className: 'rk-row-d' }, desc) : null,
140
+ ),
141
+ h(Switch, { on, onChange, disabled }),
142
+ )
143
+ }
144
+
145
+ function toneOf(status) {
146
+ if (status === 'healthy') return 'ok'
147
+ if (status === 'degraded') return 'warn'
148
+ return 'bad'
149
+ }
150
+
151
+ function stamp(iso) {
152
+ const d = new Date(iso)
153
+ if (Number.isNaN(d.getTime())) return String(iso || '')
154
+ const p = (n) => String(n).padStart(2, '0')
155
+ return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
156
+ }
157
+
158
+ // -------------------------------------------------------------- panel ---
159
+
160
+ function RuntimeKitPanel({ initial }) {
161
+ ensureCss()
162
+ // `initial` exists for the render tests: SSR runs no effects, so a page
163
+ // that only fetches would always screenshot as "loading".
164
+ const [state, setState] = useState(initial || null)
165
+ const [governance, setGovernance] = useState(initial?.governance || null)
166
+ const [busy, setBusy] = useState('')
167
+ const [note, setNote] = useState('')
168
+ const [error, setError] = useState('')
169
+
170
+ const load = useCallback(async () => {
171
+ setBusy('load')
172
+ setError('')
173
+ try {
174
+ const value = await call('state')
175
+ setState(value)
176
+ setGovernance(value.governance)
177
+ } catch (e) {
178
+ setError(String(e.message || e))
179
+ } finally {
180
+ setBusy('')
181
+ }
182
+ }, [])
183
+
184
+ useEffect(() => {
185
+ load()
186
+ }, [load])
187
+
188
+ const save = async () => {
189
+ setBusy('save')
190
+ setNote('')
191
+ setError('')
192
+ try {
193
+ const value = await call('governance', { governance })
194
+ setGovernance(value.governance)
195
+ setNote(L('已保存,立即生效。', 'Saved. Takes effect immediately.'))
196
+ await load()
197
+ } catch (e) {
198
+ setError(String(e.message || e))
199
+ } finally {
200
+ setBusy('')
201
+ }
202
+ }
203
+
204
+ const dirty = Boolean(
205
+ state && governance && JSON.stringify(state.governance) !== JSON.stringify(governance),
206
+ )
207
+
208
+ const health = state?.health || {}
209
+ const audit = state?.audit || []
210
+ const registry = state?.registry || { tools: [], templates: [], workspaces: [] }
211
+ const sandbox = state?.sandbox || {}
212
+
213
+ return h(
214
+ 'div',
215
+ { className: 'rk-root' },
216
+ h(
217
+ 'div',
218
+ { className: 'rk-hero' },
219
+ h('div', { className: 'rk-h1' }, L('运行时治理', 'Runtime governance')),
220
+ h(
221
+ 'span',
222
+ { className: `rk-badge ${toneOf(health.status)}` },
223
+ health.status || (busy === 'load' ? L('读取中', 'loading') : L('未知', 'unknown')),
224
+ ),
225
+ h('div', { className: 'rk-sub' }, `v${health.version || '—'} · dsh ${health.dshVersion || '—'}`),
226
+ h('div', { style: { flex: '1' } }),
227
+ h('button', { className: 'rk-btn', onClick: load, disabled: Boolean(busy) }, L('刷新', 'Refresh')),
228
+ ),
229
+
230
+ error ? h('div', { className: 'rk-err' }, error) : null,
231
+
232
+ h(
233
+ Card,
234
+ { title: L('总览', 'Overview') },
235
+ h(
236
+ 'div',
237
+ { className: 'rk-grid' },
238
+ h(Stat, { label: L('注册工具', 'Tools'), value: registry.tools.length }),
239
+ h(Stat, { label: L('快路径模板', 'Templates'), value: registry.templates.length }),
240
+ h(Stat, { label: L('沙箱工作区', 'Workspaces'), value: registry.workspaces.length }),
241
+ h(Stat, { label: L('审计条数', 'Audit entries'), value: audit.length }),
242
+ ),
243
+ h(
244
+ 'div',
245
+ { className: 'rk-sub' },
246
+ L('这些数字来自插件自身注册表,不是推测。', 'Read from the plugin registry, not guessed.'),
247
+ ),
248
+ ),
249
+
250
+ h(
251
+ Card,
252
+ {
253
+ title: L('治理开关', 'Governance switches'),
254
+ right: h(
255
+ 'button',
256
+ { className: 'rk-btn primary', onClick: save, disabled: !dirty || Boolean(busy) },
257
+ busy === 'save' ? L('保存中', 'Saving') : L('保存', 'Save'),
258
+ ),
259
+ },
260
+ governance
261
+ ? [
262
+ h(ToggleRow, {
263
+ key: 'enabled',
264
+ title: L('总开关', 'Master switch'),
265
+ desc: L('关闭后钩子仍在,但每次调用都直接放行', 'Hooks stay registered; every call passes straight through'),
266
+ on: governance.enabled,
267
+ disabled: Boolean(busy),
268
+ onChange: (on) => setGovernance({ ...governance, enabled: on }),
269
+ }),
270
+ h(ToggleRow, {
271
+ key: 'audit',
272
+ title: L('审计', 'Audit'),
273
+ desc: L('把每次工具调用写进审计流水(只读)', 'Record every tool call (read-only)'),
274
+ on: governance.audit,
275
+ disabled: Boolean(busy),
276
+ onChange: (on) => setGovernance({ ...governance, audit: on }),
277
+ }),
278
+ h(ToggleRow, {
279
+ key: 'redaction',
280
+ title: L('脱敏', 'Redaction'),
281
+ desc: L('返回体里的密钥替换成 [redacted:kind]', 'Replace secrets with [redacted:kind]'),
282
+ on: governance.redaction,
283
+ disabled: Boolean(busy),
284
+ onChange: (on) => setGovernance({ ...governance, redaction: on }),
285
+ }),
286
+ h(
287
+ 'div',
288
+ { className: 'rk-row' },
289
+ h(
290
+ 'div',
291
+ { className: 'rk-row-main' },
292
+ h('div', { className: 'rk-row-t' }, L('提示注入', 'Prompt injection')),
293
+ h(
294
+ 'div',
295
+ { className: 'rk-row-d' },
296
+ L('检测工具返回里“改变模型行为”的指令', 'Detect instructions inside tool output'),
297
+ ),
298
+ ),
299
+ h(
300
+ 'div',
301
+ { className: 'rk-seg' },
302
+ ...[
303
+ ['off', L('不检测', 'off')],
304
+ ['annotate', L('标注', 'annotate')],
305
+ ['block', L('拦截', 'block')],
306
+ ].map(([value, label]) =>
307
+ h(
308
+ 'button',
309
+ {
310
+ key: value,
311
+ className: governance.injection === value ? 'on' : '',
312
+ onClick: () => setGovernance({ ...governance, injection: value }),
313
+ },
314
+ label,
315
+ ),
316
+ ),
317
+ ),
318
+ ),
319
+ h(ToggleRow, {
320
+ key: 'sandbox',
321
+ title: L('沙箱拦截', 'Sandbox gate'),
322
+ desc: L(
323
+ '按文件/命令/网络规则拒绝调用;打开后可以真的挡住操作',
324
+ 'Deny calls by file/command/network rules; this one can block work',
325
+ ),
326
+ on: governance.sandbox,
327
+ disabled: Boolean(busy),
328
+ onChange: (on) => setGovernance({ ...governance, sandbox: on }),
329
+ }),
330
+ note ? h('div', { className: 'rk-sub' }, note) : null,
331
+ ]
332
+ : h('div', { className: 'rk-empty' }, L('读取中…', 'Loading…')),
333
+ ),
334
+
335
+ h(
336
+ Card,
337
+ { title: L('沙箱规则', 'Sandbox rules') },
338
+ h(
339
+ 'div',
340
+ { className: 'rk-grid' },
341
+ h(Stat, { label: L('基线', 'Mode'), value: sandbox.mode || '—' }),
342
+ h(Stat, { label: L('审批', 'Approval'), value: sandbox.approval || '—' }),
343
+ h(Stat, { label: L('网络默认', 'Network default'), value: sandbox.networkDefault || '—' }),
344
+ h(Stat, {
345
+ label: L('规则数(文件/命令/网络)', 'Rules (file/cmd/net)'),
346
+ value: `${sandbox.files ?? 0}/${sandbox.commands ?? 0}/${sandbox.network ?? 0}`,
347
+ }),
348
+ ),
349
+ h(
350
+ 'div',
351
+ { className: 'rk-sub' },
352
+ `${L('注入防护', 'Injection guard')}: ${sandbox.injectionGuard ? L('开', 'on') : L('关', 'off')} · ${L(
353
+ '敏感保护',
354
+ 'Sensitive protection',
355
+ )}: ${sandbox.sensitiveProtection ? L('开', 'on') : L('关', 'off')} · ${L(
356
+ '工具粒度管控',
357
+ 'Tool control',
358
+ )}: ${sandbox.toolControl ?? 0}`,
359
+ ),
360
+ ),
361
+
362
+ h(
363
+ Card,
364
+ { title: L('审计流水', 'Audit log') },
365
+ audit.length
366
+ ? h(
367
+ 'table',
368
+ { className: 'rk-table' },
369
+ h(
370
+ 'thead',
371
+ null,
372
+ h(
373
+ 'tr',
374
+ null,
375
+ h('th', null, L('时间', 'Time')),
376
+ h('th', null, L('工具', 'Tool')),
377
+ h('th', null, L('动作', 'Action')),
378
+ h('th', null, L('结果', 'Result')),
379
+ h('th', null, L('备注', 'Note')),
380
+ ),
381
+ ),
382
+ h(
383
+ 'tbody',
384
+ null,
385
+ audit
386
+ .slice(-20)
387
+ .reverse()
388
+ .map((entry, i) =>
389
+ h(
390
+ 'tr',
391
+ { key: `${entry.timestamp}-${i}` },
392
+ h('td', { className: 'rk-mono' }, stamp(entry.timestamp)),
393
+ h('td', { className: 'rk-mono' }, entry.tool),
394
+ h('td', null, entry.action),
395
+ h(
396
+ 'td',
397
+ null,
398
+ h(
399
+ 'span',
400
+ {
401
+ className: `rk-badge ${
402
+ entry.result === 'SUCCESS' ? 'ok' : entry.result === 'ASKED' ? 'warn' : 'bad'
403
+ }`,
404
+ },
405
+ entry.result,
406
+ ),
407
+ ),
408
+ h('td', { className: 'rk-mono', title: entry.error || '' }, entry.error || ''),
409
+ ),
410
+ ),
411
+ ),
412
+ )
413
+ : h('div', { className: 'rk-empty' }, L('还没有调用记录。', 'No calls recorded yet.')),
414
+ ),
415
+
416
+ h(
417
+ Card,
418
+ { title: L('注册表', 'Registry') },
419
+ h('div', { className: 'rk-sub' }, L('工具', 'Tools')),
420
+ h(
421
+ 'div',
422
+ { className: 'rk-chips' },
423
+ ...(registry.tools.length
424
+ ? registry.tools.map((name) => h('span', { key: name, className: 'rk-chip' }, name))
425
+ : [h('span', { key: 'none', className: 'rk-empty' }, L('无', 'none'))]),
426
+ ),
427
+ h('div', { className: 'rk-sub' }, L('模板', 'Templates')),
428
+ h(
429
+ 'div',
430
+ { className: 'rk-chips' },
431
+ ...(registry.templates.length
432
+ ? registry.templates.map((name) => h('span', { key: name, className: 'rk-chip' }, name))
433
+ : [h('span', { key: 'none', className: 'rk-empty' }, L('无', 'none'))]),
434
+ ),
435
+ h('div', { className: 'rk-sub' }, L('工作区', 'Workspaces')),
436
+ h(
437
+ 'div',
438
+ { className: 'rk-chips' },
439
+ ...(registry.workspaces.length
440
+ ? registry.workspaces.map((ws) =>
441
+ h('span', { key: ws.profile, className: 'rk-chip' }, `${ws.profile} → ${ws.root}`),
442
+ )
443
+ : [h('span', { key: 'none', className: 'rk-empty' }, L('未注册', 'none registered'))]),
444
+ ),
445
+ ),
446
+ )
447
+ }
448
+
449
+ function SettingsSection() {
450
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '12px' } }, h(RuntimeKitPanel))
451
+ }
452
+
453
+ // Icon: a small shield, drawn inline so the client half stays dependency-free.
454
+ function Icon({ size = 16 } = {}) {
455
+ const s = typeof size === 'number' ? size : 16
456
+ return h(
457
+ 'svg',
458
+ { width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true' },
459
+ h('path', { d: 'M8 1.6 13 3.4v4.2c0 3-2 5.6-5 6.8-3-1.2-5-3.8-5-6.8V3.4L8 1.6Z', stroke: 'currentColor', 'stroke-width': '1.2' }),
460
+ h('path', { d: 'M6.2 8.1 7.6 9.5l2.4-2.6', stroke: 'currentColor', 'stroke-width': '1.2', 'stroke-linecap': 'round' }),
461
+ )
462
+ }
463
+
464
+ return {
465
+ inject: ['slots'],
466
+ __test: { RuntimeKitPanel, SettingsSection, css: CSS },
467
+ apply(ctx) {
468
+ ctx.slots.inject('settings.section', () =>
469
+ ctx.slots.register(
470
+ { name: 'settings.section', id: 'runtime-kit', order: 51, label: () => L('运行时治理', 'Runtime governance') },
471
+ SettingsSection,
472
+ ),
473
+ )
474
+
475
+ const registerTab = (sidebar) => {
476
+ if (!sidebar || typeof sidebar.registerTab !== 'function') return false
477
+ try {
478
+ sidebar.registerTab({
479
+ id: 'runtime-kit',
480
+ title: () => L('运行时', 'Runtime'),
481
+ description: () => L('上下文治理、沙箱与审计', 'Context governance, sandbox and audit'),
482
+ icon: Icon,
483
+ order: 41,
484
+ single: true,
485
+ component: RuntimeKitPanel,
486
+ })
487
+ return true
488
+ } catch {
489
+ return false
490
+ }
491
+ }
492
+
493
+ let sidebar
494
+ try {
495
+ sidebar = typeof ctx.get === 'function' ? ctx.get('betterSidebar') : undefined
496
+ } catch {
497
+ sidebar = undefined
498
+ }
499
+ if (!sidebar) {
500
+ try {
501
+ sidebar = ctx.betterSidebar
502
+ } catch {
503
+ sidebar = undefined
504
+ }
505
+ }
506
+ if (!registerTab(sidebar) && typeof ctx.inject === 'function') {
507
+ try {
508
+ ctx.inject(['betterSidebar'], (inner) => {
509
+ try {
510
+ registerTab(inner.betterSidebar)
511
+ } catch {
512
+ /* service absent in that scope */
513
+ }
514
+ })
515
+ } catch {
516
+ /* no injectable service; the settings page carries the feature */
517
+ }
518
+ }
519
+ },
520
+ }
521
+ },
522
+ })
@@ -195,6 +195,43 @@
195
195
  }
196
196
  },
197
197
  "additionalProperties": false
198
+ },
199
+ "governance": {
200
+ "type": "object",
201
+ "properties": {
202
+ "enabled": {
203
+ "type": "boolean",
204
+ "default": true,
205
+ "description": "治理钩子总开关;关闭后不注册任何钩子"
206
+ },
207
+ "audit": {
208
+ "type": "boolean",
209
+ "default": true,
210
+ "description": "把每次工具调用写入审计流水(只读观察,不改变行为)"
211
+ },
212
+ "redaction": {
213
+ "type": "boolean",
214
+ "default": true,
215
+ "description": "把工具返回里的密钥替换为 [redacted:kind]"
216
+ },
217
+ "injection": {
218
+ "type": "string",
219
+ "enum": ["off", "annotate", "block"],
220
+ "default": "annotate",
221
+ "description": "提示注入处理:off 不检测 / annotate 追加警告块 / block 拦截本次返回"
222
+ },
223
+ "sandbox": {
224
+ "type": "boolean",
225
+ "default": false,
226
+ "description": "启用调用前拦截(文件/命令/网络/工具启停);开启后会真的拒绝调用"
227
+ },
228
+ "profile": {
229
+ "type": "string",
230
+ "default": "default",
231
+ "description": "沙箱判定使用的工作区 profile"
232
+ }
233
+ },
234
+ "additionalProperties": false
198
235
  }
199
236
  },
200
237
  "required": ["context", "loop", "sandbox", "audit"],