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 +41 -17
- package/client.js +199 -82
- package/cordis.patch.yml +1 -1
- package/lib/index.js +46 -17
- package/package.json +1 -1
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
|
-
不改宿主源码,不增加一排数字预设,也不用记斜杠命令。展开 `
|
|
22
|
+
不改宿主源码,不增加一排数字预设,也不用记斜杠命令。展开 `Bash Guard 自定义`,直接调整系统、外部、网络和工作区的读、写、执行权限。
|
|
22
23
|
|
|
23
|
-
>
|
|
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
|
-
需要
|
|
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
|
-
在输入框附近打开权限菜单,选择 **
|
|
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` 时显示,点击恢复 `
|
|
86
|
+
| 默认 | 当前是 `7777` 时显示,点击恢复 `4667` |
|
|
65
87
|
| 原生权限预设 | 停用 Guard,保留记忆的四位值 |
|
|
66
88
|
|
|
67
89
|
方向键在矩阵中移动焦点,不修改权限;空格切换当前按钮,Escape 关闭菜单。状态由服务端确认,保存时不插入提示行或改变菜单高度。
|
|
68
90
|
|
|
69
|
-
|
|
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
|
-
| `
|
|
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
|
-
-
|
|
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
|
-
当前回归测试共
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
78
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
228
|
-
|
|
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' ? `
|
|
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, '
|
|
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':
|
|
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 ? '
|
|
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: () => {
|
|
278
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
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(
|
|
337
|
+
await ctx.remote.$mount(createGuardRemoteContribution(t));
|
|
326
338
|
}
|
|
327
339
|
catch (error) {
|
|
328
|
-
console.warn('
|
|
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 = [{
|
|
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'
|
|
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' ? '
|
|
374
|
+
title: t(mode === '7777' ? 'restoreDefaults' : 'enableAll'),
|
|
362
375
|
onClick: event => { event.stopPropagation(); if (!disabled)
|
|
363
|
-
onModeChange(mode === '7777' ? '
|
|
364
|
-
}, mode === '7777' ? '
|
|
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':
|
|
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':
|
|
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
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 ?? '
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (baseline.
|
|
101
|
-
throw new Error('
|
|
102
|
-
ctx.permissionPresets.
|
|
103
|
-
|
|
104
|
-
|
|
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) => {
|