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