dsh-perm-gate 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +54 -1
- package/CHANGELOG.ko.md +54 -1
- package/CHANGELOG.md +134 -4
- package/README.ja.md +43 -0
- package/README.ko.md +43 -1
- package/README.md +73 -0
- package/README.zh.md +41 -0
- package/cordis.patch.yml +16 -1
- package/dsh.plugin.json +1 -1
- package/lib/ask-context.d.ts +49 -1
- package/lib/ask-context.js +89 -5
- package/lib/client.js +714 -86
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +152 -1
- package/lib/config.js +20 -1
- package/lib/credential-paths.d.ts +35 -0
- package/lib/credential-paths.js +119 -0
- package/lib/engine.d.ts +4 -6
- package/lib/engine.js +53 -42
- package/lib/evaluate.d.ts +20 -0
- package/lib/evaluate.js +34 -11
- package/lib/feedback.d.ts +42 -0
- package/lib/feedback.js +85 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +119 -17
- package/lib/irreversible.js +109 -1
- package/lib/path.d.ts +31 -0
- package/lib/path.js +35 -0
- package/lib/preset.d.ts +8 -5
- package/lib/preset.js +15 -6
- package/lib/risk.d.ts +29 -1
- package/lib/risk.js +36 -2
- package/lib/runtime.d.ts +164 -0
- package/lib/runtime.js +423 -54
- package/lib/scan-text.d.ts +69 -0
- package/lib/scan-text.js +162 -0
- package/lib/script-review.d.ts +50 -0
- package/lib/script-review.js +202 -0
- package/lib/shell.d.ts +18 -0
- package/lib/shell.js +103 -1
- package/lib/unattended.d.ts +61 -0
- package/lib/unattended.js +77 -0
- package/package.json +1 -1
- package/scripts/patch-permission-glyph.mjs +27 -13
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# dsh-perm-gate profile patch.
|
|
2
2
|
#
|
|
3
3
|
# Two jobs:
|
|
4
|
-
# 1) Add the plugin's own "自动审查" session approval
|
|
4
|
+
# 1) Add the plugin's own "自动审查" session approval tiers to the DSH
|
|
5
5
|
# permission picker, and restate the built-in tiers it must not drop.
|
|
6
6
|
# 2) Insert the plugin itself (host half) into the profile.
|
|
7
7
|
#
|
|
@@ -77,6 +77,21 @@
|
|
|
77
77
|
approval: ask
|
|
78
78
|
name: 自动审查(高权限)
|
|
79
79
|
description: 只由本插件的审批门过滤 —— 文件修改与子进程网络访问不再受沙箱限制,审批仍逐次生效,但没有系统沙箱兜底。
|
|
80
|
+
permissive-snet:
|
|
81
|
+
sandbox: workspace-write
|
|
82
|
+
approval: ask
|
|
83
|
+
name: 自动审查·机械过滤
|
|
84
|
+
description: 不调用审查模型 —— 确定性机械审查器先筛并列出疑点,剩下的仍然交人工确认。适用于无审查 LLM 可用、或不习惯模型替你拿主意的场景(不常用)。
|
|
85
|
+
permissive-mech:
|
|
86
|
+
sandbox: danger-full-access
|
|
87
|
+
approval: ask
|
|
88
|
+
name: 自动审查·机械直通
|
|
89
|
+
description: 无人确认 —— 确定性机械审查器替代人工:命中不可逆 / 不可沉淀 / 读不懂的载荷即拦截,并把理由回喂主模型,其余照常执行。不调用审查模型,零 token。
|
|
90
|
+
permissive-yolo:
|
|
91
|
+
sandbox: danger-full-access
|
|
92
|
+
approval: ask
|
|
93
|
+
name: 自动审查·YOLO
|
|
94
|
+
description: 无人确认 —— 审查模型逐条替代人工裁决,报硬风险即拦截并回喂理由;模型未配置或未能给出裁决时退回机械处置。永不升级人工。
|
|
80
95
|
danger-full-access:
|
|
81
96
|
sandbox: danger-full-access
|
|
82
97
|
approval: never
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-perm-gate",
|
|
3
3
|
"name": "dsh-perm-gate",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.3.0",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Deterministic-first, fail-closed permission gate for DeepSeek Harness 0.1.2+: a P0 hard-deny -> P1 precise session grant -> P2 static allow/deny rule chain -> P3 optional LLM classifier -> P4 ask pipeline over the tools/pre-execute waterfall, with a standalone CLI dry-run evaluator and an auditable decision log. It only vetoes with deny/ask and always delegates otherwise, and unknown or malformed rules fail loud instead of silently standing the gate down.",
|
|
7
7
|
"zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
|
package/lib/ask-context.d.ts
CHANGED
|
@@ -10,6 +10,17 @@ export type Reversibility =
|
|
|
10
10
|
| 'R';
|
|
11
11
|
/** What the call is aimed at. */
|
|
12
12
|
export type TargetKind = 'read' | 'write' | 'delete' | 'remote-write' | 'exec-external' | 'unknown';
|
|
13
|
+
/**
|
|
14
|
+
* One script body handed to the reviewers, as the runtime read it. Structural here so
|
|
15
|
+
* this module keeps its dependency-free contract (the fs half lives in `script-review.ts`).
|
|
16
|
+
*/
|
|
17
|
+
export interface ScriptContentIn {
|
|
18
|
+
readonly ref: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly body: string;
|
|
21
|
+
readonly truncated: boolean;
|
|
22
|
+
readonly unreadable?: string;
|
|
23
|
+
}
|
|
13
24
|
/** The session's three tier knobs, folded out of the durable session log. */
|
|
14
25
|
export interface SessionKnobs {
|
|
15
26
|
readonly permissionPreset?: string;
|
|
@@ -28,6 +39,18 @@ export interface AskContextSource {
|
|
|
28
39
|
readonly files?: readonly string[];
|
|
29
40
|
/** The deterministic layer's own conclusion, kept for the human, not the grader. */
|
|
30
41
|
readonly ruleReason?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The scripts this call runs, with the body the reviewer is given. Read by the node
|
|
44
|
+
* runtime (`script-review.ts`) — this module stays filesystem-free, so the text
|
|
45
|
+
* arrives pre-fetched. Absent means either no script is named or review is off.
|
|
46
|
+
*/
|
|
47
|
+
readonly scriptContents?: readonly ScriptContentIn[];
|
|
48
|
+
/**
|
|
49
|
+
* True when *every* script the call names was read, i.e. what will execute is now
|
|
50
|
+
* visible. That is what lifts a script run out of the opaque/`U` tier: the reviewer
|
|
51
|
+
* is no longer judging a filename. One unreadable script keeps the opacity.
|
|
52
|
+
*/
|
|
53
|
+
readonly scriptsCovered?: boolean;
|
|
31
54
|
/** Correlation key, mixed into the event digest. */
|
|
32
55
|
readonly callId?: string;
|
|
33
56
|
/** Per-session salt so `inputDigest` cannot be dictionary-reversed or joined across sessions. */
|
|
@@ -64,7 +87,24 @@ export interface GraderPayload {
|
|
|
64
87
|
readonly reason: string;
|
|
65
88
|
readonly target_resolvable: boolean;
|
|
66
89
|
readonly workspace_root?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The bodies of the scripts this call runs, redacted like any other egress text.
|
|
92
|
+
* Without them the grader judges a filename and can say `safe` about code it never
|
|
93
|
+
* saw; with them the answer is about what will execute.
|
|
94
|
+
*/
|
|
95
|
+
readonly script_contents?: readonly GraderScript[];
|
|
67
96
|
}
|
|
97
|
+
/** One attached script, as it leaves the host. */
|
|
98
|
+
export interface GraderScript {
|
|
99
|
+
readonly ref: string;
|
|
100
|
+
readonly path: string;
|
|
101
|
+
/** Absent when `unreadable` names why there is no body. */
|
|
102
|
+
readonly body?: string;
|
|
103
|
+
readonly truncated?: boolean;
|
|
104
|
+
readonly unreadable?: string;
|
|
105
|
+
}
|
|
106
|
+
/** Budget for one script body on the wire: a real script, one round-trip. */
|
|
107
|
+
export declare const GRADER_SCRIPT_BODY_CHARS = 20000;
|
|
68
108
|
/**
|
|
69
109
|
* Project for egress to the grader. Tier knobs and the rule layer's own verdict
|
|
70
110
|
* are stripped by design — see the module comment.
|
|
@@ -136,6 +176,14 @@ export declare const DOUBT_COPY: {
|
|
|
136
176
|
readonly zh: "会执行工作区之外的代码,本次调用无法静态看清它做什么";
|
|
137
177
|
readonly en: "it runs code from outside the workspace, which this call cannot inspect";
|
|
138
178
|
};
|
|
179
|
+
readonly 'script-attached': {
|
|
180
|
+
readonly zh: "它要跑的脚本正文已读出来、随本次审查附上(模型看到的和你看到的是同一份)";
|
|
181
|
+
readonly en: "the script it runs was read and attached to this review — the model and you see the same text";
|
|
182
|
+
};
|
|
183
|
+
readonly 'script-unreadable': {
|
|
184
|
+
readonly zh: "脚本正文没读到(缺失/过大/二进制),没有人看见真正要执行的代码";
|
|
185
|
+
readonly en: "a script body could not be read (missing, over budget, or binary) — nobody has seen the code that will run";
|
|
186
|
+
};
|
|
139
187
|
readonly 'outside-paths': {
|
|
140
188
|
readonly zh: "涉及工作区之外的路径";
|
|
141
189
|
readonly en: "it touches paths outside the workspace";
|
|
@@ -171,7 +219,7 @@ export declare function askDoubtKinds(ctx: AskContext, extra?: {
|
|
|
171
219
|
* The event still carries the command separately (`humanCommand`) because the
|
|
172
220
|
* review list has no host-provided command block next to it.
|
|
173
221
|
*/
|
|
174
|
-
export declare function humanBrief(ctx: AskContext, lang: 'zh' | 'en', kinds?: ("write" | "delete" | "remote-write" | "exec-external" | "never-learn" | "no-rule" | "opaque" | "irreversible" | "outside-paths" | "model-flagged")[]): string;
|
|
222
|
+
export declare function humanBrief(ctx: AskContext, lang: 'zh' | 'en', kinds?: ("write" | "delete" | "remote-write" | "exec-external" | "never-learn" | "no-rule" | "opaque" | "irreversible" | "script-attached" | "script-unreadable" | "outside-paths" | "model-flagged")[]): string;
|
|
175
223
|
/** The redacted command alone, for the channels that have nowhere else to show it. */
|
|
176
224
|
export declare function humanCommand(ctx: AskContext): string;
|
|
177
225
|
/** What goes on the durable event, for later attribution. */
|
package/lib/ask-context.js
CHANGED
|
@@ -124,6 +124,48 @@ function unseenScript(text, root) {
|
|
|
124
124
|
}
|
|
125
125
|
return text.match(/(?:^|[\s"'`(])\.\.[\/][^\s"']*\.(?:py|mjs|js|sh)\b/i) !== null;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The statements a script body actually contains, for the deterministic nets.
|
|
129
|
+
*
|
|
130
|
+
* Judging a whole file as one string is too coarse: `irreversible.ts` matches a delete
|
|
131
|
+
* verb, a force flag and a home-variable *anywhere* in the text, and a 300-line build
|
|
132
|
+
* script is full of all three in unrelated places (`cd "$HOME"` at the top,
|
|
133
|
+
* `rm -rf ./node_modules` at the bottom). So the body is cut into statements (newline
|
|
134
|
+
* and `;` are the shell separators) and each is judged on its own.
|
|
135
|
+
*
|
|
136
|
+
* But the incident shape is a **block**, and one line of it says nothing:
|
|
137
|
+
* `for f in ~/.*; do` / `rm -rf "$f"` / `done`, or `if (Test-Path $home) {` on its own
|
|
138
|
+
* line. So a line that *ends* in a continuation (`do`, `then`, `{`, `&&`, `;`, a
|
|
139
|
+
* trailing backslash) is joined with the lines it hands off to, up to a small depth.
|
|
140
|
+
* Joining is keyed on the continuation, not on a fixed window: an arbitrary two-line
|
|
141
|
+
* window is exactly the collision the split avoids (`cd "$HOME"` followed by
|
|
142
|
+
* `rm -rf ./build`).
|
|
143
|
+
*
|
|
144
|
+
* The cost of an over-match here is a human ask, never a silent deny: this feeds the
|
|
145
|
+
* `I` tier, which the model cannot clear.
|
|
146
|
+
*/
|
|
147
|
+
const LINE_CONTINUATION = /(?:&\s*&\s*$|\|\s*\|\s*$|;\s*(?:do|then|fi|else)\s*$|[{}]\s*$|;\s*$|\\\s*$|\b(?:do|then|else|begin)\s*$)/;
|
|
148
|
+
function bodyStatements(bodies) {
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const body of bodies) {
|
|
151
|
+
const lines = body.split('\n');
|
|
152
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
153
|
+
let group = lines[i] ?? '';
|
|
154
|
+
// A block opener swallows the lines it governs (max 3 more), so a loop or an `if`
|
|
155
|
+
// wrapping a destructive statement is judged as the one act it is.
|
|
156
|
+
for (let j = 1; j <= 3 && i + j < lines.length && LINE_CONTINUATION.test(group); j += 1) {
|
|
157
|
+
group = `${group}\n${lines[i + j] ?? ''}`;
|
|
158
|
+
}
|
|
159
|
+
for (const stmt of [group, ...group.split(/[;\n]+/)]) {
|
|
160
|
+
if (stmt.trim() !== '')
|
|
161
|
+
out.push(stmt);
|
|
162
|
+
}
|
|
163
|
+
// The individual statements of the swallowed lines are still judged separately on
|
|
164
|
+
// their own turn, so nothing is *only* seen as part of a block.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
127
169
|
/** Classify what the call targets, and whether its payload is inspectable. */
|
|
128
170
|
function inspect(source) {
|
|
129
171
|
const text = (source.commandText ?? '').trim();
|
|
@@ -132,10 +174,14 @@ function inspect(source) {
|
|
|
132
174
|
// hiding `Remove-Item -Recurse -Force C:\` is irreversible even though the
|
|
133
175
|
// visible text is opaque, and the human must see both facts.
|
|
134
176
|
const decoded = executableVariants(normalizeCommandText(text));
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
// A script run is judged on the file too: `bash ./clean-home.sh` is a filename, the
|
|
178
|
+
// *body* is the operation. This is the clause that closes the incident the whole
|
|
179
|
+
// feature exists for — a script that wipes the profile while the command line says
|
|
180
|
+
// nothing dangerous, judged deterministically so no grader `safe` can clear it.
|
|
181
|
+
const read = (source.scriptContents ?? []).filter((c) => c.unreadable === undefined && c.body !== '');
|
|
182
|
+
const judged = [cmd, ...decoded, ...bodyStatements(read.map((c) => `${c.ref}\n${c.body}`))];
|
|
183
|
+
const irreversible = judged.some((t) => irreversibleReason(t) !== undefined);
|
|
184
|
+
const unlearnable = judged.some((t) => neverClearReason(t) !== undefined);
|
|
139
185
|
if (cmd.trim() === '{}') {
|
|
140
186
|
// A tool invoked with no arguments at all: nothing to read, nothing to trust.
|
|
141
187
|
return { kind: 'unknown', resolvable: false, readOnly: false, irreversible, unlearnable };
|
|
@@ -156,7 +202,12 @@ function inspect(source) {
|
|
|
156
202
|
})();
|
|
157
203
|
const first = text.split(/\s/)[0] ?? '';
|
|
158
204
|
const runner = /\b(?:python|node|pwsh|powershell|bash|sh|zsh|npx|deno|ruby|perl)\b/i.test(cmd);
|
|
159
|
-
|
|
205
|
+
// "Cannot see the script" is what the U tier existed for. Once every script the call
|
|
206
|
+
// names has been read and attached, the reviewer *can* see it — so the call stops
|
|
207
|
+
// being unknowable and becomes an ordinary graded call (whose body the nets have just
|
|
208
|
+
// judged above). One unreadable file keeps the whole call opaque.
|
|
209
|
+
const covered = source.scriptsCovered === true && read.length > 0;
|
|
210
|
+
const execExternal = runner && !covered && (outsideWorkspace(cmd, source.workspaceRoot) || unseenScript(cmd, source.workspaceRoot));
|
|
160
211
|
const unnamed = text !== '' && unnamedOperation(text);
|
|
161
212
|
const opaque = OPAQUE.test(cmd) || execExternal;
|
|
162
213
|
if (irreversible)
|
|
@@ -216,18 +267,28 @@ export function buildAskContext(source) {
|
|
|
216
267
|
reversibility: grade(seen),
|
|
217
268
|
};
|
|
218
269
|
}
|
|
270
|
+
/** Budget for one script body on the wire: a real script, one round-trip. */
|
|
271
|
+
export const GRADER_SCRIPT_BODY_CHARS = 20_000;
|
|
219
272
|
/**
|
|
220
273
|
* Project for egress to the grader. Tier knobs and the rule layer's own verdict
|
|
221
274
|
* are stripped by design — see the module comment.
|
|
222
275
|
*/
|
|
223
276
|
export function toGraderPayload(ctx) {
|
|
224
277
|
const root = ctx.workspaceRoot;
|
|
278
|
+
const scripts = (ctx.scriptContents ?? []).map((c) => ({
|
|
279
|
+
ref: c.ref,
|
|
280
|
+
path: c.path,
|
|
281
|
+
...(c.unreadable === undefined
|
|
282
|
+
? { body: redactCommandText(c.body, { maxChars: GRADER_SCRIPT_BODY_CHARS }), truncated: c.truncated }
|
|
283
|
+
: { unreadable: c.unreadable }),
|
|
284
|
+
}));
|
|
225
285
|
return {
|
|
226
286
|
tool: ctx.tool,
|
|
227
287
|
args: redactArgs(ctx.args ?? {}),
|
|
228
288
|
reason: ctx.ruleReason ?? '',
|
|
229
289
|
target_resolvable: ctx.targetResolvable,
|
|
230
290
|
...(root === undefined || root === '' ? {} : { workspace_root: root }),
|
|
291
|
+
...(scripts.length === 0 ? {} : { script_contents: scripts }),
|
|
231
292
|
};
|
|
232
293
|
}
|
|
233
294
|
/**
|
|
@@ -285,6 +346,11 @@ export function humanCardLine(ctx, maxFiles = 3) {
|
|
|
285
346
|
bits.push('payload=opaque');
|
|
286
347
|
if (ctx.unlearnable)
|
|
287
348
|
bits.push('never-learn');
|
|
349
|
+
const scripts = ctx.scriptContents ?? [];
|
|
350
|
+
if (scripts.length > 0) {
|
|
351
|
+
const unread = scripts.filter((c) => c.unreadable !== undefined).length;
|
|
352
|
+
bits.push(unread === 0 ? `scripts=${scripts.length}:body-attached` : `scripts=${scripts.length}:unread=${unread}`);
|
|
353
|
+
}
|
|
288
354
|
if (card.fileCount > 0) {
|
|
289
355
|
const shown = card.files.length;
|
|
290
356
|
bits.push(shown < card.fileCount ? `files=${shown}/${card.fileCount}` : `files=${card.fileCount}`);
|
|
@@ -334,6 +400,14 @@ export const DOUBT_COPY = {
|
|
|
334
400
|
zh: '会执行工作区之外的代码,本次调用无法静态看清它做什么',
|
|
335
401
|
en: 'it runs code from outside the workspace, which this call cannot inspect',
|
|
336
402
|
},
|
|
403
|
+
'script-attached': {
|
|
404
|
+
zh: '它要跑的脚本正文已读出来、随本次审查附上(模型看到的和你看到的是同一份)',
|
|
405
|
+
en: 'the script it runs was read and attached to this review — the model and you see the same text',
|
|
406
|
+
},
|
|
407
|
+
'script-unreadable': {
|
|
408
|
+
zh: '脚本正文没读到(缺失/过大/二进制),没有人看见真正要执行的代码',
|
|
409
|
+
en: 'a script body could not be read (missing, over budget, or binary) — nobody has seen the code that will run',
|
|
410
|
+
},
|
|
337
411
|
'outside-paths': {
|
|
338
412
|
zh: '涉及工作区之外的路径',
|
|
339
413
|
en: 'it touches paths outside the workspace',
|
|
@@ -395,6 +469,16 @@ export function askDoubtKinds(ctx, extra = {}) {
|
|
|
395
469
|
}
|
|
396
470
|
if (extra.modelFlagged === true)
|
|
397
471
|
kinds.push('model-flagged');
|
|
472
|
+
// Whether the code that will run was actually looked at is a fact the human decides
|
|
473
|
+
// with, and it is not implied by any other clause: an attached body means the
|
|
474
|
+
// judgment covered it, an unreadable one means nothing ever will.
|
|
475
|
+
const scripts = ctx.scriptContents ?? [];
|
|
476
|
+
if (scripts.length > 0) {
|
|
477
|
+
if (scripts.some((c) => c.unreadable !== undefined))
|
|
478
|
+
kinds.push('script-unreadable');
|
|
479
|
+
else
|
|
480
|
+
kinds.push('script-attached');
|
|
481
|
+
}
|
|
398
482
|
if (ctx.ruleReason !== undefined && /no rule matched|default action/i.test(ctx.ruleReason))
|
|
399
483
|
kinds.push('no-rule');
|
|
400
484
|
const root = ctx.workspaceRoot;
|