dsh-bash-guard 0.1.1 → 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
@@ -21,7 +21,7 @@
21
21
 
22
22
  不改宿主源码,不增加一排数字预设,也不用记斜杠命令。展开 `Bash Guard 自定义`,直接调整系统、外部、网络和工作区的读、写、执行权限。
23
23
 
24
- > **它是额外的工具权限检查,不是沙箱替代品。** 即使设为 `7777`,DSH 原生沙箱和审批仍然适用。
24
+ > **Bash Guard 使用原生 Full access 基线,不使用工作区沙箱。** 激活前需要确认风险;仅支持的工具受权限位限制。未接管工具和无法准确分类的动态行为不受同等保护,Guard 不是安全沙箱。
25
25
 
26
26
  ## 为什么使用它
27
27
 
@@ -135,7 +135,7 @@ dsh --profile web
135
135
  | `4667` | 默认:系统只读;外部与网络读写;工作区全部 |
136
136
  | `4447` | 系统、外部、网络只读;工作区全部 |
137
137
  | `0007` | 仅开放工作区的三个权限位 |
138
- | `7777` | Guard 全部位开放,**不等于原生完全权限** |
138
+ | `7777` | 在原生完全权限基线上开放全部 Guard 位,不再提供权限位限制 |
139
139
 
140
140
  模式不是安全等级排序。请按需要开启具体位,不要仅因为数字更大而认为它更合适。
141
141
 
@@ -149,7 +149,7 @@ Agent 工具调用
149
149
  └─ Guard 已激活且支持该工具
150
150
  ├─ 累计需要的权限位 required
151
151
  ├─ 不满足 allowed ──────────────► BASH_GUARD_DENIED
152
- └─ 全部满足 ────────────────────► 原生工具、沙箱与审批
152
+ └─ 全部满足 ────────────────────► 原生 Full access 工具执行
153
153
  ```
154
154
 
155
155
  判定规则是 `(allowed & required) === required`,不是比较数字大小。复合命令会累计多个范围的要求,不会只取一个“最高”范围。
@@ -168,9 +168,9 @@ Agent 工具调用
168
168
  请在使用前了解这些限制:
169
169
 
170
170
  - **不是不可绕过的隔离层。** 分类器是保守词法扫描器,不是完整 Bash AST,也无法证明任意脚本或程序的运行时行为。
171
- - **不提高宿主权限。** 激活时选择原生 `workspace-write` 预设;Guard 放行不绕过底层沙箱和审批。该预设的具体行为由宿主配置决定。
171
+ - **激活会提高原生权限。** 风险确认后选择 `danger-full-access` 预设,由 Guard 限制已支持的操作。要求该预设为 `approval=never`,并通过宿主 sandboxPolicy 与 approval 服务核验会话实际状态;不符合则拒绝激活并回退。`never` 表示不会弹审批,其他原生规则要求审批的操作会被拒绝,而不是自动批准。旧版 workspace-write 基线的已激活记录不会静默升级,需要重新激活。
172
172
  - **不覆盖所有工具。** 未识别的工具不会自动被本插件拦截,仍依赖宿主策略。
173
- - **不保证符号链接与动态路径隔离。** 实际文件系统、网络和子进程隔离仍由宿主负责。
173
+ - **不保证符号链接与动态路径隔离。** Full access 不提供工作区沙箱边界,不能依赖词法分类器隔离实际文件系统、网络或子进程。
174
174
  - **不保证子代理继承。** 跨代理任务不能假定自动沿用父会话的 Guard 配置。
175
175
  - **不是 Bash Guard Rust 运行时。** 本插件没有集成其 Jev 决策层、Claude/Codex Hook 或审计日志功能,也不宣称相同的安全保证。
176
176
 
@@ -199,7 +199,7 @@ npm pack --dry-run
199
199
  - [矩阵编辑器](src/GuardEditor.js):单格、整行及全局开关。
200
200
  - [测试](tests/):分类器、RPC、原生菜单和矩阵交互。
201
201
 
202
- 当前回归测试共 177 项,包括全部 4096 种模式的客户端校验。测试覆盖不代表对任意 Shell 程序的安全证明。npm 打包前会自动构建;构建产物与依赖目录不提交到 Git。
202
+ 当前回归测试共 194 项,包括全部 4096 种模式的客户端校验。测试覆盖不代表对任意 Shell 程序的安全证明。npm 打包前会自动构建;构建产物与依赖目录不提交到 Git。
203
203
 
204
204
  ## 来源与致谢
205
205
 
package/client.js CHANGED
@@ -142,7 +142,7 @@ function createPermissionControl(React, primitives) {
142
142
  React.useEffect(() => { read(); if (open)
143
143
  readCatalog(); }, [open, sessionId, selection]);
144
144
  React.useEffect(() => {
145
- 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))) {
146
146
  setOpen(false);
147
147
  setConfirmation(null);
148
148
  setAcknowledged(false);
@@ -218,8 +218,11 @@ function createPermissionControl(React, primitives) {
218
218
  if (locked || picking)
219
219
  return;
220
220
  if (id === 'bash-guard') {
221
- if (!pending && !state?.active)
222
- submit('activate');
221
+ if (!pending && state && !state.active) {
222
+ setOpen(false);
223
+ setAcknowledged(false);
224
+ setConfirmation('bash-guard');
225
+ }
223
226
  return;
224
227
  }
225
228
  if (!catalog?.options.some(option => option.value === id))
@@ -256,7 +259,7 @@ function createPermissionControl(React, primitives) {
256
259
  label: h('span', null, t('guard.custom'), state && h('span', { className: 'dsh-bg-mode' }, ` ${state.mode}`)),
257
260
  });
258
261
  const closeConfirmation = () => { setConfirmation(null); setAcknowledged(false); };
259
- const riskPrefix = confirmation === 'auto' ? 'auto.confirm.' : 'confirm.';
262
+ const riskPrefix = confirmation === 'bash-guard' ? 'guard.confirm.' : confirmation === 'auto' ? 'auto.confirm.' : 'confirm.';
260
263
  return h(React.Fragment, null, h(Menu, { open, items, selectedId: current, onSelect: choose, onClose: () => setOpen(false), side: 'top', portal: true,
261
264
  anchor: h('button', { type: 'button', className: 'dsh-bg-trigger', disabled: !!locked || picking || confirmation !== null,
262
265
  'aria-label': t('mode', { name: label }), 'aria-haspopup': 'menu', 'aria-expanded': open,
@@ -268,8 +271,18 @@ function createPermissionControl(React, primitives) {
268
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`),
269
272
  acknowledgeLabel: t(`${riskPrefix}acknowledge`), cancelLabel: t('confirm.cancel'), closeLabel: t('close'), confirmLabel: t(`${riskPrefix}enable`),
270
273
  acknowledged, disabled: !!locked || picking, onAcknowledgedChange: setAcknowledged, onCancel: closeConfirmation,
271
- onConfirm: () => { if (!acknowledged || locked || picking)
272
- 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
+ },
273
286
  }));
274
287
  };
275
288
  }
@@ -409,6 +422,10 @@ exports.zh = {
409
422
  'selectAll': '全选', 'clearAll': '取消全选', 'defaults': '默认',
410
423
  'restoreDefaults': '恢复默认权限 4667',
411
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',
412
429
  'error.permissionRequest': '权限请求失败',
413
430
  'error.permissionCommand': '宿主未提供此权限命令',
414
431
  'error.unavailable': 'Bash Guard 不可用',
@@ -448,6 +465,10 @@ exports.en = {
448
465
  'selectAll': 'Select all', 'clearAll': 'Clear all', 'defaults': 'Default',
449
466
  'restoreDefaults': 'Restore default permissions 4667',
450
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',
451
472
  'error.permissionRequest': 'Permission request failed',
452
473
  'error.permissionCommand': 'The host does not provide this permission command',
453
474
  'error.unavailable': 'Bash Guard is unavailable',
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) {
@@ -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.1",
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",