prae-cli 0.10.0 → 0.10.2

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.
@@ -0,0 +1,1691 @@
1
+ //#region ../kernel/src/command.ts
2
+ /** Wrappers whose first operand is the program, and their options that take a value. */
3
+ const WRAPPERS = new Map(Object.entries({
4
+ sudo: [
5
+ "-u",
6
+ "-g",
7
+ "-p",
8
+ "-h",
9
+ "-C",
10
+ "-r",
11
+ "-t",
12
+ "-U",
13
+ "-D",
14
+ "-T",
15
+ "-R"
16
+ ],
17
+ doas: ["-u", "-C"],
18
+ env: [
19
+ "-u",
20
+ "-C",
21
+ "-P",
22
+ "--unset",
23
+ "--chdir"
24
+ ],
25
+ command: [],
26
+ builtin: [],
27
+ exec: ["-a"],
28
+ nohup: [],
29
+ time: ["-f", "-o"],
30
+ nice: ["-n"],
31
+ timeout: [
32
+ "-s",
33
+ "-k",
34
+ "--signal",
35
+ "--kill-after"
36
+ ],
37
+ setsid: [],
38
+ stdbuf: [
39
+ "-i",
40
+ "-o",
41
+ "-e"
42
+ ],
43
+ xargs: [
44
+ "-I",
45
+ "-n",
46
+ "-P",
47
+ "-L",
48
+ "-s",
49
+ "-d",
50
+ "-E",
51
+ "-a",
52
+ "--arg-file"
53
+ ]
54
+ }).map(([name, options]) => [name, new Set(options)]));
55
+ const SHELLS = new Set([
56
+ "sh",
57
+ "bash",
58
+ "zsh",
59
+ "dash",
60
+ "ksh",
61
+ "mksh",
62
+ "ash"
63
+ ]);
64
+ /** Words that open or continue a compound command, not a program. */
65
+ const RESERVED = new Set([
66
+ "!",
67
+ "if",
68
+ "then",
69
+ "elif",
70
+ "else",
71
+ "fi",
72
+ "while",
73
+ "until",
74
+ "do",
75
+ "done"
76
+ ]);
77
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
78
+ /** Scripts inside scripts (`bash -c`, `eval`, backticks, here-documents). */
79
+ const MAX_DEPTH = 8;
80
+ /** Brackets inside one script. */
81
+ const MAX_NESTING = 64;
82
+ /** A part of the line that cannot be read with confidence. */
83
+ var Undecomposable = class extends Error {};
84
+ const basename = (word) => (word.split("/").pop() ?? word).toLowerCase();
85
+ const isBlank = (c) => c === " " || c === " " || c === "\r";
86
+ const isSpecial = (c) => /^[0-9@*#?$!-]$/.test(c);
87
+ const expands = (w) => /[*?]|\[.*\]|\{.*(,|\.\.).*\}/.test(w.bare);
88
+ /**
89
+ * Read one script into `out`. `expansionsOnly` reads it as the body of an
90
+ * unquoted here-document: text, in which only substitutions run.
91
+ */
92
+ function read(src, out, depth, expansionsOnly = false) {
93
+ if (depth > MAX_DEPTH) throw new Undecomposable(`scripts nested more than ${MAX_DEPTH} deep`);
94
+ let at = 0;
95
+ let nesting = 0;
96
+ /** Commands here read their standard input from a pipe. */
97
+ let piped = false;
98
+ const pending = [];
99
+ const ch = (k = 0) => src[at + k] ?? "";
100
+ const ahead = (s) => src.startsWith(s, at);
101
+ const ends = (k) => {
102
+ const c = ch(k);
103
+ return c === "" || isBlank(c) || c === "\n" || ";&|()<>".includes(c);
104
+ };
105
+ const deeper = () => {
106
+ nesting += 1;
107
+ if (nesting > MAX_NESTING) throw new Undecomposable(`brackets nested more than ${MAX_NESTING} deep`);
108
+ };
109
+ const fresh = () => ({
110
+ text: "",
111
+ literal: true,
112
+ quoted: false,
113
+ bare: ""
114
+ });
115
+ /** A script inside this one: its unreadable parts are recorded, not fatal. */
116
+ const nested = (text, onlyExpansions = false) => {
117
+ try {
118
+ read(text, out, depth + 1, onlyExpansions);
119
+ } catch (error) {
120
+ if (!(error instanceof Undecomposable)) throw error;
121
+ out.undecomposable.push(error.message);
122
+ }
123
+ };
124
+ function blanks() {
125
+ for (;;) {
126
+ const c = ch();
127
+ if (isBlank(c)) at += 1;
128
+ else if (c === "\\" && ch(1) === "\n") at += 2;
129
+ else if (c === "#") while (at < src.length && ch() !== "\n") at += 1;
130
+ else return;
131
+ }
132
+ }
133
+ /** One unquoted newline, then the here-document bodies it starts. */
134
+ function newline() {
135
+ at += 1;
136
+ for (let doc = pending.shift(); doc; doc = pending.shift()) body(doc);
137
+ }
138
+ function body(doc) {
139
+ let text = "";
140
+ while (at < src.length) {
141
+ const stop = src.indexOf("\n", at);
142
+ const end = stop === -1 ? src.length : stop;
143
+ const line = src.slice(at, end);
144
+ at = Math.min(end + 1, src.length);
145
+ if ((doc.strip ? line.replace(/^\t+/, "") : line) === doc.delim) break;
146
+ text += `${line}\n`;
147
+ }
148
+ if (doc.feed.shell !== void 0) if (!doc.quoted && /[$`\\]/.test(text)) out.undecomposable.push(`a here-document with expansions is run by ${doc.feed.shell}`);
149
+ else nested(text);
150
+ else if (!doc.quoted) nested(text, true);
151
+ }
152
+ function list$1(end) {
153
+ for (;;) {
154
+ blanks();
155
+ const c = ch();
156
+ if (c === "") {
157
+ if (end === void 0) return;
158
+ throw new Undecomposable(end === ")" ? "an unclosed `(`" : "an unclosed `{`");
159
+ }
160
+ if (c === "\n") newline();
161
+ else if (c === ";") {
162
+ if (ch(1) === ";") throw new Undecomposable("`;;` outside a case statement");
163
+ at += 1;
164
+ } else if (c === "&" && ch(1) !== ">") at += ch(1) === "&" ? 2 : 1;
165
+ else if (c === "|" && ch(1) === "|") at += 2;
166
+ else if (c === "|") throw new Undecomposable("a pipe with nothing before it");
167
+ else if (c === ")") {
168
+ if (end !== ")") throw new Undecomposable("an unbalanced `)`");
169
+ at += 1;
170
+ return;
171
+ } else if (end === "}" && c === "}" && ends(1)) {
172
+ at += 1;
173
+ return;
174
+ } else pipeline();
175
+ }
176
+ }
177
+ function pipeline() {
178
+ const names = [];
179
+ for (let position = 0;; position += 1) {
180
+ names.push(...command(position > 0 || piped));
181
+ blanks();
182
+ if (ch() !== "|" || ch(1) === "|") break;
183
+ at += ch(1) === "&" ? 2 : 1;
184
+ for (blanks(); ch() === "\n"; blanks()) newline();
185
+ }
186
+ if (names.length > 0) out.pipelines.push(names.join("|"));
187
+ }
188
+ function command(fromPipe) {
189
+ blanks();
190
+ if (ahead("((") && arithmetic()) {
191
+ trailing();
192
+ return [];
193
+ }
194
+ if (ch() === "(") {
195
+ at += 1;
196
+ return compound(")", fromPipe);
197
+ }
198
+ if (ch() === "{" && (isBlank(ch(1)) || ch(1) === "\n")) {
199
+ at += 1;
200
+ return compound("}", fromPipe);
201
+ }
202
+ return simple(fromPipe);
203
+ }
204
+ function compound(end, fromPipe) {
205
+ deeper();
206
+ const before = out.segments.length;
207
+ const outer = piped;
208
+ piped = fromPipe;
209
+ list$1(end);
210
+ piped = outer;
211
+ nesting -= 1;
212
+ trailing();
213
+ return out.segments.slice(before).map((s) => s.name);
214
+ }
215
+ /** Redirections after a compound command; anything else is not shell. */
216
+ function trailing() {
217
+ blanks();
218
+ while (redirection() !== void 0) blanks();
219
+ if (!ends(0) && ch() !== "}") throw new Undecomposable("a word after a compound command");
220
+ }
221
+ function simple(fromPipe) {
222
+ const mark = out.segments.length;
223
+ const words = [];
224
+ const redirects = [];
225
+ const feed = {};
226
+ let runsNothing = false;
227
+ let conditional = false;
228
+ for (;;) {
229
+ blanks();
230
+ const c = ch();
231
+ if (conditional) {
232
+ if (c === "") throw new Undecomposable("an unclosed `[[`");
233
+ if (c === "\n") {
234
+ newline();
235
+ continue;
236
+ }
237
+ if (ahead("]]") && ends(2)) {
238
+ at += 2;
239
+ conditional = false;
240
+ continue;
241
+ }
242
+ const op = /^(&&|\|\||[()<>!])/.exec(src.slice(at, at + 2));
243
+ if (op !== null) {
244
+ at += op[0].length;
245
+ continue;
246
+ }
247
+ }
248
+ if (c === "" || c === "\n" || c === ";" || c === ")" || c === "|") break;
249
+ if (c === "&" && ch(1) !== ">") break;
250
+ if (c === "(") {
251
+ if (ahead("((")) {
252
+ if (arithmetic()) continue;
253
+ throw new Undecomposable("an unexpected `(`");
254
+ }
255
+ if (words.length === 1) {
256
+ at += 1;
257
+ blanks();
258
+ if (ch() === ")") {
259
+ at += 1;
260
+ for (blanks(); ch() === "\n"; blanks()) newline();
261
+ return command(fromPipe);
262
+ }
263
+ }
264
+ throw new Undecomposable("an unexpected `(`");
265
+ }
266
+ const redirected = redirection(feed);
267
+ if (redirected !== void 0) {
268
+ redirects.push(...redirected);
269
+ continue;
270
+ }
271
+ const w = word();
272
+ if (w === void 0) throw new Undecomposable("an unexpected character");
273
+ if (words.length === 0 && w.literal && !w.quoted) {
274
+ if (RESERVED.has(w.text)) return command(fromPipe);
275
+ if (w.text === "case" || w.text === "coproc") throw new Undecomposable(`a \`${w.text}\` statement`);
276
+ if (w.text === "function") {
277
+ blanks();
278
+ word();
279
+ blanks();
280
+ if (ahead("()")) at += 2;
281
+ for (blanks(); ch() === "\n"; blanks()) newline();
282
+ return command(fromPipe);
283
+ }
284
+ if (w.text === "for" || w.text === "select") runsNothing = true;
285
+ if (w.text === "[[") {
286
+ runsNothing = true;
287
+ conditional = true;
288
+ }
289
+ }
290
+ words.push(w);
291
+ }
292
+ if (runsNothing || words.length === 0) return [];
293
+ const name = resolve(words, redirects, [], false, mark, feed, fromPipe);
294
+ return name === void 0 ? [] : [name];
295
+ }
296
+ /** A redirection at `at`: the operator and its target, as tokens. */
297
+ function redirection(feed) {
298
+ if ((ch() === "<" || ch() === ">") && ch(1) === "(") return void 0;
299
+ const m = /^(\d*)(<<<|<<-|<<|<>|<&|>>|>\||>&|&>>|&>|<|>)/.exec(src.slice(at, at + 12));
300
+ if (m === null) return void 0;
301
+ at += m[0].length;
302
+ blanks();
303
+ const target = word();
304
+ if (target === void 0) throw new Undecomposable("a redirection with no target");
305
+ const op = m[2];
306
+ if (op === "<<" || op === "<<-") pending.push({
307
+ delim: target.text,
308
+ strip: op === "<<-",
309
+ quoted: target.quoted,
310
+ feed: feed ?? {}
311
+ });
312
+ else if (op === "<<<" && feed !== void 0) feed.herestring = target;
313
+ if (op === ">&" || op === "<&") return [`${m[0]}${target.text}`];
314
+ return [m[0], target.text];
315
+ }
316
+ /** One word at `at`, quotes and expansions read; undefined at an operator. */
317
+ function word() {
318
+ const w = fresh();
319
+ const start = at;
320
+ for (;;) {
321
+ const c = ch();
322
+ if (c === "" || isBlank(c) || c === "\n" || ";&|()".includes(c)) break;
323
+ if (c === "<" || c === ">") {
324
+ if (ch(1) !== "(") break;
325
+ const from = at;
326
+ at += 2;
327
+ substitution();
328
+ w.text += src.slice(from, at);
329
+ w.literal = false;
330
+ } else if (c === "\\") {
331
+ if (ch(1) !== "\n") {
332
+ w.text += ch(1);
333
+ w.quoted = true;
334
+ }
335
+ at += 2;
336
+ } else if (c === "'") {
337
+ const end = src.indexOf("'", at + 1);
338
+ if (end === -1) throw new Undecomposable("an unbalanced single quote");
339
+ w.text += src.slice(at + 1, end);
340
+ w.quoted = true;
341
+ at = end + 1;
342
+ } else if (c === "$" && ch(1) === "'") ansi(w);
343
+ else if (c === "$" && ch(1) === "\"") {
344
+ at += 1;
345
+ double(w, true);
346
+ } else if (c === "\"") double(w, true);
347
+ else if (c === "`") backtick(w, false);
348
+ else if (c === "$") dollar(w);
349
+ else {
350
+ w.text += c;
351
+ w.bare += c;
352
+ at += 1;
353
+ }
354
+ }
355
+ return at === start ? void 0 : w;
356
+ }
357
+ /** `"…"` (or, unclosed, a here-document body): only `$` and backticks are live. */
358
+ function double(w, closed) {
359
+ w.quoted = true;
360
+ if (closed) at += 1;
361
+ for (;;) {
362
+ const c = ch();
363
+ if (c === "") {
364
+ if (closed) throw new Undecomposable("an unbalanced double quote");
365
+ return;
366
+ }
367
+ if (c === "\"" && closed) {
368
+ at += 1;
369
+ return;
370
+ }
371
+ const n = ch(1);
372
+ if (c === "\\" && n !== "" && (n === "\n" || "$`\"\\".includes(n))) {
373
+ if (n !== "\n") w.text += n;
374
+ at += 2;
375
+ } else if (c === "`") backtick(w, closed);
376
+ else if (c === "$") dollar(w);
377
+ else {
378
+ w.text += c;
379
+ at += 1;
380
+ }
381
+ }
382
+ }
383
+ /** `$'…'`: escapes can spell anything, so one with escapes is not a literal. */
384
+ function ansi(w) {
385
+ let k = at + 2;
386
+ for (;;) {
387
+ const c = src[k];
388
+ if (c === void 0) throw new Undecomposable("an unbalanced `$'` quote");
389
+ if (c === "'") break;
390
+ if (c === "\\") {
391
+ w.literal = false;
392
+ w.text += c + (src[k + 1] ?? "");
393
+ k += 2;
394
+ } else {
395
+ w.text += c;
396
+ k += 1;
397
+ }
398
+ }
399
+ w.quoted = true;
400
+ at = k + 1;
401
+ }
402
+ /** `$(…)`, `$((…))`, `${…}`, `$NAME`; a lone `$` is a dollar sign. */
403
+ function dollar(w) {
404
+ const from = at;
405
+ if (ahead("$(")) {
406
+ if (!(ahead("$((") && arithmetic())) {
407
+ at += 2;
408
+ substitution();
409
+ }
410
+ } else if (ahead("${")) {
411
+ at += 2;
412
+ parameter();
413
+ } else if (/[A-Za-z_]/.test(ch(1))) {
414
+ at += 1;
415
+ while (/[A-Za-z0-9_]/.test(ch())) at += 1;
416
+ } else if (isSpecial(ch(1))) at += 2;
417
+ else {
418
+ w.text += "$";
419
+ w.bare += "$";
420
+ at += 1;
421
+ return;
422
+ }
423
+ w.text += src.slice(from, at);
424
+ w.literal = false;
425
+ }
426
+ /** After `$(`, `<(` or `>(`: a list up to its `)`, with its own stdin. */
427
+ function substitution() {
428
+ deeper();
429
+ const outer = piped;
430
+ piped = false;
431
+ list$1(")");
432
+ piped = outer;
433
+ nesting -= 1;
434
+ }
435
+ /** After `${`: up to the matching `}`; a default value can run a command. */
436
+ function parameter() {
437
+ deeper();
438
+ const sink = fresh();
439
+ for (;;) {
440
+ const c = ch();
441
+ if (c === "") throw new Undecomposable("an unclosed `${`");
442
+ if (c === "}") break;
443
+ if (c === "\\") at += 2;
444
+ else if (c === "'") {
445
+ const end = src.indexOf("'", at + 1);
446
+ if (end === -1) throw new Undecomposable("an unbalanced single quote");
447
+ at = end + 1;
448
+ } else if (c === "\"") double(sink, true);
449
+ else if (c === "`") backtick(sink, false);
450
+ else if (c === "$") dollar(sink);
451
+ else at += 1;
452
+ }
453
+ at += 1;
454
+ nesting -= 1;
455
+ }
456
+ /**
457
+ * Where `((…))` / `$((…))` at `at` ends, if bash reads it as arithmetic:
458
+ * only when the `)` matching the second `(` is followed at once by `)`.
459
+ * Otherwise bash reads `(` `(` — nested subshells, or `$(` and a subshell —
460
+ * and `((rm -rf /) )` runs rm. Quotes and backticks are skipped whole;
461
+ * anything unclosed is not arithmetic, so it is read the stricter way.
462
+ */
463
+ function arithmeticEnd() {
464
+ let depth$1 = 1;
465
+ for (let k = at + (ch() === "$" ? 3 : 2); k < src.length; k += 1) {
466
+ const c = src[k];
467
+ if (c === "\\") k += 1;
468
+ else if (c === "'" || c === "\"" || c === "`") {
469
+ let end = k + 1;
470
+ while (end < src.length && src[end] !== c) {
471
+ if (src[end] === "\\" && c !== "'") end += 1;
472
+ end += 1;
473
+ }
474
+ if (end >= src.length) return void 0;
475
+ k = end;
476
+ } else if (c === "(") depth$1 += 1;
477
+ else if (c === ")") {
478
+ depth$1 -= 1;
479
+ if (depth$1 === 0) return src[k + 1] === ")" ? k + 2 : void 0;
480
+ }
481
+ }
482
+ }
483
+ /**
484
+ * `((…))` or `$((…))` read as arithmetic, in which a substitution still
485
+ * runs; false, and nothing read, when bash would not read it so.
486
+ */
487
+ function arithmetic() {
488
+ const end = arithmeticEnd();
489
+ if (end === void 0) return false;
490
+ deeper();
491
+ at += ch() === "$" ? 3 : 2;
492
+ const sink = fresh();
493
+ while (at < end) {
494
+ const c = ch();
495
+ if (c === "$") dollar(sink);
496
+ else if (c === "`") backtick(sink, false);
497
+ else at += 1;
498
+ }
499
+ if (at !== end) throw new Undecomposable("an ambiguous `((`");
500
+ nesting -= 1;
501
+ return true;
502
+ }
503
+ /** `` `…` ``: its text, unescaped, is a script of its own. */
504
+ function backtick(w, inDouble) {
505
+ let inner = "";
506
+ let k = at + 1;
507
+ for (;;) {
508
+ const c = src[k];
509
+ if (c === void 0) throw new Undecomposable("an unbalanced backtick");
510
+ if (c === "`") break;
511
+ const n = src[k + 1] ?? "";
512
+ const escaped = "`$\\".includes(n) || inDouble && n === "\"";
513
+ if (c === "\\" && n !== "" && escaped) {
514
+ inner += n;
515
+ k += 2;
516
+ } else {
517
+ inner += c;
518
+ k += 1;
519
+ }
520
+ }
521
+ w.text += src.slice(at, k + 1);
522
+ w.literal = false;
523
+ at = k + 1;
524
+ nested(inner);
525
+ }
526
+ /**
527
+ * Words to the program they run, inserted at `insertAt` so a command comes
528
+ * before the substitutions in its words; then whatever that program runs in
529
+ * turn (`bash -c`, `eval`, `find -exec`).
530
+ */
531
+ function resolve(words, redirects, seen, open, insertAt, feed, fromPipe = false) {
532
+ const wrappers = [...seen];
533
+ let openEnded = open;
534
+ let replace;
535
+ let k = 0;
536
+ while (k < words.length) {
537
+ const w = words[k];
538
+ if (ASSIGNMENT.test(w.text)) {
539
+ k += 1;
540
+ continue;
541
+ }
542
+ if (!w.literal || expands(w)) break;
543
+ const base = basename(w.text);
544
+ const options = WRAPPERS.get(base);
545
+ if (options === void 0) break;
546
+ wrappers.push(base);
547
+ if (base === "xargs") openEnded = true;
548
+ k += 1;
549
+ while (k < words.length) {
550
+ const option = words[k].text;
551
+ if (option === "--") {
552
+ k += 1;
553
+ break;
554
+ }
555
+ if (!option.startsWith("-")) break;
556
+ if (base === "env" && /^(-S|--split-string)/.test(option)) {
557
+ out.undecomposable.push("`env -S` splits a string into a command");
558
+ return insert(insertAt, {
559
+ name: base,
560
+ tokens: [],
561
+ wrappers
562
+ });
563
+ }
564
+ if (base === "xargs" && option.startsWith("-I")) replace = option.length > 2 ? option.slice(2) : words[k + 1]?.text;
565
+ k += options.has(option) ? 2 : 1;
566
+ }
567
+ if (base === "timeout") k += 1;
568
+ }
569
+ const program = words[k];
570
+ const rest = words.slice(k + 1);
571
+ const tokens = [...rest.map((w) => w.text), ...redirects];
572
+ const last = wrappers.at(-1);
573
+ if (program === void 0) return last === void 0 || wrappers.length === seen.length ? void 0 : insert(insertAt, {
574
+ name: last,
575
+ tokens: [...redirects],
576
+ wrappers
577
+ });
578
+ if (!program.literal || expands(program) || openEnded && (program.text.includes("{}") || replace !== void 0 && replace !== "" && program.text.includes(replace))) {
579
+ out.undecomposable.push("the program word is only known at run time");
580
+ if (last !== void 0) insert(insertAt, {
581
+ name: last,
582
+ tokens: [program.text, ...tokens],
583
+ wrappers
584
+ });
585
+ return;
586
+ }
587
+ const name = basename(program.text);
588
+ insert(insertAt, {
589
+ name,
590
+ tokens,
591
+ wrappers,
592
+ ...openEnded ? { openEnded: true } : {}
593
+ });
594
+ if (SHELLS.has(name)) shell(name, rest, feed, fromPipe);
595
+ else if (name === "eval") if (rest.every((w) => w.literal)) nested(rest.map((w) => w.text).join(" "));
596
+ else out.undecomposable.push("`eval` of a string that is not a literal");
597
+ else if (name === "source" || name === ".") {
598
+ if (rest[0] !== void 0 && !rest[0].literal) out.undecomposable.push(`\`${name}\` of a file only known at run time`);
599
+ } else if (name === "find") for (let i = 0; i < rest.length; i += 1) {
600
+ const flag = rest[i].text;
601
+ if (![
602
+ "-exec",
603
+ "-execdir",
604
+ "-ok",
605
+ "-okdir"
606
+ ].includes(flag)) continue;
607
+ let end = i + 1;
608
+ while (end < rest.length && !/^[;+]$/.test(rest[end].text)) end += 1;
609
+ resolve(rest.slice(i + 1, end), [], [...wrappers, "find"], true, out.segments.length);
610
+ i = end;
611
+ }
612
+ return name;
613
+ }
614
+ function insert(where, segment) {
615
+ out.segments.splice(where, 0, segment);
616
+ return segment.name;
617
+ }
618
+ /** What a shell program runs: a `-c` string, a script file, or its stdin. */
619
+ function shell(name, args, feed, fromPipe) {
620
+ let inline = false;
621
+ let stdin = false;
622
+ let k = 0;
623
+ while (k < args.length) {
624
+ const option = args[k].text;
625
+ if (option === "--" || option === "-") {
626
+ k += 1;
627
+ break;
628
+ }
629
+ if (option === "--rcfile" || option === "--init-file") k += 2;
630
+ else if (option.startsWith("--")) k += 1;
631
+ else if (/^[-+][A-Za-z]+$/.test(option)) {
632
+ if (option.startsWith("-") && option.includes("c")) inline = true;
633
+ if (option.includes("s")) stdin = true;
634
+ k += option.includes("o") ? 2 : 1;
635
+ } else break;
636
+ }
637
+ const operand = args[k];
638
+ if (inline) {
639
+ if (operand === void 0) return;
640
+ if (operand.literal) nested(operand.text);
641
+ else out.undecomposable.push(`\`${name} -c\` runs a string that is not a literal`);
642
+ return;
643
+ }
644
+ if (operand !== void 0 && !stdin) {
645
+ if (!operand.literal) out.undecomposable.push(`\`${name}\` runs a script only known at run time`);
646
+ return;
647
+ }
648
+ if (fromPipe) out.undecomposable.push(`\`${name}\` runs commands read from a pipe`);
649
+ if (feed === void 0) return;
650
+ feed.shell = `\`${name}\``;
651
+ const string = feed.herestring;
652
+ if (string === void 0) return;
653
+ if (string.literal) nested(string.text);
654
+ else out.undecomposable.push(`a here-string with expansions is run by \`${name}\``);
655
+ }
656
+ if (expansionsOnly) double(fresh(), false);
657
+ else list$1(void 0);
658
+ }
659
+ /** Read a command line into every command it would run, and what could not be read. */
660
+ function decompose(command) {
661
+ const out = {
662
+ segments: [],
663
+ pipelines: [],
664
+ undecomposable: []
665
+ };
666
+ try {
667
+ read(command, out, 0);
668
+ } catch (error) {
669
+ out.undecomposable.push(error instanceof Undecomposable ? error.message : "the line could not be read");
670
+ }
671
+ return out;
672
+ }
673
+ /** Every simple command a line would run, in the order read. */
674
+ function segments(command) {
675
+ return [...decompose(command).segments];
676
+ }
677
+ /** `git*` matches `git`, `github`; bare strings match exactly. */
678
+ function matches$1(pattern, value) {
679
+ if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
680
+ return value === pattern;
681
+ }
682
+ /**
683
+ * A glob with `*` anywhere, for pipeline signatures: `curl*|*sh` is the
684
+ * shape of download-and-execute however the two ends are spelled.
685
+ */
686
+ function globMatches(pattern, value) {
687
+ const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
688
+ return (/* @__PURE__ */ new RegExp(`^${source}$`)).test(value);
689
+ }
690
+ /**
691
+ * Every wanted token holds in a segment: a plain pattern must match some
692
+ * token, a `!pattern` must match none. `["-rf", "!--dry-run"]` is a real
693
+ * force-delete, not a rehearsal of one.
694
+ */
695
+ function tokensHold(wanted, tokens) {
696
+ return wanted.every((pattern) => pattern.startsWith("!") ? !tokens.some((token) => matches$1(pattern.slice(1), token)) : tokens.some((token) => matches$1(pattern, token)));
697
+ }
698
+ const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
699
+ /**
700
+ * Every present field holds on what was read. With `open`, a segment whose
701
+ * arguments arrive at run time (`xargs`, `find -exec`) satisfies any token
702
+ * condition, because those arguments could.
703
+ */
704
+ function holds(match, read$1, open) {
705
+ const parts = read$1.segments;
706
+ if (parts.length === 0) return false;
707
+ const names = list(match.name);
708
+ const args = list(match.token);
709
+ const anyArgs = list(match.anyToken);
710
+ const pipes = list(match.pipedTo);
711
+ const shapes = list(match.pipeline);
712
+ const argsHold = (segment) => open && segment.openEnded === true || tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
713
+ if (names.length > 0) {
714
+ const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name) || segment.wrappers.some((wrapper) => matches$1(pattern, wrapper))));
715
+ if (hit.length === 0) return false;
716
+ if ((args.length > 0 || anyArgs.length > 0) && !hit.some(argsHold)) return false;
717
+ } else if (args.length > 0 || anyArgs.length > 0) {
718
+ if (!parts.some(argsHold)) return false;
719
+ }
720
+ if (pipes.length > 0) {
721
+ if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
722
+ }
723
+ if (shapes.length > 0) {
724
+ const signatures = [parts.map((segment) => segment.name).join("|"), ...read$1.pipelines];
725
+ if (!shapes.some((pattern) => signatures.some((signature) => globMatches(pattern, signature)))) return false;
726
+ }
727
+ return true;
728
+ }
729
+ /**
730
+ * Judge one command line against one matcher, on what could be read. Every
731
+ * present field must hold.
732
+ */
733
+ function commandMatches(match, command) {
734
+ return holds(match, typeof command === "string" ? decompose(command) : command, false);
735
+ }
736
+ /**
737
+ * Why a matcher that did not hold on what was read might still hold on what
738
+ * runs: part of the line could not be read, or a matched program takes
739
+ * arguments only known at run time. Undefined when there is no such reason.
740
+ * A `deny` or `ask` rule matches on it, conservatively (`evaluate.ts`).
741
+ */
742
+ function commandUnread(match, command) {
743
+ const read$1 = typeof command === "string" ? decompose(command) : command;
744
+ if (read$1.undecomposable.length > 0) return [...new Set(read$1.undecomposable)].join("; ");
745
+ if (!holds(match, read$1, true)) return void 0;
746
+ const open = read$1.segments.find((s) => s.openEnded === true);
747
+ const from = open?.wrappers.includes("xargs") ? "xargs" : "find -exec";
748
+ return `the arguments \`${open?.name ?? "?"}\` gets from ${from} are only known at run time`;
749
+ }
750
+
751
+ //#endregion
752
+ //#region ../kernel/src/path.ts
753
+ /**
754
+ * Path normalization and glob containment, with no filesystem access.
755
+ *
756
+ * The contract, stated here because getting it wrong is how path policies
757
+ * become theatre: **this module compares strings.** It resolves `.` and `..`
758
+ * lexically so that `./src/../../etc/passwd` cannot pose as a workspace path,
759
+ * but it cannot see a symlink. A link inside an allowed directory pointing out
760
+ * of it will pass every check here.
761
+ *
762
+ * That is not a gap to paper over in the matcher — it is a gap the caller
763
+ * closes by resolving before it asks. `dsh-overlay-check` learned this the
764
+ * hard way during the plugin-store gate review: lexical containment accepted a
765
+ * symlink escape cleanly, and the fix was to resolve both the path and its
766
+ * nearest existing ancestor before deciding. Adapters that can touch the
767
+ * filesystem must resolve first and pass the resolved path in.
768
+ */
769
+ /** Split a path into segments, resolving `.` and `..` lexically. */
770
+ function normalize(path) {
771
+ const absolute = path.startsWith("/");
772
+ const out = [];
773
+ let escaped = false;
774
+ for (const raw of path.split(/[/\\]+/)) {
775
+ if (raw === "" || raw === ".") continue;
776
+ if (raw === "..") {
777
+ if (out.length > 0) out.pop();
778
+ else escaped = true;
779
+ continue;
780
+ }
781
+ out.push(raw);
782
+ }
783
+ return {
784
+ segments: out,
785
+ absolute,
786
+ escaped
787
+ };
788
+ }
789
+ /** Turn one glob segment into an anchored regular expression. */
790
+ function segmentPattern(segment) {
791
+ const body = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
792
+ return /* @__PURE__ */ new RegExp(`^${body}$`);
793
+ }
794
+ /**
795
+ * Wildcard segment matching, where `**` spans zero or more segments.
796
+ *
797
+ * Two-pointer with backtracking rather than a recursive walk: the pattern
798
+ * comes from a policy pack and the path from a tool call, and a catastrophic
799
+ * backtrack in the hot path of every call would be a denial of service handed
800
+ * to whoever writes the pack.
801
+ */
802
+ function matchSegments(path, pattern) {
803
+ let i = 0;
804
+ let j = 0;
805
+ let starPattern = -1;
806
+ let starPath = 0;
807
+ while (i < path.length) {
808
+ const segment = j < pattern.length ? pattern[j] : void 0;
809
+ const here = path[i];
810
+ if (segment === "**") {
811
+ starPattern = j;
812
+ starPath = i;
813
+ j += 1;
814
+ continue;
815
+ }
816
+ if (segment !== void 0 && here !== void 0 && segmentPattern(segment).test(here)) {
817
+ i += 1;
818
+ j += 1;
819
+ continue;
820
+ }
821
+ if (starPattern >= 0) {
822
+ starPath += 1;
823
+ i = starPath;
824
+ j = starPattern + 1;
825
+ continue;
826
+ }
827
+ return false;
828
+ }
829
+ while (j < pattern.length && pattern[j] === "**") j += 1;
830
+ return j === pattern.length;
831
+ }
832
+ /**
833
+ * Is `path` contained by `glob`?
834
+ *
835
+ * A glob naming a directory contains everything under it: `./src` and
836
+ * `./src/**` both match `src/app/main.ts`. Writing the shorter form is the
837
+ * common intent, and treating it as "this exact directory entry only" is a
838
+ * trap that silently grants the whole subtree past the rule.
839
+ */
840
+ function within(path, glob) {
841
+ const target = normalize(path);
842
+ if (target.escaped) return false;
843
+ const trimmed = glob.trim();
844
+ if (!(trimmed === "**" || trimmed.startsWith("**/")) && target.absolute !== trimmed.startsWith("/")) return false;
845
+ const pattern = normalize(glob);
846
+ if (matchSegments(target.segments, pattern.segments)) return true;
847
+ return matchSegments(target.segments, [...pattern.segments, "**"]);
848
+ }
849
+ /** Is `path` inside any of these globs? */
850
+ function withinAny(path, globs) {
851
+ return globs.some((glob) => within(path, glob));
852
+ }
853
+ /**
854
+ * An absolute path under `cwd`, rewritten as the workspace-relative path a
855
+ * pack's `./` globs speak about; anything else comes back as written.
856
+ *
857
+ * A model is free to name a workspace file by its absolute path — the first
858
+ * real agent run through the gate did exactly that for its first three
859
+ * reads — and a scope rule written as `outside: ["./src/**"]` must decide
860
+ * that file by where it is, not by how it was spelled. The anchoring in
861
+ * {@link within} is right to keep `/etc/hosts` out of `./**`; this is the
862
+ * other half, applied by an adapter that knows the session's directory.
863
+ * A path that climbs out of `cwd` with `..` stays absolute, so it stays
864
+ * outside.
865
+ */
866
+ function relativeTo(path, cwd) {
867
+ const target = normalize(path);
868
+ const base = normalize(cwd);
869
+ if (!target.absolute || !base.absolute || target.escaped) return path;
870
+ if (base.segments.length > target.segments.length) return path;
871
+ if (!base.segments.every((s, i) => s === target.segments[i])) return path;
872
+ const rest = target.segments.slice(base.segments.length);
873
+ return rest.length === 0 ? "." : `./${rest.join("/")}`;
874
+ }
875
+ /**
876
+ * The host of a URL, or undefined when it is not a URL we can read.
877
+ *
878
+ * Unparseable input returns undefined, and callers treat that as "no host" —
879
+ * which for a `notIn` rule means the rule matches. Failing toward the rule is
880
+ * the only safe direction: a string we cannot parse is exactly the shape an
881
+ * evasion attempt takes.
882
+ */
883
+ function hostOf(url) {
884
+ try {
885
+ return new URL(url).hostname.toLowerCase();
886
+ } catch {
887
+ return;
888
+ }
889
+ }
890
+ /**
891
+ * Host list membership, where a leading dot means "this domain or below".
892
+ *
893
+ * `example.com` matches only that host. `.example.com` also matches
894
+ * `api.example.com`, and deliberately still matches `example.com` itself —
895
+ * an operator writing the suffix form means the organisation, not a rule that
896
+ * covers every subdomain except the one they first thought of.
897
+ */
898
+ function hostIn(host, list$1) {
899
+ return list$1.some((entry) => {
900
+ const want = entry.toLowerCase();
901
+ if (want.startsWith(".")) return host === want.slice(1) || host.endsWith(want);
902
+ return host === want;
903
+ });
904
+ }
905
+ /**
906
+ * Argument names that conventionally carry a filesystem path, in the order
907
+ * they are tried when a path rule names none.
908
+ *
909
+ * A path rule used to default to the single argument spelled `path`. DeepSeek
910
+ * Harness's own `read`, `write` and `edit` tools pass `file_path`, so every
911
+ * shipped path rule without an explicit `arg` — including the baseline's
912
+ * "deny read ./.env" — matched nothing on the very harness it was written
913
+ * for, while the ledger dutifully recorded every allowed read. A rule that
914
+ * silently never fires is the worst way for policy to be wrong; defaulting
915
+ * to the conventional set is the same call the dsh adapter already made for
916
+ * path resolution, now made once, here, for matching too.
917
+ */
918
+ const PATH_ARGS = [
919
+ "path",
920
+ "file_path",
921
+ "filePath",
922
+ "target",
923
+ "directory",
924
+ "cwd",
925
+ "file",
926
+ "dest",
927
+ "destination",
928
+ "source"
929
+ ];
930
+ /** The first conventional path argument present, as a string, or undefined. */
931
+ function pathArgOf(args, explicit) {
932
+ if (explicit !== void 0) {
933
+ const v = args[explicit];
934
+ return typeof v === "string" ? v : void 0;
935
+ }
936
+ for (const key of PATH_ARGS) {
937
+ const v = args[key];
938
+ if (typeof v === "string") return v;
939
+ }
940
+ }
941
+
942
+ //#endregion
943
+ //#region ../kernel/src/rule.ts
944
+ /**
945
+ * Strength ordering, used when several rules match one call.
946
+ *
947
+ * Higher wins. This is the property that makes a pack safe to extend: adding a
948
+ * rule can raise the strength of a decision and never lower it, so an operator
949
+ * appending to a pack can never accidentally widen what an agent may do.
950
+ */
951
+ const STRENGTH = {
952
+ allow: 0,
953
+ warn: 1,
954
+ correct: 2,
955
+ ask: 3,
956
+ deny: 4
957
+ };
958
+
959
+ //#endregion
960
+ //#region ../kernel/src/evaluate.ts
961
+ function listed(value, want) {
962
+ if (want === void 0) return true;
963
+ if (value === void 0) return false;
964
+ return typeof want === "string" ? value === want : want.includes(value);
965
+ }
966
+ function argMatches(match, args) {
967
+ const has = Object.hasOwn(args, match.name);
968
+ if (match.present !== void 0 && match.present !== has) return false;
969
+ if (!has) return match.above === void 0 && match.below === void 0 && match.equals === void 0 && match.oneOf === void 0;
970
+ const value = args[match.name];
971
+ if (match.above !== void 0) {
972
+ if (typeof value !== "number" || !(value > match.above)) return false;
973
+ }
974
+ if (match.below !== void 0) {
975
+ if (typeof value !== "number" || !(value < match.below)) return false;
976
+ }
977
+ if (match.equals !== void 0 && value !== match.equals) return false;
978
+ if (match.oneOf !== void 0) {
979
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
980
+ if (!match.oneOf.includes(value)) return false;
981
+ }
982
+ return true;
983
+ }
984
+ /**
985
+ * Whether a rule's matcher holds on a call: `false`, or a hit — carrying a
986
+ * note when the hit is conservative. `strict` rules (deny, ask) match a shell
987
+ * line they cannot fully read, because what was not read could be what they
988
+ * refuse; the note says what that was. Weaker rules never match on a guess.
989
+ */
990
+ function matches(when, facts, strict, shell) {
991
+ let note;
992
+ if (!listed(facts.tool, when.tool)) return false;
993
+ if (!listed(facts.capability, when.capability)) return false;
994
+ if (!listed(facts.plugin, when.plugin)) return false;
995
+ if (!listed(facts.agent, when.agent)) return false;
996
+ if (when.command !== void 0) {
997
+ const line = shell();
998
+ if (line === void 0) return false;
999
+ if (!commandMatches(when.command, line)) {
1000
+ note = strict ? commandUnread(when.command, line) : void 0;
1001
+ if (note === void 0) return false;
1002
+ }
1003
+ }
1004
+ if (when.path !== void 0) {
1005
+ const raw = pathArgOf(facts.args, when.path.arg);
1006
+ if (typeof raw !== "string") return false;
1007
+ if (when.path.inside !== void 0 && !withinAny(raw, when.path.inside)) return false;
1008
+ if (when.path.outside !== void 0 && withinAny(raw, when.path.outside)) return false;
1009
+ }
1010
+ if (when.host !== void 0) {
1011
+ const raw = facts.args[when.host.arg ?? "url"];
1012
+ if (typeof raw !== "string") return false;
1013
+ const host = hostOf(raw);
1014
+ if (host === void 0) {
1015
+ if (when.host.notIn === void 0) return false;
1016
+ return note === void 0 ? {} : { note };
1017
+ }
1018
+ if (when.host.in !== void 0 && !hostIn(host, when.host.in)) return false;
1019
+ if (when.host.notIn !== void 0 && hostIn(host, when.host.notIn)) return false;
1020
+ }
1021
+ if (when.arg !== void 0) {
1022
+ for (const one of when.arg) if (!argMatches(one, facts.args)) return false;
1023
+ }
1024
+ if (when.count !== void 0) {
1025
+ const key = facts.capability;
1026
+ if (key === void 0) return false;
1027
+ if (!((facts.counts?.[key] ?? 0) > when.count.above)) return false;
1028
+ }
1029
+ if (when.secrets !== void 0) {
1030
+ const floor = when.secrets.minConfidence ?? 0;
1031
+ const wanted = when.secrets.types;
1032
+ if ((facts.secrets ?? []).filter((finding) => finding.confidence >= floor && (wanted === void 0 || wanted.includes(finding.type))).length < (when.secrets.atLeast ?? 1)) return false;
1033
+ }
1034
+ return note === void 0 ? {} : { note };
1035
+ }
1036
+ function step(rule, effect) {
1037
+ return {
1038
+ rule: rule.id,
1039
+ ...rule.title === void 0 ? {} : { title: rule.title },
1040
+ effect: effect.kind,
1041
+ because: effect.because
1042
+ };
1043
+ }
1044
+ /**
1045
+ * Decide one call against one pack.
1046
+ *
1047
+ * Every matching rule lands in the trace; the strongest effect decides. Ties
1048
+ * go to the first rule in pack order, so a pack reads top-down the way its
1049
+ * author wrote it.
1050
+ *
1051
+ * `correct` is the effect the rest of this ecosystem does not have. A rival
1052
+ * guardrail can allow a call or refuse it; a rule here can also rewrite an
1053
+ * argument and let it through, recording what it changed. Refusing is not
1054
+ * always the safe option — an agent denied mid-task retries, and the retry is
1055
+ * rarely safer than the clamp would have been.
1056
+ */
1057
+ function evaluate(pack, facts) {
1058
+ const hits = [];
1059
+ let line = null;
1060
+ const shell = () => {
1061
+ if (line === null) {
1062
+ const raw = facts.args.command ?? facts.args.cmd ?? facts.args.script;
1063
+ line = typeof raw === "string" ? decompose(raw) : void 0;
1064
+ }
1065
+ return line;
1066
+ };
1067
+ for (const rule of pack.rules) {
1068
+ const strict = rule.effect.kind === "deny" || rule.effect.kind === "ask";
1069
+ const hit = matches(rule.when, facts, strict, shell);
1070
+ if (hit === false) continue;
1071
+ hits.push({
1072
+ rule,
1073
+ effect: hit.note === void 0 ? rule.effect : {
1074
+ ...rule.effect,
1075
+ because: `${rule.effect.because} (could not decompose: ${hit.note})`
1076
+ }
1077
+ });
1078
+ }
1079
+ if (hits.length === 0) return {
1080
+ kind: "allow",
1081
+ trace: [],
1082
+ evaluated: pack.rules.length
1083
+ };
1084
+ hits.sort((a, b) => STRENGTH[b.effect.kind] - STRENGTH[a.effect.kind]);
1085
+ const trace = hits.map((hit) => step(hit.rule, hit.effect));
1086
+ const evaluated = pack.rules.length;
1087
+ const strongest = hits[0];
1088
+ if (strongest === void 0) return {
1089
+ kind: "allow",
1090
+ trace: [],
1091
+ evaluated
1092
+ };
1093
+ const winner = strongest.effect.kind;
1094
+ if (winner === "deny" || winner === "ask") return {
1095
+ kind: winner,
1096
+ rule: strongest.rule.id,
1097
+ reason: strongest.effect.because,
1098
+ trace,
1099
+ evaluated
1100
+ };
1101
+ if (winner === "correct") {
1102
+ const args = { ...facts.args };
1103
+ for (const hit of hits) if (hit.effect.kind === "correct") Object.assign(args, hit.effect.set);
1104
+ return {
1105
+ kind: "correct",
1106
+ rule: strongest.rule.id,
1107
+ reason: strongest.effect.because,
1108
+ args,
1109
+ trace,
1110
+ evaluated
1111
+ };
1112
+ }
1113
+ const allowed = hits.find((hit) => hit.effect.kind === "allow");
1114
+ if (allowed !== void 0) return {
1115
+ kind: "allow",
1116
+ rule: allowed.rule.id,
1117
+ reason: allowed.effect.because,
1118
+ trace,
1119
+ evaluated
1120
+ };
1121
+ return {
1122
+ kind: "allow",
1123
+ trace,
1124
+ evaluated
1125
+ };
1126
+ }
1127
+
1128
+ //#endregion
1129
+ //#region ../kernel/src/frameworks.ts
1130
+ const FRAMEWORKS = {
1131
+ "owasp-llm-top-10-2026": {
1132
+ name: "OWASP Top 10 for LLM Applications — 2026",
1133
+ short: "OWASP LLM 2026",
1134
+ controls: {
1135
+ LLM01: {
1136
+ name: "Prompt Injection",
1137
+ summary: "Instructions in a prompt or in retrieved content alter what the model does."
1138
+ },
1139
+ LLM02: {
1140
+ name: "Sensitive Information Disclosure",
1141
+ summary: "Credentials, personal data or proprietary content reach the model or its output."
1142
+ },
1143
+ LLM03: {
1144
+ name: "Excessive Agency",
1145
+ summary: "The model can take actions, or holds permissions, beyond what its task needs."
1146
+ },
1147
+ LLM04: {
1148
+ name: "Supply Chain",
1149
+ summary: "A compromised model, dataset, plugin or dependency enters the application."
1150
+ },
1151
+ LLM05: {
1152
+ name: "Data and Model Poisoning",
1153
+ summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
1154
+ },
1155
+ LLM06: {
1156
+ name: "Unbounded Consumption",
1157
+ summary: "The application permits resource use without a limit, exhausting budget or service."
1158
+ },
1159
+ LLM07: {
1160
+ name: "Misinformation",
1161
+ summary: "The model produces false or misleading content that is acted on as if true."
1162
+ },
1163
+ LLM08: {
1164
+ name: "Hidden Context Exposure",
1165
+ summary: "System prompts, hidden instructions or internal context are revealed."
1166
+ },
1167
+ LLM09: {
1168
+ name: "Vector and Embedding Weaknesses",
1169
+ summary: "Retrieval stores are manipulated or leak across tenants and permissions."
1170
+ },
1171
+ LLM10: {
1172
+ name: "Improper Output Handling",
1173
+ summary: "Model output is passed to downstream systems without validation."
1174
+ }
1175
+ }
1176
+ },
1177
+ "owasp-agentic-top-10-2026": {
1178
+ name: "OWASP Top 10 for Agentic Applications — 2026",
1179
+ short: "OWASP Agentic 2026",
1180
+ controls: {
1181
+ ASI01: {
1182
+ name: "Agent Goal Hijack",
1183
+ summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
1184
+ },
1185
+ ASI02: {
1186
+ name: "Tool Misuse and Exploitation",
1187
+ summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
1188
+ },
1189
+ ASI03: {
1190
+ name: "Identity and Privilege Abuse",
1191
+ summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
1192
+ },
1193
+ ASI04: {
1194
+ name: "Agentic Supply Chain",
1195
+ summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
1196
+ },
1197
+ ASI05: {
1198
+ name: "Unexpected Code Execution",
1199
+ summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
1200
+ },
1201
+ ASI06: {
1202
+ name: "Memory and Context Poisoning",
1203
+ summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
1204
+ },
1205
+ ASI07: {
1206
+ name: "Insecure Inter-Agent Communication",
1207
+ summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
1208
+ },
1209
+ ASI08: {
1210
+ name: "Cascading Failures",
1211
+ summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
1212
+ },
1213
+ ASI09: {
1214
+ name: "Human-Agent Trust Exploitation",
1215
+ summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
1216
+ },
1217
+ ASI10: {
1218
+ name: "Rogue Agents",
1219
+ summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
1220
+ }
1221
+ }
1222
+ }
1223
+ };
1224
+ /**
1225
+ * Every framework control a rule cites, resolved to names.
1226
+ *
1227
+ * A pack that parsed cites only ids the registry knows, so the lookups here
1228
+ * cannot miss; the fallbacks exist for a `Rule` built by hand in code.
1229
+ */
1230
+ function cite(rule) {
1231
+ const out = [];
1232
+ for (const [framework, controls] of Object.entries(rule.frameworks ?? {})) {
1233
+ const known = FRAMEWORKS[framework];
1234
+ for (const control of controls) out.push({
1235
+ framework,
1236
+ frameworkName: known?.name ?? framework,
1237
+ frameworkShort: known?.short ?? framework,
1238
+ control,
1239
+ controlName: known?.controls[control]?.name ?? control
1240
+ });
1241
+ }
1242
+ return out;
1243
+ }
1244
+
1245
+ //#endregion
1246
+ //#region ../kernel/src/pack.ts
1247
+ function isObject(value) {
1248
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1249
+ }
1250
+ var Check = class {
1251
+ errors = [];
1252
+ fail(path, message) {
1253
+ this.errors.push({
1254
+ path,
1255
+ message
1256
+ });
1257
+ }
1258
+ /** Reject any key the shape does not define. */
1259
+ keys(path, value, allowed) {
1260
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) this.fail(`${path}.${key}`, `unknown field \`${key}\`; expected one of ${allowed.map((k) => `\`${k}\``).join(", ")}`);
1261
+ }
1262
+ text(path, value) {
1263
+ if (typeof value !== "string" || value.trim() === "") {
1264
+ this.fail(path, "expected a non-empty string");
1265
+ return;
1266
+ }
1267
+ return value;
1268
+ }
1269
+ /** A field that accepts one name or a list of them. */
1270
+ names(path, value) {
1271
+ if (typeof value === "string") return value;
1272
+ if (Array.isArray(value)) {
1273
+ const out = [];
1274
+ value.forEach((entry, index) => {
1275
+ const one = this.text(`${path}[${index}]`, entry);
1276
+ if (one !== void 0) out.push(one);
1277
+ });
1278
+ return out;
1279
+ }
1280
+ this.fail(path, "expected a string or an array of strings");
1281
+ }
1282
+ globs(path, value) {
1283
+ if (!Array.isArray(value)) {
1284
+ this.fail(path, "expected an array of path globs");
1285
+ return;
1286
+ }
1287
+ const out = [];
1288
+ value.forEach((entry, index) => {
1289
+ const one = this.text(`${path}[${index}]`, entry);
1290
+ if (one !== void 0) out.push(one);
1291
+ });
1292
+ return out;
1293
+ }
1294
+ number(path, value) {
1295
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1296
+ this.fail(path, "expected a finite number");
1297
+ return;
1298
+ }
1299
+ return value;
1300
+ }
1301
+ };
1302
+ function readCommandMatch(check, path, value) {
1303
+ if (!isObject(value)) {
1304
+ check.fail(path, "expected an object");
1305
+ return;
1306
+ }
1307
+ check.keys(path, value, [
1308
+ "name",
1309
+ "token",
1310
+ "anyToken",
1311
+ "pipedTo",
1312
+ "pipeline"
1313
+ ]);
1314
+ if (value.name === void 0 && value.token === void 0 && value.anyToken === void 0 && value.pipedTo === void 0 && value.pipeline === void 0) check.fail(path, "expected `name`, `token`, `anyToken`, `pipedTo` or `pipeline`; a command matcher with none matches nothing");
1315
+ const out = {};
1316
+ if (value.token !== void 0) {
1317
+ const tokens = check.names(`${path}.token`, value.token);
1318
+ if (tokens !== void 0) out.token = tokens;
1319
+ }
1320
+ if (value.name !== void 0) {
1321
+ const names = check.names(`${path}.name`, value.name);
1322
+ if (names !== void 0) out.name = names;
1323
+ }
1324
+ if (value.anyToken !== void 0) {
1325
+ const tokens = check.names(`${path}.anyToken`, value.anyToken);
1326
+ if (tokens !== void 0) out.anyToken = tokens;
1327
+ }
1328
+ if (value.pipedTo !== void 0) {
1329
+ const pipes = check.names(`${path}.pipedTo`, value.pipedTo);
1330
+ if (pipes !== void 0) out.pipedTo = pipes;
1331
+ }
1332
+ if (value.pipeline !== void 0) {
1333
+ const shapes = check.names(`${path}.pipeline`, value.pipeline);
1334
+ if (shapes !== void 0) out.pipeline = shapes;
1335
+ }
1336
+ return out;
1337
+ }
1338
+ function readPathMatch(check, path, value) {
1339
+ if (!isObject(value)) {
1340
+ check.fail(path, "expected an object");
1341
+ return;
1342
+ }
1343
+ check.keys(path, value, [
1344
+ "arg",
1345
+ "outside",
1346
+ "inside"
1347
+ ]);
1348
+ if (value.outside === void 0 && value.inside === void 0) check.fail(path, "expected `inside` or `outside`; a path matcher with neither matches nothing");
1349
+ const out = {};
1350
+ if (value.arg !== void 0) {
1351
+ const arg = check.text(`${path}.arg`, value.arg);
1352
+ if (arg !== void 0) out.arg = arg;
1353
+ }
1354
+ if (value.outside !== void 0) {
1355
+ const globs = check.globs(`${path}.outside`, value.outside);
1356
+ if (globs !== void 0) out.outside = globs;
1357
+ }
1358
+ if (value.inside !== void 0) {
1359
+ const globs = check.globs(`${path}.inside`, value.inside);
1360
+ if (globs !== void 0) out.inside = globs;
1361
+ }
1362
+ return out;
1363
+ }
1364
+ function readHostMatch(check, path, value) {
1365
+ if (!isObject(value)) {
1366
+ check.fail(path, "expected an object");
1367
+ return;
1368
+ }
1369
+ check.keys(path, value, [
1370
+ "arg",
1371
+ "in",
1372
+ "notIn"
1373
+ ]);
1374
+ if (value.in === void 0 && value.notIn === void 0) check.fail(path, "expected `in` or `notIn`; a host matcher with neither matches nothing");
1375
+ const out = {};
1376
+ if (value.arg !== void 0) {
1377
+ const arg = check.text(`${path}.arg`, value.arg);
1378
+ if (arg !== void 0) out.arg = arg;
1379
+ }
1380
+ if (value.in !== void 0) {
1381
+ const list$1 = check.globs(`${path}.in`, value.in);
1382
+ if (list$1 !== void 0) out.in = list$1;
1383
+ }
1384
+ if (value.notIn !== void 0) {
1385
+ const list$1 = check.globs(`${path}.notIn`, value.notIn);
1386
+ if (list$1 !== void 0) out.notIn = list$1;
1387
+ }
1388
+ return out;
1389
+ }
1390
+ function readArgMatch(check, path, value) {
1391
+ if (!isObject(value)) {
1392
+ check.fail(path, "expected an object");
1393
+ return;
1394
+ }
1395
+ check.keys(path, value, [
1396
+ "name",
1397
+ "above",
1398
+ "below",
1399
+ "equals",
1400
+ "oneOf",
1401
+ "present"
1402
+ ]);
1403
+ const name = check.text(`${path}.name`, value.name);
1404
+ if (name === void 0) return void 0;
1405
+ const out = { name };
1406
+ if (value.above !== void 0) {
1407
+ const n = check.number(`${path}.above`, value.above);
1408
+ if (n !== void 0) out.above = n;
1409
+ }
1410
+ if (value.below !== void 0) {
1411
+ const n = check.number(`${path}.below`, value.below);
1412
+ if (n !== void 0) out.below = n;
1413
+ }
1414
+ if (value.equals !== void 0) out.equals = value.equals;
1415
+ if (value.oneOf !== void 0) if (!Array.isArray(value.oneOf)) check.fail(`${path}.oneOf`, "expected an array");
1416
+ else out.oneOf = value.oneOf;
1417
+ if (value.present !== void 0) if (typeof value.present !== "boolean") check.fail(`${path}.present`, "expected a boolean");
1418
+ else out.present = value.present;
1419
+ return out;
1420
+ }
1421
+ function readSecretsMatch(check, path, value) {
1422
+ if (!isObject(value)) {
1423
+ check.fail(path, "expected an object");
1424
+ return;
1425
+ }
1426
+ check.keys(path, value, [
1427
+ "types",
1428
+ "minConfidence",
1429
+ "atLeast"
1430
+ ]);
1431
+ const out = {};
1432
+ if (value.types !== void 0) {
1433
+ const types = check.globs(`${path}.types`, value.types);
1434
+ if (types !== void 0) out.types = types;
1435
+ }
1436
+ if (value.minConfidence !== void 0) {
1437
+ const n = check.number(`${path}.minConfidence`, value.minConfidence);
1438
+ if (n !== void 0) if (n < 0 || n > 1) check.fail(`${path}.minConfidence`, "expected a number between 0 and 1");
1439
+ else out.minConfidence = n;
1440
+ }
1441
+ if (value.atLeast !== void 0) {
1442
+ const n = check.number(`${path}.atLeast`, value.atLeast);
1443
+ if (n !== void 0) if (!Number.isInteger(n) || n < 1) check.fail(`${path}.atLeast`, "expected an integer of at least 1");
1444
+ else out.atLeast = n;
1445
+ }
1446
+ return out;
1447
+ }
1448
+ function readMatch(check, path, value) {
1449
+ if (!isObject(value)) {
1450
+ check.fail(path, "expected an object");
1451
+ return {};
1452
+ }
1453
+ check.keys(path, value, [
1454
+ "tool",
1455
+ "capability",
1456
+ "plugin",
1457
+ "agent",
1458
+ "command",
1459
+ "path",
1460
+ "host",
1461
+ "arg",
1462
+ "count",
1463
+ "secrets"
1464
+ ]);
1465
+ const out = {};
1466
+ for (const key of [
1467
+ "tool",
1468
+ "capability",
1469
+ "plugin",
1470
+ "agent"
1471
+ ]) if (value[key] !== void 0) {
1472
+ const names = check.names(`${path}.${key}`, value[key]);
1473
+ if (names !== void 0) out[key] = names;
1474
+ }
1475
+ if (value.command !== void 0) {
1476
+ const match = readCommandMatch(check, `${path}.command`, value.command);
1477
+ if (match !== void 0) out.command = match;
1478
+ }
1479
+ if (value.path !== void 0) {
1480
+ const match = readPathMatch(check, `${path}.path`, value.path);
1481
+ if (match !== void 0) out.path = match;
1482
+ }
1483
+ if (value.host !== void 0) {
1484
+ const match = readHostMatch(check, `${path}.host`, value.host);
1485
+ if (match !== void 0) out.host = match;
1486
+ }
1487
+ if (value.arg !== void 0) if (!Array.isArray(value.arg)) check.fail(`${path}.arg`, "expected an array of argument matchers");
1488
+ else {
1489
+ const list$1 = [];
1490
+ value.arg.forEach((entry, index) => {
1491
+ const one = readArgMatch(check, `${path}.arg[${index}]`, entry);
1492
+ if (one !== void 0) list$1.push(one);
1493
+ });
1494
+ out.arg = list$1;
1495
+ }
1496
+ if (value.count !== void 0) if (!isObject(value.count)) check.fail(`${path}.count`, "expected an object");
1497
+ else {
1498
+ check.keys(`${path}.count`, value.count, ["above"]);
1499
+ const above = check.number(`${path}.count.above`, value.count.above);
1500
+ if (above !== void 0) out.count = { above };
1501
+ }
1502
+ if (value.secrets !== void 0) {
1503
+ const match = readSecretsMatch(check, `${path}.secrets`, value.secrets);
1504
+ if (match !== void 0) out.secrets = match;
1505
+ }
1506
+ return out;
1507
+ }
1508
+ function readEffect(check, path, value) {
1509
+ if (!isObject(value)) {
1510
+ check.fail(path, "expected an object");
1511
+ return;
1512
+ }
1513
+ const kind = value.kind === "hold" ? "ask" : value.kind;
1514
+ if (kind !== "deny" && kind !== "ask" && kind !== "correct" && kind !== "warn" && kind !== "allow") {
1515
+ check.fail(`${path}.kind`, "expected `allow`, `deny`, `hold` (or `ask`), `correct`, or `warn`");
1516
+ return;
1517
+ }
1518
+ check.keys(path, value, kind === "correct" ? [
1519
+ "kind",
1520
+ "set",
1521
+ "because"
1522
+ ] : ["kind", "because"]);
1523
+ const because = check.text(`${path}.because`, value.because);
1524
+ if (because === void 0) return void 0;
1525
+ if (kind === "correct") {
1526
+ if (!isObject(value.set) || Object.keys(value.set).length === 0) {
1527
+ check.fail(`${path}.set`, "expected a non-empty object of arguments to set");
1528
+ return;
1529
+ }
1530
+ return {
1531
+ kind,
1532
+ set: value.set,
1533
+ because
1534
+ };
1535
+ }
1536
+ return {
1537
+ kind,
1538
+ because
1539
+ };
1540
+ }
1541
+ function readRule(check, path, value) {
1542
+ if (!isObject(value)) {
1543
+ check.fail(path, "expected an object");
1544
+ return;
1545
+ }
1546
+ check.keys(path, value, [
1547
+ "id",
1548
+ "title",
1549
+ "description",
1550
+ "when",
1551
+ "effect",
1552
+ "frameworks"
1553
+ ]);
1554
+ const id = check.text(`${path}.id`, value.id);
1555
+ const effect = readEffect(check, `${path}.effect`, value.effect);
1556
+ if (value.when === void 0) check.fail(`${path}.when`, "expected an object");
1557
+ const when = readMatch(check, `${path}.when`, value.when ?? {});
1558
+ if (id === void 0 || effect === void 0) return void 0;
1559
+ const rule = {
1560
+ id,
1561
+ when,
1562
+ effect
1563
+ };
1564
+ if (value.title !== void 0) {
1565
+ const title = check.text(`${path}.title`, value.title);
1566
+ if (title !== void 0) rule.title = title;
1567
+ }
1568
+ if (value.description !== void 0) {
1569
+ const description = check.text(`${path}.description`, value.description);
1570
+ if (description !== void 0) rule.description = description;
1571
+ }
1572
+ if (value.frameworks !== void 0) {
1573
+ const frameworks = readFrameworks(check, `${path}.frameworks`, value.frameworks);
1574
+ if (frameworks !== void 0) rule.frameworks = frameworks;
1575
+ }
1576
+ return rule;
1577
+ }
1578
+ /**
1579
+ * A citation names a framework edition and controls the registry knows. A
1580
+ * typo here would be a rule that claims a mapping no view can render, so it
1581
+ * is a pack error rather than an ignored key — the same line the rest of the
1582
+ * parser takes.
1583
+ */
1584
+ function readFrameworks(check, path, value) {
1585
+ if (!isObject(value)) {
1586
+ check.fail(path, "expected an object keyed by framework id");
1587
+ return;
1588
+ }
1589
+ const out = {};
1590
+ for (const [framework, controls] of Object.entries(value)) {
1591
+ const known = FRAMEWORKS[framework];
1592
+ if (known === void 0) {
1593
+ check.fail(`${path}.${framework}`, `unknown framework; expected one of ${Object.keys(FRAMEWORKS).map((k) => `\`${k}\``).join(", ")}`);
1594
+ continue;
1595
+ }
1596
+ if (!Array.isArray(controls) || !controls.every((c) => typeof c === "string")) {
1597
+ check.fail(`${path}.${framework}`, "expected an array of control ids");
1598
+ continue;
1599
+ }
1600
+ const ids = controls;
1601
+ for (const control of ids) if (known.controls[control] === void 0) check.fail(`${path}.${framework}`, `unknown control \`${control}\` in ${known.name}`);
1602
+ if (ids.length > 0) out[framework] = [...new Set(ids)];
1603
+ }
1604
+ return out;
1605
+ }
1606
+ /** Parse and validate a pack. Never throws; every problem lands in `errors`. */
1607
+ function parsePack(source) {
1608
+ let raw;
1609
+ try {
1610
+ raw = JSON.parse(source);
1611
+ } catch (error) {
1612
+ return {
1613
+ ok: false,
1614
+ errors: [{
1615
+ path: "",
1616
+ message: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`
1617
+ }]
1618
+ };
1619
+ }
1620
+ const check = new Check();
1621
+ if (!isObject(raw)) return {
1622
+ ok: false,
1623
+ errors: [{
1624
+ path: "",
1625
+ message: "expected an object"
1626
+ }]
1627
+ };
1628
+ check.keys("", raw, [
1629
+ "prae",
1630
+ "pack",
1631
+ "version",
1632
+ "description",
1633
+ "owner",
1634
+ "expires_at",
1635
+ "baseline",
1636
+ "rules"
1637
+ ]);
1638
+ if (raw.prae !== "0.1") check.fail(".prae", "expected `\"0.1\"`; this is the only pack version this build reads");
1639
+ const name = check.text(".pack", raw.pack);
1640
+ const version = check.text(".version", raw.version);
1641
+ const description = raw.description === void 0 ? void 0 : check.text(".description", raw.description);
1642
+ const owner = raw.owner === void 0 ? void 0 : check.text(".owner", raw.owner);
1643
+ const expires = raw.expires_at === void 0 ? void 0 : check.text(".expires_at", raw.expires_at);
1644
+ if (expires !== void 0 && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(expires) || !Number.isFinite(Date.parse(expires)))) check.fail(".expires_at", "expected an ISO 8601 time with a zone, e.g. 2026-10-05T12:00:00Z");
1645
+ let baseline;
1646
+ if (raw.baseline !== void 0) if (!isObject(raw.baseline)) check.fail(".baseline", "expected an object");
1647
+ else {
1648
+ check.keys(".baseline", raw.baseline, ["capabilities"]);
1649
+ const caps = check.globs(".baseline.capabilities", raw.baseline.capabilities);
1650
+ if (caps !== void 0) baseline = { capabilities: caps };
1651
+ }
1652
+ const rules = [];
1653
+ if (!Array.isArray(raw.rules)) check.fail(".rules", "expected an array");
1654
+ else {
1655
+ const seen = /* @__PURE__ */ new Set();
1656
+ raw.rules.forEach((entry, index) => {
1657
+ const rule = readRule(check, `rules[${index}]`, entry);
1658
+ if (rule === void 0) return;
1659
+ if (seen.has(rule.id)) {
1660
+ check.fail(`rules[${index}].id`, `duplicate rule id \`${rule.id}\``);
1661
+ return;
1662
+ }
1663
+ seen.add(rule.id);
1664
+ rules.push(rule);
1665
+ });
1666
+ }
1667
+ if (check.errors.length > 0) return {
1668
+ ok: false,
1669
+ errors: check.errors
1670
+ };
1671
+ if (name === void 0 || version === void 0) return {
1672
+ ok: false,
1673
+ errors: check.errors
1674
+ };
1675
+ return {
1676
+ ok: true,
1677
+ pack: {
1678
+ prae: "0.1",
1679
+ pack: name,
1680
+ version,
1681
+ ...description === void 0 ? {} : { description },
1682
+ ...owner === void 0 ? {} : { owner },
1683
+ ...expires === void 0 ? {} : { expires_at: expires },
1684
+ ...baseline === void 0 ? {} : { baseline },
1685
+ rules
1686
+ }
1687
+ };
1688
+ }
1689
+
1690
+ //#endregion
1691
+ export { PATH_ARGS as a, withinAny as c, evaluate as i, segments as l, FRAMEWORKS as n, hostOf as o, cite as r, relativeTo as s, parsePack as t };