@odingard/guardian-hook 0.2.0 → 0.3.1
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 +156 -6
- package/dist/agent-sdk.js +1332 -28
- package/dist/index.js +1489 -48
- package/package.json +5 -4
package/dist/agent-sdk.js
CHANGED
|
@@ -3,6 +3,456 @@ import { createGuardianClient } from "@odingard/guardian-sdk";
|
|
|
3
3
|
|
|
4
4
|
// src/classify.ts
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
// src/shell.ts
|
|
8
|
+
var MAX_NESTING = 16;
|
|
9
|
+
var isBlank = (c) => c === " " || c === " ";
|
|
10
|
+
var OPERATOR_START = /* @__PURE__ */ new Set([";", "&", "|", "\n", "(", ")", "<", ">"]);
|
|
11
|
+
function parseShell(src) {
|
|
12
|
+
const st = {
|
|
13
|
+
src,
|
|
14
|
+
pos: 0,
|
|
15
|
+
ambiguous: false,
|
|
16
|
+
nesting: 0,
|
|
17
|
+
heredocs: [],
|
|
18
|
+
bodySubs: []
|
|
19
|
+
};
|
|
20
|
+
const parsed = parseList(st, false);
|
|
21
|
+
if (st.pos < src.length) st.ambiguous = true;
|
|
22
|
+
if (st.bodySubs.length > 0) {
|
|
23
|
+
parsed.commands.push(bodyCommand(st.bodySubs));
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
commands: parsed.commands,
|
|
27
|
+
ambiguous: st.ambiguous || parsed.ambiguous
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function bodyCommand(subs) {
|
|
31
|
+
return { words: [], writes: [], pipeline: -1, stage: 0, subs };
|
|
32
|
+
}
|
|
33
|
+
function parseList(st, inSub) {
|
|
34
|
+
const commands = [];
|
|
35
|
+
let pipeline = 0;
|
|
36
|
+
let stage = 0;
|
|
37
|
+
let words = [];
|
|
38
|
+
let writes = [];
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let pendingWrite = false;
|
|
41
|
+
let pendingHeredoc = null;
|
|
42
|
+
let pendingHereString = false;
|
|
43
|
+
const endCommand = (nextIsPipe) => {
|
|
44
|
+
if (words.length > 0 || writes.length > 0) {
|
|
45
|
+
commands.push({ words, writes, pipeline, stage, subs: [] });
|
|
46
|
+
}
|
|
47
|
+
words = [];
|
|
48
|
+
writes = [];
|
|
49
|
+
if (nextIsPipe) stage++;
|
|
50
|
+
else {
|
|
51
|
+
pipeline++;
|
|
52
|
+
stage = 0;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const { src } = st;
|
|
56
|
+
while (st.pos < src.length) {
|
|
57
|
+
const c = src[st.pos];
|
|
58
|
+
if (isBlank(c)) {
|
|
59
|
+
st.pos++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (c === "\\" && src[st.pos + 1] === "\n") {
|
|
63
|
+
st.pos += 2;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (c === "#") {
|
|
67
|
+
while (st.pos < src.length && src[st.pos] !== "\n") st.pos++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === "\n") {
|
|
71
|
+
st.pos++;
|
|
72
|
+
endCommand(false);
|
|
73
|
+
readHeredocBodies(st);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (c === ";" || c === "&" || c === "|") {
|
|
77
|
+
const two = src.slice(st.pos, st.pos + 2);
|
|
78
|
+
if (c === "&" && src[st.pos + 1] === ">") {
|
|
79
|
+
st.pos += src[st.pos + 2] === ">" ? 3 : 2;
|
|
80
|
+
pendingWrite = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (two === "&&" || two === "||" || two === ";;" || two === "|&") {
|
|
84
|
+
st.pos += 2;
|
|
85
|
+
endCommand(two === "|&");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
st.pos++;
|
|
89
|
+
endCommand(c === "|");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c === "(") {
|
|
93
|
+
st.pos++;
|
|
94
|
+
depth++;
|
|
95
|
+
endCommand(false);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (c === ")") {
|
|
99
|
+
if (depth > 0) {
|
|
100
|
+
st.pos++;
|
|
101
|
+
depth--;
|
|
102
|
+
endCommand(false);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (inSub) {
|
|
106
|
+
endCommand(false);
|
|
107
|
+
return { commands, ambiguous: false };
|
|
108
|
+
}
|
|
109
|
+
st.pos++;
|
|
110
|
+
st.ambiguous = true;
|
|
111
|
+
endCommand(false);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c === "<" || c === ">") {
|
|
115
|
+
if (src[st.pos + 1] === "(") {
|
|
116
|
+
const w2 = readWord(st);
|
|
117
|
+
if (w2) addWord(w2);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const op = /^(<<<|<<-|<<|<>|<&|>>|>&|>\||<|>)/.exec(src.slice(st.pos))[0];
|
|
121
|
+
st.pos += op.length;
|
|
122
|
+
if (op === "<<" || op === "<<-") pendingHeredoc = { stripTabs: op === "<<-" };
|
|
123
|
+
else if (op === "<<<") pendingHereString = true;
|
|
124
|
+
else if (op === ">&" || op === "<&") {
|
|
125
|
+
const m = /^[ \t]*([0-9]+|-)(?=[\s;&|)<>]|$)/.exec(src.slice(st.pos));
|
|
126
|
+
if (m) st.pos += m[0].length;
|
|
127
|
+
else if (op === ">&") pendingWrite = true;
|
|
128
|
+
} else if (op !== "<") pendingWrite = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const w = readWord(st);
|
|
132
|
+
if (!w) continue;
|
|
133
|
+
addWord(w);
|
|
134
|
+
}
|
|
135
|
+
if (inSub) st.ambiguous = true;
|
|
136
|
+
if (depth > 0) st.ambiguous = true;
|
|
137
|
+
endCommand(false);
|
|
138
|
+
return { commands, ambiguous: false };
|
|
139
|
+
function addWord(w) {
|
|
140
|
+
if (pendingHeredoc) {
|
|
141
|
+
st.heredocs.push({
|
|
142
|
+
delimiter: w.value,
|
|
143
|
+
stripTabs: pendingHeredoc.stripTabs,
|
|
144
|
+
quoted: w.quoted
|
|
145
|
+
});
|
|
146
|
+
pendingHeredoc = null;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (pendingHereString) {
|
|
150
|
+
pendingHereString = false;
|
|
151
|
+
collectSubs(w);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (pendingWrite) {
|
|
155
|
+
pendingWrite = false;
|
|
156
|
+
writes.push(w);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const next = src[st.pos];
|
|
160
|
+
if (/^[0-9]+$/.test(w.value) && !w.quoted && (next === ">" || next === "<")) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
words.push(w);
|
|
164
|
+
}
|
|
165
|
+
function collectSubs(w) {
|
|
166
|
+
if (w.subs.length > 0) {
|
|
167
|
+
commands.push({ words: [], writes: [], pipeline: -1, stage: 0, subs: w.subs });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function readHeredocBodies(st) {
|
|
172
|
+
while (st.heredocs.length > 0) {
|
|
173
|
+
const doc = st.heredocs.shift();
|
|
174
|
+
const lines = [];
|
|
175
|
+
let found = false;
|
|
176
|
+
while (st.pos < st.src.length) {
|
|
177
|
+
let end = st.src.indexOf("\n", st.pos);
|
|
178
|
+
if (end < 0) end = st.src.length;
|
|
179
|
+
const raw = st.src.slice(st.pos, end);
|
|
180
|
+
st.pos = Math.min(end + 1, st.src.length);
|
|
181
|
+
const line = doc.stripTabs ? raw.replace(/^\t+/, "") : raw;
|
|
182
|
+
if (line === doc.delimiter) {
|
|
183
|
+
found = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
lines.push(raw);
|
|
187
|
+
}
|
|
188
|
+
void found;
|
|
189
|
+
if (!doc.quoted) {
|
|
190
|
+
const body = lines.join("\n");
|
|
191
|
+
if (/\$\(|`/.test(body)) {
|
|
192
|
+
const inner = { ...st, src: body, pos: 0, heredocs: [], bodySubs: [] };
|
|
193
|
+
const w = readDoubleQuotedBody(inner);
|
|
194
|
+
st.ambiguous ||= inner.ambiguous;
|
|
195
|
+
if (w.subs.length > 0) st.bodySubs.push(...w.subs);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function readDoubleQuotedBody(st) {
|
|
201
|
+
const word = { value: "", dynamic: false, subs: [] };
|
|
202
|
+
while (st.pos < st.src.length) {
|
|
203
|
+
const c = st.src[st.pos];
|
|
204
|
+
if (c === "\\") {
|
|
205
|
+
st.pos += 2;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (c === "$" || c === "`") {
|
|
209
|
+
readExpansion(st, word);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
st.pos++;
|
|
213
|
+
}
|
|
214
|
+
return word;
|
|
215
|
+
}
|
|
216
|
+
function readWord(st) {
|
|
217
|
+
const { src } = st;
|
|
218
|
+
const word = {
|
|
219
|
+
value: "",
|
|
220
|
+
dynamic: false,
|
|
221
|
+
subs: [],
|
|
222
|
+
quoted: false
|
|
223
|
+
};
|
|
224
|
+
const start = st.pos;
|
|
225
|
+
while (st.pos < src.length) {
|
|
226
|
+
const c = src[st.pos];
|
|
227
|
+
if (isBlank(c) || c === "\n") break;
|
|
228
|
+
if ((c === "<" || c === ">") && src[st.pos + 1] === "(") {
|
|
229
|
+
st.pos += 2;
|
|
230
|
+
word.dynamic = true;
|
|
231
|
+
word.subs.push(parseNested(st));
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (OPERATOR_START.has(c)) break;
|
|
235
|
+
if (c === "\\") {
|
|
236
|
+
if (st.pos + 1 < src.length) {
|
|
237
|
+
if (src[st.pos + 1] !== "\n") word.value += src[st.pos + 1];
|
|
238
|
+
st.pos += 2;
|
|
239
|
+
} else {
|
|
240
|
+
st.pos++;
|
|
241
|
+
}
|
|
242
|
+
word.quoted = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (c === "'") {
|
|
246
|
+
const end = src.indexOf("'", st.pos + 1);
|
|
247
|
+
if (end < 0) {
|
|
248
|
+
st.ambiguous = true;
|
|
249
|
+
word.value += src.slice(st.pos + 1);
|
|
250
|
+
st.pos = src.length;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
word.value += src.slice(st.pos + 1, end);
|
|
254
|
+
st.pos = end + 1;
|
|
255
|
+
word.quoted = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (c === '"') {
|
|
259
|
+
st.pos++;
|
|
260
|
+
word.quoted = true;
|
|
261
|
+
let closed = false;
|
|
262
|
+
while (st.pos < src.length) {
|
|
263
|
+
const d = src[st.pos];
|
|
264
|
+
if (d === '"') {
|
|
265
|
+
st.pos++;
|
|
266
|
+
closed = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
if (d === "\\") {
|
|
270
|
+
const n = src[st.pos + 1];
|
|
271
|
+
if (n === void 0) {
|
|
272
|
+
st.pos++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if ('$`"\\\n'.includes(n)) {
|
|
276
|
+
if (n !== "\n") word.value += n;
|
|
277
|
+
} else {
|
|
278
|
+
word.value += d + n;
|
|
279
|
+
}
|
|
280
|
+
st.pos += 2;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (d === "$" || d === "`") {
|
|
284
|
+
readExpansion(st, word);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
word.value += d;
|
|
288
|
+
st.pos++;
|
|
289
|
+
}
|
|
290
|
+
if (!closed) st.ambiguous = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (c === "$" && src[st.pos + 1] === "'") {
|
|
294
|
+
let i = st.pos + 2;
|
|
295
|
+
let out = "";
|
|
296
|
+
let closed = false;
|
|
297
|
+
while (i < src.length) {
|
|
298
|
+
const d = src[i];
|
|
299
|
+
if (d === "'") {
|
|
300
|
+
closed = true;
|
|
301
|
+
i++;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
if (d === "\\" && i + 1 < src.length) {
|
|
305
|
+
const n = src[i + 1];
|
|
306
|
+
const simple = { n: "\n", t: " ", "\\": "\\", "'": "'", '"': '"' };
|
|
307
|
+
if (simple[n] !== void 0) {
|
|
308
|
+
out += simple[n];
|
|
309
|
+
i += 2;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const hex = /^x([0-9a-fA-F]{1,2})/.exec(src.slice(i + 1));
|
|
313
|
+
if (hex) {
|
|
314
|
+
out += String.fromCharCode(parseInt(hex[1], 16));
|
|
315
|
+
i += 1 + hex[0].length;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const oct = /^([0-7]{1,3})/.exec(src.slice(i + 1));
|
|
319
|
+
if (oct) {
|
|
320
|
+
out += String.fromCharCode(parseInt(oct[1], 8));
|
|
321
|
+
i += 1 + oct[0].length;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
st.ambiguous = true;
|
|
325
|
+
out += n;
|
|
326
|
+
i += 2;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
out += d;
|
|
330
|
+
i++;
|
|
331
|
+
}
|
|
332
|
+
if (!closed) st.ambiguous = true;
|
|
333
|
+
word.value += out;
|
|
334
|
+
word.quoted = true;
|
|
335
|
+
st.pos = i;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (c === "$" || c === "`") {
|
|
339
|
+
readExpansion(st, word);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
word.value += c;
|
|
343
|
+
st.pos++;
|
|
344
|
+
}
|
|
345
|
+
return st.pos > start ? word : null;
|
|
346
|
+
}
|
|
347
|
+
function readExpansion(st, word) {
|
|
348
|
+
const { src } = st;
|
|
349
|
+
const c = src[st.pos];
|
|
350
|
+
if (c === "`") {
|
|
351
|
+
let i = st.pos + 1;
|
|
352
|
+
let inner = "";
|
|
353
|
+
while (i < src.length && src[i] !== "`") {
|
|
354
|
+
if (src[i] === "\\" && i + 1 < src.length) {
|
|
355
|
+
inner += src[i + 1];
|
|
356
|
+
i += 2;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
inner += src[i];
|
|
360
|
+
i++;
|
|
361
|
+
}
|
|
362
|
+
if (i >= src.length) st.ambiguous = true;
|
|
363
|
+
word.value += "`" + inner + "`";
|
|
364
|
+
word.dynamic = true;
|
|
365
|
+
word.subs.push(parseString(st, inner));
|
|
366
|
+
st.pos = Math.min(i + 1, src.length);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const n = src[st.pos + 1];
|
|
370
|
+
if (n === "(" && src[st.pos + 2] === "(") {
|
|
371
|
+
let i = st.pos + 3;
|
|
372
|
+
let level = 2;
|
|
373
|
+
while (i < src.length && level > 0) {
|
|
374
|
+
if (src[i] === "(") level++;
|
|
375
|
+
else if (src[i] === ")") level--;
|
|
376
|
+
i++;
|
|
377
|
+
}
|
|
378
|
+
if (level > 0) st.ambiguous = true;
|
|
379
|
+
word.value += src.slice(st.pos, i);
|
|
380
|
+
word.dynamic = true;
|
|
381
|
+
st.pos = i;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (n === "(") {
|
|
385
|
+
const start = st.pos;
|
|
386
|
+
st.pos += 2;
|
|
387
|
+
word.subs.push(parseNested(st));
|
|
388
|
+
word.value += src.slice(start, st.pos);
|
|
389
|
+
word.dynamic = true;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (n === "{") {
|
|
393
|
+
let i = st.pos + 2;
|
|
394
|
+
let level = 1;
|
|
395
|
+
while (i < src.length && level > 0) {
|
|
396
|
+
if (src[i] === "{") level++;
|
|
397
|
+
else if (src[i] === "}") level--;
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
if (level > 0) st.ambiguous = true;
|
|
401
|
+
const text = src.slice(st.pos, i);
|
|
402
|
+
word.value += text;
|
|
403
|
+
word.dynamic = true;
|
|
404
|
+
if (/\$\(|`/.test(text)) {
|
|
405
|
+
word.subs.push(parseString(st, text.slice(2, -1)));
|
|
406
|
+
}
|
|
407
|
+
st.pos = i;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const name = /^\$([A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-])/.exec(src.slice(st.pos));
|
|
411
|
+
if (name) {
|
|
412
|
+
word.value += name[0];
|
|
413
|
+
word.dynamic = true;
|
|
414
|
+
st.pos += name[0].length;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
word.value += "$";
|
|
418
|
+
st.pos++;
|
|
419
|
+
}
|
|
420
|
+
function parseNested(st) {
|
|
421
|
+
if (st.nesting >= MAX_NESTING) {
|
|
422
|
+
st.ambiguous = true;
|
|
423
|
+
st.pos = st.src.length;
|
|
424
|
+
return { commands: [], ambiguous: true };
|
|
425
|
+
}
|
|
426
|
+
st.nesting++;
|
|
427
|
+
const savedHeredocs = st.heredocs;
|
|
428
|
+
st.heredocs = [];
|
|
429
|
+
const inner = parseList(st, true);
|
|
430
|
+
st.heredocs = savedHeredocs;
|
|
431
|
+
st.nesting--;
|
|
432
|
+
if (st.src[st.pos] === ")") st.pos++;
|
|
433
|
+
else st.ambiguous = true;
|
|
434
|
+
return inner;
|
|
435
|
+
}
|
|
436
|
+
function parseString(st, text) {
|
|
437
|
+
if (st.nesting >= MAX_NESTING) {
|
|
438
|
+
st.ambiguous = true;
|
|
439
|
+
return { commands: [], ambiguous: true };
|
|
440
|
+
}
|
|
441
|
+
const inner = {
|
|
442
|
+
src: text,
|
|
443
|
+
pos: 0,
|
|
444
|
+
ambiguous: false,
|
|
445
|
+
nesting: st.nesting + 1,
|
|
446
|
+
heredocs: [],
|
|
447
|
+
bodySubs: []
|
|
448
|
+
};
|
|
449
|
+
const parsed = parseList(inner, false);
|
|
450
|
+
if (inner.bodySubs.length) parsed.commands.push(bodyCommand(inner.bodySubs));
|
|
451
|
+
st.ambiguous ||= inner.ambiguous;
|
|
452
|
+
return { commands: parsed.commands, ambiguous: inner.ambiguous };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/classify.ts
|
|
6
456
|
var HOOK_TOOLS = [
|
|
7
457
|
{
|
|
8
458
|
name: "shell",
|
|
@@ -17,13 +467,18 @@ var HOOK_TOOLS = [
|
|
|
17
467
|
{
|
|
18
468
|
name: "shell_destructive",
|
|
19
469
|
suggestedMode: "ask",
|
|
20
|
-
description: "Destructive commands: rm -rf, git reset --hard, git clean, force pushes, dropping databases, disk formatting."
|
|
470
|
+
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
471
|
},
|
|
22
472
|
{
|
|
23
473
|
name: "git_push",
|
|
24
474
|
suggestedMode: "ask",
|
|
25
475
|
description: "Pushing commits to a remote."
|
|
26
476
|
},
|
|
477
|
+
{
|
|
478
|
+
name: "shell_session",
|
|
479
|
+
suggestedMode: "ask",
|
|
480
|
+
description: "Starting an interactive shell, interpreter or remote session (bash, python, ssh, docker exec -it). What is typed into it later is not seen by Guardian."
|
|
481
|
+
},
|
|
27
482
|
{
|
|
28
483
|
name: "file_write",
|
|
29
484
|
suggestedMode: "allow",
|
|
@@ -63,15 +518,697 @@ var DESTRUCTIVE = [
|
|
|
63
518
|
/\b(drop|truncate)\s+(table|database|schema)\b/i,
|
|
64
519
|
/\bdelete\s+from\s+\w+\s*(;|$)/i,
|
|
65
520
|
/\b(mkfs|dd\s+if=|shred|wipefs)\b/,
|
|
521
|
+
/\bdd\b.*\bof=/,
|
|
522
|
+
/\brsync\b.*\s--(delete\S*|remove-source-files)\b/,
|
|
523
|
+
/\btruncate\b.*(\s-s\s*0\b|\s--size[= ]0\b)/,
|
|
66
524
|
/\bchmod\s+-R\s+777\b/,
|
|
67
525
|
/\bfind\b.*\s-delete\b/
|
|
68
526
|
];
|
|
69
527
|
var GIT_PUSH = /\bgit\s+push\b/;
|
|
528
|
+
var INTERPRETERS = new Set(
|
|
529
|
+
"sh bash zsh dash ksh mksh csh tcsh fish ash pwsh powershell python python2 python3 ipython node nodejs deno bun ruby irb perl php lua luajit r julia ghci scala groovysh jshell tclsh wish expect su login tmux screen script".split(" ")
|
|
530
|
+
);
|
|
531
|
+
var DB_CLIENTS = new Set(
|
|
532
|
+
"psql mysql mariadb mongo mongosh cqlsh sqlplus clickhouse-client".split(" ")
|
|
533
|
+
);
|
|
534
|
+
var DB_INLINE = /^(-c|-e|-f|--command|--execute|--file|--eval|--query)(=|$)/;
|
|
535
|
+
var NET_PIPES = new Set("telnet nc ncat netcat socat openssl".split(" "));
|
|
536
|
+
var SSH_VALUE_OPTS = new Set(
|
|
537
|
+
"-B -b -c -D -E -e -F -I -i -J -L -l -m -O -o -p -Q -R -S -W -w".split(" ")
|
|
538
|
+
);
|
|
539
|
+
var SUDO_VALUE_OPTS = new Set("-u -g -U -C -D -h -p -r -t -T".split(" "));
|
|
540
|
+
var REDIS_VALUE_OPTS = new Set("-h -p -a -n -u --user --pass".split(" "));
|
|
541
|
+
var WRAPPERS = new Set(
|
|
542
|
+
"exec command builtin nohup time env stdbuf nice ionice".split(" ")
|
|
543
|
+
);
|
|
544
|
+
function positionals(args, valueOpts) {
|
|
545
|
+
const out = [];
|
|
546
|
+
for (let i = 0; i < args.length; i++) {
|
|
547
|
+
const a = args[i];
|
|
548
|
+
if (valueOpts.has(a)) i++;
|
|
549
|
+
else if (!a.startsWith("-")) out.push(a);
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
function startsSession(segment) {
|
|
554
|
+
const words = segment.trim().split(/\s+/).filter(Boolean);
|
|
555
|
+
let wrapped = false;
|
|
556
|
+
while (words.length > 0) {
|
|
557
|
+
const w = words[0];
|
|
558
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w) || WRAPPERS.has(w) || wrapped && w.startsWith("-")) {
|
|
559
|
+
wrapped ||= WRAPPERS.has(w);
|
|
560
|
+
words.shift();
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
if (words.length === 0) return false;
|
|
566
|
+
const base = (words[0].split(/[\\/]/).pop() ?? "").replace(/^['"]+|['"]+$/g, "").toLowerCase().replace(/\.exe$/, "");
|
|
567
|
+
const args = words.slice(1);
|
|
568
|
+
if (args.some((a) => /^(--version|-V|--help)$/.test(a))) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
if (base === "sudo" || base === "doas" || base === "busybox") {
|
|
572
|
+
if (base !== "busybox" && args.some((a) => /^-[a-zA-Z]*[is]$/.test(a))) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const rest = positionals(args, SUDO_VALUE_OPTS);
|
|
576
|
+
if (rest.length === 0) return false;
|
|
577
|
+
return startsSession(args.slice(args.indexOf(rest[0])).join(" "));
|
|
578
|
+
}
|
|
579
|
+
if (base === "ssh" || base === "mosh") {
|
|
580
|
+
if (args.some((a) => /^-[a-zA-Z]*t/.test(a))) return true;
|
|
581
|
+
return positionals(args, SSH_VALUE_OPTS).length <= 1;
|
|
582
|
+
}
|
|
583
|
+
if (["docker", "podman", "nerdctl", "kubectl", "oc"].includes(base)) {
|
|
584
|
+
if (!args.some((a) => ["exec", "run", "attach", "debug"].includes(a))) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
return args.includes("attach") || args.some(
|
|
588
|
+
(a) => /^(-[a-zA-Z]*i[a-zA-Z]*|--interactive|--stdin)(=true)?$/.test(a)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (DB_CLIENTS.has(base)) {
|
|
592
|
+
return !args.some((a) => DB_INLINE.test(a)) && !segment.includes("<");
|
|
593
|
+
}
|
|
594
|
+
if (base === "sqlite3") {
|
|
595
|
+
return positionals(args, /* @__PURE__ */ new Set(["-cmd"])).length <= 1;
|
|
596
|
+
}
|
|
597
|
+
if (base === "redis-cli") {
|
|
598
|
+
return positionals(args, REDIS_VALUE_OPTS).length === 0;
|
|
599
|
+
}
|
|
600
|
+
if (NET_PIPES.has(base)) {
|
|
601
|
+
return base === "openssl" ? args[0] === "s_client" : !args.some((a) => /^-[a-zA-Z]*z/.test(a));
|
|
602
|
+
}
|
|
603
|
+
if (!INTERPRETERS.has(base) && !/^python[0-9.]+$/.test(base)) return false;
|
|
604
|
+
if ((base === "deno" || base === "bun") && args[0] === "repl") return true;
|
|
605
|
+
const code = args.findIndex((a) => /^-[a-zA-Z]*c[a-zA-Z]*$/.test(a));
|
|
606
|
+
if (code >= 0 && /sh$/.test(base)) {
|
|
607
|
+
const inner = args.slice(code + 1).join(" ").replace(/['"]/g, " ");
|
|
608
|
+
if (startsInteractiveSession(inner)) return true;
|
|
609
|
+
}
|
|
610
|
+
if (args.some((a) => /^(-[a-zA-Z]*[is][a-zA-Z]*|--interactive|-)$/.test(a))) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return args.every((a) => a.startsWith("-") && !/^-[a-zA-Z]*[ce]/.test(a));
|
|
614
|
+
}
|
|
615
|
+
function startsInteractiveSession(command) {
|
|
616
|
+
if (/\bwhile\s+(?:IFS=\S*\s+)?read\b/.test(command)) return true;
|
|
617
|
+
return command.split(/&&|\|\||[;&|\n()]|\$\(|`/).some((segment) => startsSession(segment));
|
|
618
|
+
}
|
|
619
|
+
var RANK = {
|
|
620
|
+
shell: 0,
|
|
621
|
+
shell_session: 1,
|
|
622
|
+
git_push: 2,
|
|
623
|
+
shell_deploy: 3,
|
|
624
|
+
shell_destructive: 4
|
|
625
|
+
};
|
|
626
|
+
var BY_RANK = [
|
|
627
|
+
"shell",
|
|
628
|
+
"shell_session",
|
|
629
|
+
"git_push",
|
|
630
|
+
"shell_deploy",
|
|
631
|
+
"shell_destructive"
|
|
632
|
+
];
|
|
633
|
+
var ASK = RANK.shell_destructive;
|
|
634
|
+
var MAX_DEPTH = 12;
|
|
635
|
+
var EDGE = String.raw`(?:^|[\s/\\'"=:,(~])`;
|
|
636
|
+
var AGENT_CONFIG_PATH = new RegExp(
|
|
637
|
+
[
|
|
638
|
+
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-])`,
|
|
639
|
+
EDGE + String.raw`\.claude(?:[/\\](?:settings[^/\\\s'"]*\.json|hooks|agents|commands|skills|plugins|output-styles)(?![\w-])|[/\\]?(?=$|[\s'"]))`,
|
|
640
|
+
EDGE + String.raw`\.(?:codex|cursor|gemini|dsh)(?=$|[/\\\s'"])`
|
|
641
|
+
].join("|"),
|
|
642
|
+
"i"
|
|
643
|
+
);
|
|
644
|
+
var READERS = new Set(
|
|
645
|
+
"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(" ")
|
|
646
|
+
);
|
|
647
|
+
var GIT_READS = new Set(
|
|
648
|
+
"status log show diff blame ls-files grep add commit check-ignore cat-file rev-parse ls-tree shortlog describe".split(" ")
|
|
649
|
+
);
|
|
650
|
+
var RESERVED = new Set(
|
|
651
|
+
"if then else elif fi do done while until for in case esac ! { } select function".split(
|
|
652
|
+
" "
|
|
653
|
+
)
|
|
654
|
+
);
|
|
655
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/;
|
|
656
|
+
var SHELLS = new Set(
|
|
657
|
+
"sh bash zsh dash ksh mksh ash yash posh fish busybox".split(" ")
|
|
658
|
+
);
|
|
659
|
+
var FETCHERS = new Set(
|
|
660
|
+
"curl wget fetch http https xh aria2c iwr irm invoke-webrequest invoke-restmethod".split(
|
|
661
|
+
" "
|
|
662
|
+
)
|
|
663
|
+
);
|
|
664
|
+
var BULK_DELETE = /* @__PURE__ */ new Set(["rm", "unlink", "shred"]);
|
|
665
|
+
var EVALUATORS = /* @__PURE__ */ new Set(["eval", "source", ".", "iex", "invoke-expression"]);
|
|
666
|
+
var WRAPPER_SPECS = {
|
|
667
|
+
nohup: {},
|
|
668
|
+
command: {},
|
|
669
|
+
builtin: {},
|
|
670
|
+
chronic: {},
|
|
671
|
+
unbuffer: {},
|
|
672
|
+
busybox: {},
|
|
673
|
+
time: { value: ["-f", "-o", "--format", "--output"] },
|
|
674
|
+
nice: { value: ["-n", "--adjustment"] },
|
|
675
|
+
ionice: { value: ["-c", "-n", "-p", "-P", "-u", "--class", "--classdata"] },
|
|
676
|
+
stdbuf: { value: ["-i", "-o", "-e", "--input", "--output", "--error"] },
|
|
677
|
+
timeout: { value: ["-s", "-k", "--signal", "--kill-after"], positional: 1 },
|
|
678
|
+
exec: { value: ["-a"] },
|
|
679
|
+
sudo: {
|
|
680
|
+
value: [
|
|
681
|
+
...SUDO_VALUE_OPTS,
|
|
682
|
+
"--user",
|
|
683
|
+
"--group",
|
|
684
|
+
"--host",
|
|
685
|
+
"--prompt",
|
|
686
|
+
"--chdir",
|
|
687
|
+
"--role",
|
|
688
|
+
"--type",
|
|
689
|
+
"--close-from",
|
|
690
|
+
"--other-user",
|
|
691
|
+
"--command-timeout"
|
|
692
|
+
]
|
|
693
|
+
},
|
|
694
|
+
doas: { value: ["-u", "-C"] },
|
|
695
|
+
xargs: {
|
|
696
|
+
value: [
|
|
697
|
+
"-a",
|
|
698
|
+
"--arg-file",
|
|
699
|
+
"-d",
|
|
700
|
+
"--delimiter",
|
|
701
|
+
"-E",
|
|
702
|
+
"-I",
|
|
703
|
+
"-L",
|
|
704
|
+
"-n",
|
|
705
|
+
"--max-args",
|
|
706
|
+
"-P",
|
|
707
|
+
"--max-procs",
|
|
708
|
+
"-s",
|
|
709
|
+
"--max-chars",
|
|
710
|
+
"--process-slot-var",
|
|
711
|
+
"--max-lines"
|
|
712
|
+
]
|
|
713
|
+
},
|
|
714
|
+
chroot: { value: ["--userspec", "--groups"], positional: 1 },
|
|
715
|
+
taskset: { positional: 1 },
|
|
716
|
+
flock: {
|
|
717
|
+
value: ["-w", "-E", "--timeout", "--conflict-exit-code"],
|
|
718
|
+
positional: 1
|
|
719
|
+
},
|
|
720
|
+
setsid: {},
|
|
721
|
+
caffeinate: { value: ["-t", "-w"] }
|
|
722
|
+
};
|
|
723
|
+
var leaf = (rank, base = "", args = []) => ({
|
|
724
|
+
rank,
|
|
725
|
+
base,
|
|
726
|
+
args
|
|
727
|
+
});
|
|
728
|
+
function baseName(value) {
|
|
729
|
+
return (value.split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.exe$/, "");
|
|
730
|
+
}
|
|
731
|
+
var literal = (value) => ({ value, dynamic: false, subs: [] });
|
|
732
|
+
function skipOptions(args, spec) {
|
|
733
|
+
const value = new Set(spec.value ?? []);
|
|
734
|
+
let i = 0;
|
|
735
|
+
let unknown = false;
|
|
736
|
+
while (i < args.length) {
|
|
737
|
+
const a = args[i].value;
|
|
738
|
+
if (a === "--") {
|
|
739
|
+
i++;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
if (a.startsWith("--")) {
|
|
743
|
+
if (a.includes("=")) i++;
|
|
744
|
+
else if (value.has(a)) i += 2;
|
|
745
|
+
else {
|
|
746
|
+
unknown = true;
|
|
747
|
+
i++;
|
|
748
|
+
}
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (a.startsWith("-") && a.length > 1) {
|
|
752
|
+
let takes = false;
|
|
753
|
+
for (let k = 1; k < a.length; k++) {
|
|
754
|
+
if (value.has(`-${a[k]}`)) {
|
|
755
|
+
takes = k === a.length - 1;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
i += takes ? 2 : 1;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
let positional = spec.positional ?? 0;
|
|
765
|
+
while (positional-- > 0 && i < args.length) i++;
|
|
766
|
+
return { start: i, unknown };
|
|
767
|
+
}
|
|
768
|
+
function rankText(text, depth) {
|
|
769
|
+
if (depth > MAX_DEPTH) return ASK;
|
|
770
|
+
return rankParsed(parseShell(text), depth + 1);
|
|
771
|
+
}
|
|
772
|
+
function rankParsed(parsed, depth) {
|
|
773
|
+
if (depth > MAX_DEPTH) return ASK;
|
|
774
|
+
let rank = parsed.ambiguous ? ASK : 0;
|
|
775
|
+
const stages = /* @__PURE__ */ new Map();
|
|
776
|
+
for (const cmd of parsed.commands) {
|
|
777
|
+
const l = rankArgv(cmd.words, depth);
|
|
778
|
+
rank = Math.max(rank, l.rank);
|
|
779
|
+
if (cmd.writes.some((w) => AGENT_CONFIG_PATH.test(w.value))) rank = ASK;
|
|
780
|
+
const subs = [
|
|
781
|
+
...cmd.subs,
|
|
782
|
+
...cmd.words.flatMap((w) => w.subs),
|
|
783
|
+
...cmd.writes.flatMap((w) => w.subs)
|
|
784
|
+
];
|
|
785
|
+
for (const sub of subs) rank = Math.max(rank, rankParsed(sub, depth + 1));
|
|
786
|
+
if (runsCode(l) && cmd.words.some((w) => w.subs.some((s) => fetches(s, depth + 1)))) {
|
|
787
|
+
rank = ASK;
|
|
788
|
+
}
|
|
789
|
+
if (cmd.pipeline >= 0) {
|
|
790
|
+
const list = stages.get(cmd.pipeline) ?? [];
|
|
791
|
+
list.push(l);
|
|
792
|
+
stages.set(cmd.pipeline, list);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
for (const list of stages.values()) {
|
|
796
|
+
const first = list.findIndex((l) => FETCHERS.has(l.base));
|
|
797
|
+
if (first >= 0 && list.slice(first + 1).some((l) => readsCodeFromStdin(l))) {
|
|
798
|
+
rank = ASK;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return rank;
|
|
802
|
+
}
|
|
803
|
+
function fetches(parsed, depth) {
|
|
804
|
+
return parsed.commands.some(
|
|
805
|
+
(c) => FETCHERS.has(rankArgv(c.words, depth).base) || [...c.subs, ...c.words.flatMap((w) => w.subs)].some(
|
|
806
|
+
(s) => fetches(s, depth + 1)
|
|
807
|
+
)
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
var isInterpreter = (base) => INTERPRETERS.has(base) || /^python[0-9.]*$/.test(base);
|
|
811
|
+
function runsCode(l) {
|
|
812
|
+
return EVALUATORS.has(l.base) || isInterpreter(l.base);
|
|
813
|
+
}
|
|
814
|
+
function readsCodeFromStdin(l) {
|
|
815
|
+
if (EVALUATORS.has(l.base)) return true;
|
|
816
|
+
if (!isInterpreter(l.base)) return false;
|
|
817
|
+
const options = [];
|
|
818
|
+
let script = false;
|
|
819
|
+
for (const a of l.args) {
|
|
820
|
+
if (a === "--") break;
|
|
821
|
+
if (a === "-") return true;
|
|
822
|
+
if (a.startsWith("-") || a.startsWith("+")) options.push(a);
|
|
823
|
+
else {
|
|
824
|
+
script = true;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (options.some((a) => /^-[a-zA-Z]*s[a-zA-Z]*$/.test(a))) return true;
|
|
829
|
+
if (options.some((a) => /^-[a-zA-Z]*[cem][a-zA-Z]*$/.test(a))) return false;
|
|
830
|
+
return !script;
|
|
831
|
+
}
|
|
832
|
+
function rankArgv(words, depth) {
|
|
833
|
+
if (depth > MAX_DEPTH) return leaf(ASK);
|
|
834
|
+
let i = 0;
|
|
835
|
+
while (i < words.length && (ASSIGNMENT.test(words[i].value) || RESERVED.has(words[i].value))) {
|
|
836
|
+
i++;
|
|
837
|
+
}
|
|
838
|
+
if (i >= words.length) return leaf(0);
|
|
839
|
+
const head = words[i];
|
|
840
|
+
const argWords = words.slice(i + 1);
|
|
841
|
+
if (head.dynamic) return leaf(ASK);
|
|
842
|
+
const base = baseName(head.value);
|
|
843
|
+
const args = argWords.map((w) => w.value);
|
|
844
|
+
const joined = [base, ...args].join(" ");
|
|
845
|
+
let rank = 0;
|
|
846
|
+
if (DESTRUCTIVE.some((re) => re.test(joined))) rank = ASK;
|
|
847
|
+
else if (DEPLOY.some((re) => re.test(joined))) rank = RANK.shell_deploy;
|
|
848
|
+
const done = (r, b = base, a = args) => leaf(Math.max(rank, r), b, a);
|
|
849
|
+
const inner = (from) => rankArgv(from, depth + 1);
|
|
850
|
+
const pathCheck = (values) => values.some((v) => AGENT_CONFIG_PATH.test(v)) ? ASK : 0;
|
|
851
|
+
if (base === "env") return done(rankEnv(argWords, depth));
|
|
852
|
+
if (base === "sudoedit") return done(pathCheck(args));
|
|
853
|
+
if (Object.hasOwn(WRAPPER_SPECS, base)) {
|
|
854
|
+
if (base === "sudo" && args.some((a) => /^-[a-zA-Z]*e[a-zA-Z]*$/.test(a))) {
|
|
855
|
+
return done(pathCheck(args));
|
|
856
|
+
}
|
|
857
|
+
if (base === "command" && args.some((a) => /^-[a-zA-Z]*[vV]/.test(a))) {
|
|
858
|
+
return done(0);
|
|
859
|
+
}
|
|
860
|
+
if (base === "flock") {
|
|
861
|
+
const c = args.findIndex((a) => a === "-c" || a === "--command");
|
|
862
|
+
if (c >= 0) return done(rankText(args[c + 1] ?? "", depth));
|
|
863
|
+
}
|
|
864
|
+
const { start, unknown } = skipOptions(argWords, WRAPPER_SPECS[base]);
|
|
865
|
+
let rest = argWords.slice(start);
|
|
866
|
+
if (base === "xargs" && rest.length === 0) rest = [literal("echo")];
|
|
867
|
+
const l = inner(rest);
|
|
868
|
+
if (base === "xargs" && BULK_DELETE.has(l.base)) l.rank = ASK;
|
|
869
|
+
const alt = unknown ? inner(argWords.slice(start + 1)).rank : 0;
|
|
870
|
+
return leaf(Math.max(rank, l.rank, alt), l.base, l.args);
|
|
871
|
+
}
|
|
872
|
+
if (base === "watch") {
|
|
873
|
+
const { start } = skipOptions(argWords, {
|
|
874
|
+
value: ["-n", "--interval", "-d", "-q", "--equexit"]
|
|
875
|
+
});
|
|
876
|
+
return done(rankText(args.slice(start).join(" "), depth));
|
|
877
|
+
}
|
|
878
|
+
if (SHELLS.has(base)) {
|
|
879
|
+
return done(rankShell(base, argWords, depth));
|
|
880
|
+
}
|
|
881
|
+
if (base === "su") {
|
|
882
|
+
const c = args.findIndex((a) => /^(-c|--command)$/.test(a));
|
|
883
|
+
const eq = args.find((a) => a.startsWith("--command="));
|
|
884
|
+
if (c >= 0) return done(rankText(args[c + 1] ?? "", depth));
|
|
885
|
+
if (eq) return done(rankText(eq.slice("--command=".length), depth));
|
|
886
|
+
return done(0);
|
|
887
|
+
}
|
|
888
|
+
if (base === "eval" || base === "iex" || base === "invoke-expression") {
|
|
889
|
+
return done(rankText(args.join(" "), depth));
|
|
890
|
+
}
|
|
891
|
+
if (base === "find") return done(rankFind(argWords, depth));
|
|
892
|
+
if (["npx", "bunx", "pnpx", "npm", "pnpm", "yarn"].includes(base)) {
|
|
893
|
+
const l = rankRunner(base, argWords, depth);
|
|
894
|
+
if (l) return leaf(Math.max(rank, l.rank), l.base, l.args);
|
|
895
|
+
return done(0);
|
|
896
|
+
}
|
|
897
|
+
if (base === "rm") {
|
|
898
|
+
const recursive = argsBeforeDashDash(args).some(
|
|
899
|
+
(a) => a === "--recursive" || /^-[a-zA-Z]*[rR]/.test(a)
|
|
900
|
+
);
|
|
901
|
+
return done(Math.max(recursive ? ASK : 0, pathCheck(args)));
|
|
902
|
+
}
|
|
903
|
+
if (base === "chmod") return done(Math.max(rankChmod(args), pathCheck(args)));
|
|
904
|
+
if (base === "git") return done(rankGit(argWords, depth));
|
|
905
|
+
if (base === "claude" || base === "codex" || base === "gemini") {
|
|
906
|
+
return done(rankAgentCli(base, args));
|
|
907
|
+
}
|
|
908
|
+
if (READERS.has(base)) return done(0);
|
|
909
|
+
return done(pathCheck(args));
|
|
910
|
+
}
|
|
911
|
+
function argsBeforeDashDash(args) {
|
|
912
|
+
const end = args.indexOf("--");
|
|
913
|
+
return end < 0 ? args : args.slice(0, end);
|
|
914
|
+
}
|
|
915
|
+
function rankEnv(argWords, depth) {
|
|
916
|
+
let i = 0;
|
|
917
|
+
while (i < argWords.length) {
|
|
918
|
+
const a = argWords[i].value;
|
|
919
|
+
if (a === "--") {
|
|
920
|
+
i++;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
if (a === "-S" || a === "--split-string") {
|
|
924
|
+
const text = [a, ...argWords.slice(i + 1).map((w) => w.value)].slice(1).join(" ");
|
|
925
|
+
return rankText(text, depth);
|
|
926
|
+
}
|
|
927
|
+
if (a.startsWith("--split-string=") || /^-S./.test(a)) {
|
|
928
|
+
const first = a.startsWith("--") ? a.slice(15) : a.slice(2);
|
|
929
|
+
return rankText(
|
|
930
|
+
[first, ...argWords.slice(i + 1).map((w) => w.value)].join(" "),
|
|
931
|
+
depth
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
if (/^(-u|-C|-P|--unset|--chdir)$/.test(a)) {
|
|
935
|
+
i += 2;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (a.startsWith("-") && a.length > 1) {
|
|
939
|
+
i++;
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
return rankArgv(argWords.slice(i), depth + 1).rank;
|
|
945
|
+
}
|
|
946
|
+
function rankShell(base, argWords, depth) {
|
|
947
|
+
let code = false;
|
|
948
|
+
for (let i = 0; i < argWords.length; i++) {
|
|
949
|
+
const a = argWords[i].value;
|
|
950
|
+
if (a === "--") break;
|
|
951
|
+
if (/^(--rcfile|--init-file|-o|\+o|-O|\+O)$/.test(a)) {
|
|
952
|
+
i++;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (a === "--command" || base === "fish" && a === "-c") {
|
|
956
|
+
return rankText(argWords[i + 1]?.value ?? "", depth);
|
|
957
|
+
}
|
|
958
|
+
if (/^[-+][a-zA-Z]+$/.test(a)) {
|
|
959
|
+
if (/^-[a-zA-Z]*c/.test(a)) code = true;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (a.startsWith("-")) continue;
|
|
963
|
+
if (code) {
|
|
964
|
+
if (base === "busybox")
|
|
965
|
+
return rankArgv(argWords.slice(i), depth + 1).rank;
|
|
966
|
+
return rankText(a, depth);
|
|
967
|
+
}
|
|
968
|
+
if (base === "busybox") return rankArgv(argWords.slice(i), depth + 1).rank;
|
|
969
|
+
return 0;
|
|
970
|
+
}
|
|
971
|
+
return 0;
|
|
972
|
+
}
|
|
973
|
+
function rankFind(argWords, depth) {
|
|
974
|
+
let rank = 0;
|
|
975
|
+
for (let i = 0; i < argWords.length; i++) {
|
|
976
|
+
const a = argWords[i].value;
|
|
977
|
+
if (a === "-delete") rank = ASK;
|
|
978
|
+
if (/^-(exec|execdir|ok|okdir)$/.test(a)) {
|
|
979
|
+
let end = i + 1;
|
|
980
|
+
while (end < argWords.length && argWords[end].value !== ";" && argWords[end].value !== "+") {
|
|
981
|
+
end++;
|
|
982
|
+
}
|
|
983
|
+
const l = rankArgv(argWords.slice(i + 1, end), depth + 1);
|
|
984
|
+
rank = Math.max(rank, BULK_DELETE.has(l.base) ? ASK : l.rank);
|
|
985
|
+
i = end;
|
|
986
|
+
}
|
|
987
|
+
if (/^-f(print|print0|printf|ls)$/.test(a)) {
|
|
988
|
+
if (AGENT_CONFIG_PATH.test(argWords[i + 1]?.value ?? "")) rank = ASK;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return rank;
|
|
992
|
+
}
|
|
993
|
+
function rankRunner(base, argWords, depth) {
|
|
994
|
+
let rest = argWords;
|
|
995
|
+
if (base === "npm" || base === "pnpm" || base === "yarn") {
|
|
996
|
+
const sub = rest[0]?.value;
|
|
997
|
+
if (!["exec", "x", "dlx"].includes(sub ?? "")) return null;
|
|
998
|
+
rest = rest.slice(1);
|
|
999
|
+
}
|
|
1000
|
+
let i = 0;
|
|
1001
|
+
while (i < rest.length) {
|
|
1002
|
+
const a = rest[i].value;
|
|
1003
|
+
if (a === "--") {
|
|
1004
|
+
i++;
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
if (a === "-c" || a === "--call")
|
|
1008
|
+
return leaf(rankText(rest[i + 1]?.value ?? "", depth));
|
|
1009
|
+
if (a.startsWith("--call=")) return leaf(rankText(a.slice(7), depth));
|
|
1010
|
+
if (a === "-p" || a === "--package") {
|
|
1011
|
+
i += 2;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (a.startsWith("-")) {
|
|
1015
|
+
i++;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
const pkg = rest[i];
|
|
1021
|
+
if (!pkg) return null;
|
|
1022
|
+
if (pkg.dynamic) return leaf(ASK);
|
|
1023
|
+
const name = /^(?:@[^/]+\/)?([^@]+)(?:@.*)?$/.exec(pkg.value)?.[1] ?? pkg.value;
|
|
1024
|
+
const bin = name === "claude-code" ? "claude" : name;
|
|
1025
|
+
return rankArgv([literal(bin), ...rest.slice(i + 1)], depth + 1);
|
|
1026
|
+
}
|
|
1027
|
+
function rankChmod(args) {
|
|
1028
|
+
const opts = argsBeforeDashDash(args);
|
|
1029
|
+
const recursive = opts.some(
|
|
1030
|
+
(a) => a === "--recursive" || /^-[a-zA-Z]*R/.test(a)
|
|
1031
|
+
);
|
|
1032
|
+
if (!recursive) return 0;
|
|
1033
|
+
const modes = args.filter(
|
|
1034
|
+
(a) => !a.startsWith("-") || /^-[rwxXst]+$/.test(a)
|
|
1035
|
+
);
|
|
1036
|
+
const worldWritable = modes.some(
|
|
1037
|
+
(m) => /^0*[0-7]?[0-7]?[0-7]?[2367]$/.test(m) && m.length >= 3 || /(^|,)[ugo]*[ao][ugo]*[+=][rwxXst]*w/.test(m)
|
|
1038
|
+
);
|
|
1039
|
+
return worldWritable ? ASK : 0;
|
|
1040
|
+
}
|
|
1041
|
+
var GIT_VALUE_OPTS = /* @__PURE__ */ new Set([
|
|
1042
|
+
"-C",
|
|
1043
|
+
"-c",
|
|
1044
|
+
"--git-dir",
|
|
1045
|
+
"--work-tree",
|
|
1046
|
+
"--namespace",
|
|
1047
|
+
"--super-prefix",
|
|
1048
|
+
"--config-env",
|
|
1049
|
+
"--exec-path",
|
|
1050
|
+
"--list-cmds",
|
|
1051
|
+
"--attr-source"
|
|
1052
|
+
]);
|
|
1053
|
+
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;
|
|
1054
|
+
function rankGit(argWords, depth) {
|
|
1055
|
+
const args = argWords.map((w) => w.value);
|
|
1056
|
+
let rank = 0;
|
|
1057
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1058
|
+
let i = 0;
|
|
1059
|
+
while (i < args.length) {
|
|
1060
|
+
const a = args[i];
|
|
1061
|
+
if (!a.startsWith("-")) break;
|
|
1062
|
+
let opt = a;
|
|
1063
|
+
let val;
|
|
1064
|
+
if (a.startsWith("--") && a.includes("=")) {
|
|
1065
|
+
opt = a.slice(0, a.indexOf("="));
|
|
1066
|
+
val = a.slice(a.indexOf("=") + 1);
|
|
1067
|
+
i++;
|
|
1068
|
+
} else if (GIT_VALUE_OPTS.has(a)) {
|
|
1069
|
+
val = args[i + 1] ?? "";
|
|
1070
|
+
if (argWords[i + 1]?.dynamic && (a === "-c" || a === "--config-env")) {
|
|
1071
|
+
rank = ASK;
|
|
1072
|
+
}
|
|
1073
|
+
i += 2;
|
|
1074
|
+
} else {
|
|
1075
|
+
i++;
|
|
1076
|
+
}
|
|
1077
|
+
if (opt === "-c" && val !== void 0) {
|
|
1078
|
+
const eq = val.indexOf("=");
|
|
1079
|
+
const key = (eq < 0 ? val : val.slice(0, eq)).toLowerCase();
|
|
1080
|
+
const value = eq < 0 ? "" : val.slice(eq + 1);
|
|
1081
|
+
if (key.startsWith("alias.")) aliases.set(key.slice(6), value);
|
|
1082
|
+
else if (key === "core.hookspath" || key === "core.fsmonitor") {
|
|
1083
|
+
if (!/^(false|0|no|off|)$/i.test(value)) rank = ASK;
|
|
1084
|
+
} else if (GIT_EXEC_KEY.test(key)) {
|
|
1085
|
+
rank = Math.max(rank, rankText(value, depth));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (opt === "--config-env" && val !== void 0) {
|
|
1089
|
+
const key = val.split("=")[0].toLowerCase();
|
|
1090
|
+
if (key.startsWith("alias.") || GIT_EXEC_KEY.test(key) || /hookspath|fsmonitor/.test(key)) {
|
|
1091
|
+
rank = ASK;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (i >= args.length) return rank;
|
|
1096
|
+
if (argWords[i].dynamic) return ASK;
|
|
1097
|
+
const sub = args[i];
|
|
1098
|
+
const rest = args.slice(i + 1);
|
|
1099
|
+
const alias = aliases.get(sub.toLowerCase());
|
|
1100
|
+
if (alias !== void 0) {
|
|
1101
|
+
if (alias.startsWith("!")) {
|
|
1102
|
+
return Math.max(
|
|
1103
|
+
rank,
|
|
1104
|
+
rankText(`${alias.slice(1)} ${rest.join(" ")}`, depth)
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const expanded = parseShell(alias);
|
|
1108
|
+
const aliasWords = expanded.commands[0]?.words ?? [];
|
|
1109
|
+
return Math.max(
|
|
1110
|
+
rank,
|
|
1111
|
+
expanded.ambiguous ? ASK : 0,
|
|
1112
|
+
rankGit([...aliasWords, ...argWords.slice(i + 1)], depth + 1)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const opts = argsBeforeDashDash(rest);
|
|
1116
|
+
const has = (...names) => opts.some((a) => names.includes(a.split("=")[0]));
|
|
1117
|
+
const short = (letter) => opts.some((a) => new RegExp(`^-[a-zA-Z]*${letter}`).test(a));
|
|
1118
|
+
let sr = 0;
|
|
1119
|
+
switch (sub) {
|
|
1120
|
+
case "push": {
|
|
1121
|
+
const forced = has(
|
|
1122
|
+
"--force",
|
|
1123
|
+
"--force-with-lease",
|
|
1124
|
+
"--force-if-includes",
|
|
1125
|
+
"--mirror",
|
|
1126
|
+
"--delete",
|
|
1127
|
+
"--prune"
|
|
1128
|
+
) || opts.some(
|
|
1129
|
+
(a) => /^-[a-zA-Z]+$/.test(a) && /[fd]/.test(a.replace(/o.*$/, ""))
|
|
1130
|
+
) || rest.some((a) => /^[+:]/.test(a));
|
|
1131
|
+
sr = forced ? ASK : RANK.git_push;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
case "reset":
|
|
1135
|
+
sr = has("--hard", "--merge") ? ASK : 0;
|
|
1136
|
+
break;
|
|
1137
|
+
case "clean":
|
|
1138
|
+
sr = has("--force") || short("f") ? ASK : 0;
|
|
1139
|
+
break;
|
|
1140
|
+
case "branch":
|
|
1141
|
+
sr = short("D") || (has("--delete") || short("d")) && (has("--force") || short("f")) ? ASK : 0;
|
|
1142
|
+
break;
|
|
1143
|
+
case "checkout":
|
|
1144
|
+
sr = rest.includes("--") || has("--force") || short("f") ? ASK : 0;
|
|
1145
|
+
break;
|
|
1146
|
+
case "switch":
|
|
1147
|
+
sr = has("--discard-changes", "--force") || short("f") ? ASK : 0;
|
|
1148
|
+
break;
|
|
1149
|
+
case "restore":
|
|
1150
|
+
sr = has("--source") || short("s") ? ASK : 0;
|
|
1151
|
+
break;
|
|
1152
|
+
case "stash":
|
|
1153
|
+
sr = ["drop", "clear"].includes(rest[0] ?? "") ? ASK : 0;
|
|
1154
|
+
break;
|
|
1155
|
+
case "update-ref":
|
|
1156
|
+
sr = short("d") ? ASK : 0;
|
|
1157
|
+
break;
|
|
1158
|
+
case "reflog":
|
|
1159
|
+
sr = ["expire", "delete"].includes(rest[0] ?? "") ? ASK : 0;
|
|
1160
|
+
break;
|
|
1161
|
+
case "filter-branch":
|
|
1162
|
+
case "filter-repo":
|
|
1163
|
+
sr = ASK;
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
if (!GIT_READS.has(sub) && rest.some((a) => AGENT_CONFIG_PATH.test(a)))
|
|
1167
|
+
sr = ASK;
|
|
1168
|
+
return Math.max(rank, sr);
|
|
1169
|
+
}
|
|
1170
|
+
var AGENT_CONFIG_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1171
|
+
"mcp",
|
|
1172
|
+
"plugin",
|
|
1173
|
+
"plugins",
|
|
1174
|
+
"config"
|
|
1175
|
+
]);
|
|
1176
|
+
var AGENT_READ_ACTIONS = /* @__PURE__ */ new Set([
|
|
1177
|
+
"list",
|
|
1178
|
+
"ls",
|
|
1179
|
+
"get",
|
|
1180
|
+
"show",
|
|
1181
|
+
"serve",
|
|
1182
|
+
"help",
|
|
1183
|
+
"--help",
|
|
1184
|
+
"-h"
|
|
1185
|
+
]);
|
|
1186
|
+
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;
|
|
1187
|
+
function rankAgentCli(base, args) {
|
|
1188
|
+
if (args.some((a) => AGENT_WIDENING_OPTS.test(a))) return ASK;
|
|
1189
|
+
if (base === "codex" && args.includes("-c")) return ASK;
|
|
1190
|
+
const mode = args.findIndex((a) => a === "--permission-mode");
|
|
1191
|
+
const modeValue = mode >= 0 ? args[mode + 1] : args.find((a) => a.startsWith("--permission-mode="))?.slice(18);
|
|
1192
|
+
if (modeValue !== void 0 && !/^(default|plan)$/i.test(modeValue))
|
|
1193
|
+
return ASK;
|
|
1194
|
+
const positionals2 = args.filter((a) => !a.startsWith("-"));
|
|
1195
|
+
const sub = positionals2.findIndex((p) => AGENT_CONFIG_SUBCOMMANDS.has(p));
|
|
1196
|
+
if (sub < 0) return 0;
|
|
1197
|
+
const action = positionals2[sub + 1];
|
|
1198
|
+
if (action === void 0 || AGENT_READ_ACTIONS.has(action)) return 0;
|
|
1199
|
+
return ASK;
|
|
1200
|
+
}
|
|
70
1201
|
function classifyCommand(command) {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
if (
|
|
74
|
-
|
|
1202
|
+
let rank = 0;
|
|
1203
|
+
if (DESTRUCTIVE.some((re) => re.test(command))) rank = ASK;
|
|
1204
|
+
else if (DEPLOY.some((re) => re.test(command))) rank = RANK.shell_deploy;
|
|
1205
|
+
else if (GIT_PUSH.test(command)) rank = RANK.git_push;
|
|
1206
|
+
else if (startsInteractiveSession(command)) rank = RANK.shell_session;
|
|
1207
|
+
if (rank < ASK) rank = Math.max(rank, rankText(command, 0));
|
|
1208
|
+
return BY_RANK[rank];
|
|
1209
|
+
}
|
|
1210
|
+
function isAgentConfigPath(path) {
|
|
1211
|
+
return AGENT_CONFIG_PATH.test(path);
|
|
75
1212
|
}
|
|
76
1213
|
function mcpToolName(server, tool) {
|
|
77
1214
|
const part = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^(\d)/, "x$1") || "x";
|
|
@@ -86,34 +1223,124 @@ function digest(text) {
|
|
|
86
1223
|
function str(value) {
|
|
87
1224
|
return typeof value === "string" ? value : "";
|
|
88
1225
|
}
|
|
1226
|
+
var SHELL_TOOLS = {
|
|
1227
|
+
Bash: "command",
|
|
1228
|
+
// Claude Code; Codex shell and unified exec
|
|
1229
|
+
shell: "command",
|
|
1230
|
+
bash: "command",
|
|
1231
|
+
// DeepSeek Harness (plain and persistent)
|
|
1232
|
+
pwsh: "command",
|
|
1233
|
+
// DeepSeek Harness on Windows
|
|
1234
|
+
terminal_send: "text"
|
|
1235
|
+
// DeepSeek Harness PTY: typed input runs as a command
|
|
1236
|
+
};
|
|
1237
|
+
var FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
1238
|
+
"Write",
|
|
1239
|
+
"Edit",
|
|
1240
|
+
"MultiEdit",
|
|
1241
|
+
"NotebookEdit",
|
|
1242
|
+
"write",
|
|
1243
|
+
// DeepSeek Harness
|
|
1244
|
+
"edit"
|
|
1245
|
+
// DeepSeek Harness
|
|
1246
|
+
]);
|
|
1247
|
+
function commandText(value) {
|
|
1248
|
+
if (typeof value === "string") return value;
|
|
1249
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
1250
|
+
return value.join(" ");
|
|
1251
|
+
}
|
|
1252
|
+
return "";
|
|
1253
|
+
}
|
|
1254
|
+
var MAX_PATHS = 50;
|
|
1255
|
+
function patchTargets(patch) {
|
|
1256
|
+
const targets = [];
|
|
1257
|
+
const re = /^[ \t]*\*\*\* (Add File|Update File|Delete File|Move to): (.+?)[ \t\r]*$/gm;
|
|
1258
|
+
for (const m of patch.matchAll(re)) {
|
|
1259
|
+
const kind = m[1];
|
|
1260
|
+
const op = kind === "Add File" ? "add" : kind === "Update File" ? "update" : kind === "Delete File" ? "delete" : "move";
|
|
1261
|
+
targets.push({ op, path: m[2] });
|
|
1262
|
+
}
|
|
1263
|
+
return targets;
|
|
1264
|
+
}
|
|
1265
|
+
var AGENT_CONFIG_NOTE = "(agent MCP, hook, permission or plugin config)";
|
|
1266
|
+
function fileWrite(operation, path, content, cwd, summaryVerb) {
|
|
1267
|
+
const config = isAgentConfigPath(path);
|
|
1268
|
+
return {
|
|
1269
|
+
toolName: config ? "shell_destructive" : "file_write",
|
|
1270
|
+
arguments: {
|
|
1271
|
+
operation,
|
|
1272
|
+
path,
|
|
1273
|
+
contentBytes: Buffer.byteLength(content),
|
|
1274
|
+
contentDigest: digest(content),
|
|
1275
|
+
...config ? { touches: "agent-config" } : {},
|
|
1276
|
+
...cwd
|
|
1277
|
+
},
|
|
1278
|
+
summary: `${summaryVerb} ${path}${config ? ` ${AGENT_CONFIG_NOTE}` : ""}`
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
89
1281
|
function governedCall(call) {
|
|
90
1282
|
const { tool, input } = call;
|
|
91
1283
|
const cwd = call.cwd ? { cwd: call.cwd } : {};
|
|
92
|
-
|
|
93
|
-
|
|
1284
|
+
const shellField = Object.hasOwn(SHELL_TOOLS, tool) ? SHELL_TOOLS[tool] : void 0;
|
|
1285
|
+
if (shellField) {
|
|
1286
|
+
const command = commandText(input[shellField]);
|
|
94
1287
|
const toolName = classifyCommand(command);
|
|
95
1288
|
return {
|
|
96
1289
|
toolName,
|
|
97
|
-
arguments: {
|
|
1290
|
+
arguments: {
|
|
1291
|
+
command: bounded(command),
|
|
1292
|
+
...tool === "Bash" || tool === "shell" ? {} : { agentTool: tool },
|
|
1293
|
+
...cwd
|
|
1294
|
+
},
|
|
98
1295
|
summary: `Run: ${bounded(command, 160)}`
|
|
99
1296
|
};
|
|
100
1297
|
}
|
|
101
|
-
if (
|
|
1298
|
+
if (FILE_TOOLS.has(tool)) {
|
|
102
1299
|
const path = str(input.file_path) || str(input.notebook_path);
|
|
103
1300
|
const content = str(input.content) || str(input.new_string) || str(input.new_source) || (Array.isArray(input.edits) ? JSON.stringify(input.edits) : "");
|
|
1301
|
+
return fileWrite(
|
|
1302
|
+
tool,
|
|
1303
|
+
path,
|
|
1304
|
+
content,
|
|
1305
|
+
cwd,
|
|
1306
|
+
tool === "Write" || tool === "write" ? "Write" : "Edit"
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
if (tool === "str_replace_editor") {
|
|
1310
|
+
if (input.command === "view") return null;
|
|
1311
|
+
const sub = str(input.command) || "unknown";
|
|
1312
|
+
const content = str(input.file_text) || str(input.new_str);
|
|
1313
|
+
return fileWrite(
|
|
1314
|
+
`str_replace_editor.${sub}`,
|
|
1315
|
+
str(input.path),
|
|
1316
|
+
content,
|
|
1317
|
+
cwd,
|
|
1318
|
+
sub === "create" ? "Write" : "Edit"
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
if (tool === "apply_patch") {
|
|
1322
|
+
const patch = commandText(input.command) || str(input.patch) || str(input.input);
|
|
1323
|
+
const targets = patchTargets(patch);
|
|
1324
|
+
const paths = [...new Set(targets.map((t) => t.path))];
|
|
1325
|
+
const shown = paths.slice(0, MAX_PATHS).map((p) => bounded(p, 500));
|
|
1326
|
+
const config = paths.some((p) => isAgentConfigPath(p));
|
|
104
1327
|
return {
|
|
105
|
-
toolName: "file_write",
|
|
1328
|
+
toolName: config ? "shell_destructive" : "file_write",
|
|
106
1329
|
arguments: {
|
|
107
|
-
operation:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1330
|
+
operation: "apply_patch",
|
|
1331
|
+
...config ? { touches: "agent-config" } : {},
|
|
1332
|
+
path: shown[0] ?? "",
|
|
1333
|
+
paths: shown,
|
|
1334
|
+
...paths.length > MAX_PATHS ? { morePaths: paths.length - MAX_PATHS } : {},
|
|
1335
|
+
operations: targets.slice(0, MAX_PATHS).map((t) => `${t.op} ${bounded(t.path, 500)}`),
|
|
1336
|
+
contentBytes: Buffer.byteLength(patch),
|
|
1337
|
+
contentDigest: digest(patch),
|
|
111
1338
|
...cwd
|
|
112
1339
|
},
|
|
113
|
-
summary:
|
|
1340
|
+
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}` : "")
|
|
114
1341
|
};
|
|
115
1342
|
}
|
|
116
|
-
if (tool === "WebFetch") {
|
|
1343
|
+
if (tool === "WebFetch" || tool === "web_fetch") {
|
|
117
1344
|
const url = str(input.url);
|
|
118
1345
|
return {
|
|
119
1346
|
toolName: "web_fetch",
|
|
@@ -164,6 +1391,7 @@ function denied(call, code, extra = {}) {
|
|
|
164
1391
|
...extra
|
|
165
1392
|
};
|
|
166
1393
|
}
|
|
1394
|
+
var FINAL_CHECK_LEAD_MS = 1500;
|
|
167
1395
|
var isPending = (r) => !r.ok && (r.reasonCode === "APPROVAL_REQUIRED" || r.reasonCode === "APPROVAL_PENDING") || r.ok && r.decision === "ASK";
|
|
168
1396
|
async function decide(call, options) {
|
|
169
1397
|
const wait = options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
@@ -183,6 +1411,7 @@ async function decide(call, options) {
|
|
|
183
1411
|
} catch {
|
|
184
1412
|
return denied(call, "SERVICE_UNAVAILABLE");
|
|
185
1413
|
}
|
|
1414
|
+
const answeredAt = now();
|
|
186
1415
|
const requestId = result.requestId;
|
|
187
1416
|
let approvedByHuman = false;
|
|
188
1417
|
if (isPending(result)) {
|
|
@@ -190,14 +1419,19 @@ async function decide(call, options) {
|
|
|
190
1419
|
log(
|
|
191
1420
|
`Guardian: "${call.summary}" is held for approval (request ${requestId}). Approve it on your phone or in the console.`
|
|
192
1421
|
);
|
|
193
|
-
const
|
|
1422
|
+
const window = Math.min(
|
|
1423
|
+
options.approvalTimeoutMs,
|
|
1424
|
+
options.requestTtlMs ?? options.approvalTimeoutMs
|
|
1425
|
+
);
|
|
1426
|
+
const lead = Math.min(FINAL_CHECK_LEAD_MS, Math.floor(window / 4));
|
|
1427
|
+
const lastCheckAt = answeredAt + window - lead;
|
|
194
1428
|
const maxInterval = Math.max(
|
|
195
1429
|
options.maxPollIntervalMs ?? 1e4,
|
|
196
1430
|
options.pollIntervalMs
|
|
197
1431
|
);
|
|
198
1432
|
let interval = options.pollIntervalMs;
|
|
199
1433
|
for (; ; ) {
|
|
200
|
-
await wait(interval);
|
|
1434
|
+
await wait(Math.max(0, Math.min(interval, lastCheckAt - now())));
|
|
201
1435
|
interval = Math.min(Math.ceil(interval * 1.5), maxInterval);
|
|
202
1436
|
let claimed;
|
|
203
1437
|
try {
|
|
@@ -210,7 +1444,7 @@ async function decide(call, options) {
|
|
|
210
1444
|
approvedByHuman = claimed.ok && claimed.decision === "ALLOW";
|
|
211
1445
|
break;
|
|
212
1446
|
}
|
|
213
|
-
if (now() >=
|
|
1447
|
+
if (now() >= lastCheckAt) {
|
|
214
1448
|
return {
|
|
215
1449
|
decision: "deny",
|
|
216
1450
|
reason: `Guardian is still waiting for a human to approve ${call.toolName} (request ${requestId}). Nothing ran. Tell the developer it needs approval in the Guardian app, then try again.`,
|
|
@@ -256,6 +1490,39 @@ async function decide(call, options) {
|
|
|
256
1490
|
var DEFAULT_BASE_URL = "https://guardian-api.odingard.com";
|
|
257
1491
|
var REQUEST_TTL_MS = 3e5;
|
|
258
1492
|
var MAX_WAIT_MS = REQUEST_TTL_MS;
|
|
1493
|
+
var DEFAULT_DEADLINE_MS = 54e4;
|
|
1494
|
+
var AGENT_NAMES = {
|
|
1495
|
+
"claude-code": "Claude Code",
|
|
1496
|
+
codex: "Codex",
|
|
1497
|
+
dsh: "DeepSeek Harness"
|
|
1498
|
+
};
|
|
1499
|
+
function formatVerdict(dialect, verdict) {
|
|
1500
|
+
if (verdict === null) {
|
|
1501
|
+
return { stdout: "", stderr: "", exitCode: 0, verdict };
|
|
1502
|
+
}
|
|
1503
|
+
if (dialect === "claude-code") {
|
|
1504
|
+
return {
|
|
1505
|
+
stdout: claudeCodeOutput(verdict),
|
|
1506
|
+
stderr: "",
|
|
1507
|
+
exitCode: 0,
|
|
1508
|
+
verdict
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
if (verdict.decision === "allow") {
|
|
1512
|
+
return {
|
|
1513
|
+
stdout: dialect === "codex" ? "" : claudeCodeOutput(verdict),
|
|
1514
|
+
stderr: "",
|
|
1515
|
+
exitCode: 0,
|
|
1516
|
+
verdict
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
return {
|
|
1520
|
+
stdout: claudeCodeOutput(verdict),
|
|
1521
|
+
stderr: verdict.reason,
|
|
1522
|
+
exitCode: 2,
|
|
1523
|
+
verdict
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
259
1526
|
function claudeCodeOutput(verdict) {
|
|
260
1527
|
return JSON.stringify({
|
|
261
1528
|
hookSpecificOutput: {
|
|
@@ -271,16 +1538,53 @@ function connectionFor(toolName, env) {
|
|
|
271
1538
|
return env.GUARDIAN_HOOK_ASK_CONNECTION_ID?.trim() || allow;
|
|
272
1539
|
}
|
|
273
1540
|
async function runHook(stdin, env, deps = {}) {
|
|
274
|
-
|
|
1541
|
+
return (await runHookAs("claude-code", stdin, env, deps)).stdout;
|
|
1542
|
+
}
|
|
1543
|
+
async function runHookAs(dialect, stdin, env, deps = {}) {
|
|
1544
|
+
const requested = Number(
|
|
1545
|
+
env.GUARDIAN_HOOK_DEADLINE_MS ?? DEFAULT_DEADLINE_MS
|
|
1546
|
+
);
|
|
1547
|
+
const deadlineMs = Number.isFinite(requested) && requested > 0 ? Math.min(requested, DEFAULT_DEADLINE_MS) : DEFAULT_DEADLINE_MS;
|
|
1548
|
+
let timer;
|
|
1549
|
+
const timedOut = new Promise((resolve) => {
|
|
1550
|
+
timer = setTimeout(
|
|
1551
|
+
() => resolve({
|
|
1552
|
+
decision: "deny",
|
|
1553
|
+
reason: "Guardian hook: no decision before the hook's deadline. Nothing ran."
|
|
1554
|
+
}),
|
|
1555
|
+
deadlineMs
|
|
1556
|
+
);
|
|
1557
|
+
timer.unref?.();
|
|
1558
|
+
});
|
|
1559
|
+
try {
|
|
1560
|
+
const verdict = await Promise.race([
|
|
1561
|
+
evaluate(dialect, stdin, env, deps, deadlineMs).catch(
|
|
1562
|
+
() => ({
|
|
1563
|
+
decision: "deny",
|
|
1564
|
+
reason: "Guardian hook: crashed. Nothing ran."
|
|
1565
|
+
})
|
|
1566
|
+
),
|
|
1567
|
+
timedOut
|
|
1568
|
+
]);
|
|
1569
|
+
return formatVerdict(dialect, verdict);
|
|
1570
|
+
} finally {
|
|
1571
|
+
clearTimeout(timer);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
async function evaluate(dialect, stdinText, env, deps, deadlineMs) {
|
|
1575
|
+
const failClosed = (reason) => ({
|
|
1576
|
+
decision: "deny",
|
|
1577
|
+
reason: `Guardian hook: ${reason}`
|
|
1578
|
+
});
|
|
275
1579
|
let input;
|
|
276
1580
|
try {
|
|
277
|
-
input = JSON.parse(
|
|
1581
|
+
input = JSON.parse(await stdinText);
|
|
278
1582
|
} catch {
|
|
279
1583
|
return failClosed(
|
|
280
1584
|
"could not read the tool call from the agent. Nothing ran."
|
|
281
1585
|
);
|
|
282
1586
|
}
|
|
283
|
-
if (typeof input.tool_name !== "string") {
|
|
1587
|
+
if (!input || typeof input.tool_name !== "string") {
|
|
284
1588
|
return failClosed("the tool call had no tool name. Nothing ran.");
|
|
285
1589
|
}
|
|
286
1590
|
const call = governedCall({
|
|
@@ -288,7 +1592,7 @@ async function runHook(stdin, env, deps = {}) {
|
|
|
288
1592
|
input: input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {},
|
|
289
1593
|
...typeof input.cwd === "string" ? { cwd: input.cwd } : {}
|
|
290
1594
|
});
|
|
291
|
-
if (call === null) return
|
|
1595
|
+
if (call === null) return null;
|
|
292
1596
|
const connectionId = connectionFor(call.toolName, env);
|
|
293
1597
|
const credential = env.GUARDIAN_AGENT_CREDENTIAL?.trim();
|
|
294
1598
|
const baseUrl = (env.GUARDIAN_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(
|
|
@@ -297,7 +1601,7 @@ async function runHook(stdin, env, deps = {}) {
|
|
|
297
1601
|
);
|
|
298
1602
|
if (!deps.client && (!credential || !connectionId)) {
|
|
299
1603
|
return failClosed(
|
|
300
|
-
|
|
1604
|
+
`GUARDIAN_AGENT_CREDENTIAL and GUARDIAN_CONNECTION_ID must be set (run \`${dialect === "claude-code" ? "npx @odingard/guardian-hook" : `guardian-hook ${dialect}`} setup\`). Nothing ran.`
|
|
301
1605
|
);
|
|
302
1606
|
}
|
|
303
1607
|
if (!deps.client && !baseUrl.startsWith("https://")) {
|
|
@@ -306,15 +1610,16 @@ async function runHook(stdin, env, deps = {}) {
|
|
|
306
1610
|
const requested = Number(
|
|
307
1611
|
env.GUARDIAN_HOOK_APPROVAL_TIMEOUT_MS ?? MAX_WAIT_MS
|
|
308
1612
|
);
|
|
309
|
-
const
|
|
310
|
-
const
|
|
1613
|
+
const ceiling = Math.max(0, Math.min(MAX_WAIT_MS, deadlineMs - 3e4));
|
|
1614
|
+
const approvalTimeoutMs = Number.isFinite(requested) ? Math.min(Math.max(requested, 0), ceiling) : ceiling;
|
|
1615
|
+
return decide(call, {
|
|
311
1616
|
client: deps.client ?? createGuardianClient({
|
|
312
1617
|
baseUrl,
|
|
313
1618
|
credential,
|
|
314
1619
|
...env.GUARDIAN_ADAPTER_ID ? { adapterId: env.GUARDIAN_ADAPTER_ID } : {}
|
|
315
1620
|
}),
|
|
316
1621
|
connectionId: connectionId ?? "",
|
|
317
|
-
agentName: env.GUARDIAN_HOOK_AGENT_NAME?.trim() ||
|
|
1622
|
+
agentName: env.GUARDIAN_HOOK_AGENT_NAME?.trim() || AGENT_NAMES[dialect],
|
|
318
1623
|
approvalTimeoutMs,
|
|
319
1624
|
requestTtlMs: REQUEST_TTL_MS,
|
|
320
1625
|
pollIntervalMs: 2e3,
|
|
@@ -322,7 +1627,6 @@ async function runHook(stdin, env, deps = {}) {
|
|
|
322
1627
|
...deps.now ? { now: deps.now } : {},
|
|
323
1628
|
...deps.log ? { log: deps.log } : {}
|
|
324
1629
|
});
|
|
325
|
-
return claudeCodeOutput(verdict);
|
|
326
1630
|
}
|
|
327
1631
|
|
|
328
1632
|
// src/agent-sdk.ts
|