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.
@@ -1,521 +0,0 @@
1
- //#region ../kernel/src/command.ts
2
- /** Wrappers whose first operand is the program actually being run. */
3
- const WRAPPERS = new Set([
4
- "sudo",
5
- "command",
6
- "env",
7
- "nohup",
8
- "time",
9
- "xargs"
10
- ]);
11
- /** Wrapper options that consume the next token, so it is not the program. */
12
- const OPTION_WITH_VALUE = new Set([
13
- "-u",
14
- "-g",
15
- "-p",
16
- "-h",
17
- "-C",
18
- "-r",
19
- "-t",
20
- "-U",
21
- "-I",
22
- "-n",
23
- "-P",
24
- "-L",
25
- "-s"
26
- ]);
27
- const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
28
- /**
29
- * Split a line into pipeline segments of words, one scan, quote-aware.
30
- *
31
- * Splitting on `|` before parsing quotes is the classic mistake — it turns
32
- * `echo "a | b"` into two segments — so quoting and separators are read
33
- * together.
34
- */
35
- function scan(command) {
36
- const parts = [];
37
- let tokens = [];
38
- let current = "";
39
- let quote;
40
- const endToken = () => {
41
- if (current !== "") tokens.push(current);
42
- current = "";
43
- };
44
- const endSegment = () => {
45
- endToken();
46
- if (tokens.length > 0) parts.push(tokens);
47
- tokens = [];
48
- };
49
- for (let at = 0; at < command.length; at += 1) {
50
- const ch = command[at];
51
- if (quote !== void 0) {
52
- if (ch === quote) quote = void 0;
53
- else current += ch;
54
- continue;
55
- }
56
- if (ch === "'" || ch === "\"") {
57
- quote = ch;
58
- continue;
59
- }
60
- if (ch === ";") {
61
- endSegment();
62
- continue;
63
- }
64
- if (ch === "|" || ch === "&") {
65
- endSegment();
66
- if (command[at + 1] === ch) at += 1;
67
- continue;
68
- }
69
- if (/\s/.test(ch)) {
70
- endToken();
71
- continue;
72
- }
73
- current += ch;
74
- }
75
- endSegment();
76
- return parts;
77
- }
78
- /** Decompose a command line into pipeline segments. */
79
- function segments(command) {
80
- const found = [];
81
- for (const tokens of scan(command)) {
82
- let at = 0;
83
- const wrappers = [];
84
- while (at < tokens.length) {
85
- const token = tokens[at];
86
- if (ASSIGNMENT.test(token)) {
87
- at += 1;
88
- continue;
89
- }
90
- const word = (token.split("/").pop() ?? token).toLowerCase();
91
- if (WRAPPERS.has(word)) {
92
- wrappers.push(word);
93
- at += 1;
94
- while (at < tokens.length) {
95
- const option = tokens[at];
96
- if (!option.startsWith("-")) break;
97
- at += OPTION_WITH_VALUE.has(option) ? 2 : 1;
98
- }
99
- continue;
100
- }
101
- break;
102
- }
103
- const name = tokens[at];
104
- if (name === void 0) {
105
- const last = wrappers.at(-1);
106
- if (last !== void 0) found.push({
107
- name: last,
108
- tokens: [],
109
- wrappers
110
- });
111
- continue;
112
- }
113
- found.push({
114
- name: (name.split("/").pop() ?? name).toLowerCase(),
115
- tokens: tokens.slice(at + 1),
116
- wrappers
117
- });
118
- }
119
- return found;
120
- }
121
- /** `git*` matches `git`, `github`; bare strings match exactly. */
122
- function matches$1(pattern, value) {
123
- if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
124
- return value === pattern;
125
- }
126
- /**
127
- * A glob with `*` anywhere, for pipeline signatures: `curl*|*sh` is the
128
- * shape of download-and-execute however the two ends are spelled.
129
- */
130
- function globMatches(pattern, value) {
131
- const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
132
- return (/* @__PURE__ */ new RegExp(`^${source}$`)).test(value);
133
- }
134
- /**
135
- * Every wanted token holds in a segment: a plain pattern must match some
136
- * token, a `!pattern` must match none. `["-rf", "!--dry-run"]` is a real
137
- * force-delete, not a rehearsal of one.
138
- */
139
- function tokensHold(wanted, tokens) {
140
- return wanted.every((pattern) => pattern.startsWith("!") ? !tokens.some((token) => matches$1(pattern.slice(1), token)) : tokens.some((token) => matches$1(pattern, token)));
141
- }
142
- const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
143
- /**
144
- * Judge one command line against one matcher. Every present field must hold.
145
- */
146
- function commandMatches(match, command) {
147
- const parts = segments(command);
148
- if (parts.length === 0) return false;
149
- const names = list(match.name);
150
- const args = list(match.token);
151
- const anyArgs = list(match.anyToken);
152
- const pipes = list(match.pipedTo);
153
- const shapes = list(match.pipeline);
154
- const argsHold = (segment) => tokensHold(args, segment.tokens) && (anyArgs.length === 0 || anyArgs.some((wanted) => segment.tokens.some((token) => matches$1(wanted, token))));
155
- if (names.length > 0) {
156
- const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name) || segment.wrappers.some((wrapper) => matches$1(pattern, wrapper))));
157
- if (hit.length === 0) return false;
158
- if ((args.length > 0 || anyArgs.length > 0) && !hit.some(argsHold)) return false;
159
- } else if (args.length > 0 || anyArgs.length > 0) {
160
- if (!parts.some(argsHold)) return false;
161
- }
162
- if (pipes.length > 0) {
163
- if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
164
- }
165
- if (shapes.length > 0) {
166
- const signature = parts.map((segment) => segment.name).join("|");
167
- if (!shapes.some((pattern) => globMatches(pattern, signature))) return false;
168
- }
169
- return true;
170
- }
171
-
172
- //#endregion
173
- //#region ../kernel/src/path.ts
174
- /**
175
- * Path normalization and glob containment, with no filesystem access.
176
- *
177
- * The contract, stated here because getting it wrong is how path policies
178
- * become theatre: **this module compares strings.** It resolves `.` and `..`
179
- * lexically so that `./src/../../etc/passwd` cannot pose as a workspace path,
180
- * but it cannot see a symlink. A link inside an allowed directory pointing out
181
- * of it will pass every check here.
182
- *
183
- * That is not a gap to paper over in the matcher — it is a gap the caller
184
- * closes by resolving before it asks. `dsh-overlay-check` learned this the
185
- * hard way during the plugin-store gate review: lexical containment accepted a
186
- * symlink escape cleanly, and the fix was to resolve both the path and its
187
- * nearest existing ancestor before deciding. Adapters that can touch the
188
- * filesystem must resolve first and pass the resolved path in.
189
- */
190
- /** Split a path into segments, resolving `.` and `..` lexically. */
191
- function normalize(path) {
192
- const absolute = path.startsWith("/");
193
- const out = [];
194
- let escaped = false;
195
- for (const raw of path.split(/[/\\]+/)) {
196
- if (raw === "" || raw === ".") continue;
197
- if (raw === "..") {
198
- if (out.length > 0) out.pop();
199
- else escaped = true;
200
- continue;
201
- }
202
- out.push(raw);
203
- }
204
- return {
205
- segments: out,
206
- absolute,
207
- escaped
208
- };
209
- }
210
- /** Turn one glob segment into an anchored regular expression. */
211
- function segmentPattern(segment) {
212
- const body = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
213
- return /* @__PURE__ */ new RegExp(`^${body}$`);
214
- }
215
- /**
216
- * Wildcard segment matching, where `**` spans zero or more segments.
217
- *
218
- * Two-pointer with backtracking rather than a recursive walk: the pattern
219
- * comes from a policy pack and the path from a tool call, and a catastrophic
220
- * backtrack in the hot path of every call would be a denial of service handed
221
- * to whoever writes the pack.
222
- */
223
- function matchSegments(path, pattern) {
224
- let i = 0;
225
- let j = 0;
226
- let starPattern = -1;
227
- let starPath = 0;
228
- while (i < path.length) {
229
- const segment = j < pattern.length ? pattern[j] : void 0;
230
- const here = path[i];
231
- if (segment === "**") {
232
- starPattern = j;
233
- starPath = i;
234
- j += 1;
235
- continue;
236
- }
237
- if (segment !== void 0 && here !== void 0 && segmentPattern(segment).test(here)) {
238
- i += 1;
239
- j += 1;
240
- continue;
241
- }
242
- if (starPattern >= 0) {
243
- starPath += 1;
244
- i = starPath;
245
- j = starPattern + 1;
246
- continue;
247
- }
248
- return false;
249
- }
250
- while (j < pattern.length && pattern[j] === "**") j += 1;
251
- return j === pattern.length;
252
- }
253
- /**
254
- * Is `path` contained by `glob`?
255
- *
256
- * A glob naming a directory contains everything under it: `./src` and
257
- * `./src/**` both match `src/app/main.ts`. Writing the shorter form is the
258
- * common intent, and treating it as "this exact directory entry only" is a
259
- * trap that silently grants the whole subtree past the rule.
260
- */
261
- function within(path, glob) {
262
- const target = normalize(path);
263
- if (target.escaped) return false;
264
- const trimmed = glob.trim();
265
- if (!(trimmed === "**" || trimmed.startsWith("**/")) && target.absolute !== trimmed.startsWith("/")) return false;
266
- const pattern = normalize(glob);
267
- if (matchSegments(target.segments, pattern.segments)) return true;
268
- return matchSegments(target.segments, [...pattern.segments, "**"]);
269
- }
270
- /** Is `path` inside any of these globs? */
271
- function withinAny(path, globs) {
272
- return globs.some((glob) => within(path, glob));
273
- }
274
- /**
275
- * An absolute path under `cwd`, rewritten as the workspace-relative path a
276
- * pack's `./` globs speak about; anything else comes back as written.
277
- *
278
- * A model is free to name a workspace file by its absolute path — the first
279
- * real agent run through the gate did exactly that for its first three
280
- * reads — and a scope rule written as `outside: ["./src/**"]` must decide
281
- * that file by where it is, not by how it was spelled. The anchoring in
282
- * {@link within} is right to keep `/etc/hosts` out of `./**`; this is the
283
- * other half, applied by an adapter that knows the session's directory.
284
- * A path that climbs out of `cwd` with `..` stays absolute, so it stays
285
- * outside.
286
- */
287
- function relativeTo(path, cwd) {
288
- const target = normalize(path);
289
- const base = normalize(cwd);
290
- if (!target.absolute || !base.absolute || target.escaped) return path;
291
- if (base.segments.length > target.segments.length) return path;
292
- if (!base.segments.every((s, i) => s === target.segments[i])) return path;
293
- const rest = target.segments.slice(base.segments.length);
294
- return rest.length === 0 ? "." : `./${rest.join("/")}`;
295
- }
296
- /**
297
- * The host of a URL, or undefined when it is not a URL we can read.
298
- *
299
- * Unparseable input returns undefined, and callers treat that as "no host" —
300
- * which for a `notIn` rule means the rule matches. Failing toward the rule is
301
- * the only safe direction: a string we cannot parse is exactly the shape an
302
- * evasion attempt takes.
303
- */
304
- function hostOf(url) {
305
- try {
306
- return new URL(url).hostname.toLowerCase();
307
- } catch {
308
- return;
309
- }
310
- }
311
- /**
312
- * Host list membership, where a leading dot means "this domain or below".
313
- *
314
- * `example.com` matches only that host. `.example.com` also matches
315
- * `api.example.com`, and deliberately still matches `example.com` itself —
316
- * an operator writing the suffix form means the organisation, not a rule that
317
- * covers every subdomain except the one they first thought of.
318
- */
319
- function hostIn(host, list$1) {
320
- return list$1.some((entry) => {
321
- const want = entry.toLowerCase();
322
- if (want.startsWith(".")) return host === want.slice(1) || host.endsWith(want);
323
- return host === want;
324
- });
325
- }
326
- /**
327
- * Argument names that conventionally carry a filesystem path, in the order
328
- * they are tried when a path rule names none.
329
- *
330
- * A path rule used to default to the single argument spelled `path`. DeepSeek
331
- * Harness's own `read`, `write` and `edit` tools pass `file_path`, so every
332
- * shipped path rule without an explicit `arg` — including the baseline's
333
- * "deny read ./.env" — matched nothing on the very harness it was written
334
- * for, while the ledger dutifully recorded every allowed read. A rule that
335
- * silently never fires is the worst way for policy to be wrong; defaulting
336
- * to the conventional set is the same call the dsh adapter already made for
337
- * path resolution, now made once, here, for matching too.
338
- */
339
- const PATH_ARGS = [
340
- "path",
341
- "file_path",
342
- "filePath",
343
- "target",
344
- "directory",
345
- "cwd",
346
- "file",
347
- "dest",
348
- "destination",
349
- "source"
350
- ];
351
- /** The first conventional path argument present, as a string, or undefined. */
352
- function pathArgOf(args, explicit) {
353
- if (explicit !== void 0) {
354
- const v = args[explicit];
355
- return typeof v === "string" ? v : void 0;
356
- }
357
- for (const key of PATH_ARGS) {
358
- const v = args[key];
359
- if (typeof v === "string") return v;
360
- }
361
- }
362
-
363
- //#endregion
364
- //#region ../kernel/src/rule.ts
365
- /**
366
- * Strength ordering, used when several rules match one call.
367
- *
368
- * Higher wins. This is the property that makes a pack safe to extend: adding a
369
- * rule can raise the strength of a decision and never lower it, so an operator
370
- * appending to a pack can never accidentally widen what an agent may do.
371
- */
372
- const STRENGTH = {
373
- allow: 0,
374
- warn: 1,
375
- correct: 2,
376
- ask: 3,
377
- deny: 4
378
- };
379
-
380
- //#endregion
381
- //#region ../kernel/src/evaluate.ts
382
- function listed(value, want) {
383
- if (want === void 0) return true;
384
- if (value === void 0) return false;
385
- return typeof want === "string" ? value === want : want.includes(value);
386
- }
387
- function argMatches(match, args) {
388
- const has = Object.hasOwn(args, match.name);
389
- if (match.present !== void 0 && match.present !== has) return false;
390
- if (!has) return match.above === void 0 && match.below === void 0 && match.equals === void 0 && match.oneOf === void 0;
391
- const value = args[match.name];
392
- if (match.above !== void 0) {
393
- if (typeof value !== "number" || !(value > match.above)) return false;
394
- }
395
- if (match.below !== void 0) {
396
- if (typeof value !== "number" || !(value < match.below)) return false;
397
- }
398
- if (match.equals !== void 0 && value !== match.equals) return false;
399
- if (match.oneOf !== void 0) {
400
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
401
- if (!match.oneOf.includes(value)) return false;
402
- }
403
- return true;
404
- }
405
- function matches(when, facts) {
406
- if (!listed(facts.tool, when.tool)) return false;
407
- if (!listed(facts.capability, when.capability)) return false;
408
- if (!listed(facts.plugin, when.plugin)) return false;
409
- if (!listed(facts.agent, when.agent)) return false;
410
- if (when.command !== void 0) {
411
- const raw = facts.args.command ?? facts.args.cmd ?? facts.args.script;
412
- if (typeof raw !== "string") return false;
413
- if (!commandMatches(when.command, raw)) return false;
414
- }
415
- if (when.path !== void 0) {
416
- const raw = pathArgOf(facts.args, when.path.arg);
417
- if (typeof raw !== "string") return false;
418
- if (when.path.inside !== void 0 && !withinAny(raw, when.path.inside)) return false;
419
- if (when.path.outside !== void 0 && withinAny(raw, when.path.outside)) return false;
420
- }
421
- if (when.host !== void 0) {
422
- const raw = facts.args[when.host.arg ?? "url"];
423
- if (typeof raw !== "string") return false;
424
- const host = hostOf(raw);
425
- if (host === void 0) return when.host.notIn !== void 0;
426
- if (when.host.in !== void 0 && !hostIn(host, when.host.in)) return false;
427
- if (when.host.notIn !== void 0 && hostIn(host, when.host.notIn)) return false;
428
- }
429
- if (when.arg !== void 0) {
430
- for (const one of when.arg) if (!argMatches(one, facts.args)) return false;
431
- }
432
- if (when.count !== void 0) {
433
- const key = facts.capability;
434
- if (key === void 0) return false;
435
- if (!((facts.counts?.[key] ?? 0) > when.count.above)) return false;
436
- }
437
- if (when.secrets !== void 0) {
438
- const floor = when.secrets.minConfidence ?? 0;
439
- const wanted = when.secrets.types;
440
- if ((facts.secrets ?? []).filter((finding) => finding.confidence >= floor && (wanted === void 0 || wanted.includes(finding.type))).length < (when.secrets.atLeast ?? 1)) return false;
441
- }
442
- return true;
443
- }
444
- function step(rule, effect) {
445
- return {
446
- rule: rule.id,
447
- ...rule.title === void 0 ? {} : { title: rule.title },
448
- effect: effect.kind,
449
- because: effect.because
450
- };
451
- }
452
- /**
453
- * Decide one call against one pack.
454
- *
455
- * Every matching rule lands in the trace; the strongest effect decides. Ties
456
- * go to the first rule in pack order, so a pack reads top-down the way its
457
- * author wrote it.
458
- *
459
- * `correct` is the effect the rest of this ecosystem does not have. A rival
460
- * guardrail can allow a call or refuse it; a rule here can also rewrite an
461
- * argument and let it through, recording what it changed. Refusing is not
462
- * always the safe option — an agent denied mid-task retries, and the retry is
463
- * rarely safer than the clamp would have been.
464
- */
465
- function evaluate(pack, facts) {
466
- const hits = [];
467
- for (const rule of pack.rules) if (matches(rule.when, facts)) hits.push({
468
- rule,
469
- effect: rule.effect
470
- });
471
- if (hits.length === 0) return {
472
- kind: "allow",
473
- trace: [],
474
- evaluated: pack.rules.length
475
- };
476
- hits.sort((a, b) => STRENGTH[b.effect.kind] - STRENGTH[a.effect.kind]);
477
- const trace = hits.map((hit) => step(hit.rule, hit.effect));
478
- const evaluated = pack.rules.length;
479
- const strongest = hits[0];
480
- if (strongest === void 0) return {
481
- kind: "allow",
482
- trace: [],
483
- evaluated
484
- };
485
- const winner = strongest.effect.kind;
486
- if (winner === "deny" || winner === "ask") return {
487
- kind: winner,
488
- rule: strongest.rule.id,
489
- reason: strongest.effect.because,
490
- trace,
491
- evaluated
492
- };
493
- if (winner === "correct") {
494
- const args = { ...facts.args };
495
- for (const hit of hits) if (hit.effect.kind === "correct") Object.assign(args, hit.effect.set);
496
- return {
497
- kind: "correct",
498
- rule: strongest.rule.id,
499
- reason: strongest.effect.because,
500
- args,
501
- trace,
502
- evaluated
503
- };
504
- }
505
- const allowed = hits.find((hit) => hit.effect.kind === "allow");
506
- if (allowed !== void 0) return {
507
- kind: "allow",
508
- rule: allowed.rule.id,
509
- reason: allowed.effect.because,
510
- trace,
511
- evaluated
512
- };
513
- return {
514
- kind: "allow",
515
- trace,
516
- evaluated
517
- };
518
- }
519
-
520
- //#endregion
521
- export { withinAny as a, relativeTo as i, PATH_ARGS as n, segments as o, hostOf as r, evaluate as t };
@@ -1,118 +0,0 @@
1
- //#region ../kernel/src/frameworks.ts
2
- const FRAMEWORKS = {
3
- "owasp-llm-top-10-2026": {
4
- name: "OWASP Top 10 for LLM Applications — 2026",
5
- short: "OWASP LLM 2026",
6
- controls: {
7
- LLM01: {
8
- name: "Prompt Injection",
9
- summary: "Instructions in a prompt or in retrieved content alter what the model does."
10
- },
11
- LLM02: {
12
- name: "Sensitive Information Disclosure",
13
- summary: "Credentials, personal data or proprietary content reach the model or its output."
14
- },
15
- LLM03: {
16
- name: "Excessive Agency",
17
- summary: "The model can take actions, or holds permissions, beyond what its task needs."
18
- },
19
- LLM04: {
20
- name: "Supply Chain",
21
- summary: "A compromised model, dataset, plugin or dependency enters the application."
22
- },
23
- LLM05: {
24
- name: "Data and Model Poisoning",
25
- summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
26
- },
27
- LLM06: {
28
- name: "Unbounded Consumption",
29
- summary: "The application permits resource use without a limit, exhausting budget or service."
30
- },
31
- LLM07: {
32
- name: "Misinformation",
33
- summary: "The model produces false or misleading content that is acted on as if true."
34
- },
35
- LLM08: {
36
- name: "Hidden Context Exposure",
37
- summary: "System prompts, hidden instructions or internal context are revealed."
38
- },
39
- LLM09: {
40
- name: "Vector and Embedding Weaknesses",
41
- summary: "Retrieval stores are manipulated or leak across tenants and permissions."
42
- },
43
- LLM10: {
44
- name: "Improper Output Handling",
45
- summary: "Model output is passed to downstream systems without validation."
46
- }
47
- }
48
- },
49
- "owasp-agentic-top-10-2026": {
50
- name: "OWASP Top 10 for Agentic Applications — 2026",
51
- short: "OWASP Agentic 2026",
52
- controls: {
53
- ASI01: {
54
- name: "Agent Goal Hijack",
55
- summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
56
- },
57
- ASI02: {
58
- name: "Tool Misuse and Exploitation",
59
- summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
60
- },
61
- ASI03: {
62
- name: "Identity and Privilege Abuse",
63
- summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
64
- },
65
- ASI04: {
66
- name: "Agentic Supply Chain",
67
- summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
68
- },
69
- ASI05: {
70
- name: "Unexpected Code Execution",
71
- summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
72
- },
73
- ASI06: {
74
- name: "Memory and Context Poisoning",
75
- summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
76
- },
77
- ASI07: {
78
- name: "Insecure Inter-Agent Communication",
79
- summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
80
- },
81
- ASI08: {
82
- name: "Cascading Failures",
83
- summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
84
- },
85
- ASI09: {
86
- name: "Human-Agent Trust Exploitation",
87
- summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
88
- },
89
- ASI10: {
90
- name: "Rogue Agents",
91
- summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
92
- }
93
- }
94
- }
95
- };
96
- /**
97
- * Every framework control a rule cites, resolved to names.
98
- *
99
- * A pack that parsed cites only ids the registry knows, so the lookups here
100
- * cannot miss; the fallbacks exist for a `Rule` built by hand in code.
101
- */
102
- function cite(rule) {
103
- const out = [];
104
- for (const [framework, controls] of Object.entries(rule.frameworks ?? {})) {
105
- const known = FRAMEWORKS[framework];
106
- for (const control of controls) out.push({
107
- framework,
108
- frameworkName: known?.name ?? framework,
109
- frameworkShort: known?.short ?? framework,
110
- control,
111
- controlName: known?.controls[control]?.name ?? control
112
- });
113
- }
114
- return out;
115
- }
116
-
117
- //#endregion
118
- export { cite as n, FRAMEWORKS as t };