dsh-bash-guard 0.1.0 → 0.1.2

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
@@ -3,6 +3,7 @@
3
3
  <p align="center">4 个范围 × 3 种权限。12 个开关,一个原生菜单。</p>
4
4
 
5
5
  <p align="center">
6
+ <a href="https://www.npmjs.com/package/dsh-bash-guard"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-bash-guard"></a>
6
7
  <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
7
8
  <img alt="DSH Web plugin" src="https://img.shields.io/badge/DSH-Web_plugin-555">
8
9
  <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-policy_classifier-3178C6?logo=typescript&logoColor=white">
@@ -18,9 +19,9 @@
18
19
 
19
20
  **DSH Bash Guard 是 DeepSeek Harness 的独立 Web 插件。** 它把 Bash Agent / Bash Guard 的四位权限思路带进 DSH 原生权限菜单,让你按范围控制工具操作,而不必在“只读”和“全部放开”之间二选一。
20
21
 
21
- 不改宿主源码,不增加一排数字预设,也不用记斜杠命令。展开 `bash-guard 自定义`,直接调整系统、外部、网络和工作区的读、写、执行权限。
22
+ 不改宿主源码,不增加一排数字预设,也不用记斜杠命令。展开 `Bash Guard 自定义`,直接调整系统、外部、网络和工作区的读、写、执行权限。
22
23
 
23
- > **它是额外的工具权限检查,不是沙箱替代品。** 即使设为 `7777`,DSH 原生沙箱和审批仍然适用。
24
+ > **Bash Guard 使用原生 Full access 基线,不使用工作区沙箱。** 激活前需要确认风险;仅支持的工具受权限位限制。未接管工具和无法准确分类的动态行为不受同等保护,Guard 不是安全沙箱。
24
25
 
25
26
  ## 为什么使用它
26
27
 
@@ -32,9 +33,34 @@
32
33
 
33
34
  ## 快速开始
34
35
 
35
- ### 从源码安装
36
+ ### 从 npm 安装(推荐)
36
37
 
37
- 需要 Git、Node.js / npm,以及支持插件的 DSH。当前版本针对 DSH Web 集成验证,尚未声明跨宿主版本兼容。
38
+ 需要 DSH `0.2.0-rc.2` 或更新版本;兼容范围说明见下文。插件安装到 DSH 的 `web` profile,无需全局安装 npm 包,也无需克隆或手动构建。
39
+
40
+ ```bash
41
+ dsh plugin --profile web add dsh-bash-guard
42
+ dsh --profile web
43
+ ```
44
+
45
+ 安装后重新启动对应的 DSH Web 实例,在权限菜单中选择 **Bash Guard 自定义**。
46
+
47
+ [npm 包页面](https://www.npmjs.com/package/dsh-bash-guard) · 当前发布版本:`0.1.0`
48
+
49
+ 升级到最新发布版本:
50
+
51
+ ```bash
52
+ dsh plugin --profile web add dsh-bash-guard@latest
53
+ ```
54
+
55
+ 如果 npm 镜像尚未同步新版本,可以显式使用官方 registry:
56
+
57
+ ```bash
58
+ dsh plugin --profile web add dsh-bash-guard@latest --registry=https://registry.npmjs.org
59
+ ```
60
+
61
+ ### 从源码安装(开发者)
62
+
63
+ 需要 Git、Node.js / npm,以及上述版本的 DSH。
38
64
 
39
65
  ```bash
40
66
  git clone https://github.com/lloydzhou/dsh-bash-guard.git
@@ -46,14 +72,10 @@ dsh plugin --profile web add .
46
72
  dsh --profile web
47
73
  ```
48
74
 
49
- 在输入框附近打开权限菜单,选择 **bash-guard 自定义**。
75
+ 在输入框附近打开权限菜单,选择 **Bash Guard 自定义**。
50
76
 
51
77
  目录安装采用链接。更新插件后重新构建;宿主入口或插件配置变化时,重启对应的 DSH 实例。
52
78
 
53
- ### npm 发布状态
54
-
55
- 包名为 `dsh-bash-guard`。本仓库已准备 npm 打包配置,但本说明不表示该包已经发布;发布前请使用上面的源码安装方式。
56
-
57
79
  ## 在菜单中操作
58
80
 
59
81
  | 入口 | 行为 |
@@ -61,12 +83,14 @@ dsh --profile web
61
83
  | 单个权限格 | 独立切换该范围的读、写或执行位 |
62
84
  | 系统 / 外部 / 网络 / 工作区 | 已全选则清空该行,否则全选该行 |
63
85
  | 全选 | 当前不是 `7777` 时显示,点击将全部 Guard 位设为 `7777` |
64
- | 默认 | 当前是 `7777` 时显示,点击恢复 `0477` |
86
+ | 默认 | 当前是 `7777` 时显示,点击恢复 `4667` |
65
87
  | 原生权限预设 | 停用 Guard,保留记忆的四位值 |
66
88
 
67
89
  方向键在矩阵中移动焦点,不修改权限;空格切换当前按钮,Escape 关闭菜单。状态由服务端确认,保存时不插入提示行或改变菜单高度。
68
90
 
69
- **当前默认值有两种用途:** 新会话的初始记忆值是 `0467`,且 Guard 默认不激活;菜单“默认”按钮的恢复值是 `0477`。已有会话不会因升级自动重置。
91
+ **统一默认值为 `4667`:** 系统只读、外部读写、网络读写、工作区全部。新会话初始记忆值和菜单“默认”按钮的恢复值一致,Guard 初始不激活;已有会话不会因升级自动重置。
92
+
93
+ 界面支持中文和英文,跟随 DSH 的语言设置,无需单独配置。品牌名统一显示为 **Bash Guard**。
70
94
 
71
95
  ## 权限怎么读
72
96
 
@@ -108,10 +132,10 @@ dsh --profile web
108
132
  | 模式 | 表达的策略 |
109
133
  | :---: | :--- |
110
134
  | `0467` | 系统关闭;外部只读;网络读写;工作区全部 |
111
- | `0477` | 与上项相比,增加网络执行位;菜单“默认”的恢复值 |
135
+ | `4667` | 默认:系统只读;外部与网络读写;工作区全部 |
112
136
  | `4447` | 系统、外部、网络只读;工作区全部 |
113
137
  | `0007` | 仅开放工作区的三个权限位 |
114
- | `7777` | Guard 全部位开放,**不等于原生完全权限** |
138
+ | `7777` | 在原生完全权限基线上开放全部 Guard 位,不再提供权限位限制 |
115
139
 
116
140
  模式不是安全等级排序。请按需要开启具体位,不要仅因为数字更大而认为它更合适。
117
141
 
@@ -125,7 +149,7 @@ Agent 工具调用
125
149
  └─ Guard 已激活且支持该工具
126
150
  ├─ 累计需要的权限位 required
127
151
  ├─ 不满足 allowed ──────────────► BASH_GUARD_DENIED
128
- └─ 全部满足 ────────────────────► 原生工具、沙箱与审批
152
+ └─ 全部满足 ────────────────────► 原生 Full access 工具执行
129
153
  ```
130
154
 
131
155
  判定规则是 `(allowed & required) === required`,不是比较数字大小。复合命令会累计多个范围的要求,不会只取一个“最高”范围。
@@ -144,9 +168,9 @@ Agent 工具调用
144
168
  请在使用前了解这些限制:
145
169
 
146
170
  - **不是不可绕过的隔离层。** 分类器是保守词法扫描器,不是完整 Bash AST,也无法证明任意脚本或程序的运行时行为。
147
- - **不提高宿主权限。** 激活时选择原生 `workspace-write` 预设;Guard 放行不绕过底层沙箱和审批。该预设的具体行为由宿主配置决定。
171
+ - **激活会提高原生权限。** 风险确认后选择 `danger-full-access` 预设,由 Guard 限制已支持的操作。要求该预设为 `approval=never`,并通过宿主 sandboxPolicy 与 approval 服务核验会话实际状态;不符合则拒绝激活并回退。`never` 表示不会弹审批,其他原生规则要求审批的操作会被拒绝,而不是自动批准。旧版 workspace-write 基线的已激活记录不会静默升级,需要重新激活。
148
172
  - **不覆盖所有工具。** 未识别的工具不会自动被本插件拦截,仍依赖宿主策略。
149
- - **不保证符号链接与动态路径隔离。** 实际文件系统、网络和子进程隔离仍由宿主负责。
173
+ - **不保证符号链接与动态路径隔离。** Full access 不提供工作区沙箱边界,不能依赖词法分类器隔离实际文件系统、网络或子进程。
150
174
  - **不保证子代理继承。** 跨代理任务不能假定自动沿用父会话的 Guard 配置。
151
175
  - **不是 Bash Guard Rust 运行时。** 本插件没有集成其 Jev 决策层、Claude/Codex Hook 或审计日志功能,也不宣称相同的安全保证。
152
176
 
@@ -175,7 +199,7 @@ npm pack --dry-run
175
199
  - [矩阵编辑器](src/GuardEditor.js):单格、整行及全局开关。
176
200
  - [测试](tests/):分类器、RPC、原生菜单和矩阵交互。
177
201
 
178
- 当前回归测试共 174 项,包括全部 4096 种模式的客户端校验。测试覆盖不代表对任意 Shell 程序的安全证明。npm 打包前会自动构建;构建产物与依赖目录不提交到 Git。
202
+ 当前回归测试共 194 项,包括全部 4096 种模式的客户端校验。测试覆盖不代表对任意 Shell 程序的安全证明。npm 打包前会自动构建;构建产物与依赖目录不提交到 Git。
179
203
 
180
204
  ## 来源与致谢
181
205
 
package/client.js CHANGED
@@ -7,77 +7,71 @@ exports.commandResult = commandResult;
7
7
  exports.decodeGuardState = decodeGuardState;
8
8
  exports.createGuardTransport = createGuardTransport;
9
9
  exports.createPermissionControl = createPermissionControl;
10
+ exports.createGuardRemoteContribution = createGuardRemoteContribution;
10
11
  exports.apply = apply;
11
12
  const GuardEditor_js_1 = require("./GuardEditor.js");
13
+ const locales_js_1 = require("./locales.js");
12
14
  // A missing guard service must never hide the host's native permission choices.
13
- exports.inject = ['sessions', 'slots', 'remote', 'remote.permissionPresets'];
14
- function commandResult(reply) {
15
+ exports.inject = ['sessions', 'slots', 'locale', 'remote', 'remote.permissionPresets'];
16
+ function commandResult(reply, t = locales_js_1.defaultT) {
15
17
  if (!reply?.ok)
16
- throw new Error(reply?.error?.message || '权限请求失败');
18
+ throw (reply?.error?.message ? new Error(reply?.error?.message) : (0, locales_js_1.localeError)(t, 'error.permissionRequest'));
17
19
  if (!reply.value?.matched)
18
- throw new Error('宿主未提供此权限命令');
20
+ throw (0, locales_js_1.localeError)(t, 'error.permissionCommand');
19
21
  const result = reply.value.result;
20
22
  if (result?.kind === 'error')
21
- throw new Error(result.text || '权限请求失败');
23
+ throw (result.text ? new Error(result.text) : (0, locales_js_1.localeError)(t, 'error.permissionRequest'));
22
24
  return result;
23
25
  }
24
- function decodeGuardState(reply) {
26
+ function decodeGuardState(reply, t = locales_js_1.defaultT) {
25
27
  if (!reply?.ok)
26
- throw new Error(reply?.error?.message || 'Bash Guard 不可用');
28
+ throw (reply?.error?.message ? new Error(reply?.error?.message) : (0, locales_js_1.localeError)(t, 'error.unavailable'));
27
29
  const state = reply.value;
28
30
  if (!/^[0-7]{4}$(?![\s\S])/.test(state?.mode) || typeof state.active !== 'boolean' || !Number.isSafeInteger(state.revision) || state.revision < 0) {
29
- throw new Error('Bash Guard 返回了无效状态');
31
+ throw (0, locales_js_1.localeError)(t, 'error.invalidState');
30
32
  }
31
33
  return { mode: state.mode, active: state.active, revision: state.revision };
32
34
  }
33
- function createGuardTransport(remote, sessionId, sessionFor) {
35
+ function createGuardTransport(remote, sessionId, sessionFor, t = locales_js_1.defaultT) {
34
36
  const guard = () => {
35
37
  if (!remote.bashGuard)
36
- throw new Error('Bash Guard 不可用');
38
+ throw (0, locales_js_1.localeError)(t, 'error.unavailable');
37
39
  return remote.bashGuard;
38
40
  };
39
41
  return {
40
- get: async () => decodeGuardState(await guard().get(sessionId)),
42
+ get: async () => decodeGuardState(await guard().get(sessionId), t),
41
43
  write: async (action, state, mode) => decodeGuardState(await guard().update(sessionId, {
42
44
  action, revision: state.revision, ...(mode === undefined ? {} : { mode }),
43
- })),
45
+ }), t),
44
46
  // Deliberately independent of guard reads, readiness, and failures.
45
47
  preset: async (id) => {
46
48
  const live = sessionFor();
47
49
  if (!live)
48
- throw new Error('会话尚未就绪,请稍后重试');
49
- return commandResult(await live.command(`/permission ${id}`));
50
+ throw (0, locales_js_1.localeError)(t, 'error.sessionNotReady');
51
+ return commandResult(await live.command(`/permission ${id}`), t);
50
52
  },
51
53
  catalog: async () => {
52
54
  const reply = await remote.permissionPresets.catalog();
53
55
  if (!reply?.ok)
54
- throw new Error(reply?.error?.message || '权限列表不可用');
56
+ throw (reply?.error?.message ? new Error(reply?.error?.message) : (0, locales_js_1.localeError)(t, 'error.catalogUnavailable'));
55
57
  if (!Array.isArray(reply.value?.options))
56
- throw new Error('权限列表无效');
58
+ throw (0, locales_js_1.localeError)(t, 'error.invalidCatalog');
57
59
  return reply.value;
58
60
  },
59
61
  subscribeCatalog: callback => remote.$on?.('permission-presets/catalog-changed', callback),
60
62
  };
61
63
  }
62
- const copy = {
63
- 'preset.readOnly': '仅可查看', 'preset.workspaceWrite': '工作区内修改', 'preset.fullAccess': '完全权限',
64
- 'close': '关闭', 'confirm.cancel': '取消',
65
- 'confirm.title': '确认启用完全权限?',
66
- 'confirm.description': '启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。',
67
- 'confirm.acknowledge': '我已了解风险,并愿意继续', 'confirm.enable': '启用完全权限',
68
- 'auto.label': 'Auto review', 'auto.badge': 'EXP',
69
- 'auto.description': '无沙箱运行;每次原生工具调用和 PTC 内层调用前由同一模型进行实验性审查。',
70
- 'auto.confirm.title': '确认启用 Auto review(实验)?',
71
- 'auto.confirm.description': 'Auto review 不使用沙箱。每次原生工具调用和 PTC 内层调用前,都会由与当前 agent 相同的模型进行审查;审查拒绝的调用由你批准或拒绝。此功能仍属实验性,可能误放行或误拒绝,并会消耗额外 token。',
72
- 'auto.confirm.acknowledge': '我已了解这些风险,并愿意继续', 'auto.confirm.enable': '启用 Auto review',
73
- };
74
64
  const labels = { 'read-only': ['preset.readOnly', 'Read Only'], 'workspace-write': ['preset.workspaceWrite', 'Workspace Write'], 'danger-full-access': ['preset.fullAccess', 'Full access'] };
75
65
  function createPermissionControl(React, primitives) {
76
66
  const h = React.createElement;
77
- const { Menu, RiskConfirmation, PermissionIconReadOnlyRegular, PermissionIconWorkspaceWriteRegular, PermissionIconFullAccessRegular, IconChevronDownOutlineRegular, IconSettingsOutlineRegular } = primitives;
78
- const icons = { 'read-only': PermissionIconReadOnlyRegular, 'workspace-write': PermissionIconWorkspaceWriteRegular, 'danger-full-access': PermissionIconFullAccessRegular, 'bash-guard': IconSettingsOutlineRegular };
67
+ const { Menu, RiskConfirmation, PermissionIconReadOnlyRegular, PermissionIconWorkspaceWriteRegular, PermissionIconFullAccessRegular, IconChevronDownOutlineRegular, IconShieldOutlineRegular } = primitives;
68
+ // Keep the host shield artwork; only the inner adjustment marks are plugin-owned.
69
+ function GuardPermissionIcon() {
70
+ return h('span', { className: 'dsh-bg-guard-icon', 'aria-hidden': true }, h(IconShieldOutlineRegular), h('svg', { className: 'dsh-bg-guard-marks', viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1 }, h('path', { d: 'M5 6.5h6M5 9.5h6M7 5.5v2M9.5 8.5v2', strokeLinecap: 'round' })));
71
+ }
72
+ const icons = { 'read-only': PermissionIconReadOnlyRegular, 'workspace-write': PermissionIconWorkspaceWriteRegular, 'danger-full-access': PermissionIconFullAccessRegular, 'bash-guard': GuardPermissionIcon };
79
73
  const GuardEditor = (0, GuardEditor_js_1.createGuardEditor)(React, primitives.IconCheckOutlineRegular);
80
- return function PermissionControl({ sessionId, transport, locked, useProjection, t = key => copy[key] }) {
74
+ return function PermissionControl({ sessionId, transport, locked, useProjection, t = locales_js_1.defaultT }) {
81
75
  const selection = useProjection('permissions');
82
76
  const [state, setState] = React.useState(null);
83
77
  const [catalog, setCatalog] = React.useState(null);
@@ -93,7 +87,7 @@ function createPermissionControl(React, primitives) {
93
87
  const stateRef = React.useRef(null), api = React.useRef(transport);
94
88
  api.current = transport;
95
89
  const publish = next => { stateRef.current = next; setState(next); };
96
- const message = cause => cause instanceof Error ? cause.message : String(cause);
90
+ const message = cause => cause?.localeKey ? t(cause.localeKey) : cause instanceof Error ? cause.message : String(cause);
97
91
  const read = async () => {
98
92
  if (writing.current)
99
93
  return;
@@ -109,7 +103,7 @@ function createPermissionControl(React, primitives) {
109
103
  catch (cause) {
110
104
  if (generation === epoch.current && ticket === request.current) {
111
105
  publish(null);
112
- setError(message(cause));
106
+ setError(cause);
113
107
  }
114
108
  }
115
109
  finally {
@@ -128,7 +122,7 @@ function createPermissionControl(React, primitives) {
128
122
  }
129
123
  catch (cause) {
130
124
  if (generation === epoch.current && ticket === catalogRequest.current)
131
- setNativeError(message(cause));
125
+ setNativeError(cause);
132
126
  }
133
127
  };
134
128
  React.useEffect(() => {
@@ -148,7 +142,7 @@ function createPermissionControl(React, primitives) {
148
142
  React.useEffect(() => { read(); if (open)
149
143
  readCatalog(); }, [open, sessionId, selection]);
150
144
  React.useEffect(() => {
151
- if (locked || (confirmation !== null && catalog !== null && !catalog.options.some(option => option.value === confirmation))) {
145
+ if (locked || (confirmation !== null && confirmation !== 'bash-guard' && catalog !== null && !catalog.options.some(option => option.value === confirmation))) {
152
146
  setOpen(false);
153
147
  setConfirmation(null);
154
148
  setAcknowledged(false);
@@ -170,7 +164,7 @@ function createPermissionControl(React, primitives) {
170
164
  }
171
165
  catch (cause) {
172
166
  if (generation === epoch.current) {
173
- setError(message(cause));
167
+ setError(cause);
174
168
  try {
175
169
  const next = await api.current.get();
176
170
  if (generation === epoch.current)
@@ -211,7 +205,7 @@ function createPermissionControl(React, primitives) {
211
205
  }
212
206
  catch (cause) {
213
207
  if (generation === epoch.current)
214
- setNativeError(message(cause));
208
+ setNativeError(cause);
215
209
  }
216
210
  finally {
217
211
  if (generation === epoch.current) {
@@ -224,8 +218,11 @@ function createPermissionControl(React, primitives) {
224
218
  if (locked || picking)
225
219
  return;
226
220
  if (id === 'bash-guard') {
227
- if (!pending && !state?.active)
228
- submit('activate');
221
+ if (!pending && state && !state.active) {
222
+ setOpen(false);
223
+ setAcknowledged(false);
224
+ setConfirmation('bash-guard');
225
+ }
229
226
  return;
230
227
  }
231
228
  if (!catalog?.options.some(option => option.value === id))
@@ -251,7 +248,7 @@ function createPermissionControl(React, primitives) {
251
248
  const icon = id => icons[id] ? h(icons[id]) : undefined;
252
249
  const current = state?.active ? 'bash-guard' : selection?.currentValue;
253
250
  const option = catalog?.options.find(item => item.value === current);
254
- const label = current === 'bash-guard' ? `bash-guard ${state.mode}` : labelFor(current || '权限', option?.name);
251
+ const label = current === 'bash-guard' ? `Bash Guard ${state.mode}` : (current ? labelFor(current, option?.name) : t('permission'));
255
252
  const items = (catalog?.options || []).map(option => ({
256
253
  id: option.value, icon: icon(option.value),
257
254
  label: option.value === 'auto' ? h('span', { className: 'dsh-bg-option-label', 'aria-label': `${t('auto.label')} ${t('auto.badge')}` }, t('auto.label'), h('sup', { className: 'dsh-bg-badge' }, t('auto.badge'))) : labelFor(option.value, option.name),
@@ -259,49 +256,64 @@ function createPermissionControl(React, primitives) {
259
256
  }));
260
257
  items.push({ id: 'guard-separator', type: 'separator' }, {
261
258
  id: 'bash-guard', icon: icon('bash-guard'), disabled: !!locked || !state,
262
- label: h('span', null, 'bash-guard 自定义', state && h('span', { className: 'dsh-bg-mode' }, ` ${state.mode}`)),
259
+ label: h('span', null, t('guard.custom'), state && h('span', { className: 'dsh-bg-mode' }, ` ${state.mode}`)),
263
260
  });
264
261
  const closeConfirmation = () => { setConfirmation(null); setAcknowledged(false); };
265
- const riskPrefix = confirmation === 'auto' ? 'auto.confirm.' : 'confirm.';
262
+ const riskPrefix = confirmation === 'bash-guard' ? 'guard.confirm.' : confirmation === 'auto' ? 'auto.confirm.' : 'confirm.';
266
263
  return h(React.Fragment, null, h(Menu, { open, items, selectedId: current, onSelect: choose, onClose: () => setOpen(false), side: 'top', portal: true,
267
264
  anchor: h('button', { type: 'button', className: 'dsh-bg-trigger', disabled: !!locked || picking || confirmation !== null,
268
- 'aria-label': `访问模式,当前:${label}`, 'aria-haspopup': 'menu', 'aria-expanded': open,
265
+ 'aria-label': t('mode', { name: label }), 'aria-haspopup': 'menu', 'aria-expanded': open,
269
266
  title: current === 'auto' ? t('auto.description') : option?.description, onClick: () => setOpen(!open),
270
267
  }, icon(current) && h('span', { className: 'dsh-bg-trigger-icon', 'aria-hidden': true }, icon(current)), h('span', { className: 'dsh-bg-trigger-label' }, label), current === 'auto' && h('sup', { className: 'dsh-bg-badge' }, t('auto.badge')), h('span', { className: `dsh-bg-chevron${open ? ' dsh-bg-chevron-open' : ''}`, 'aria-hidden': true }, h(IconChevronDownOutlineRegular))),
271
- }, state && h(GuardEditor, { mode: state.mode, active: state.active, pending, locked,
268
+ }, state && h(GuardEditor, { mode: state.mode, active: state.active, pending, locked, t,
272
269
  onModeChange: mode => submit('mode', mode),
273
270
  onChange: (row, digit) => submit('mode', stateRef.current.mode.slice(0, row) + digit + stateRef.current.mode.slice(row + 1)),
274
- }), h('span', { className: 'dsh-bg-sr-only', role: 'status' }, pending ? '正在同步权限' : ''), error && h('div', { className: 'dsh-bg-error', role: 'alert' }, error, h('button', { type: 'button', disabled: pending, onClick: read }, '重试')), nativeError && h('div', { className: 'dsh-bg-error', role: 'alert' }, nativeError, h('button', { type: 'button', onClick: readCatalog }, '重试'))), confirmation !== null && h(RiskConfirmation, { open: true, title: t(`${riskPrefix}title`), description: t(`${riskPrefix}description`),
271
+ }), h('span', { className: 'dsh-bg-sr-only', role: 'status' }, pending ? t('syncing') : ''), error && h('div', { className: 'dsh-bg-error', role: 'alert' }, message(error), h('button', { type: 'button', disabled: pending, onClick: read }, t('retry'))), nativeError && h('div', { className: 'dsh-bg-error', role: 'alert' }, message(nativeError), h('button', { type: 'button', onClick: readCatalog }, t('retry')))), confirmation !== null && h(RiskConfirmation, { open: true, title: t(`${riskPrefix}title`), description: t(`${riskPrefix}description`),
275
272
  acknowledgeLabel: t(`${riskPrefix}acknowledge`), cancelLabel: t('confirm.cancel'), closeLabel: t('close'), confirmLabel: t(`${riskPrefix}enable`),
276
273
  acknowledged, disabled: !!locked || picking, onAcknowledgedChange: setAcknowledged, onCancel: closeConfirmation,
277
- onConfirm: () => { if (!acknowledged || locked || picking)
278
- return; const id = confirmation; closeConfirmation(); selectPreset(id); },
274
+ onConfirm: () => {
275
+ if (!acknowledged || locked || picking || pending)
276
+ return;
277
+ const id = confirmation;
278
+ closeConfirmation();
279
+ if (id === 'bash-guard') {
280
+ setOpen(true);
281
+ submit('activate');
282
+ }
283
+ else
284
+ selectPreset(id);
285
+ },
279
286
  }));
280
287
  };
281
288
  }
282
289
  // Client-owned Typert contribution: mount the namespace before rendering the seat.
283
290
  // Structural schemas keep this artifact on host modules (no bundled Zod/React).
284
- exports.guardRemoteContribution = {
285
- package: 'dsh-bash-guard',
286
- descriptors: ['get', 'update'].map(method => ({
287
- id: `dsh-bash-guard#bashGuard/${method}`, service: 'bashGuard', namespace: 'bashGuard', method,
288
- invocation: { kind: 'direct' },
289
- parameters: [
290
- { name: 'sessionId', wire: 'sessionId', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-bash-guard#SessionId', create: () => ({ parse(value) {
291
- if (typeof value !== 'string' || !value.trim())
292
- throw new Error('A session ID is required');
293
- return value;
294
- } }) } },
295
- ...(method === 'update' ? [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-bash-guard#GuardUpdate', create: () => ({ parse(value) {
296
- if (!value || !['activate', 'mode', 'deactivate'].includes(value.action) || !Number.isSafeInteger(value.revision) || value.revision < 0 || (value.mode !== undefined && !/^[0-7]{4}$(?![\s\S])/.test(value.mode)) || (value.action === 'mode' && value.mode === undefined))
297
- throw new Error('Invalid Bash Guard update');
298
- return { action: value.action, revision: value.revision, ...(value.mode === undefined ? {} : { mode: value.mode }) };
299
- } }) } }] : []),
300
- ],
301
- result: { mode: 'strict', typeSymbol: 'dsh-bash-guard#GuardRecord', create: () => ({ parse: value => decodeGuardState({ ok: true, value }) }) },
302
- })),
303
- };
291
+ function createGuardRemoteContribution(t = key => key === 'error.sessionId' || key === 'error.invalidUpdate' ? locales_js_1.en[key] : (0, locales_js_1.defaultT)(key)) {
292
+ return {
293
+ package: 'dsh-bash-guard',
294
+ descriptors: ['get', 'update'].map(method => ({
295
+ id: `dsh-bash-guard#bashGuard/${method}`, service: 'bashGuard', namespace: 'bashGuard', method,
296
+ invocation: { kind: 'direct' },
297
+ parameters: [
298
+ { name: 'sessionId', wire: 'sessionId', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-bash-guard#SessionId', create: () => ({ parse(value) {
299
+ if (typeof value !== 'string' || !value.trim())
300
+ throw (0, locales_js_1.localeError)(t, 'error.sessionId');
301
+ return value;
302
+ } }) } },
303
+ ...(method === 'update' ? [{ name: 'request', wire: 'request', source: 'json', codec: { mode: 'strict', typeSymbol: 'dsh-bash-guard#GuardUpdate', create: () => ({ parse(value) {
304
+ if (!value || !['activate', 'mode', 'deactivate'].includes(value.action) || !Number.isSafeInteger(value.revision) || value.revision < 0 || (value.mode !== undefined && !/^[0-7]{4}$(?![\s\S])/.test(value.mode)) || (value.action === 'mode' && value.mode === undefined))
305
+ throw (0, locales_js_1.localeError)(t, 'error.invalidUpdate');
306
+ return { action: value.action, revision: value.revision, ...(value.mode === undefined ? {} : { mode: value.mode }) };
307
+ } }) } }] : []),
308
+ ],
309
+ result: { mode: 'strict', typeSymbol: 'dsh-bash-guard#GuardRecord', create: () => ({ parse: value => decodeGuardState({ ok: true, value }, t) }) },
310
+ })),
311
+ };
312
+ }
313
+ exports.guardRemoteContribution = createGuardRemoteContribution();
304
314
  async function apply(ctx) {
315
+ ctx.effect(() => ctx.locale.register(locales_js_1.BASH_GUARD_NS, { zh: locales_js_1.zh, en: locales_js_1.en }), 'bash-guard: dictionaries');
316
+ const t = ctx.locale.bind(locales_js_1.BASH_GUARD_NS);
305
317
  // Resolve host modules inside apply: the browser loader owns React and styles.
306
318
  const React = require('react');
307
319
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
@@ -315,17 +327,17 @@ async function apply(ctx) {
315
327
  $on: (event, listener) => scope.remote.$on(event, listener),
316
328
  };
317
329
  scope.slots.inject('conversation.input.permission', () => scope.slots.register({
318
- name: 'conversation.input.permission', priority: -100, locale: 'permission.access',
319
- inject: sessionId => ({ sessionId, transport: createGuardTransport(remote, sessionId, () => scope.sessions.binding(sessionId)?.session) }),
330
+ name: 'conversation.input.permission', priority: -100, locale: locales_js_1.BASH_GUARD_NS,
331
+ inject: sessionId => ({ sessionId, transport: createGuardTransport(remote, sessionId, () => scope.sessions.binding(sessionId)?.session, t) }),
320
332
  }, Control));
321
333
  };
322
334
  // Leave the host seat intact while the injected guard namespace resolves.
323
335
  // On mount failure, native choices still work through the guard-free adapter.
324
336
  try {
325
- await ctx.remote.$mount(exports.guardRemoteContribution);
337
+ await ctx.remote.$mount(createGuardRemoteContribution(t));
326
338
  }
327
339
  catch (error) {
328
- console.warn('Bash Guard RPC unavailable', error);
340
+ console.warn(t('error.rpcUnavailable'), error);
329
341
  register(ctx, undefined);
330
342
  return;
331
343
  }
@@ -337,12 +349,13 @@ async function apply(ctx) {
337
349
  "use strict";
338
350
  Object.defineProperty(exports, "__esModule", { value: true });
339
351
  exports.createGuardEditor = createGuardEditor;
352
+ const locales_js_1 = require("./locales.js");
340
353
  // Independent permission bits; the host owns React and the check glyph.
341
- const scopes = ['系统', '外部', '网络', '工作区'];
342
- const permissions = [{ label: '读', bit: 4 }, { label: '写', bit: 2 }, { label: '执行', bit: 1 }];
354
+ const scopes = ['system', 'external', 'network', 'workspace'];
355
+ const permissions = [{ key: 'read', bit: 4 }, { key: 'write', bit: 2 }, { key: 'execute', bit: 1 }];
343
356
  function createGuardEditor(React, CheckIcon) {
344
357
  const h = React.createElement;
345
- return function GuardEditor({ mode, active, pending = false, locked = false, error, onChange, onModeChange }) {
358
+ return function GuardEditor({ mode, active, pending = false, locked = false, error, onChange, onModeChange, t = locales_js_1.defaultT }) {
346
359
  if (!active)
347
360
  return null;
348
361
  const disabled = pending || locked;
@@ -355,24 +368,24 @@ function createGuardEditor(React, CheckIcon) {
355
368
  const root = event.currentTarget.closest('[data-guard-editor]');
356
369
  root?.querySelector(`[data-cell="${Math.max(0, Math.min(3, row + delta[0]))}-${Math.max(0, Math.min(2, col + delta[1]))}"]`)?.focus();
357
370
  };
358
- return h('div', { className: 'dsh-bg-editor', 'data-guard-editor': '', 'aria-busy': pending }, h('div', { className: 'dsh-bg-scope dsh-bg-head' }, h('button', {
371
+ return h('div', { className: `dsh-bg-editor${t('scope.workspace') === 'Workspace' ? ' dsh-bg-editor-en' : ''}`, 'data-guard-editor': '', 'aria-busy': pending }, h('div', { className: 'dsh-bg-scope dsh-bg-head' }, h('button', {
359
372
  type: 'button', className: 'dsh-bg-scope-label dsh-bg-all-toggle dsh-bg-scope-toggle',
360
373
  disabled: locked, 'aria-disabled': disabled,
361
- title: mode === '7777' ? '恢复默认权限 0477' : '开启全部 Guard 权限',
374
+ title: t(mode === '7777' ? 'restoreDefaults' : 'enableAll'),
362
375
  onClick: event => { event.stopPropagation(); if (!disabled)
363
- onModeChange(mode === '7777' ? '0477' : '7777'); },
364
- }, mode === '7777' ? '默认' : '全选'), ...permissions.map(p => h('span', { className: 'dsh-bg-column', key: p.bit }, p.label))), ...scopes.map((scope, row) => h('div', { className: 'dsh-bg-scope', key: row, role: 'group', 'aria-label': `${scope}权限` }, h('button', {
376
+ onModeChange(mode === '7777' ? '4667' : '7777'); },
377
+ }, t(mode === '7777' ? 'defaults' : 'selectAll')), ...permissions.map(p => h('span', { className: 'dsh-bg-column', key: p.bit }, t(`bit.${p.key}`)))), ...scopes.map(key => t(`scope.${key}`)).map((scope, row) => h('div', { className: 'dsh-bg-scope', key: row, role: 'group', 'aria-label': t('scope.permissions', { scope }) }, h('button', {
365
378
  type: 'button', className: 'dsh-bg-scope-label dsh-bg-scope-toggle',
366
379
  disabled: locked, 'aria-disabled': disabled,
367
- 'aria-label': `${scope}:${mode[row] === '7' ? '取消全选' : '全选'}`,
368
- title: mode[row] === '7' ? '取消该范围全部权限' : '开启该范围全部权限',
380
+ 'aria-label': t('scope.toggle', { scope, action: t(mode[row] === '7' ? 'clearAll' : 'selectAll') }),
381
+ title: t(mode[row] === '7' ? 'scope.clearAll' : 'scope.enableAll'),
369
382
  onClick: event => { event.stopPropagation(); if (!disabled)
370
383
  onChange(row, mode[row] === '7' ? 0 : 7); },
371
384
  }, scope), ...permissions.map((permission, col) => {
372
385
  const checked = (Number(mode[row]) & permission.bit) !== 0;
373
386
  return h('button', {
374
387
  type: 'button', key: permission.bit, role: 'checkbox', 'aria-checked': checked,
375
- 'aria-label': `${scope} · ${permission.label}权限`, title: `${scope} · ${permission.label}权限:${checked ? '开启' : '关闭'}`,
388
+ 'aria-label': t('bit.label', { scope, permission: t(`bit.${permission.key}`) }), title: t('bit.status', { scope, permission: t(`bit.${permission.key}`), status: t(checked ? 'enabled' : 'disabled') }),
376
389
  'data-cell': `${row}-${col}`, disabled: locked, 'aria-disabled': disabled, className: 'dsh-bg-level',
377
390
  onClick: event => { event.stopPropagation(); if (!disabled)
378
391
  onChange(row, Number(mode[row]) ^ permission.bit); },
@@ -382,5 +395,109 @@ function createGuardEditor(React, CheckIcon) {
382
395
  };
383
396
  }
384
397
 
398
+ },
399
+ "./locales.js": (module,exports,require) => {
400
+ "use strict";
401
+ Object.defineProperty(exports, "__esModule", { value: true });
402
+ exports.en = exports.zh = exports.BASH_GUARD_NS = void 0;
403
+ exports.defaultT = defaultT;
404
+ exports.localeError = localeError;
405
+ // Plugin-owned dictionaries: the host locale service is the only language setting.
406
+ exports.BASH_GUARD_NS = 'bash-guard';
407
+ exports.zh = {
408
+ 'permission': '权限',
409
+ 'mode': '访问模式,当前:{name}',
410
+ 'guard.custom': 'Bash Guard 自定义',
411
+ 'syncing': '正在同步权限',
412
+ 'retry': '重试',
413
+ 'scope.system': '系统', 'scope.external': '外部', 'scope.network': '网络', 'scope.workspace': '工作区',
414
+ 'bit.read': '读', 'bit.write': '写', 'bit.execute': '执行',
415
+ 'scope.permissions': '{scope}权限',
416
+ 'scope.toggle': '{scope}:{action}',
417
+ 'scope.enableAll': '开启该范围全部权限',
418
+ 'scope.clearAll': '取消该范围全部权限',
419
+ 'bit.label': '{scope} · {permission}权限',
420
+ 'bit.status': '{scope} · {permission}权限:{status}',
421
+ 'enabled': '开启', 'disabled': '关闭',
422
+ 'selectAll': '全选', 'clearAll': '取消全选', 'defaults': '默认',
423
+ 'restoreDefaults': '恢复默认权限 4667',
424
+ 'enableAll': '开启全部 Guard 权限',
425
+ 'guard.confirm.title': '启用 Bash Guard 自定义权限?',
426
+ 'guard.confirm.description': 'Bash Guard 将以原生完全权限运行,不再使用工作区沙箱。仅已支持的 Bash 和文件工具受权限位限制;其他工具及无法准确识别的动态程序行为不受同等保护。Guard 不是安全沙箱。',
427
+ 'guard.confirm.acknowledge': '我了解未接管工具不受 Guard 限制,并同意使用完全权限基线',
428
+ 'guard.confirm.enable': '启用 Bash Guard',
429
+ 'error.permissionRequest': '权限请求失败',
430
+ 'error.permissionCommand': '宿主未提供此权限命令',
431
+ 'error.unavailable': 'Bash Guard 不可用',
432
+ 'error.invalidState': 'Bash Guard 返回了无效状态',
433
+ 'error.sessionNotReady': '会话尚未就绪,请稍后重试',
434
+ 'error.catalogUnavailable': '权限列表不可用',
435
+ 'error.invalidCatalog': '权限列表无效',
436
+ 'error.sessionId': '必须提供会话 ID',
437
+ 'error.invalidUpdate': '无效的 Bash Guard 更新',
438
+ 'error.rpcUnavailable': 'Bash Guard RPC 不可用',
439
+ 'preset.readOnly': '仅可查看', 'preset.workspaceWrite': '工作区内修改', 'preset.fullAccess': '完全权限',
440
+ 'close': '关闭', 'confirm.cancel': '取消',
441
+ 'confirm.title': '确认启用完全权限?',
442
+ 'confirm.description': '启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。',
443
+ 'confirm.acknowledge': '我已了解风险,并愿意继续', 'confirm.enable': '启用完全权限',
444
+ 'auto.label': 'Auto review', 'auto.badge': 'EXP',
445
+ 'auto.description': '无沙箱运行;每次原生工具调用和 PTC 内层调用前由同一模型进行实验性审查。',
446
+ 'auto.confirm.title': '确认启用 Auto review(实验)?',
447
+ 'auto.confirm.description': 'Auto review 不使用沙箱。每次原生工具调用和 PTC 内层调用前,都会由与当前 agent 相同的模型进行审查;审查拒绝的调用由你批准或拒绝。此功能仍属实验性,可能误放行或误拒绝,并会消耗额外 token。',
448
+ 'auto.confirm.acknowledge': '我已了解这些风险,并愿意继续', 'auto.confirm.enable': '启用 Auto review',
449
+ };
450
+ exports.en = {
451
+ 'permission': 'Permission',
452
+ 'mode': 'Access mode, current: {name}',
453
+ 'guard.custom': 'Bash Guard Custom',
454
+ 'syncing': 'Syncing permissions',
455
+ 'retry': 'Retry',
456
+ 'scope.system': 'System', 'scope.external': 'External', 'scope.network': 'Network', 'scope.workspace': 'Workspace',
457
+ 'bit.read': 'Read', 'bit.write': 'Write', 'bit.execute': 'Execute',
458
+ 'scope.permissions': '{scope} permissions',
459
+ 'scope.toggle': '{scope}: {action}',
460
+ 'scope.enableAll': 'Enable all permissions in this scope',
461
+ 'scope.clearAll': 'Clear all permissions in this scope',
462
+ 'bit.label': '{scope} · {permission} permission',
463
+ 'bit.status': '{scope} · {permission} permission: {status}',
464
+ 'enabled': 'On', 'disabled': 'Off',
465
+ 'selectAll': 'Select all', 'clearAll': 'Clear all', 'defaults': 'Default',
466
+ 'restoreDefaults': 'Restore default permissions 4667',
467
+ 'enableAll': 'Enable all Guard permissions',
468
+ 'guard.confirm.title': 'Enable Bash Guard custom permissions?',
469
+ 'guard.confirm.description': 'Bash Guard uses native Full access without the workspace sandbox. Permission bits restrict only supported Bash and file tools. Other tools and dynamic program behavior that cannot be classified do not have equivalent protection. Guard is not a security sandbox.',
470
+ 'guard.confirm.acknowledge': 'I understand unsupported tools are not restricted by Guard and accept the Full access baseline',
471
+ 'guard.confirm.enable': 'Enable Bash Guard',
472
+ 'error.permissionRequest': 'Permission request failed',
473
+ 'error.permissionCommand': 'The host does not provide this permission command',
474
+ 'error.unavailable': 'Bash Guard is unavailable',
475
+ 'error.invalidState': 'Bash Guard returned an invalid state',
476
+ 'error.sessionNotReady': 'The session is not ready yet. Please try again later',
477
+ 'error.catalogUnavailable': 'Permission catalog is unavailable',
478
+ 'error.invalidCatalog': 'Invalid permission catalog',
479
+ 'error.sessionId': 'A session ID is required',
480
+ 'error.invalidUpdate': 'Invalid Bash Guard update',
481
+ 'error.rpcUnavailable': 'Bash Guard RPC unavailable',
482
+ 'preset.readOnly': 'Read Only', 'preset.workspaceWrite': 'Workspace Write', 'preset.fullAccess': 'Full access',
483
+ 'close': 'Close', 'confirm.cancel': 'Cancel',
484
+ 'confirm.title': 'Enable Full access?',
485
+ 'confirm.description': 'Full access reduces confirmation steps and lets the agent perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust the current task.',
486
+ 'confirm.acknowledge': 'I understand the risks and want to continue', 'confirm.enable': 'Enable Full access',
487
+ 'auto.label': 'Auto review', 'auto.badge': 'EXP',
488
+ 'auto.description': 'Run without a sandbox after an experimental same-model review of every native tool call and PTC inner call.',
489
+ 'auto.confirm.title': 'Enable Auto review (experimental)?',
490
+ 'auto.confirm.description': 'Auto review runs without a sandbox. Before every native tool call and PTC inner call, the same model as the current agent reviews whether to allow it; you approve or reject each call it denies. This feature is experimental, can falsely allow or deny actions, and uses additional tokens.',
491
+ 'auto.confirm.acknowledge': 'I understand these risks and want to continue', 'auto.confirm.enable': 'Enable Auto review',
492
+ };
493
+ // Keep the historical Chinese defaults for standalone helpers and editor consumers.
494
+ function defaultT(key, values = {}) {
495
+ return (exports.zh[key] ?? key).replace(/\{(\w+)\}/g, (match, name) => values[name] ?? match);
496
+ }
497
+ // Retain the key so an already-visible local error follows reactive slot language changes.
498
+ function localeError(t, key) {
499
+ return Object.assign(new Error(t(key)), { localeKey: key });
500
+ }
501
+
385
502
  }};const cache={};function require(id){if(!factories[id])return hostRequire(id);if(cache[id])return cache[id].exports;const module={exports:{}};cache[id]=module;factories[id](module,module.exports,require);return module.exports;}
386
- const api=require('./client.js');const original=api.apply;api.apply=(ctx)=>{ctx.effect(()=>{const style=document.createElement('style');style.dataset.plugin='dsh-bash-guard';style.textContent="/* Native permission trigger declarations, namespaced; Menu owns all menu chrome. */\n.dsh-bg-trigger { border-radius: var(--dsw-radius-sm); min-width: 0; max-width: 220px; height: 28px; color: var(--dsw-alias-label-secondary); cursor: pointer; background: transparent; border: none; outline: none; align-items: center; gap: 4px; padding: 0 4px 0 8px; font-size: 13px; font-weight: 500; line-height: 20px; display: inline-flex; }\n.dsh-bg-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dsh-bg-trigger:focus-visible { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }\n.dsh-bg-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }\n.dsh-bg-trigger-icon { flex: none; display: inline-flex; }\n.dsh-bg-trigger-icon svg { width: 14px; height: 14px; }\n.dsh-bg-trigger-label { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }\n.dsh-bg-option-label { align-items: baseline; gap: 4px; min-width: 0; max-width: 100%; display: inline-flex; }\n.dsh-bg-badge { color: var(--dsw-alias-label-tertiary); letter-spacing: .2px; flex: none; align-self: flex-start; margin-top: -1px; font-size: 8px; font-weight: 600; line-height: 10px; }\n.dsh-bg-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform .12s; display: inline-flex; }\n.dsh-bg-chevron-open { transform: rotate(180deg); }\n@container (width <= 460px) { .dsh-bg-trigger:has(.dsh-bg-trigger-icon) .dsh-bg-trigger-label { display: none; } }\n.dsh-bg-editor { padding: 0 8px 4px 32px; color: var(--dsw-alias-label-primary); }\n.dsh-bg-scope { display: grid; grid-template-columns: 48px repeat(3, 32px); align-items: center; gap: 8px; min-height: 32px; }\n.dsh-bg-head { min-height: 26px; }\n.dsh-bg-column { text-align: center; font-size: 12px; color: var(--dsw-alias-label-secondary); }\n.dsh-bg-level svg { width: 16px; height: 16px; }\n.dsh-bg-scope-label { width: 42px; flex: 0 0 42px; color: var(--dsw-alias-label-secondary); font-size: 12px; }\n.dsh-bg-scope-toggle { appearance: none; border: 0; background: transparent; border-radius: var(--dsw-radius-sm); box-sizing: border-box; width: max-content; max-width: none; justify-self: start; height: 24px; margin-left: -6px; padding: 0 6px; text-align: left; white-space: nowrap; font-family: inherit; cursor: pointer; }\n.dsh-bg-scope-toggle:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }\n.dsh-bg-level { appearance: none; border: 1px solid var(--dsw-alias-border-l1); background: transparent; color: var(--dsw-alias-label-secondary); border-radius: var(--dsw-radius-sm); width: 32px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }\n.dsh-bg-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n.dsh-bg-level:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }\n.dsh-bg-level[aria-checked=\"true\"] { background: var(--dsw-alias-interactive-bg-hover-solid); color: var(--dsw-alias-label-primary); }\n.dsh-bg-editor button:focus-visible, .dsh-bg-error button:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-label-secondary)); outline-offset: 1px; }\n.dsh-bg-editor button:disabled { cursor: default; color: var(--dsw-alias-label-tertiary); }\n.dsh-bg-error, .dsh-bg-status { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 6px 8px; overflow-wrap: anywhere; max-width: 300px; }\n.dsh-bg-error button { margin-left: 8px; padding: 2px 6px; border: 1px solid var(--dsw-alias-border-l1); border-radius: var(--dsw-radius-sm); background: transparent; color: inherit; font: inherit; cursor: pointer; }\n.dsh-bg-mode { font: 12px var(--ds-font-family-code, monospace); color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }\n@media (max-width: 360px) { .dsh-bg-editor { padding-left: 24px; } .dsh-bg-level { padding-inline: 4px; } }\n@media (prefers-reduced-motion: reduce) { .dsh-bg-chevron { transition: none; } }\n";document.head.append(style);return ()=>style.remove()});return original(ctx)};return api;}});
503
+ const api=require('./client.js');const original=api.apply;api.apply=(ctx)=>{ctx.effect(()=>{const style=document.createElement('style');style.dataset.plugin='dsh-bash-guard';style.textContent="/* Native permission trigger declarations, namespaced; Menu owns all menu chrome. */\n.dsh-bg-trigger { border-radius: var(--dsw-radius-sm); min-width: 0; max-width: 220px; height: 28px; color: var(--dsw-alias-label-secondary); cursor: pointer; background: transparent; border: none; outline: none; align-items: center; gap: 4px; padding: 0 4px 0 8px; font-size: 13px; font-weight: 500; line-height: 20px; display: inline-flex; }\n.dsh-bg-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dsh-bg-trigger:focus-visible { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }\n.dsh-bg-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }\n.dsh-bg-trigger-icon { flex: none; display: inline-flex; }\n.dsh-bg-trigger-icon svg { width: 14px; height: 14px; }\n.dsh-bg-guard-icon { position: relative; display: inline-flex; width: 16px; height: 16px; flex: none; }\n.dsh-bg-guard-icon > svg { width: 100%; height: 100%; }\n.dsh-bg-guard-marks { position: absolute; inset: 0; pointer-events: none; }\n.dsh-bg-trigger-icon .dsh-bg-guard-icon { width: 14px; height: 14px; }\n.dsh-bg-trigger-label { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }\n.dsh-bg-option-label { align-items: baseline; gap: 4px; min-width: 0; max-width: 100%; display: inline-flex; }\n.dsh-bg-badge { color: var(--dsw-alias-label-tertiary); letter-spacing: .2px; flex: none; align-self: flex-start; margin-top: -1px; font-size: 8px; font-weight: 600; line-height: 10px; }\n.dsh-bg-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform .12s; display: inline-flex; }\n.dsh-bg-chevron-open { transform: rotate(180deg); }\n@container (width <= 460px) { .dsh-bg-trigger:has(.dsh-bg-trigger-icon) .dsh-bg-trigger-label { display: none; } }\n.dsh-bg-editor { padding: 0 8px 4px 32px; color: var(--dsw-alias-label-primary); }\n.dsh-bg-scope { display: grid; grid-template-columns: 48px repeat(3, 32px); align-items: center; gap: 8px; min-height: 32px; }\n.dsh-bg-head { min-height: 26px; }\n.dsh-bg-editor-en .dsh-bg-scope { grid-template-columns: 72px repeat(3, 44px); }\n.dsh-bg-editor-en .dsh-bg-level { justify-self: center; }\n.dsh-bg-column { text-align: center; font-size: 12px; color: var(--dsw-alias-label-secondary); }\n.dsh-bg-level svg { width: 16px; height: 16px; }\n.dsh-bg-scope-label { width: 42px; flex: 0 0 42px; color: var(--dsw-alias-label-secondary); font-size: 12px; }\n.dsh-bg-scope-toggle { appearance: none; border: 0; background: transparent; border-radius: var(--dsw-radius-sm); box-sizing: border-box; width: max-content; max-width: none; justify-self: start; height: 24px; margin-left: -6px; padding: 0 6px; text-align: left; white-space: nowrap; font-family: inherit; cursor: pointer; }\n.dsh-bg-scope-toggle:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }\n.dsh-bg-level { appearance: none; border: 1px solid var(--dsw-alias-border-l1); background: transparent; color: var(--dsw-alias-label-secondary); border-radius: var(--dsw-radius-sm); width: 32px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }\n.dsh-bg-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n.dsh-bg-level:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }\n.dsh-bg-level[aria-checked=\"true\"] { background: var(--dsw-alias-interactive-bg-hover-solid); color: var(--dsw-alias-label-primary); }\n.dsh-bg-editor button:focus-visible, .dsh-bg-error button:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-label-secondary)); outline-offset: 1px; }\n.dsh-bg-editor button:disabled { cursor: default; color: var(--dsw-alias-label-tertiary); }\n.dsh-bg-error, .dsh-bg-status { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 6px 8px; overflow-wrap: anywhere; max-width: 300px; }\n.dsh-bg-error button { margin-left: 8px; padding: 2px 6px; border: 1px solid var(--dsw-alias-border-l1); border-radius: var(--dsw-radius-sm); background: transparent; color: inherit; font: inherit; cursor: pointer; }\n.dsh-bg-mode { font: 12px var(--ds-font-family-code, monospace); color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }\n@media (max-width: 360px) { .dsh-bg-editor { padding-left: 24px; } .dsh-bg-level { padding-inline: 4px; } }\n@media (prefers-reduced-motion: reduce) { .dsh-bg-chevron { transition: none; } }\n";document.head.append(style);return ()=>style.remove()});return original(ctx)};return api;}});
package/cordis.patch.yml CHANGED
@@ -3,4 +3,4 @@
3
3
  - id: bash-guard
4
4
  name: dsh-bash-guard
5
5
  config:
6
- mode: '0467'
6
+ mode: '4667'
package/lib/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
2
2
  import { allows, classifyTool } from './classify.js';
3
3
  export const name = 'bash-guard';
4
- export const inject = ['storageDomain', 'sessions', 'permissionPresets'];
4
+ export const inject = ['storageDomain', 'sessions', 'permissionPresets', 'sandboxPolicy', 'approval'];
5
5
  export const BASH_GUARD_DENIED = 'BASH_GUARD_DENIED';
6
+ const BASELINE = 'danger-full-access';
6
7
  const valid = (value) => typeof value === 'string' && /^[0-7]{4}$(?![\s\S])/.test(value);
7
8
  export const recordSchema = {
8
9
  parse(value) {
@@ -48,7 +49,7 @@ export class BashGuardService extends TypertRemoteService {
48
49
  update(sessionId, request) { return this.mutate(sessionId, request); }
49
50
  }
50
51
  export async function apply(ctx, config = {}) {
51
- const fallback = config.mode ?? '0467';
52
+ const fallback = config.mode ?? '4667';
52
53
  if (!valid(fallback))
53
54
  throw new Error('bash-guard: expected four octal digits');
54
55
  const domain = await ctx.storageDomain.open({ name: 'bash_guard', version: 1, layout: 'per-record', tables: { sessions: { valueSchema: recordSchema } } });
@@ -60,6 +61,9 @@ export async function apply(ctx, config = {}) {
60
61
  throw new Error('A session ID is required');
61
62
  return id;
62
63
  };
64
+ const baselineEffective = (session) => ctx.permissionPresets.current(session) === BASELINE &&
65
+ ctx.sandboxPolicy.resolve({ session }).mode === BASELINE &&
66
+ ctx.approval.effectivePolicy(session) === 'never';
63
67
  const read = (id, session = ctx.sessions.get(sessionId(id))) => {
64
68
  const stored = records.get(id);
65
69
  const state = stored === undefined ? { mode: fallback, active: false, revision: 0, presetSeq: -1 } : recordSchema.parse(stored);
@@ -68,7 +72,7 @@ export async function apply(ctx, config = {}) {
68
72
  if (!session)
69
73
  return state.active ? { ...state, active: false, revision: state.revision + 1 } : state;
70
74
  const boundary = presetBoundary(session);
71
- return state.active && boundary > state.presetSeq
75
+ return state.active && (boundary > state.presetSeq || !baselineEffective(session))
72
76
  ? { ...state, active: false, revision: state.revision + 1, presetSeq: boundary }
73
77
  : state;
74
78
  };
@@ -93,22 +97,47 @@ export async function apply(ctx, config = {}) {
93
97
  throw new Error('Guard revision exhausted');
94
98
  if (request.action === 'mode' && (!state.active || !valid(request.mode)))
95
99
  throw new Error('An active guard and four octal digits are required');
100
+ let previousPreset;
96
101
  if (request.action === 'activate') {
97
- // Installed permissionPresets.set uses canonical sandbox/approval setters.
98
- // Never recursively execute a command against an already-running handler.
99
- const baseline = ctx.permissionPresets.resolve('workspace-write');
100
- if (baseline.sandbox !== 'workspace-write')
101
- throw new Error('Workspace baseline must confine writes to the workspace');
102
- ctx.permissionPresets.set(session, 'workspace-write');
103
- if (ctx.permissionPresets.current(session) !== 'workspace-write')
104
- throw new Error('Cannot activate workspace-write baseline');
102
+ const baseline = ctx.permissionPresets.resolve(BASELINE);
103
+ if (baseline.sandbox !== BASELINE)
104
+ throw new Error('Full-access baseline must use danger-full-access sandbox');
105
+ if (baseline.approval !== 'never')
106
+ throw new Error('Bash Guard requires Full access with approval=never; the configured preset still requests approval');
107
+ previousPreset = ctx.permissionPresets.current(session);
108
+ // A custom/unavailable baseline cannot be restored through the preset API.
109
+ // Refuse before elevating instead of risking an unguarded full-access session.
110
+ if (!previousPreset || previousPreset === 'custom')
111
+ throw new Error('Cannot activate Bash Guard without a restorable native preset');
112
+ ctx.permissionPresets.resolve(previousPreset);
113
+ }
114
+ try {
115
+ if (previousPreset !== undefined) {
116
+ // Canonical native setters; never recursively execute a session command.
117
+ ctx.permissionPresets.set(session, BASELINE);
118
+ if (!baselineEffective(session))
119
+ throw new Error('Cannot activate danger-full-access baseline with effective approval=never');
120
+ }
121
+ const next = recordSchema.parse({
122
+ mode: request.action === 'mode' ? request.mode : state.mode,
123
+ active: request.action !== 'deactivate', revision: state.revision + 1,
124
+ presetSeq: presetBoundary(session),
125
+ });
126
+ await records.put(id, next);
127
+ }
128
+ catch (error) {
129
+ if (previousPreset !== undefined) {
130
+ try {
131
+ ctx.permissionPresets.set(session, previousPreset);
132
+ if (ctx.permissionPresets.current(session) !== previousPreset)
133
+ throw new Error('Native preset restoration did not take effect');
134
+ }
135
+ catch (rollbackError) {
136
+ throw new AggregateError([error, rollbackError], 'Bash Guard activation failed and native preset rollback failed; full access may remain enabled. Restore native permissions immediately.');
137
+ }
138
+ }
139
+ throw error;
105
140
  }
106
- const next = recordSchema.parse({
107
- mode: request.action === 'mode' ? request.mode : state.mode,
108
- active: request.action !== 'deactivate', revision: state.revision + 1,
109
- presetSeq: presetBoundary(session),
110
- });
111
- await records.put(id, next);
112
141
  return read(id);
113
142
  }));
114
143
  ctx.on('tools/execute', async (exec, next) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bash-guard",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "author": "lloydzhou (https://github.com/lloydzhou)",
6
6
  "homepage": "https://github.com/lloydzhou/dsh-bash-guard#readme",