dsh-codex-approval 0.4.0 → 0.4.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/LICENSE +21 -21
- package/README.md +251 -193
- package/client-card-style.js +79 -0
- package/client-model-picker.js +122 -0
- package/client-remote.js +39 -0
- package/cordis.patch.yml +4 -4
- package/enrich.js +89 -68
- package/i18n.js +144 -140
- package/index.js +794 -541
- package/judge.js +198 -176
- package/lib/client.js +429 -0
- package/modes.js +62 -62
- package/package.json +76 -43
- package/rules.js +90 -90
- package/scripts/build-client.mjs +48 -0
- package/transcript.js +13 -4
package/package.json
CHANGED
|
@@ -1,43 +1,76 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-codex-approval",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Codex-style approval autopilot for DeepSeek Harness: ordered glob rules (allow/ask/deny) plus an AI risk judge (low/medium/high) mapped through a risk tolerance, as an approval answerer.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-codex-approval",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Codex-style approval autopilot for DeepSeek Harness: ordered glob rules (allow/ask/deny) plus an AI risk judge (low/medium/high) mapped through a risk tolerance, as an approval answerer.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build:client": "node scripts/build-client.mjs",
|
|
9
|
+
"test": "node --test"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"default": "./index.js"
|
|
14
|
+
},
|
|
15
|
+
"./client": "./lib/client.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"lib/client.js",
|
|
20
|
+
"client-model-picker.js",
|
|
21
|
+
"client-remote.js",
|
|
22
|
+
"client-card-style.js",
|
|
23
|
+
"rules.js",
|
|
24
|
+
"enrich.js",
|
|
25
|
+
"i18n.js",
|
|
26
|
+
"judge.js",
|
|
27
|
+
"modes.js",
|
|
28
|
+
"transcript.js",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"scripts/build-client.mjs",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"dsh": {
|
|
35
|
+
"bundle": {
|
|
36
|
+
"patch": "./cordis.patch.yml"
|
|
37
|
+
},
|
|
38
|
+
"client": {
|
|
39
|
+
"platform": "web",
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
44
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
45
|
+
"@deepseek-ai/dsh-client-locale"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"compatibility": {
|
|
49
|
+
"dsh": ">=0.1.2-rc.1",
|
|
50
|
+
"dshReleases": {
|
|
51
|
+
"0.1.0-rc.6": "compatible",
|
|
52
|
+
"0.1.2-rc.1": "compatible",
|
|
53
|
+
"0.1.5-rc.1": "compatible"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"keywords": [
|
|
58
|
+
"dsh",
|
|
59
|
+
"dsh-plugin",
|
|
60
|
+
"deepseek-harness",
|
|
61
|
+
"approval",
|
|
62
|
+
"codex",
|
|
63
|
+
"risk"
|
|
64
|
+
],
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://github.com/040822/dsh-codex-approval.git"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=22.19"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/rules.js
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-codex-approval — rules.js
|
|
3
|
-
*
|
|
4
|
-
* Codex-style rule matching. A rule matches a request via a single glob
|
|
5
|
-
* pattern over the "matchable text": `ToolName(args preview) reason:<reason>`.
|
|
6
|
-
* Examples:
|
|
7
|
-
* - `Bash(git *)` — the recovered bash command starts with "git "
|
|
8
|
-
* - `Bash(rm -rf /*)` — destructive command
|
|
9
|
-
* - `reason:*curl*` — the approval reason mentions curl
|
|
10
|
-
*
|
|
11
|
-
* Evaluation priority is safety-first regardless of list order:
|
|
12
|
-
* deny > ask > allow
|
|
13
|
-
* (an explicit ask or deny can never be overridden by a blanket allow,
|
|
14
|
-
* mirroring Codex where ask/reject rules take precedence over auto-approve).
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** Classic glob match: `*` = any sequence (incl. empty), `?` = one char. Case-insensitive. */
|
|
18
|
-
export function wildcardMatch(pattern, text) {
|
|
19
|
-
if (typeof pattern !== "string" || typeof text !== "string") return false;
|
|
20
|
-
pattern = pattern.toLowerCase();
|
|
21
|
-
text = text.toLowerCase();
|
|
22
|
-
let pi = 0;
|
|
23
|
-
let ti = 0;
|
|
24
|
-
let star = -1;
|
|
25
|
-
let mark = 0;
|
|
26
|
-
while (ti < text.length) {
|
|
27
|
-
if (pi < pattern.length && (pattern[pi] === "?" || pattern[pi] === text[ti])) {
|
|
28
|
-
pi += 1;
|
|
29
|
-
ti += 1;
|
|
30
|
-
} else if (pi < pattern.length && pattern[pi] === "*") {
|
|
31
|
-
star = pi;
|
|
32
|
-
pi += 1;
|
|
33
|
-
mark = ti;
|
|
34
|
-
} else if (star !== -1) {
|
|
35
|
-
pi = star + 1;
|
|
36
|
-
ti = mark + 1;
|
|
37
|
-
mark += 1;
|
|
38
|
-
} else {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
while (pi < pattern.length && pattern[pi] === "*") pi += 1;
|
|
43
|
-
return pi === pattern.length;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Build the single string rules match against.
|
|
48
|
-
* @param req - { toolName, argsText, reason }
|
|
49
|
-
*/
|
|
50
|
-
export function matchableText(req) {
|
|
51
|
-
const bits = [];
|
|
52
|
-
if (req.toolName) bits.push(`${req.toolName}(${req.argsText ?? ""})`);
|
|
53
|
-
if (req.reason) bits.push(`reason:${req.reason}`);
|
|
54
|
-
return bits.join(" ");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* The surfaces a rule pattern is tested against, in order: the tool call
|
|
59
|
-
* alone (`ToolName(args)`), the reason alone (`reason:...`), then the
|
|
60
|
-
* combined string. This lets `Bash(git *)` match regardless of an appended
|
|
61
|
-
* reason, and `reason:*curl*` match the reason alone.
|
|
62
|
-
*/
|
|
63
|
-
export function matchSurfaces(req) {
|
|
64
|
-
const surfaces = [];
|
|
65
|
-
if (req.toolName) surfaces.push(`${req.toolName}(${req.argsText ?? ""})`);
|
|
66
|
-
if (req.reason) surfaces.push(`reason:${req.reason}`);
|
|
67
|
-
const combined = surfaces.join(" ");
|
|
68
|
-
if (!surfaces.includes(combined)) surfaces.push(combined);
|
|
69
|
-
return surfaces.filter((surface) => surface !== "");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Evaluate an ordered rule list against one request.
|
|
74
|
-
* @param rules - [{ match: string, action: "allow"|"ask"|"deny" }]
|
|
75
|
-
* @param req - { toolName, argsText, reason }
|
|
76
|
-
* @returns the first matching rule under deny > ask > allow priority, or null.
|
|
77
|
-
*/
|
|
78
|
-
export function evaluateRules(rules, req) {
|
|
79
|
-
const surfaces = matchSurfaces(req);
|
|
80
|
-
if (surfaces.length === 0) return null;
|
|
81
|
-
for (const action of ["deny", "ask", "allow"]) {
|
|
82
|
-
for (const rule of rules) {
|
|
83
|
-
if (rule.action !== action) continue;
|
|
84
|
-
for (const surface of surfaces) {
|
|
85
|
-
if (wildcardMatch(rule.match, surface)) return rule;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-codex-approval — rules.js
|
|
3
|
+
*
|
|
4
|
+
* Codex-style rule matching. A rule matches a request via a single glob
|
|
5
|
+
* pattern over the "matchable text": `ToolName(args preview) reason:<reason>`.
|
|
6
|
+
* Examples:
|
|
7
|
+
* - `Bash(git *)` — the recovered bash command starts with "git "
|
|
8
|
+
* - `Bash(rm -rf /*)` — destructive command
|
|
9
|
+
* - `reason:*curl*` — the approval reason mentions curl
|
|
10
|
+
*
|
|
11
|
+
* Evaluation priority is safety-first regardless of list order:
|
|
12
|
+
* deny > ask > allow
|
|
13
|
+
* (an explicit ask or deny can never be overridden by a blanket allow,
|
|
14
|
+
* mirroring Codex where ask/reject rules take precedence over auto-approve).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Classic glob match: `*` = any sequence (incl. empty), `?` = one char. Case-insensitive. */
|
|
18
|
+
export function wildcardMatch(pattern, text) {
|
|
19
|
+
if (typeof pattern !== "string" || typeof text !== "string") return false;
|
|
20
|
+
pattern = pattern.toLowerCase();
|
|
21
|
+
text = text.toLowerCase();
|
|
22
|
+
let pi = 0;
|
|
23
|
+
let ti = 0;
|
|
24
|
+
let star = -1;
|
|
25
|
+
let mark = 0;
|
|
26
|
+
while (ti < text.length) {
|
|
27
|
+
if (pi < pattern.length && (pattern[pi] === "?" || pattern[pi] === text[ti])) {
|
|
28
|
+
pi += 1;
|
|
29
|
+
ti += 1;
|
|
30
|
+
} else if (pi < pattern.length && pattern[pi] === "*") {
|
|
31
|
+
star = pi;
|
|
32
|
+
pi += 1;
|
|
33
|
+
mark = ti;
|
|
34
|
+
} else if (star !== -1) {
|
|
35
|
+
pi = star + 1;
|
|
36
|
+
ti = mark + 1;
|
|
37
|
+
mark += 1;
|
|
38
|
+
} else {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
while (pi < pattern.length && pattern[pi] === "*") pi += 1;
|
|
43
|
+
return pi === pattern.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build the single string rules match against.
|
|
48
|
+
* @param req - { toolName, argsText, reason }
|
|
49
|
+
*/
|
|
50
|
+
export function matchableText(req) {
|
|
51
|
+
const bits = [];
|
|
52
|
+
if (req.toolName) bits.push(`${req.toolName}(${req.argsText ?? ""})`);
|
|
53
|
+
if (req.reason) bits.push(`reason:${req.reason}`);
|
|
54
|
+
return bits.join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The surfaces a rule pattern is tested against, in order: the tool call
|
|
59
|
+
* alone (`ToolName(args)`), the reason alone (`reason:...`), then the
|
|
60
|
+
* combined string. This lets `Bash(git *)` match regardless of an appended
|
|
61
|
+
* reason, and `reason:*curl*` match the reason alone.
|
|
62
|
+
*/
|
|
63
|
+
export function matchSurfaces(req) {
|
|
64
|
+
const surfaces = [];
|
|
65
|
+
if (req.toolName) surfaces.push(`${req.toolName}(${req.argsText ?? ""})`);
|
|
66
|
+
if (req.reason) surfaces.push(`reason:${req.reason}`);
|
|
67
|
+
const combined = surfaces.join(" ");
|
|
68
|
+
if (!surfaces.includes(combined)) surfaces.push(combined);
|
|
69
|
+
return surfaces.filter((surface) => surface !== "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Evaluate an ordered rule list against one request.
|
|
74
|
+
* @param rules - [{ match: string, action: "allow"|"ask"|"deny" }]
|
|
75
|
+
* @param req - { toolName, argsText, reason }
|
|
76
|
+
* @returns the first matching rule under deny > ask > allow priority, or null.
|
|
77
|
+
*/
|
|
78
|
+
export function evaluateRules(rules, req) {
|
|
79
|
+
const surfaces = matchSurfaces(req);
|
|
80
|
+
if (surfaces.length === 0) return null;
|
|
81
|
+
for (const action of ["deny", "ask", "allow"]) {
|
|
82
|
+
for (const rule of rules) {
|
|
83
|
+
if (rule.action !== action) continue;
|
|
84
|
+
for (const surface of surfaces) {
|
|
85
|
+
if (wildcardMatch(rule.match, surface)) return rule;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-codex-approval — build the browser half (lib/client.js).
|
|
4
|
+
*
|
|
5
|
+
* DSH loads a client plugin as `window.__ModuleLoader__.load({ id, factory })`,
|
|
6
|
+
* so the shipped artifact is an esbuild CJS bundle wrapped in that loader call.
|
|
7
|
+
* React and every `@deepseek-ai/*` package stay external: the host's module
|
|
8
|
+
* loader supplies them, and bundling them would duplicate the client runtime.
|
|
9
|
+
*
|
|
10
|
+
* Usage (from the package root):
|
|
11
|
+
* node scripts/build-client.mjs
|
|
12
|
+
*
|
|
13
|
+
* esbuild is not a dependency of this package (the published tarball has none),
|
|
14
|
+
* so the script resolves it from npx unless ESBUILD_BIN is set.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
const PACKAGE_ID = "dsh-codex-approval";
|
|
22
|
+
const ENTRY = "src/client/index.ts";
|
|
23
|
+
const OUT = "lib/client.js";
|
|
24
|
+
|
|
25
|
+
const workdir = mkdtempSync(join(tmpdir(), "dsh-codex-approval-build-"));
|
|
26
|
+
const bundlePath = join(workdir, "client.bundle.cjs");
|
|
27
|
+
const esbuild = process.env.ESBUILD_BIN ?? "npx";
|
|
28
|
+
const esbuildArgs = process.env.ESBUILD_BIN === undefined ? ["--yes", "esbuild@0.25.0"] : [];
|
|
29
|
+
|
|
30
|
+
execFileSync(esbuild, [
|
|
31
|
+
...esbuildArgs,
|
|
32
|
+
ENTRY,
|
|
33
|
+
"--bundle",
|
|
34
|
+
"--format=cjs",
|
|
35
|
+
"--platform=browser",
|
|
36
|
+
"--target=es2022",
|
|
37
|
+
"--jsx=transform",
|
|
38
|
+
`--outfile=${bundlePath}`,
|
|
39
|
+
"--external:react",
|
|
40
|
+
"--external:react/*",
|
|
41
|
+
"--external:@deepseek-ai/*",
|
|
42
|
+
"--log-level=warning"
|
|
43
|
+
], { stdio: "inherit" });
|
|
44
|
+
|
|
45
|
+
const bundle = readFileSync(bundlePath, "utf8");
|
|
46
|
+
const wrapped = `window.__ModuleLoader__.load({ id: ${JSON.stringify(PACKAGE_ID)}, factory: (require) => { var module = { exports: {} }; var exports = module.exports;\n${bundle}\nreturn module.exports; } });\n`;
|
|
47
|
+
writeFileSync(OUT, wrapped);
|
|
48
|
+
console.log(`built ${OUT} (${wrapped.length} bytes) from ${ENTRY}`);
|
package/transcript.js
CHANGED
|
@@ -61,15 +61,24 @@ function userText(data) {
|
|
|
61
61
|
/**
|
|
62
62
|
* Collect semantic items from the raw event stream, newest first.
|
|
63
63
|
* Plugin-sourced user messages and streaming chunks are excluded here.
|
|
64
|
-
* @param events -
|
|
64
|
+
* @param events - a Session-like object or event array
|
|
65
65
|
* @returns array of { seq, kind, ... } with seq counting only semantic items
|
|
66
66
|
* (newest first, so index 0 is the most recent).
|
|
67
67
|
*/
|
|
68
68
|
export function collectSemanticItems(events) {
|
|
69
|
-
|
|
69
|
+
const list = Array.isArray(events)
|
|
70
|
+
? events
|
|
71
|
+
: typeof events?.snapshotEvents === "function"
|
|
72
|
+
? (() => { try { return events.snapshotEvents(); } catch { return []; } })()
|
|
73
|
+
: typeof events?.ownEvents === "function"
|
|
74
|
+
? (() => { try { return events.ownEvents(); } catch { return []; } })()
|
|
75
|
+
: Array.isArray(events?.events)
|
|
76
|
+
? events.events
|
|
77
|
+
: [];
|
|
78
|
+
if (!Array.isArray(list)) return [];
|
|
70
79
|
const items = [];
|
|
71
|
-
for (let i =
|
|
72
|
-
const event =
|
|
80
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
81
|
+
const event = list[i];
|
|
73
82
|
if (event === null || typeof event !== "object") continue;
|
|
74
83
|
const type = event.type;
|
|
75
84
|
if (type === "user/message") {
|