@goodandready/dsh-approval-gate 0.1.3 → 0.1.4
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.md +14 -0
- package/lib/index.js +53 -3
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 0.1.4
|
|
2
|
+
|
|
3
|
+
- The gate no longer raises an ask it knows cannot be answered. It reads the effective
|
|
4
|
+
approval policy (`ctx.approval.config.policy`) and, when that policy is in
|
|
5
|
+
`unattendedPolicies` (default `["never"]`, i.e. the full-access preset), an allowed command
|
|
6
|
+
proceeds instead of dying as a rejection nobody could consent to. On 20.09.2026 exactly that
|
|
7
|
+
turned every agreed write in such a session into `the user rejected tool`.
|
|
8
|
+
- Denials never depend on the policy: dangerous shell commands and writes to secret files are
|
|
9
|
+
still blocked unconditionally, and the uncertain-syntax case stays denied where no
|
|
10
|
+
pre-execute hook exists.
|
|
11
|
+
- New settings: `unattendedPolicies` and an explicit `unattended` flag for deployments where
|
|
12
|
+
the policy is not visible. An unknown policy keeps the safe direction and still asks.
|
|
13
|
+
- 8 tests for the new behaviour (23 in total).
|
|
14
|
+
|
|
1
15
|
# Changelog
|
|
2
16
|
|
|
3
17
|
## 0.1.3
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
// commands and writes to secret files even when sandbox mode would allow
|
|
5
5
|
// them. A guard return value (non-undefined) denies execution, so the
|
|
6
6
|
// dangerous call never runs on its own - it needs an explicit owner
|
|
7
|
-
// Approval is handled by DSH's native flow; no separate word is implemented.
|
|
7
|
+
// Approval is handled by DSH's native flow; no separate word is implemented. When the
|
|
8
|
+
// effective approval policy cannot be answered ("never", i.e. the full-access preset),
|
|
9
|
+
// the gate does not raise an ask at all: an allowed command proceeds instead of dying as
|
|
10
|
+
// a rejection nobody could consent to. Denials never depend on the policy.
|
|
8
11
|
//
|
|
9
12
|
// Only the agent tool calls pass through the guard (cron/systemd scripts
|
|
10
13
|
// do not), so legitimate unattended operations are unaffected.
|
|
@@ -22,11 +25,50 @@ import { LOCALE_NS, MESSAGES } from "./messages.js";
|
|
|
22
25
|
export const name = "dsh-approval-gate";
|
|
23
26
|
export const inject = ["tools"];
|
|
24
27
|
|
|
28
|
+
/** Policies under which a prompt cannot be answered, so raising one is a silent denial. */
|
|
29
|
+
export const DEFAULT_UNATTENDED_POLICIES = ["never"];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a raised ask would be answerable in this session.
|
|
33
|
+
*
|
|
34
|
+
* `policy` comes from the approval service (`ctx.approval.config.policy`); an empty value
|
|
35
|
+
* means the knob is unknown and the gate keeps asking, which is the safe direction.
|
|
36
|
+
* @param {{policy?: string, unattendedPolicies?: string[]}} knobs
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
export function isUnattended(knobs) {
|
|
40
|
+
const policy = knobs && knobs.policy;
|
|
41
|
+
const policies = (knobs && knobs.unattendedPolicies) || DEFAULT_UNATTENDED_POLICIES;
|
|
42
|
+
if (typeof policy !== "string" || policy === "") return false;
|
|
43
|
+
return policies.includes(policy);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Effective permission knobs, as the harness exposes them. */
|
|
47
|
+
function readKnobs(ctx) {
|
|
48
|
+
const read = (name) => {
|
|
49
|
+
try {
|
|
50
|
+
return ctx && typeof ctx.get === "function" ? ctx.get(name) : undefined;
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const approval = read("approval");
|
|
56
|
+
const shell = read("shell");
|
|
57
|
+
return {
|
|
58
|
+
policy: (approval && approval.config && approval.config.policy) || "",
|
|
59
|
+
sandbox: (shell && shell.sandboxMode) || "",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
25
63
|
export const Config = Schema.object({
|
|
26
64
|
/** Name of the tool whose `command` arg is inspected (the DSH bash tool). */
|
|
27
65
|
toolName: Schema.string().default(DEFAULT_BASH_TOOL),
|
|
28
66
|
/** Extra tool names treated as file writes; paths are matched against secret files. */
|
|
29
67
|
fileWriteTools: Schema.array(Schema.string()).default(DEFAULT_FILE_WRITE_TOOLS.slice()),
|
|
68
|
+
/** Approval policies under which an ask could not be answered, so it is not raised. */
|
|
69
|
+
unattendedPolicies: Schema.array(Schema.string()).default(DEFAULT_UNATTENDED_POLICIES.slice()),
|
|
70
|
+
/** Force unattended behaviour where the approval policy is not visible. */
|
|
71
|
+
unattended: Schema.boolean().default(false),
|
|
30
72
|
});
|
|
31
73
|
|
|
32
74
|
function logBlocked(ctx, hit, who) {
|
|
@@ -54,12 +96,20 @@ export function apply(ctx, config) {
|
|
|
54
96
|
if (locale && typeof locale.bind === "function") return locale.bind(LOCALE_NS)(key);
|
|
55
97
|
return key;
|
|
56
98
|
};
|
|
99
|
+
const unattendedPolicies = cfg.unattendedPolicies || DEFAULT_UNATTENDED_POLICIES.slice();
|
|
100
|
+
const forcedUnattended = cfg.unattended === true;
|
|
57
101
|
const hasPreExecute = ctx && typeof ctx.on === "function";
|
|
58
102
|
if (hasPreExecute) {
|
|
59
103
|
ctx.on("tools/pre-execute", function (execution, next) {
|
|
60
104
|
const hit = inspectExecution(execution, { toolName: toolName, fileWriteTools: fileWriteTools });
|
|
61
|
-
if (hit.ask) return
|
|
62
|
-
|
|
105
|
+
if (!hit.ask) return next();
|
|
106
|
+
const knobs = readKnobs(ctx);
|
|
107
|
+
if (forcedUnattended || isUnattended({ policy: knobs.policy, unattendedPolicies })) {
|
|
108
|
+
// No one can answer here: an allowed command proceeds. Denials are the guard's job
|
|
109
|
+
// and do not depend on this decision.
|
|
110
|
+
return next();
|
|
111
|
+
}
|
|
112
|
+
return { kind: "ask", reason: askMessage(hit, sessionTag(execution), translate, ctx && ctx.logger) };
|
|
63
113
|
});
|
|
64
114
|
}
|
|
65
115
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-approval-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Host-only command safety gate for DeepSeek Harness: blocks recognized dangerous operations and requests DSH approval when a command cannot be inspected.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -42,5 +42,13 @@
|
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"registry": "https://registry.npmjs.org",
|
|
44
44
|
"access": "public"
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"dsh",
|
|
48
|
+
"dsh-plugin",
|
|
49
|
+
"deepseek-harness",
|
|
50
|
+
"security",
|
|
51
|
+
"approval-gate",
|
|
52
|
+
"safety"
|
|
53
|
+
]
|
|
46
54
|
}
|