@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 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 { kind: "ask", reason: askMessage(hit, sessionTag(execution), translate, ctx && ctx.logger) };
62
- return next();
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",
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
  }