@odingard/guardian-hook 0.3.0 → 0.4.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/README.md +26 -4
- package/dist/agent-sdk.js +1142 -13
- package/dist/index.js +1142 -13
- package/package.json +3 -2
package/dist/agent-sdk.js
CHANGED
|
@@ -3,6 +3,461 @@ import { createGuardianClient } from "@odingard/guardian-sdk";
|
|
|
3
3
|
|
|
4
4
|
// src/classify.ts
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
import {
|
|
7
|
+
CONTENT_PIN_INVALID_ARGUMENT,
|
|
8
|
+
CONTENT_PIN_KEY,
|
|
9
|
+
pinFile
|
|
10
|
+
} from "@odingard/guardian-sdk";
|
|
11
|
+
|
|
12
|
+
// src/shell.ts
|
|
13
|
+
var MAX_NESTING = 16;
|
|
14
|
+
var isBlank = (c) => c === " " || c === " ";
|
|
15
|
+
var OPERATOR_START = /* @__PURE__ */ new Set([";", "&", "|", "\n", "(", ")", "<", ">"]);
|
|
16
|
+
function parseShell(src) {
|
|
17
|
+
const st = {
|
|
18
|
+
src,
|
|
19
|
+
pos: 0,
|
|
20
|
+
ambiguous: false,
|
|
21
|
+
nesting: 0,
|
|
22
|
+
heredocs: [],
|
|
23
|
+
bodySubs: []
|
|
24
|
+
};
|
|
25
|
+
const parsed = parseList(st, false);
|
|
26
|
+
if (st.pos < src.length) st.ambiguous = true;
|
|
27
|
+
if (st.bodySubs.length > 0) {
|
|
28
|
+
parsed.commands.push(bodyCommand(st.bodySubs));
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
commands: parsed.commands,
|
|
32
|
+
ambiguous: st.ambiguous || parsed.ambiguous
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function bodyCommand(subs) {
|
|
36
|
+
return { words: [], writes: [], pipeline: -1, stage: 0, subs };
|
|
37
|
+
}
|
|
38
|
+
function parseList(st, inSub) {
|
|
39
|
+
const commands = [];
|
|
40
|
+
let pipeline = 0;
|
|
41
|
+
let stage = 0;
|
|
42
|
+
let words = [];
|
|
43
|
+
let writes = [];
|
|
44
|
+
let depth = 0;
|
|
45
|
+
let pendingWrite = false;
|
|
46
|
+
let pendingHeredoc = null;
|
|
47
|
+
let pendingHereString = false;
|
|
48
|
+
const endCommand = (nextIsPipe) => {
|
|
49
|
+
if (words.length > 0 || writes.length > 0) {
|
|
50
|
+
commands.push({ words, writes, pipeline, stage, subs: [] });
|
|
51
|
+
}
|
|
52
|
+
words = [];
|
|
53
|
+
writes = [];
|
|
54
|
+
if (nextIsPipe) stage++;
|
|
55
|
+
else {
|
|
56
|
+
pipeline++;
|
|
57
|
+
stage = 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const { src } = st;
|
|
61
|
+
while (st.pos < src.length) {
|
|
62
|
+
const c = src[st.pos];
|
|
63
|
+
if (isBlank(c)) {
|
|
64
|
+
st.pos++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (c === "\\" && src[st.pos + 1] === "\n") {
|
|
68
|
+
st.pos += 2;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === "#") {
|
|
72
|
+
while (st.pos < src.length && src[st.pos] !== "\n") st.pos++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === "\n") {
|
|
76
|
+
st.pos++;
|
|
77
|
+
endCommand(false);
|
|
78
|
+
readHeredocBodies(st);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (c === ";" || c === "&" || c === "|") {
|
|
82
|
+
const two = src.slice(st.pos, st.pos + 2);
|
|
83
|
+
if (c === "&" && src[st.pos + 1] === ">") {
|
|
84
|
+
st.pos += src[st.pos + 2] === ">" ? 3 : 2;
|
|
85
|
+
pendingWrite = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (two === "&&" || two === "||" || two === ";;" || two === "|&") {
|
|
89
|
+
st.pos += 2;
|
|
90
|
+
endCommand(two === "|&");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
st.pos++;
|
|
94
|
+
endCommand(c === "|");
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === "(") {
|
|
98
|
+
st.pos++;
|
|
99
|
+
depth++;
|
|
100
|
+
endCommand(false);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (c === ")") {
|
|
104
|
+
if (depth > 0) {
|
|
105
|
+
st.pos++;
|
|
106
|
+
depth--;
|
|
107
|
+
endCommand(false);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (inSub) {
|
|
111
|
+
endCommand(false);
|
|
112
|
+
return { commands, ambiguous: false };
|
|
113
|
+
}
|
|
114
|
+
st.pos++;
|
|
115
|
+
st.ambiguous = true;
|
|
116
|
+
endCommand(false);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (c === "<" || c === ">") {
|
|
120
|
+
if (src[st.pos + 1] === "(") {
|
|
121
|
+
const w2 = readWord(st);
|
|
122
|
+
if (w2) addWord(w2);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const op = /^(<<<|<<-|<<|<>|<&|>>|>&|>\||<|>)/.exec(src.slice(st.pos))[0];
|
|
126
|
+
st.pos += op.length;
|
|
127
|
+
if (op === "<<" || op === "<<-") pendingHeredoc = { stripTabs: op === "<<-" };
|
|
128
|
+
else if (op === "<<<") pendingHereString = true;
|
|
129
|
+
else if (op === ">&" || op === "<&") {
|
|
130
|
+
const m = /^[ \t]*([0-9]+|-)(?=[\s;&|)<>]|$)/.exec(src.slice(st.pos));
|
|
131
|
+
if (m) st.pos += m[0].length;
|
|
132
|
+
else if (op === ">&") pendingWrite = true;
|
|
133
|
+
} else if (op !== "<") pendingWrite = true;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const w = readWord(st);
|
|
137
|
+
if (!w) continue;
|
|
138
|
+
addWord(w);
|
|
139
|
+
}
|
|
140
|
+
if (inSub) st.ambiguous = true;
|
|
141
|
+
if (depth > 0) st.ambiguous = true;
|
|
142
|
+
endCommand(false);
|
|
143
|
+
return { commands, ambiguous: false };
|
|
144
|
+
function addWord(w) {
|
|
145
|
+
if (pendingHeredoc) {
|
|
146
|
+
st.heredocs.push({
|
|
147
|
+
delimiter: w.value,
|
|
148
|
+
stripTabs: pendingHeredoc.stripTabs,
|
|
149
|
+
quoted: w.quoted
|
|
150
|
+
});
|
|
151
|
+
pendingHeredoc = null;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (pendingHereString) {
|
|
155
|
+
pendingHereString = false;
|
|
156
|
+
collectSubs(w);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (pendingWrite) {
|
|
160
|
+
pendingWrite = false;
|
|
161
|
+
writes.push(w);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const next = src[st.pos];
|
|
165
|
+
if (/^[0-9]+$/.test(w.value) && !w.quoted && (next === ">" || next === "<")) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
words.push(w);
|
|
169
|
+
}
|
|
170
|
+
function collectSubs(w) {
|
|
171
|
+
if (w.subs.length > 0) {
|
|
172
|
+
commands.push({ words: [], writes: [], pipeline: -1, stage: 0, subs: w.subs });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function readHeredocBodies(st) {
|
|
177
|
+
while (st.heredocs.length > 0) {
|
|
178
|
+
const doc = st.heredocs.shift();
|
|
179
|
+
const lines = [];
|
|
180
|
+
let found = false;
|
|
181
|
+
while (st.pos < st.src.length) {
|
|
182
|
+
let end = st.src.indexOf("\n", st.pos);
|
|
183
|
+
if (end < 0) end = st.src.length;
|
|
184
|
+
const raw = st.src.slice(st.pos, end);
|
|
185
|
+
st.pos = Math.min(end + 1, st.src.length);
|
|
186
|
+
const line = doc.stripTabs ? raw.replace(/^\t+/, "") : raw;
|
|
187
|
+
if (line === doc.delimiter) {
|
|
188
|
+
found = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
lines.push(raw);
|
|
192
|
+
}
|
|
193
|
+
void found;
|
|
194
|
+
if (!doc.quoted) {
|
|
195
|
+
const body = lines.join("\n");
|
|
196
|
+
if (/\$\(|`/.test(body)) {
|
|
197
|
+
const inner = { ...st, src: body, pos: 0, heredocs: [], bodySubs: [] };
|
|
198
|
+
const w = readDoubleQuotedBody(inner);
|
|
199
|
+
st.ambiguous ||= inner.ambiguous;
|
|
200
|
+
if (w.subs.length > 0) st.bodySubs.push(...w.subs);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function readDoubleQuotedBody(st) {
|
|
206
|
+
const word = { value: "", dynamic: false, subs: [] };
|
|
207
|
+
while (st.pos < st.src.length) {
|
|
208
|
+
const c = st.src[st.pos];
|
|
209
|
+
if (c === "\\") {
|
|
210
|
+
st.pos += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (c === "$" || c === "`") {
|
|
214
|
+
readExpansion(st, word);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
st.pos++;
|
|
218
|
+
}
|
|
219
|
+
return word;
|
|
220
|
+
}
|
|
221
|
+
function readWord(st) {
|
|
222
|
+
const { src } = st;
|
|
223
|
+
const word = {
|
|
224
|
+
value: "",
|
|
225
|
+
dynamic: false,
|
|
226
|
+
subs: [],
|
|
227
|
+
quoted: false
|
|
228
|
+
};
|
|
229
|
+
const start = st.pos;
|
|
230
|
+
while (st.pos < src.length) {
|
|
231
|
+
const c = src[st.pos];
|
|
232
|
+
if (isBlank(c) || c === "\n") break;
|
|
233
|
+
if ((c === "<" || c === ">") && src[st.pos + 1] === "(") {
|
|
234
|
+
st.pos += 2;
|
|
235
|
+
word.dynamic = true;
|
|
236
|
+
word.subs.push(parseNested(st));
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (OPERATOR_START.has(c)) break;
|
|
240
|
+
if (c === "\\") {
|
|
241
|
+
if (st.pos + 1 < src.length) {
|
|
242
|
+
if (src[st.pos + 1] !== "\n") word.value += src[st.pos + 1];
|
|
243
|
+
st.pos += 2;
|
|
244
|
+
} else {
|
|
245
|
+
st.pos++;
|
|
246
|
+
}
|
|
247
|
+
word.quoted = true;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (c === "'") {
|
|
251
|
+
const end = src.indexOf("'", st.pos + 1);
|
|
252
|
+
if (end < 0) {
|
|
253
|
+
st.ambiguous = true;
|
|
254
|
+
word.value += src.slice(st.pos + 1);
|
|
255
|
+
st.pos = src.length;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
word.value += src.slice(st.pos + 1, end);
|
|
259
|
+
st.pos = end + 1;
|
|
260
|
+
word.quoted = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (c === '"') {
|
|
264
|
+
st.pos++;
|
|
265
|
+
word.quoted = true;
|
|
266
|
+
let closed = false;
|
|
267
|
+
while (st.pos < src.length) {
|
|
268
|
+
const d = src[st.pos];
|
|
269
|
+
if (d === '"') {
|
|
270
|
+
st.pos++;
|
|
271
|
+
closed = true;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
if (d === "\\") {
|
|
275
|
+
const n = src[st.pos + 1];
|
|
276
|
+
if (n === void 0) {
|
|
277
|
+
st.pos++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if ('$`"\\\n'.includes(n)) {
|
|
281
|
+
if (n !== "\n") word.value += n;
|
|
282
|
+
} else {
|
|
283
|
+
word.value += d + n;
|
|
284
|
+
}
|
|
285
|
+
st.pos += 2;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (d === "$" || d === "`") {
|
|
289
|
+
readExpansion(st, word);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
word.value += d;
|
|
293
|
+
st.pos++;
|
|
294
|
+
}
|
|
295
|
+
if (!closed) st.ambiguous = true;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (c === "$" && src[st.pos + 1] === "'") {
|
|
299
|
+
let i = st.pos + 2;
|
|
300
|
+
let out = "";
|
|
301
|
+
let closed = false;
|
|
302
|
+
while (i < src.length) {
|
|
303
|
+
const d = src[i];
|
|
304
|
+
if (d === "'") {
|
|
305
|
+
closed = true;
|
|
306
|
+
i++;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (d === "\\" && i + 1 < src.length) {
|
|
310
|
+
const n = src[i + 1];
|
|
311
|
+
const simple = { n: "\n", t: " ", "\\": "\\", "'": "'", '"': '"' };
|
|
312
|
+
if (simple[n] !== void 0) {
|
|
313
|
+
out += simple[n];
|
|
314
|
+
i += 2;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const hex = /^x([0-9a-fA-F]{1,2})/.exec(src.slice(i + 1));
|
|
318
|
+
if (hex) {
|
|
319
|
+
out += String.fromCharCode(parseInt(hex[1], 16));
|
|
320
|
+
i += 1 + hex[0].length;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const oct = /^([0-7]{1,3})/.exec(src.slice(i + 1));
|
|
324
|
+
if (oct) {
|
|
325
|
+
out += String.fromCharCode(parseInt(oct[1], 8));
|
|
326
|
+
i += 1 + oct[0].length;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
st.ambiguous = true;
|
|
330
|
+
out += n;
|
|
331
|
+
i += 2;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
out += d;
|
|
335
|
+
i++;
|
|
336
|
+
}
|
|
337
|
+
if (!closed) st.ambiguous = true;
|
|
338
|
+
word.value += out;
|
|
339
|
+
word.quoted = true;
|
|
340
|
+
st.pos = i;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (c === "$" || c === "`") {
|
|
344
|
+
readExpansion(st, word);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
word.value += c;
|
|
348
|
+
st.pos++;
|
|
349
|
+
}
|
|
350
|
+
return st.pos > start ? word : null;
|
|
351
|
+
}
|
|
352
|
+
function readExpansion(st, word) {
|
|
353
|
+
const { src } = st;
|
|
354
|
+
const c = src[st.pos];
|
|
355
|
+
if (c === "`") {
|
|
356
|
+
let i = st.pos + 1;
|
|
357
|
+
let inner = "";
|
|
358
|
+
while (i < src.length && src[i] !== "`") {
|
|
359
|
+
if (src[i] === "\\" && i + 1 < src.length) {
|
|
360
|
+
inner += src[i + 1];
|
|
361
|
+
i += 2;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
inner += src[i];
|
|
365
|
+
i++;
|
|
366
|
+
}
|
|
367
|
+
if (i >= src.length) st.ambiguous = true;
|
|
368
|
+
word.value += "`" + inner + "`";
|
|
369
|
+
word.dynamic = true;
|
|
370
|
+
word.subs.push(parseString(st, inner));
|
|
371
|
+
st.pos = Math.min(i + 1, src.length);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const n = src[st.pos + 1];
|
|
375
|
+
if (n === "(" && src[st.pos + 2] === "(") {
|
|
376
|
+
let i = st.pos + 3;
|
|
377
|
+
let level = 2;
|
|
378
|
+
while (i < src.length && level > 0) {
|
|
379
|
+
if (src[i] === "(") level++;
|
|
380
|
+
else if (src[i] === ")") level--;
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
383
|
+
if (level > 0) st.ambiguous = true;
|
|
384
|
+
word.value += src.slice(st.pos, i);
|
|
385
|
+
word.dynamic = true;
|
|
386
|
+
st.pos = i;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (n === "(") {
|
|
390
|
+
const start = st.pos;
|
|
391
|
+
st.pos += 2;
|
|
392
|
+
word.subs.push(parseNested(st));
|
|
393
|
+
word.value += src.slice(start, st.pos);
|
|
394
|
+
word.dynamic = true;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (n === "{") {
|
|
398
|
+
let i = st.pos + 2;
|
|
399
|
+
let level = 1;
|
|
400
|
+
while (i < src.length && level > 0) {
|
|
401
|
+
if (src[i] === "{") level++;
|
|
402
|
+
else if (src[i] === "}") level--;
|
|
403
|
+
i++;
|
|
404
|
+
}
|
|
405
|
+
if (level > 0) st.ambiguous = true;
|
|
406
|
+
const text = src.slice(st.pos, i);
|
|
407
|
+
word.value += text;
|
|
408
|
+
word.dynamic = true;
|
|
409
|
+
if (/\$\(|`/.test(text)) {
|
|
410
|
+
word.subs.push(parseString(st, text.slice(2, -1)));
|
|
411
|
+
}
|
|
412
|
+
st.pos = i;
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const name = /^\$([A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-])/.exec(src.slice(st.pos));
|
|
416
|
+
if (name) {
|
|
417
|
+
word.value += name[0];
|
|
418
|
+
word.dynamic = true;
|
|
419
|
+
st.pos += name[0].length;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
word.value += "$";
|
|
423
|
+
st.pos++;
|
|
424
|
+
}
|
|
425
|
+
function parseNested(st) {
|
|
426
|
+
if (st.nesting >= MAX_NESTING) {
|
|
427
|
+
st.ambiguous = true;
|
|
428
|
+
st.pos = st.src.length;
|
|
429
|
+
return { commands: [], ambiguous: true };
|
|
430
|
+
}
|
|
431
|
+
st.nesting++;
|
|
432
|
+
const savedHeredocs = st.heredocs;
|
|
433
|
+
st.heredocs = [];
|
|
434
|
+
const inner = parseList(st, true);
|
|
435
|
+
st.heredocs = savedHeredocs;
|
|
436
|
+
st.nesting--;
|
|
437
|
+
if (st.src[st.pos] === ")") st.pos++;
|
|
438
|
+
else st.ambiguous = true;
|
|
439
|
+
return inner;
|
|
440
|
+
}
|
|
441
|
+
function parseString(st, text) {
|
|
442
|
+
if (st.nesting >= MAX_NESTING) {
|
|
443
|
+
st.ambiguous = true;
|
|
444
|
+
return { commands: [], ambiguous: true };
|
|
445
|
+
}
|
|
446
|
+
const inner = {
|
|
447
|
+
src: text,
|
|
448
|
+
pos: 0,
|
|
449
|
+
ambiguous: false,
|
|
450
|
+
nesting: st.nesting + 1,
|
|
451
|
+
heredocs: [],
|
|
452
|
+
bodySubs: []
|
|
453
|
+
};
|
|
454
|
+
const parsed = parseList(inner, false);
|
|
455
|
+
if (inner.bodySubs.length) parsed.commands.push(bodyCommand(inner.bodySubs));
|
|
456
|
+
st.ambiguous ||= inner.ambiguous;
|
|
457
|
+
return { commands: parsed.commands, ambiguous: inner.ambiguous };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/classify.ts
|
|
6
461
|
var HOOK_TOOLS = [
|
|
7
462
|
{
|
|
8
463
|
name: "shell",
|
|
@@ -17,7 +472,7 @@ var HOOK_TOOLS = [
|
|
|
17
472
|
{
|
|
18
473
|
name: "shell_destructive",
|
|
19
474
|
suggestedMode: "ask",
|
|
20
|
-
description: "Destructive commands: rm -rf, git reset --hard, git clean, force pushes, dropping databases, disk formatting."
|
|
475
|
+
description: "Destructive commands: rm -rf (also through xargs or find -exec), git reset --hard, git clean, force pushes and remote branch deletes, dropping databases, disk formatting, piping a download into a shell. Also changes to the agent's own MCP servers, hooks or permissions (claude mcp add, writes to .mcp.json or .claude/settings.json), and commands the hook cannot parse."
|
|
21
476
|
},
|
|
22
477
|
{
|
|
23
478
|
name: "git_push",
|
|
@@ -68,6 +523,9 @@ var DESTRUCTIVE = [
|
|
|
68
523
|
/\b(drop|truncate)\s+(table|database|schema)\b/i,
|
|
69
524
|
/\bdelete\s+from\s+\w+\s*(;|$)/i,
|
|
70
525
|
/\b(mkfs|dd\s+if=|shred|wipefs)\b/,
|
|
526
|
+
/\bdd\b.*\bof=/,
|
|
527
|
+
/\brsync\b.*\s--(delete\S*|remove-source-files)\b/,
|
|
528
|
+
/\btruncate\b.*(\s-s\s*0\b|\s--size[= ]0\b)/,
|
|
71
529
|
/\bchmod\s+-R\s+777\b/,
|
|
72
530
|
/\bfind\b.*\s-delete\b/
|
|
73
531
|
];
|
|
@@ -163,12 +621,599 @@ function startsInteractiveSession(command) {
|
|
|
163
621
|
if (/\bwhile\s+(?:IFS=\S*\s+)?read\b/.test(command)) return true;
|
|
164
622
|
return command.split(/&&|\|\||[;&|\n()]|\$\(|`/).some((segment) => startsSession(segment));
|
|
165
623
|
}
|
|
624
|
+
var RANK = {
|
|
625
|
+
shell: 0,
|
|
626
|
+
shell_session: 1,
|
|
627
|
+
git_push: 2,
|
|
628
|
+
shell_deploy: 3,
|
|
629
|
+
shell_destructive: 4
|
|
630
|
+
};
|
|
631
|
+
var BY_RANK = [
|
|
632
|
+
"shell",
|
|
633
|
+
"shell_session",
|
|
634
|
+
"git_push",
|
|
635
|
+
"shell_deploy",
|
|
636
|
+
"shell_destructive"
|
|
637
|
+
];
|
|
638
|
+
var ASK = RANK.shell_destructive;
|
|
639
|
+
var MAX_DEPTH = 12;
|
|
640
|
+
var EDGE = String.raw`(?:^|[\s/\\'"=:,(~])`;
|
|
641
|
+
var AGENT_CONFIG_PATH = new RegExp(
|
|
642
|
+
[
|
|
643
|
+
EDGE + String.raw`(?:\.?mcp\.json|\.claude\.json|settings\.local\.json|managed-settings\.json|managed-mcp\.json|claude_desktop_config\.json|guardian-hooks\.json|cordis\.patch\.ya?ml)(?![\w-])`,
|
|
644
|
+
EDGE + String.raw`\.claude(?:[/\\](?:settings[^/\\\s'"]*\.json|hooks|agents|commands|skills|plugins|output-styles)(?![\w-])|[/\\]?(?=$|[\s'"]))`,
|
|
645
|
+
EDGE + String.raw`\.(?:codex|cursor|gemini|dsh)(?=$|[/\\\s'"])`
|
|
646
|
+
].join("|"),
|
|
647
|
+
"i"
|
|
648
|
+
);
|
|
649
|
+
var READERS = new Set(
|
|
650
|
+
"cat tac less more head tail grep egrep fgrep zgrep rg ag ack ls dir stat file wc diff cmp comm jq bat batcat md5sum sha1sum sha256sum sha512sum shasum b2sum cksum echo printf test [ [[ find fd realpath readlink dirname basename du tree type which".split(" ")
|
|
651
|
+
);
|
|
652
|
+
var GIT_READS = new Set(
|
|
653
|
+
"status log show diff blame ls-files grep add commit check-ignore cat-file rev-parse ls-tree shortlog describe".split(" ")
|
|
654
|
+
);
|
|
655
|
+
var RESERVED = new Set(
|
|
656
|
+
"if then else elif fi do done while until for in case esac ! { } select function".split(
|
|
657
|
+
" "
|
|
658
|
+
)
|
|
659
|
+
);
|
|
660
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/;
|
|
661
|
+
var SHELLS = new Set(
|
|
662
|
+
"sh bash zsh dash ksh mksh ash yash posh fish busybox".split(" ")
|
|
663
|
+
);
|
|
664
|
+
var FETCHERS = new Set(
|
|
665
|
+
"curl wget fetch http https xh aria2c iwr irm invoke-webrequest invoke-restmethod".split(
|
|
666
|
+
" "
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
var BULK_DELETE = /* @__PURE__ */ new Set(["rm", "unlink", "shred"]);
|
|
670
|
+
var EVALUATORS = /* @__PURE__ */ new Set(["eval", "source", ".", "iex", "invoke-expression"]);
|
|
671
|
+
var WRAPPER_SPECS = {
|
|
672
|
+
nohup: {},
|
|
673
|
+
command: {},
|
|
674
|
+
builtin: {},
|
|
675
|
+
chronic: {},
|
|
676
|
+
unbuffer: {},
|
|
677
|
+
busybox: {},
|
|
678
|
+
time: { value: ["-f", "-o", "--format", "--output"] },
|
|
679
|
+
nice: { value: ["-n", "--adjustment"] },
|
|
680
|
+
ionice: { value: ["-c", "-n", "-p", "-P", "-u", "--class", "--classdata"] },
|
|
681
|
+
stdbuf: { value: ["-i", "-o", "-e", "--input", "--output", "--error"] },
|
|
682
|
+
timeout: { value: ["-s", "-k", "--signal", "--kill-after"], positional: 1 },
|
|
683
|
+
exec: { value: ["-a"] },
|
|
684
|
+
sudo: {
|
|
685
|
+
value: [
|
|
686
|
+
...SUDO_VALUE_OPTS,
|
|
687
|
+
"--user",
|
|
688
|
+
"--group",
|
|
689
|
+
"--host",
|
|
690
|
+
"--prompt",
|
|
691
|
+
"--chdir",
|
|
692
|
+
"--role",
|
|
693
|
+
"--type",
|
|
694
|
+
"--close-from",
|
|
695
|
+
"--other-user",
|
|
696
|
+
"--command-timeout"
|
|
697
|
+
]
|
|
698
|
+
},
|
|
699
|
+
doas: { value: ["-u", "-C"] },
|
|
700
|
+
xargs: {
|
|
701
|
+
value: [
|
|
702
|
+
"-a",
|
|
703
|
+
"--arg-file",
|
|
704
|
+
"-d",
|
|
705
|
+
"--delimiter",
|
|
706
|
+
"-E",
|
|
707
|
+
"-I",
|
|
708
|
+
"-L",
|
|
709
|
+
"-n",
|
|
710
|
+
"--max-args",
|
|
711
|
+
"-P",
|
|
712
|
+
"--max-procs",
|
|
713
|
+
"-s",
|
|
714
|
+
"--max-chars",
|
|
715
|
+
"--process-slot-var",
|
|
716
|
+
"--max-lines"
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
chroot: { value: ["--userspec", "--groups"], positional: 1 },
|
|
720
|
+
taskset: { positional: 1 },
|
|
721
|
+
flock: {
|
|
722
|
+
value: ["-w", "-E", "--timeout", "--conflict-exit-code"],
|
|
723
|
+
positional: 1
|
|
724
|
+
},
|
|
725
|
+
setsid: {},
|
|
726
|
+
caffeinate: { value: ["-t", "-w"] }
|
|
727
|
+
};
|
|
728
|
+
var leaf = (rank, base = "", args = []) => ({
|
|
729
|
+
rank,
|
|
730
|
+
base,
|
|
731
|
+
args
|
|
732
|
+
});
|
|
733
|
+
function baseName(value) {
|
|
734
|
+
return (value.split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.exe$/, "");
|
|
735
|
+
}
|
|
736
|
+
var literal = (value) => ({ value, dynamic: false, subs: [] });
|
|
737
|
+
function skipOptions(args, spec) {
|
|
738
|
+
const value = new Set(spec.value ?? []);
|
|
739
|
+
let i = 0;
|
|
740
|
+
let unknown = false;
|
|
741
|
+
while (i < args.length) {
|
|
742
|
+
const a = args[i].value;
|
|
743
|
+
if (a === "--") {
|
|
744
|
+
i++;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
if (a.startsWith("--")) {
|
|
748
|
+
if (a.includes("=")) i++;
|
|
749
|
+
else if (value.has(a)) i += 2;
|
|
750
|
+
else {
|
|
751
|
+
unknown = true;
|
|
752
|
+
i++;
|
|
753
|
+
}
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (a.startsWith("-") && a.length > 1) {
|
|
757
|
+
let takes = false;
|
|
758
|
+
for (let k = 1; k < a.length; k++) {
|
|
759
|
+
if (value.has(`-${a[k]}`)) {
|
|
760
|
+
takes = k === a.length - 1;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
i += takes ? 2 : 1;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
let positional = spec.positional ?? 0;
|
|
770
|
+
while (positional-- > 0 && i < args.length) i++;
|
|
771
|
+
return { start: i, unknown };
|
|
772
|
+
}
|
|
773
|
+
function rankText(text, depth) {
|
|
774
|
+
if (depth > MAX_DEPTH) return ASK;
|
|
775
|
+
return rankParsed(parseShell(text), depth + 1);
|
|
776
|
+
}
|
|
777
|
+
function rankParsed(parsed, depth) {
|
|
778
|
+
if (depth > MAX_DEPTH) return ASK;
|
|
779
|
+
let rank = parsed.ambiguous ? ASK : 0;
|
|
780
|
+
const stages = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const cmd of parsed.commands) {
|
|
782
|
+
const l = rankArgv(cmd.words, depth);
|
|
783
|
+
rank = Math.max(rank, l.rank);
|
|
784
|
+
if (cmd.writes.some((w) => AGENT_CONFIG_PATH.test(w.value))) rank = ASK;
|
|
785
|
+
const subs = [
|
|
786
|
+
...cmd.subs,
|
|
787
|
+
...cmd.words.flatMap((w) => w.subs),
|
|
788
|
+
...cmd.writes.flatMap((w) => w.subs)
|
|
789
|
+
];
|
|
790
|
+
for (const sub of subs) rank = Math.max(rank, rankParsed(sub, depth + 1));
|
|
791
|
+
if (runsCode(l) && cmd.words.some((w) => w.subs.some((s) => fetches(s, depth + 1)))) {
|
|
792
|
+
rank = ASK;
|
|
793
|
+
}
|
|
794
|
+
if (cmd.pipeline >= 0) {
|
|
795
|
+
const list = stages.get(cmd.pipeline) ?? [];
|
|
796
|
+
list.push(l);
|
|
797
|
+
stages.set(cmd.pipeline, list);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const list of stages.values()) {
|
|
801
|
+
const first = list.findIndex((l) => FETCHERS.has(l.base));
|
|
802
|
+
if (first >= 0 && list.slice(first + 1).some((l) => readsCodeFromStdin(l))) {
|
|
803
|
+
rank = ASK;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return rank;
|
|
807
|
+
}
|
|
808
|
+
function fetches(parsed, depth) {
|
|
809
|
+
return parsed.commands.some(
|
|
810
|
+
(c) => FETCHERS.has(rankArgv(c.words, depth).base) || [...c.subs, ...c.words.flatMap((w) => w.subs)].some(
|
|
811
|
+
(s) => fetches(s, depth + 1)
|
|
812
|
+
)
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
var isInterpreter = (base) => INTERPRETERS.has(base) || /^python[0-9.]*$/.test(base);
|
|
816
|
+
function runsCode(l) {
|
|
817
|
+
return EVALUATORS.has(l.base) || isInterpreter(l.base);
|
|
818
|
+
}
|
|
819
|
+
function readsCodeFromStdin(l) {
|
|
820
|
+
if (EVALUATORS.has(l.base)) return true;
|
|
821
|
+
if (!isInterpreter(l.base)) return false;
|
|
822
|
+
const options = [];
|
|
823
|
+
let script = false;
|
|
824
|
+
for (const a of l.args) {
|
|
825
|
+
if (a === "--") break;
|
|
826
|
+
if (a === "-") return true;
|
|
827
|
+
if (a.startsWith("-") || a.startsWith("+")) options.push(a);
|
|
828
|
+
else {
|
|
829
|
+
script = true;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (options.some((a) => /^-[a-zA-Z]*s[a-zA-Z]*$/.test(a))) return true;
|
|
834
|
+
if (options.some((a) => /^-[a-zA-Z]*[cem][a-zA-Z]*$/.test(a))) return false;
|
|
835
|
+
return !script;
|
|
836
|
+
}
|
|
837
|
+
function rankArgv(words, depth) {
|
|
838
|
+
if (depth > MAX_DEPTH) return leaf(ASK);
|
|
839
|
+
let i = 0;
|
|
840
|
+
while (i < words.length && (ASSIGNMENT.test(words[i].value) || RESERVED.has(words[i].value))) {
|
|
841
|
+
i++;
|
|
842
|
+
}
|
|
843
|
+
if (i >= words.length) return leaf(0);
|
|
844
|
+
const head = words[i];
|
|
845
|
+
const argWords = words.slice(i + 1);
|
|
846
|
+
if (head.dynamic) return leaf(ASK);
|
|
847
|
+
const base = baseName(head.value);
|
|
848
|
+
const args = argWords.map((w) => w.value);
|
|
849
|
+
const joined = [base, ...args].join(" ");
|
|
850
|
+
let rank = 0;
|
|
851
|
+
if (DESTRUCTIVE.some((re) => re.test(joined))) rank = ASK;
|
|
852
|
+
else if (DEPLOY.some((re) => re.test(joined))) rank = RANK.shell_deploy;
|
|
853
|
+
const done = (r, b = base, a = args) => leaf(Math.max(rank, r), b, a);
|
|
854
|
+
const inner = (from) => rankArgv(from, depth + 1);
|
|
855
|
+
const pathCheck = (values) => values.some((v) => AGENT_CONFIG_PATH.test(v)) ? ASK : 0;
|
|
856
|
+
if (base === "env") return done(rankEnv(argWords, depth));
|
|
857
|
+
if (base === "sudoedit") return done(pathCheck(args));
|
|
858
|
+
if (Object.hasOwn(WRAPPER_SPECS, base)) {
|
|
859
|
+
if (base === "sudo" && args.some((a) => /^-[a-zA-Z]*e[a-zA-Z]*$/.test(a))) {
|
|
860
|
+
return done(pathCheck(args));
|
|
861
|
+
}
|
|
862
|
+
if (base === "command" && args.some((a) => /^-[a-zA-Z]*[vV]/.test(a))) {
|
|
863
|
+
return done(0);
|
|
864
|
+
}
|
|
865
|
+
if (base === "flock") {
|
|
866
|
+
const c = args.findIndex((a) => a === "-c" || a === "--command");
|
|
867
|
+
if (c >= 0) return done(rankText(args[c + 1] ?? "", depth));
|
|
868
|
+
}
|
|
869
|
+
const { start, unknown } = skipOptions(argWords, WRAPPER_SPECS[base]);
|
|
870
|
+
let rest = argWords.slice(start);
|
|
871
|
+
if (base === "xargs" && rest.length === 0) rest = [literal("echo")];
|
|
872
|
+
const l = inner(rest);
|
|
873
|
+
if (base === "xargs" && BULK_DELETE.has(l.base)) l.rank = ASK;
|
|
874
|
+
const alt = unknown ? inner(argWords.slice(start + 1)).rank : 0;
|
|
875
|
+
return leaf(Math.max(rank, l.rank, alt), l.base, l.args);
|
|
876
|
+
}
|
|
877
|
+
if (base === "watch") {
|
|
878
|
+
const { start } = skipOptions(argWords, {
|
|
879
|
+
value: ["-n", "--interval", "-d", "-q", "--equexit"]
|
|
880
|
+
});
|
|
881
|
+
return done(rankText(args.slice(start).join(" "), depth));
|
|
882
|
+
}
|
|
883
|
+
if (SHELLS.has(base)) {
|
|
884
|
+
return done(rankShell(base, argWords, depth));
|
|
885
|
+
}
|
|
886
|
+
if (base === "su") {
|
|
887
|
+
const c = args.findIndex((a) => /^(-c|--command)$/.test(a));
|
|
888
|
+
const eq = args.find((a) => a.startsWith("--command="));
|
|
889
|
+
if (c >= 0) return done(rankText(args[c + 1] ?? "", depth));
|
|
890
|
+
if (eq) return done(rankText(eq.slice("--command=".length), depth));
|
|
891
|
+
return done(0);
|
|
892
|
+
}
|
|
893
|
+
if (base === "eval" || base === "iex" || base === "invoke-expression") {
|
|
894
|
+
return done(rankText(args.join(" "), depth));
|
|
895
|
+
}
|
|
896
|
+
if (base === "find") return done(rankFind(argWords, depth));
|
|
897
|
+
if (["npx", "bunx", "pnpx", "npm", "pnpm", "yarn"].includes(base)) {
|
|
898
|
+
const l = rankRunner(base, argWords, depth);
|
|
899
|
+
if (l) return leaf(Math.max(rank, l.rank), l.base, l.args);
|
|
900
|
+
return done(0);
|
|
901
|
+
}
|
|
902
|
+
if (base === "rm") {
|
|
903
|
+
const recursive = argsBeforeDashDash(args).some(
|
|
904
|
+
(a) => a === "--recursive" || /^-[a-zA-Z]*[rR]/.test(a)
|
|
905
|
+
);
|
|
906
|
+
return done(Math.max(recursive ? ASK : 0, pathCheck(args)));
|
|
907
|
+
}
|
|
908
|
+
if (base === "chmod") return done(Math.max(rankChmod(args), pathCheck(args)));
|
|
909
|
+
if (base === "git") return done(rankGit(argWords, depth));
|
|
910
|
+
if (base === "claude" || base === "codex" || base === "gemini") {
|
|
911
|
+
return done(rankAgentCli(base, args));
|
|
912
|
+
}
|
|
913
|
+
if (READERS.has(base)) return done(0);
|
|
914
|
+
return done(pathCheck(args));
|
|
915
|
+
}
|
|
916
|
+
function argsBeforeDashDash(args) {
|
|
917
|
+
const end = args.indexOf("--");
|
|
918
|
+
return end < 0 ? args : args.slice(0, end);
|
|
919
|
+
}
|
|
920
|
+
function rankEnv(argWords, depth) {
|
|
921
|
+
let i = 0;
|
|
922
|
+
while (i < argWords.length) {
|
|
923
|
+
const a = argWords[i].value;
|
|
924
|
+
if (a === "--") {
|
|
925
|
+
i++;
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
if (a === "-S" || a === "--split-string") {
|
|
929
|
+
const text = [a, ...argWords.slice(i + 1).map((w) => w.value)].slice(1).join(" ");
|
|
930
|
+
return rankText(text, depth);
|
|
931
|
+
}
|
|
932
|
+
if (a.startsWith("--split-string=") || /^-S./.test(a)) {
|
|
933
|
+
const first = a.startsWith("--") ? a.slice(15) : a.slice(2);
|
|
934
|
+
return rankText(
|
|
935
|
+
[first, ...argWords.slice(i + 1).map((w) => w.value)].join(" "),
|
|
936
|
+
depth
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
if (/^(-u|-C|-P|--unset|--chdir)$/.test(a)) {
|
|
940
|
+
i += 2;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (a.startsWith("-") && a.length > 1) {
|
|
944
|
+
i++;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
return rankArgv(argWords.slice(i), depth + 1).rank;
|
|
950
|
+
}
|
|
951
|
+
function rankShell(base, argWords, depth) {
|
|
952
|
+
let code = false;
|
|
953
|
+
for (let i = 0; i < argWords.length; i++) {
|
|
954
|
+
const a = argWords[i].value;
|
|
955
|
+
if (a === "--") break;
|
|
956
|
+
if (/^(--rcfile|--init-file|-o|\+o|-O|\+O)$/.test(a)) {
|
|
957
|
+
i++;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (a === "--command" || base === "fish" && a === "-c") {
|
|
961
|
+
return rankText(argWords[i + 1]?.value ?? "", depth);
|
|
962
|
+
}
|
|
963
|
+
if (/^[-+][a-zA-Z]+$/.test(a)) {
|
|
964
|
+
if (/^-[a-zA-Z]*c/.test(a)) code = true;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (a.startsWith("-")) continue;
|
|
968
|
+
if (code) {
|
|
969
|
+
if (base === "busybox")
|
|
970
|
+
return rankArgv(argWords.slice(i), depth + 1).rank;
|
|
971
|
+
return rankText(a, depth);
|
|
972
|
+
}
|
|
973
|
+
if (base === "busybox") return rankArgv(argWords.slice(i), depth + 1).rank;
|
|
974
|
+
return 0;
|
|
975
|
+
}
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
function rankFind(argWords, depth) {
|
|
979
|
+
let rank = 0;
|
|
980
|
+
for (let i = 0; i < argWords.length; i++) {
|
|
981
|
+
const a = argWords[i].value;
|
|
982
|
+
if (a === "-delete") rank = ASK;
|
|
983
|
+
if (/^-(exec|execdir|ok|okdir)$/.test(a)) {
|
|
984
|
+
let end = i + 1;
|
|
985
|
+
while (end < argWords.length && argWords[end].value !== ";" && argWords[end].value !== "+") {
|
|
986
|
+
end++;
|
|
987
|
+
}
|
|
988
|
+
const l = rankArgv(argWords.slice(i + 1, end), depth + 1);
|
|
989
|
+
rank = Math.max(rank, BULK_DELETE.has(l.base) ? ASK : l.rank);
|
|
990
|
+
i = end;
|
|
991
|
+
}
|
|
992
|
+
if (/^-f(print|print0|printf|ls)$/.test(a)) {
|
|
993
|
+
if (AGENT_CONFIG_PATH.test(argWords[i + 1]?.value ?? "")) rank = ASK;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return rank;
|
|
997
|
+
}
|
|
998
|
+
function rankRunner(base, argWords, depth) {
|
|
999
|
+
let rest = argWords;
|
|
1000
|
+
if (base === "npm" || base === "pnpm" || base === "yarn") {
|
|
1001
|
+
const sub = rest[0]?.value;
|
|
1002
|
+
if (!["exec", "x", "dlx"].includes(sub ?? "")) return null;
|
|
1003
|
+
rest = rest.slice(1);
|
|
1004
|
+
}
|
|
1005
|
+
let i = 0;
|
|
1006
|
+
while (i < rest.length) {
|
|
1007
|
+
const a = rest[i].value;
|
|
1008
|
+
if (a === "--") {
|
|
1009
|
+
i++;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
if (a === "-c" || a === "--call")
|
|
1013
|
+
return leaf(rankText(rest[i + 1]?.value ?? "", depth));
|
|
1014
|
+
if (a.startsWith("--call=")) return leaf(rankText(a.slice(7), depth));
|
|
1015
|
+
if (a === "-p" || a === "--package") {
|
|
1016
|
+
i += 2;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (a.startsWith("-")) {
|
|
1020
|
+
i++;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
const pkg = rest[i];
|
|
1026
|
+
if (!pkg) return null;
|
|
1027
|
+
if (pkg.dynamic) return leaf(ASK);
|
|
1028
|
+
const name = /^(?:@[^/]+\/)?([^@]+)(?:@.*)?$/.exec(pkg.value)?.[1] ?? pkg.value;
|
|
1029
|
+
const bin = name === "claude-code" ? "claude" : name;
|
|
1030
|
+
return rankArgv([literal(bin), ...rest.slice(i + 1)], depth + 1);
|
|
1031
|
+
}
|
|
1032
|
+
function rankChmod(args) {
|
|
1033
|
+
const opts = argsBeforeDashDash(args);
|
|
1034
|
+
const recursive = opts.some(
|
|
1035
|
+
(a) => a === "--recursive" || /^-[a-zA-Z]*R/.test(a)
|
|
1036
|
+
);
|
|
1037
|
+
if (!recursive) return 0;
|
|
1038
|
+
const modes = args.filter(
|
|
1039
|
+
(a) => !a.startsWith("-") || /^-[rwxXst]+$/.test(a)
|
|
1040
|
+
);
|
|
1041
|
+
const worldWritable = modes.some(
|
|
1042
|
+
(m) => /^0*[0-7]?[0-7]?[0-7]?[2367]$/.test(m) && m.length >= 3 || /(^|,)[ugo]*[ao][ugo]*[+=][rwxXst]*w/.test(m)
|
|
1043
|
+
);
|
|
1044
|
+
return worldWritable ? ASK : 0;
|
|
1045
|
+
}
|
|
1046
|
+
var GIT_VALUE_OPTS = /* @__PURE__ */ new Set([
|
|
1047
|
+
"-C",
|
|
1048
|
+
"-c",
|
|
1049
|
+
"--git-dir",
|
|
1050
|
+
"--work-tree",
|
|
1051
|
+
"--namespace",
|
|
1052
|
+
"--super-prefix",
|
|
1053
|
+
"--config-env",
|
|
1054
|
+
"--exec-path",
|
|
1055
|
+
"--list-cmds",
|
|
1056
|
+
"--attr-source"
|
|
1057
|
+
]);
|
|
1058
|
+
var GIT_EXEC_KEY = /^(core\.(pager|editor|sshcommand|askpass|gitproxy)|pager\..+|sequence\.editor|.+\.(textconv|command|cmd|helper|program|clean|smudge|process|driver|tool)|credential\..*|uploadpack\.packobjectshook|gpg\..*program|.*\.(receivepack|uploadpack))$/i;
|
|
1059
|
+
function rankGit(argWords, depth) {
|
|
1060
|
+
const args = argWords.map((w) => w.value);
|
|
1061
|
+
let rank = 0;
|
|
1062
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1063
|
+
let i = 0;
|
|
1064
|
+
while (i < args.length) {
|
|
1065
|
+
const a = args[i];
|
|
1066
|
+
if (!a.startsWith("-")) break;
|
|
1067
|
+
let opt = a;
|
|
1068
|
+
let val;
|
|
1069
|
+
if (a.startsWith("--") && a.includes("=")) {
|
|
1070
|
+
opt = a.slice(0, a.indexOf("="));
|
|
1071
|
+
val = a.slice(a.indexOf("=") + 1);
|
|
1072
|
+
i++;
|
|
1073
|
+
} else if (GIT_VALUE_OPTS.has(a)) {
|
|
1074
|
+
val = args[i + 1] ?? "";
|
|
1075
|
+
if (argWords[i + 1]?.dynamic && (a === "-c" || a === "--config-env")) {
|
|
1076
|
+
rank = ASK;
|
|
1077
|
+
}
|
|
1078
|
+
i += 2;
|
|
1079
|
+
} else {
|
|
1080
|
+
i++;
|
|
1081
|
+
}
|
|
1082
|
+
if (opt === "-c" && val !== void 0) {
|
|
1083
|
+
const eq = val.indexOf("=");
|
|
1084
|
+
const key = (eq < 0 ? val : val.slice(0, eq)).toLowerCase();
|
|
1085
|
+
const value = eq < 0 ? "" : val.slice(eq + 1);
|
|
1086
|
+
if (key.startsWith("alias.")) aliases.set(key.slice(6), value);
|
|
1087
|
+
else if (key === "core.hookspath" || key === "core.fsmonitor") {
|
|
1088
|
+
if (!/^(false|0|no|off|)$/i.test(value)) rank = ASK;
|
|
1089
|
+
} else if (GIT_EXEC_KEY.test(key)) {
|
|
1090
|
+
rank = Math.max(rank, rankText(value, depth));
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (opt === "--config-env" && val !== void 0) {
|
|
1094
|
+
const key = val.split("=")[0].toLowerCase();
|
|
1095
|
+
if (key.startsWith("alias.") || GIT_EXEC_KEY.test(key) || /hookspath|fsmonitor/.test(key)) {
|
|
1096
|
+
rank = ASK;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (i >= args.length) return rank;
|
|
1101
|
+
if (argWords[i].dynamic) return ASK;
|
|
1102
|
+
const sub = args[i];
|
|
1103
|
+
const rest = args.slice(i + 1);
|
|
1104
|
+
const alias = aliases.get(sub.toLowerCase());
|
|
1105
|
+
if (alias !== void 0) {
|
|
1106
|
+
if (alias.startsWith("!")) {
|
|
1107
|
+
return Math.max(
|
|
1108
|
+
rank,
|
|
1109
|
+
rankText(`${alias.slice(1)} ${rest.join(" ")}`, depth)
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
const expanded = parseShell(alias);
|
|
1113
|
+
const aliasWords = expanded.commands[0]?.words ?? [];
|
|
1114
|
+
return Math.max(
|
|
1115
|
+
rank,
|
|
1116
|
+
expanded.ambiguous ? ASK : 0,
|
|
1117
|
+
rankGit([...aliasWords, ...argWords.slice(i + 1)], depth + 1)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
const opts = argsBeforeDashDash(rest);
|
|
1121
|
+
const has = (...names) => opts.some((a) => names.includes(a.split("=")[0]));
|
|
1122
|
+
const short = (letter) => opts.some((a) => new RegExp(`^-[a-zA-Z]*${letter}`).test(a));
|
|
1123
|
+
let sr = 0;
|
|
1124
|
+
switch (sub) {
|
|
1125
|
+
case "push": {
|
|
1126
|
+
const forced = has(
|
|
1127
|
+
"--force",
|
|
1128
|
+
"--force-with-lease",
|
|
1129
|
+
"--force-if-includes",
|
|
1130
|
+
"--mirror",
|
|
1131
|
+
"--delete",
|
|
1132
|
+
"--prune"
|
|
1133
|
+
) || opts.some(
|
|
1134
|
+
(a) => /^-[a-zA-Z]+$/.test(a) && /[fd]/.test(a.replace(/o.*$/, ""))
|
|
1135
|
+
) || rest.some((a) => /^[+:]/.test(a));
|
|
1136
|
+
sr = forced ? ASK : RANK.git_push;
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
case "reset":
|
|
1140
|
+
sr = has("--hard", "--merge") ? ASK : 0;
|
|
1141
|
+
break;
|
|
1142
|
+
case "clean":
|
|
1143
|
+
sr = has("--force") || short("f") ? ASK : 0;
|
|
1144
|
+
break;
|
|
1145
|
+
case "branch":
|
|
1146
|
+
sr = short("D") || (has("--delete") || short("d")) && (has("--force") || short("f")) ? ASK : 0;
|
|
1147
|
+
break;
|
|
1148
|
+
case "checkout":
|
|
1149
|
+
sr = rest.includes("--") || has("--force") || short("f") ? ASK : 0;
|
|
1150
|
+
break;
|
|
1151
|
+
case "switch":
|
|
1152
|
+
sr = has("--discard-changes", "--force") || short("f") ? ASK : 0;
|
|
1153
|
+
break;
|
|
1154
|
+
case "restore":
|
|
1155
|
+
sr = has("--source") || short("s") ? ASK : 0;
|
|
1156
|
+
break;
|
|
1157
|
+
case "stash":
|
|
1158
|
+
sr = ["drop", "clear"].includes(rest[0] ?? "") ? ASK : 0;
|
|
1159
|
+
break;
|
|
1160
|
+
case "update-ref":
|
|
1161
|
+
sr = short("d") ? ASK : 0;
|
|
1162
|
+
break;
|
|
1163
|
+
case "reflog":
|
|
1164
|
+
sr = ["expire", "delete"].includes(rest[0] ?? "") ? ASK : 0;
|
|
1165
|
+
break;
|
|
1166
|
+
case "filter-branch":
|
|
1167
|
+
case "filter-repo":
|
|
1168
|
+
sr = ASK;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
if (!GIT_READS.has(sub) && rest.some((a) => AGENT_CONFIG_PATH.test(a)))
|
|
1172
|
+
sr = ASK;
|
|
1173
|
+
return Math.max(rank, sr);
|
|
1174
|
+
}
|
|
1175
|
+
var AGENT_CONFIG_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1176
|
+
"mcp",
|
|
1177
|
+
"plugin",
|
|
1178
|
+
"plugins",
|
|
1179
|
+
"config"
|
|
1180
|
+
]);
|
|
1181
|
+
var AGENT_READ_ACTIONS = /* @__PURE__ */ new Set([
|
|
1182
|
+
"list",
|
|
1183
|
+
"ls",
|
|
1184
|
+
"get",
|
|
1185
|
+
"show",
|
|
1186
|
+
"serve",
|
|
1187
|
+
"help",
|
|
1188
|
+
"--help",
|
|
1189
|
+
"-h"
|
|
1190
|
+
]);
|
|
1191
|
+
var AGENT_WIDENING_OPTS = /^(--mcp-config|--settings|--dangerously-skip-permissions|--allow-dangerously-skip-permissions|--allowed-tools|--allowedtools|--add-dir|--plugin-dir|--dangerously-bypass-approvals-and-sandbox|--yolo|--config)(=|$)/i;
|
|
1192
|
+
function rankAgentCli(base, args) {
|
|
1193
|
+
if (args.some((a) => AGENT_WIDENING_OPTS.test(a))) return ASK;
|
|
1194
|
+
if (base === "codex" && args.includes("-c")) return ASK;
|
|
1195
|
+
const mode = args.findIndex((a) => a === "--permission-mode");
|
|
1196
|
+
const modeValue = mode >= 0 ? args[mode + 1] : args.find((a) => a.startsWith("--permission-mode="))?.slice(18);
|
|
1197
|
+
if (modeValue !== void 0 && !/^(default|plan)$/i.test(modeValue))
|
|
1198
|
+
return ASK;
|
|
1199
|
+
const positionals2 = args.filter((a) => !a.startsWith("-"));
|
|
1200
|
+
const sub = positionals2.findIndex((p) => AGENT_CONFIG_SUBCOMMANDS.has(p));
|
|
1201
|
+
if (sub < 0) return 0;
|
|
1202
|
+
const action = positionals2[sub + 1];
|
|
1203
|
+
if (action === void 0 || AGENT_READ_ACTIONS.has(action)) return 0;
|
|
1204
|
+
return ASK;
|
|
1205
|
+
}
|
|
166
1206
|
function classifyCommand(command) {
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
if (
|
|
170
|
-
if (
|
|
171
|
-
|
|
1207
|
+
let rank = 0;
|
|
1208
|
+
if (DESTRUCTIVE.some((re) => re.test(command))) rank = ASK;
|
|
1209
|
+
else if (DEPLOY.some((re) => re.test(command))) rank = RANK.shell_deploy;
|
|
1210
|
+
else if (GIT_PUSH.test(command)) rank = RANK.git_push;
|
|
1211
|
+
else if (startsInteractiveSession(command)) rank = RANK.shell_session;
|
|
1212
|
+
if (rank < ASK) rank = Math.max(rank, rankText(command, 0));
|
|
1213
|
+
return BY_RANK[rank];
|
|
1214
|
+
}
|
|
1215
|
+
function isAgentConfigPath(path) {
|
|
1216
|
+
return AGENT_CONFIG_PATH.test(path);
|
|
172
1217
|
}
|
|
173
1218
|
function mcpToolName(server, tool) {
|
|
174
1219
|
const part = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^(\d)/, "x$1") || "x";
|
|
@@ -204,6 +1249,54 @@ var FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
204
1249
|
"edit"
|
|
205
1250
|
// DeepSeek Harness
|
|
206
1251
|
]);
|
|
1252
|
+
var CONTENT_PIN_TOOLS = /* @__PURE__ */ new Set([
|
|
1253
|
+
"Edit",
|
|
1254
|
+
"MultiEdit",
|
|
1255
|
+
"NotebookEdit",
|
|
1256
|
+
"edit"
|
|
1257
|
+
// DeepSeek Harness
|
|
1258
|
+
]);
|
|
1259
|
+
var EditPinRefused = class extends Error {
|
|
1260
|
+
constructor(reasonCode, message) {
|
|
1261
|
+
super(message);
|
|
1262
|
+
this.reasonCode = reasonCode;
|
|
1263
|
+
this.name = "EditPinRefused";
|
|
1264
|
+
}
|
|
1265
|
+
reasonCode;
|
|
1266
|
+
};
|
|
1267
|
+
async function pinEditTarget(call, governed) {
|
|
1268
|
+
if (!CONTENT_PIN_TOOLS.has(call.tool)) return governed;
|
|
1269
|
+
const path = governed.arguments.path;
|
|
1270
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
1271
|
+
throw new EditPinRefused(
|
|
1272
|
+
CONTENT_PIN_INVALID_ARGUMENT,
|
|
1273
|
+
`${call.tool} needs an absolute file path; the hook does not resolve relative paths`
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
let pin;
|
|
1277
|
+
try {
|
|
1278
|
+
pin = await pinFile(path, { allowAbsent: true });
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
const code = typeof err?.reasonCode === "string" ? err.reasonCode : "CONTENT_PIN_UNREADABLE";
|
|
1281
|
+
throw new EditPinRefused(
|
|
1282
|
+
code,
|
|
1283
|
+
`the current contents of ${path} could not be pinned`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
const record = "absent" in pin ? { arg: "path", index: 0, mode: "recheck", absent: true } : {
|
|
1287
|
+
arg: "path",
|
|
1288
|
+
index: 0,
|
|
1289
|
+
mode: "recheck",
|
|
1290
|
+
name: pin.name,
|
|
1291
|
+
size: pin.size,
|
|
1292
|
+
sha256: pin.sha256
|
|
1293
|
+
};
|
|
1294
|
+
return {
|
|
1295
|
+
...governed,
|
|
1296
|
+
arguments: { ...governed.arguments, [CONTENT_PIN_KEY]: [record] },
|
|
1297
|
+
contentPin: { path, pin }
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
207
1300
|
function commandText(value) {
|
|
208
1301
|
if (typeof value === "string") return value;
|
|
209
1302
|
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
@@ -222,17 +1315,20 @@ function patchTargets(patch) {
|
|
|
222
1315
|
}
|
|
223
1316
|
return targets;
|
|
224
1317
|
}
|
|
1318
|
+
var AGENT_CONFIG_NOTE = "(agent MCP, hook, permission or plugin config)";
|
|
225
1319
|
function fileWrite(operation, path, content, cwd, summaryVerb) {
|
|
1320
|
+
const config = isAgentConfigPath(path);
|
|
226
1321
|
return {
|
|
227
|
-
toolName: "file_write",
|
|
1322
|
+
toolName: config ? "shell_destructive" : "file_write",
|
|
228
1323
|
arguments: {
|
|
229
1324
|
operation,
|
|
230
1325
|
path,
|
|
231
1326
|
contentBytes: Buffer.byteLength(content),
|
|
232
1327
|
contentDigest: digest(content),
|
|
1328
|
+
...config ? { touches: "agent-config" } : {},
|
|
233
1329
|
...cwd
|
|
234
1330
|
},
|
|
235
|
-
summary: `${summaryVerb} ${path}`
|
|
1331
|
+
summary: `${summaryVerb} ${path}${config ? ` ${AGENT_CONFIG_NOTE}` : ""}`
|
|
236
1332
|
};
|
|
237
1333
|
}
|
|
238
1334
|
function governedCall(call) {
|
|
@@ -280,10 +1376,12 @@ function governedCall(call) {
|
|
|
280
1376
|
const targets = patchTargets(patch);
|
|
281
1377
|
const paths = [...new Set(targets.map((t) => t.path))];
|
|
282
1378
|
const shown = paths.slice(0, MAX_PATHS).map((p) => bounded(p, 500));
|
|
1379
|
+
const config = paths.some((p) => isAgentConfigPath(p));
|
|
283
1380
|
return {
|
|
284
|
-
toolName: "file_write",
|
|
1381
|
+
toolName: config ? "shell_destructive" : "file_write",
|
|
285
1382
|
arguments: {
|
|
286
1383
|
operation: "apply_patch",
|
|
1384
|
+
...config ? { touches: "agent-config" } : {},
|
|
287
1385
|
path: shown[0] ?? "",
|
|
288
1386
|
paths: shown,
|
|
289
1387
|
...paths.length > MAX_PATHS ? { morePaths: paths.length - MAX_PATHS } : {},
|
|
@@ -292,7 +1390,7 @@ function governedCall(call) {
|
|
|
292
1390
|
contentDigest: digest(patch),
|
|
293
1391
|
...cwd
|
|
294
1392
|
},
|
|
295
|
-
summary: paths.length === 0 ? "Apply a patch (no file headers found)" : paths.length === 1 ? `Patch ${bounded(paths[0], 160)}` : `Patch ${paths.length} files: ${bounded(paths.join(", "), 160)}`
|
|
1393
|
+
summary: (paths.length === 0 ? "Apply a patch (no file headers found)" : paths.length === 1 ? `Patch ${bounded(paths[0], 160)}` : `Patch ${paths.length} files: ${bounded(paths.join(", "), 160)}`) + (config ? ` ${AGENT_CONFIG_NOTE}` : "")
|
|
296
1394
|
};
|
|
297
1395
|
}
|
|
298
1396
|
if (tool === "WebFetch" || tool === "web_fetch") {
|
|
@@ -326,6 +1424,7 @@ function tierOf(toolName) {
|
|
|
326
1424
|
}
|
|
327
1425
|
|
|
328
1426
|
// src/decide.ts
|
|
1427
|
+
import { verifyPin } from "@odingard/guardian-sdk";
|
|
329
1428
|
var PLAIN = {
|
|
330
1429
|
NOT_AUTHORIZED: "this agent is not authorized for it",
|
|
331
1430
|
APPROVAL_DENIED: "the approver declined it",
|
|
@@ -336,6 +1435,16 @@ var PLAIN = {
|
|
|
336
1435
|
SERVICE_UNAVAILABLE: "Guardian could not be reached",
|
|
337
1436
|
REQUEST_CONFLICT: "Guardian saw a conflicting request"
|
|
338
1437
|
};
|
|
1438
|
+
async function contentPinRefusal(call) {
|
|
1439
|
+
if (!call.contentPin) return void 0;
|
|
1440
|
+
try {
|
|
1441
|
+
await verifyPin(call.contentPin.path, call.contentPin.pin);
|
|
1442
|
+
return void 0;
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
const code = err?.reasonCode;
|
|
1445
|
+
return typeof code === "string" ? code : "CONTENT_PIN_UNREADABLE";
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
339
1448
|
function plain(code) {
|
|
340
1449
|
return PLAIN[code] ?? code;
|
|
341
1450
|
}
|
|
@@ -416,6 +1525,17 @@ async function decide(call, options) {
|
|
|
416
1525
|
guardianDecision: "DENY"
|
|
417
1526
|
});
|
|
418
1527
|
}
|
|
1528
|
+
const pinRefusal = await contentPinRefusal(call);
|
|
1529
|
+
if (pinRefusal !== void 0) {
|
|
1530
|
+
const path = call.contentPin.path;
|
|
1531
|
+
return {
|
|
1532
|
+
decision: "deny",
|
|
1533
|
+
reason: pinRefusal === "CONTENT_CHANGED_SINCE_APPROVAL" ? `Guardian hook: ${path} changed since this edit was proposed (CONTENT_CHANGED_SINCE_APPROVAL). Nothing ran. Read the file again and propose the edit again; it needs a new approval.` : `Guardian hook: ${path} could not be checked against its approval (${pinRefusal}). Nothing ran.`,
|
|
1534
|
+
requestId: result.requestId,
|
|
1535
|
+
guardianDecision: approvedByHuman ? "ASK" : "ALLOW",
|
|
1536
|
+
approvedByHuman
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
419
1539
|
let executed;
|
|
420
1540
|
try {
|
|
421
1541
|
executed = await options.client.execute({
|
|
@@ -542,12 +1662,21 @@ async function evaluate(dialect, stdinText, env, deps, deadlineMs) {
|
|
|
542
1662
|
if (!input || typeof input.tool_name !== "string") {
|
|
543
1663
|
return failClosed("the tool call had no tool name. Nothing ran.");
|
|
544
1664
|
}
|
|
545
|
-
const
|
|
1665
|
+
const toolCall = {
|
|
546
1666
|
tool: input.tool_name,
|
|
547
1667
|
input: input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {},
|
|
548
1668
|
...typeof input.cwd === "string" ? { cwd: input.cwd } : {}
|
|
549
|
-
}
|
|
550
|
-
|
|
1669
|
+
};
|
|
1670
|
+
const classified = governedCall(toolCall);
|
|
1671
|
+
if (classified === null) return null;
|
|
1672
|
+
let call;
|
|
1673
|
+
try {
|
|
1674
|
+
call = await pinEditTarget(toolCall, classified);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
const code = err instanceof EditPinRefused ? err.reasonCode : "CONTENT_PIN_UNREADABLE";
|
|
1677
|
+
const detail = err instanceof EditPinRefused ? err.message : "the target file could not be pinned";
|
|
1678
|
+
return failClosed(`${detail} (${code}). Nothing ran.`);
|
|
1679
|
+
}
|
|
551
1680
|
const connectionId = connectionFor(call.toolName, env);
|
|
552
1681
|
const credential = env.GUARDIAN_AGENT_CREDENTIAL?.trim();
|
|
553
1682
|
const baseUrl = (env.GUARDIAN_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(
|