@goodandready/dsh-approval-gate 0.1.12 → 0.1.14
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 +23 -7
- package/lib/audit-log.js +76 -0
- package/lib/breaker.js +26 -2
- package/lib/cache.js +146 -0
- package/lib/dispatcher.js +208 -0
- package/lib/engine.js +129 -17
- package/lib/fingerprint.js +100 -0
- package/lib/index.js +80 -0
- package/lib/inspect.js +103 -160
- package/lib/ledger.js +164 -0
- package/lib/loopback.js +85 -0
- package/lib/messages.js +49 -1
- package/lib/parsers/git.js +321 -0
- package/lib/parsers/shell-cmds.js +267 -0
- package/lib/retry-exhaustion.js +171 -0
- package/package.json +1 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// lib/parsers/shell-cmds.js
|
|
2
|
+
// Specialized shell AST parser for redirections, subshells, pipelines, and error masking.
|
|
3
|
+
// Resolves: #126, #90
|
|
4
|
+
|
|
5
|
+
import { REDIR_OPS, SPLIT_OPS } from "../tokenizer.js";
|
|
6
|
+
import { isProtectedConfigPath, isPathTraversal, normalizeSeparators } from "../paths.js";
|
|
7
|
+
|
|
8
|
+
const SHELLS = new Set(["bash", "sh", "dash", "zsh", "ksh", "ash"]);
|
|
9
|
+
const INTERPRETERS = new Set(["python", "python2", "python3", "node", "nodejs", "perl", "ruby", "php"]);
|
|
10
|
+
const DB_CLIENTS = new Set([
|
|
11
|
+
"sqlite3", "mysql", "psql", "pg_restore", "mongo", "mongosh", "redis-cli", "clickhouse-client",
|
|
12
|
+
]);
|
|
13
|
+
const DESTRUCTIVE_SQL = /\b(?:ALTER|DROP|TRUNCATE|DELETE\s+FROM|CREATE\s+(?:TABLE|DATABASE|INDEX))\b/i;
|
|
14
|
+
const WRAPPERS = new Set(["sudo", "command", "builtin", "nohup", "time", "nice", "ionice", "stdbuf", "timeout", "exec"]);
|
|
15
|
+
|
|
16
|
+
const SILENT_STDERR_PATTERN = /(?:2\s*>\s*\/dev\/null|&>\s*\/dev\/null|2>&1\s*>\s*\/dev\/null|>\s*\/dev\/null\s*2>&1)/i;
|
|
17
|
+
const SILENT_EXIT_ZERO_PATTERN = /\|\|\s*(?:true|:|exit\s+0)\b/i;
|
|
18
|
+
const SET_PLUS_E_PATTERN = /\bset\s+(?:\+[eE]|\+o\s+errexit)\b/i;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks whether a path represents a raw physical or virtual disk device.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} path
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function isDevicePath(path) {
|
|
27
|
+
if (typeof path !== "string" || !path.startsWith("/dev/")) return false;
|
|
28
|
+
const device = path.slice(5);
|
|
29
|
+
if (["null", "zero", "full", "random", "urandom", "stdin", "stdout", "stderr", "tty"].includes(device)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (/^(?:sd[a-z]+\d*|hd[a-z]+\d*|vd[a-z]+\d*|xvd[a-z]+\d*|nvme\d+n\d+(?:p\d+)?|mmcblk\d+(?:p\d+)?|loop\d+)$/i.test(device)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return device.startsWith("mapper/") || device.startsWith("disk/by-id/") || device.startsWith("disk/by-uuid/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts redirection operators and their target paths from segment tokens.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<{ kind: string, value: string, dynamic?: boolean }>} seg
|
|
42
|
+
* @returns {Array<{ operator: string, path: string, dynamic: boolean }>}
|
|
43
|
+
*/
|
|
44
|
+
export function extractRedirects(seg) {
|
|
45
|
+
if (!Array.isArray(seg)) return [];
|
|
46
|
+
const redirects = [];
|
|
47
|
+
for (let i = 0; i < seg.length; i += 1) {
|
|
48
|
+
const t = seg[i];
|
|
49
|
+
if (t.kind === "op" && REDIR_OPS.has(t.value)) {
|
|
50
|
+
const next = seg[i + 1];
|
|
51
|
+
if (next && next.kind === "word") {
|
|
52
|
+
redirects.push({
|
|
53
|
+
operator: t.value,
|
|
54
|
+
path: next.value,
|
|
55
|
+
dynamic: Boolean(next.dynamic),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return redirects;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Evaluates security of extracted redirections (device writes, secret writes, path traversals).
|
|
65
|
+
*
|
|
66
|
+
* @param {Array<{ operator: string, path: string, dynamic: boolean }>} redirects
|
|
67
|
+
* @param {{ workspaceDir?: string, env?: Record<string, string> }} [options]
|
|
68
|
+
* @returns {{ deny?: boolean, ask?: boolean, reason?: string, detail?: string, path?: string } | null}
|
|
69
|
+
*/
|
|
70
|
+
export function inspectRedirectSecurity(redirects, options = {}) {
|
|
71
|
+
const workspaceDir = options.workspaceDir || process.cwd();
|
|
72
|
+
const env = options.env || process.env;
|
|
73
|
+
|
|
74
|
+
for (const redir of redirects) {
|
|
75
|
+
if (redir.dynamic) {
|
|
76
|
+
return {
|
|
77
|
+
ask: true,
|
|
78
|
+
reason: "dynamic-redirect",
|
|
79
|
+
detail: "The expanded redirect target cannot be verified; review before allowing",
|
|
80
|
+
path: redir.path,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const norm = normalizeSeparators(redir.path);
|
|
85
|
+
|
|
86
|
+
// Block device redirection
|
|
87
|
+
if (isDevicePath(norm)) {
|
|
88
|
+
return {
|
|
89
|
+
deny: true,
|
|
90
|
+
reason: "device-write",
|
|
91
|
+
detail: `Redirection targets block device ${norm}`,
|
|
92
|
+
path: redir.path,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Block protected secrets redirection
|
|
97
|
+
if (isProtectedConfigPath(norm, env)) {
|
|
98
|
+
return {
|
|
99
|
+
deny: true,
|
|
100
|
+
reason: "secret-write",
|
|
101
|
+
detail: `Redirection targets protected configuration or credentials file ${norm}`,
|
|
102
|
+
path: redir.path,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Block path traversal outside workspace
|
|
107
|
+
const isRel = !/^(?:\/|[A-Za-z]:\/|\/\/)/.test(norm);
|
|
108
|
+
const isExempt = !isRel && (
|
|
109
|
+
/^\/dev\/(?:null|zero|stdout|stderr|fd\/\d+)$/.test(norm) ||
|
|
110
|
+
/^(?:\/tmp|\/var\/tmp)(?:\/|$)/i.test(norm)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!isExempt && isPathTraversal(redir.path, workspaceDir)) {
|
|
114
|
+
return {
|
|
115
|
+
deny: true,
|
|
116
|
+
reason: "path-traversal",
|
|
117
|
+
detail: `Redirection escapes workspace boundary: ${redir.path}`,
|
|
118
|
+
path: redir.path,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detects error masking and suppressed failures in shell commands.
|
|
128
|
+
* Resolves: #90 (SilentErrorGuard).
|
|
129
|
+
*
|
|
130
|
+
* @param {string} command - Raw command line
|
|
131
|
+
* @param {string[]} [argv] - Optional parsed argv
|
|
132
|
+
* @returns {{
|
|
133
|
+
* masked: boolean,
|
|
134
|
+
* kind?: "stderr-suppressed" | "exit-zero-forced" | "errexit-disabled",
|
|
135
|
+
* pattern?: string,
|
|
136
|
+
* reason?: string,
|
|
137
|
+
* detail?: string
|
|
138
|
+
* }}
|
|
139
|
+
*/
|
|
140
|
+
export function inspectSilentErrors(command, argv = []) {
|
|
141
|
+
if (typeof command !== "string" || !command.trim()) {
|
|
142
|
+
return { masked: false };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 1. Check stderr suppression to /dev/null
|
|
146
|
+
if (SILENT_STDERR_PATTERN.test(command)) {
|
|
147
|
+
return {
|
|
148
|
+
masked: true,
|
|
149
|
+
kind: "stderr-suppressed",
|
|
150
|
+
pattern: "2>/dev/null",
|
|
151
|
+
reason: "silent-error-masked",
|
|
152
|
+
detail: "Command suppresses error output via stderr redirection to /dev/null",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Check forced success masking (|| true, || :)
|
|
157
|
+
if (SILENT_EXIT_ZERO_PATTERN.test(command)) {
|
|
158
|
+
return {
|
|
159
|
+
masked: true,
|
|
160
|
+
kind: "exit-zero-forced",
|
|
161
|
+
pattern: "|| true",
|
|
162
|
+
reason: "silent-error-masked",
|
|
163
|
+
detail: "Command forces a success exit code via logical OR fallback (|| true or || :)",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 3. Check shell errexit unsetting (set +e)
|
|
168
|
+
if (SET_PLUS_E_PATTERN.test(command)) {
|
|
169
|
+
return {
|
|
170
|
+
masked: true,
|
|
171
|
+
kind: "errexit-disabled",
|
|
172
|
+
pattern: "set +e",
|
|
173
|
+
reason: "silent-error-masked",
|
|
174
|
+
detail: "Command disables shell error-exit checking (set +e)",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { masked: false };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function unwrapExecutableFromSegment(seg) {
|
|
182
|
+
const words = seg.filter((t) => t.kind === "word").map((t) => t.value);
|
|
183
|
+
const takesValue = new Set(["-u", "--user", "-g", "--group", "-p", "-s", "--signal", "-k", "--kill-after", "-o", "-e", "-i"]);
|
|
184
|
+
let i = 0;
|
|
185
|
+
while (i < words.length) {
|
|
186
|
+
const val = words[i];
|
|
187
|
+
if (val === "then" || val === "do" || val === "{" || val === "!" || val === "}") {
|
|
188
|
+
i += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (val === "env") {
|
|
192
|
+
i += 1;
|
|
193
|
+
while (i < words.length && words[i].includes("=") && !words[i].startsWith("-")) i += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (val.includes("=") && !val.startsWith("-")) {
|
|
197
|
+
i += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!WRAPPERS.has(val)) {
|
|
201
|
+
const parts = val.replace(/\\/g, "/").split("/");
|
|
202
|
+
return parts[parts.length - 1] || "";
|
|
203
|
+
}
|
|
204
|
+
i += 1;
|
|
205
|
+
while (i < words.length && words[i].startsWith("-")) {
|
|
206
|
+
const flag = words[i];
|
|
207
|
+
if (flag === "--") { i += 1; break; }
|
|
208
|
+
if (takesValue.has(flag)) i += 2;
|
|
209
|
+
else i += 1;
|
|
210
|
+
}
|
|
211
|
+
if (val === "timeout" && i < words.length) i += 1;
|
|
212
|
+
if (val === "nice" && i < words.length && /^[0-9-]+$/.test(words[i])) i += 1;
|
|
213
|
+
}
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Scans tokens for dangerous downloader-to-shell or destructive SQL pipelines.
|
|
219
|
+
*
|
|
220
|
+
* @param {Array<{ kind: string, value: string }>} tokens
|
|
221
|
+
* @returns {{ dangerousPipeline: boolean, kind?: "curl-pipe" | "destructive-sql", reason?: string }}
|
|
222
|
+
*/
|
|
223
|
+
export function inspectPipelines(tokens) {
|
|
224
|
+
if (!Array.isArray(tokens)) return { dangerousPipeline: false };
|
|
225
|
+
|
|
226
|
+
let segment = [];
|
|
227
|
+
let pipelineHasDownloader = false;
|
|
228
|
+
let pipelineHasDestructiveSql = false;
|
|
229
|
+
let inPipeline = false;
|
|
230
|
+
|
|
231
|
+
for (const token of tokens) {
|
|
232
|
+
if (token.kind === "op" && SPLIT_OPS.has(token.value)) {
|
|
233
|
+
const cmd = unwrapExecutableFromSegment(segment);
|
|
234
|
+
if (inPipeline && (SHELLS.has(cmd) || INTERPRETERS.has(cmd)) && pipelineHasDownloader) {
|
|
235
|
+
return { dangerousPipeline: true, kind: "curl-pipe", reason: "curl-pipe" };
|
|
236
|
+
}
|
|
237
|
+
if (inPipeline && DB_CLIENTS.has(cmd) && pipelineHasDestructiveSql) {
|
|
238
|
+
return { dangerousPipeline: true, kind: "destructive-sql", reason: "destructive-sql" };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const continues = token.value === "|" || token.value === "|&";
|
|
242
|
+
if (continues) {
|
|
243
|
+
pipelineHasDownloader = pipelineHasDownloader || cmd === "curl" || cmd === "wget";
|
|
244
|
+
const segText = segment.filter((t) => t.kind === "word").map((t) => t.value).join(" ");
|
|
245
|
+
pipelineHasDestructiveSql = pipelineHasDestructiveSql || DESTRUCTIVE_SQL.test(segText);
|
|
246
|
+
inPipeline = true;
|
|
247
|
+
} else {
|
|
248
|
+
pipelineHasDownloader = false;
|
|
249
|
+
pipelineHasDestructiveSql = false;
|
|
250
|
+
inPipeline = false;
|
|
251
|
+
}
|
|
252
|
+
segment = [];
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
segment.push(token);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const finalCmd = unwrapExecutableFromSegment(segment);
|
|
259
|
+
if (inPipeline && (SHELLS.has(finalCmd) || INTERPRETERS.has(finalCmd)) && pipelineHasDownloader) {
|
|
260
|
+
return { dangerousPipeline: true, kind: "curl-pipe", reason: "curl-pipe" };
|
|
261
|
+
}
|
|
262
|
+
if (inPipeline && DB_CLIENTS.has(finalCmd) && pipelineHasDestructiveSql) {
|
|
263
|
+
return { dangerousPipeline: true, kind: "destructive-sql", reason: "destructive-sql" };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { dangerousPipeline: false };
|
|
267
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// lib/retry-exhaustion.js
|
|
2
|
+
// Anti-replay and Retry Exhaustion guard for dsh-approval-gate (#96).
|
|
3
|
+
// Blocks looping/thrashing agents attempting repeated bypasses after consecutive denials.
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MAX_RETRY_ATTEMPTS = 3;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts base command root from execution for grouping repeated bypass attempts.
|
|
9
|
+
*
|
|
10
|
+
* @param {any} execution
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function extractCommandRoot(execution) {
|
|
14
|
+
if (!execution) return "";
|
|
15
|
+
const tool = String(execution.name || execution.tool || "").trim().toLowerCase();
|
|
16
|
+
const args = execution.arguments || {};
|
|
17
|
+
if (tool === "bash" || tool === "sh" || tool === "shell") {
|
|
18
|
+
const cmd = typeof args.command === "string" ? args.command.trim() : "";
|
|
19
|
+
if (!cmd) return "bash";
|
|
20
|
+
const parts = cmd.split(/[\s;&|]+/);
|
|
21
|
+
for (const p of parts) {
|
|
22
|
+
const clean = p.replace(/^['"]+|['"]+$/g, "").trim();
|
|
23
|
+
if (!clean) continue;
|
|
24
|
+
if (clean === "sudo" || clean === "env" || clean === "nohup") continue;
|
|
25
|
+
return clean;
|
|
26
|
+
}
|
|
27
|
+
return "bash";
|
|
28
|
+
}
|
|
29
|
+
const path = args.path || args.file || args.target;
|
|
30
|
+
if (typeof path === "string") return `${tool}:${path}`;
|
|
31
|
+
return tool;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} RetrySessionState
|
|
36
|
+
* @property {string} lastRoot
|
|
37
|
+
* @property {number} rootRetries
|
|
38
|
+
* @property {number} totalDenies
|
|
39
|
+
* @property {boolean} exhausted
|
|
40
|
+
* @property {number} trippedAt
|
|
41
|
+
* @property {string[]} recentRoots
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export class RetryExhaustionGuard {
|
|
45
|
+
/**
|
|
46
|
+
* @param {{ maxAttempts?: number }} [options]
|
|
47
|
+
*/
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.maxAttempts = options.maxAttempts || DEFAULT_MAX_RETRY_ATTEMPTS;
|
|
50
|
+
/** @type {Map<string, RetrySessionState>} */
|
|
51
|
+
this.sessions = new Map();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
_getOrCreate(sessionId) {
|
|
58
|
+
const key = sessionId || "default";
|
|
59
|
+
let state = this.sessions.get(key);
|
|
60
|
+
if (!state) {
|
|
61
|
+
state = {
|
|
62
|
+
lastRoot: "",
|
|
63
|
+
rootRetries: 0,
|
|
64
|
+
totalDenies: 0,
|
|
65
|
+
exhausted: false,
|
|
66
|
+
trippedAt: 0,
|
|
67
|
+
recentRoots: [],
|
|
68
|
+
};
|
|
69
|
+
this.sessions.set(key, state);
|
|
70
|
+
}
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if session has exhausted retries.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} [sessionId]
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
isExhausted(sessionId) {
|
|
81
|
+
const key = sessionId || "default";
|
|
82
|
+
const state = this.sessions.get(key);
|
|
83
|
+
return Boolean(state && state.exhausted);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get consecutive denials count for session.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} [sessionId]
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
getConsecutiveDenials(sessionId) {
|
|
93
|
+
const key = sessionId || "default";
|
|
94
|
+
const state = this.sessions.get(key);
|
|
95
|
+
return state ? state.rootRetries : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Record a verdict for session and evaluate exhaustion threshold.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} sessionId
|
|
102
|
+
* @param {any} execution
|
|
103
|
+
* @param {"allow" | "deny" | "ask"} verdict
|
|
104
|
+
* @returns {boolean} True if this verdict caused exhaustion to trip
|
|
105
|
+
*/
|
|
106
|
+
recordVerdict(sessionId, execution, verdict) {
|
|
107
|
+
const state = this._getOrCreate(sessionId);
|
|
108
|
+
const root = extractCommandRoot(execution);
|
|
109
|
+
|
|
110
|
+
if (verdict === "deny") {
|
|
111
|
+
state.totalDenies += 1;
|
|
112
|
+
if (state.lastRoot === root) {
|
|
113
|
+
state.rootRetries += 1;
|
|
114
|
+
} else {
|
|
115
|
+
state.lastRoot = root;
|
|
116
|
+
state.rootRetries = 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
state.recentRoots.push(root);
|
|
120
|
+
if (state.recentRoots.length > 10) {
|
|
121
|
+
state.recentRoots.shift();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (state.rootRetries >= this.maxAttempts && !state.exhausted) {
|
|
125
|
+
state.exhausted = true;
|
|
126
|
+
state.trippedAt = Date.now();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
} else if (verdict === "allow") {
|
|
130
|
+
state.rootRetries = 0;
|
|
131
|
+
state.lastRoot = "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reset consecutive denies without clearing total denies or exhaustion.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} [sessionId]
|
|
141
|
+
*/
|
|
142
|
+
resetConsecutive(sessionId) {
|
|
143
|
+
const key = sessionId || "default";
|
|
144
|
+
const state = this.sessions.get(key);
|
|
145
|
+
if (state && state.rootRetries > 0) {
|
|
146
|
+
state.rootRetries = 0;
|
|
147
|
+
state.lastRoot = "";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resume / reset a session to active state.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} [sessionId]
|
|
155
|
+
*/
|
|
156
|
+
reset(sessionId) {
|
|
157
|
+
const key = sessionId || "default";
|
|
158
|
+
const state = this.sessions.get(key);
|
|
159
|
+
if (state) {
|
|
160
|
+
state.lastRoot = "";
|
|
161
|
+
state.rootRetries = 0;
|
|
162
|
+
state.exhausted = false;
|
|
163
|
+
state.trippedAt = 0;
|
|
164
|
+
state.recentRoots = [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
clear() {
|
|
169
|
+
this.sessions.clear();
|
|
170
|
+
}
|
|
171
|
+
}
|
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.14",
|
|
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",
|