approval-md 0.4.2 → 0.4.3
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/dist/src/cli/hook.d.ts +25 -2
- package/dist/src/cli/hook.js +258 -6
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/core/command-class.d.ts +57 -0
- package/dist/src/core/command-class.js +188 -10
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/live-draw.d.ts +12 -2
- package/dist/src/core/live-draw.js +12 -2
- package/dist/src/core/live-draw.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/package.json +2 -2
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* half-blocks, no box-drawing, no emoji, because those are the characters that
|
|
11
11
|
* break in Terminal.app's default font and in every CI log. And it DEGRADES —
|
|
12
12
|
* with colour off it collapses to a single line, so a pipe, a `NO_COLOR`
|
|
13
|
-
* terminal or an ASCII locale gets `approval.md v0.4.
|
|
13
|
+
* terminal or an ASCII locale gets `approval.md v0.4.3` and no decoration at
|
|
14
14
|
* all. The banner is ornament; the name and the version are the information,
|
|
15
15
|
* and only the information survives the degradation.
|
|
16
16
|
*/
|
|
17
17
|
import { type Style } from "./style.js";
|
|
18
18
|
/** Kept in step with package.json by a test rather than by memory. */
|
|
19
|
-
export declare const VERSION = "0.4.
|
|
19
|
+
export declare const VERSION = "0.4.3";
|
|
20
20
|
export declare const TAGLINE = "human approval for agent actions";
|
|
21
21
|
/** The one-line form, and the only form a pipe ever sees. */
|
|
22
22
|
export declare function plainWordmark(): string;
|
package/dist/src/cli/wordmark.js
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* half-blocks, no box-drawing, no emoji, because those are the characters that
|
|
11
11
|
* break in Terminal.app's default font and in every CI log. And it DEGRADES —
|
|
12
12
|
* with colour off it collapses to a single line, so a pipe, a `NO_COLOR`
|
|
13
|
-
* terminal or an ASCII locale gets `approval.md v0.4.
|
|
13
|
+
* terminal or an ASCII locale gets `approval.md v0.4.3` and no decoration at
|
|
14
14
|
* all. The banner is ornament; the name and the version are the information,
|
|
15
15
|
* and only the information survives the degradation.
|
|
16
16
|
*/
|
|
17
17
|
import { style as processStyle } from "./style.js";
|
|
18
18
|
/** Kept in step with package.json by a test rather than by memory. */
|
|
19
|
-
export const VERSION = "0.4.
|
|
19
|
+
export const VERSION = "0.4.3";
|
|
20
20
|
export const TAGLINE = "human approval for agent actions";
|
|
21
21
|
/**
|
|
22
22
|
* The six-line form. Written as a raw array so no editor reflows it: every
|
|
@@ -481,6 +481,15 @@ interface RuleContext {
|
|
|
481
481
|
* (APRV-114's fetch refinement) needs to know that one of them is a hole.
|
|
482
482
|
*/
|
|
483
483
|
substituted: boolean;
|
|
484
|
+
/**
|
|
485
|
+
* Did the segment carry a `NAME=value` assignment before the binary (or
|
|
486
|
+
* before a sandbox wrapper around it)? The classifier strips assignments to
|
|
487
|
+
* find the binary, so a rule never sees them; a rule whose answer rests on
|
|
488
|
+
* the argv saying WHICH file runs needs to know the environment may say
|
|
489
|
+
* otherwise (APRV-502: `BUN_OPTIONS='--cwd ../x'`, `NODE_OPTIONS`,
|
|
490
|
+
* `PYTHONPATH`, `PYTHONSTARTUP`, `HOME`).
|
|
491
|
+
*/
|
|
492
|
+
assigned: boolean;
|
|
484
493
|
/** What the caller knows about the machine (APRV-267). Never read from here. */
|
|
485
494
|
context: ClassifierContext;
|
|
486
495
|
}
|
|
@@ -531,6 +540,18 @@ export interface CommandRule {
|
|
|
531
540
|
* answer it has today.
|
|
532
541
|
*/
|
|
533
542
|
probe?: boolean;
|
|
543
|
+
/**
|
|
544
|
+
* Match only an argv that runs a skill script (APRV-502): allowlisted inert
|
|
545
|
+
* flags, then a `skills/<skill>/scripts/<file>` path, with no word of the
|
|
546
|
+
* segment carrying a command substitution and no `NAME=value` assignment
|
|
547
|
+
* before the binary ({@link skillScriptArg}, {@link matchRule}).
|
|
548
|
+
*
|
|
549
|
+
* The same device as `probe`, for the same reason: the row is a SHAPE, and
|
|
550
|
+
* an argv outside the shape must not match it at all, so `bun --cwd /etc x`
|
|
551
|
+
* and `python3 -m x` keep the `unclassified` answer they have today rather
|
|
552
|
+
* than being refined into some other refusal.
|
|
553
|
+
*/
|
|
554
|
+
skillScript?: boolean;
|
|
534
555
|
}
|
|
535
556
|
/**
|
|
536
557
|
* The suffix that marks a Muse model as training on what it is shown.
|
|
@@ -545,6 +566,42 @@ export declare const CONTRIBUTOR_SUFFIX = "-contributor";
|
|
|
545
566
|
export declare const READ_OUT_OF_SCOPE_RULE = "read-out-of-scope";
|
|
546
567
|
/** The rule id for a read target whose expansion the text cannot show. */
|
|
547
568
|
export declare const READ_UNREADABLE_TARGET_RULE = "read-unreadable-path";
|
|
569
|
+
/** The rule id for `bun` / `python` / `python3` running a skill script. */
|
|
570
|
+
export declare const SKILL_SCRIPT_RULE = "skill-script";
|
|
571
|
+
/** The rule id `refineNode` returns for `node` running a skill script. */
|
|
572
|
+
export declare const NODE_SKILL_SCRIPT_RULE = "node-skill-script";
|
|
573
|
+
/**
|
|
574
|
+
* Is `word` a skill-script path, in exactly one of the two shapes (APRV-502)?
|
|
575
|
+
*
|
|
576
|
+
* - Relative: `skills/<skill>/scripts/<file>`, with any number of leading
|
|
577
|
+
* `./` normalised away. Four segments, no more: `skills/x/scripts/sub/f.ts`
|
|
578
|
+
* and `.hermes/skills/x/scripts/f.ts` are not this shape.
|
|
579
|
+
* - Absolute: a path ending `.hermes/skills/<skill>/scripts/<file>` or
|
|
580
|
+
* `.hermes/profiles/<p>/skills/<skill>/scripts/<file>`, the same Hermes-home
|
|
581
|
+
* grammar {@link protectedPathClass} reads (a profile home is a home of its
|
|
582
|
+
* own, one level down).
|
|
583
|
+
*
|
|
584
|
+
* Every segment of either shape, the absolute prefix included, must pass
|
|
585
|
+
* {@link isSkillScriptSegment}: an empty segment (`skills//x`), a `.` or `..`
|
|
586
|
+
* anywhere, a glob, a variable or a backtick, and the shape does not match.
|
|
587
|
+
* An unexpanded `$HERMES_HOME/skills/…` or `~/.hermes/skills/…` is therefore
|
|
588
|
+
* not this rule; the text cannot say where it points.
|
|
589
|
+
*
|
|
590
|
+
* Text only, like everything in this file. Whether the file really sits in
|
|
591
|
+
* the directory it was spelled in is `src/cli/hook.ts`'s disk pass, which can
|
|
592
|
+
* only tighten this answer.
|
|
593
|
+
*/
|
|
594
|
+
export declare function isSkillScriptPath(word: string): boolean;
|
|
595
|
+
/**
|
|
596
|
+
* The skill script this interpreter argv runs, or `null` when the argv is not
|
|
597
|
+
* exactly `[allowlisted flags…] <skill-script path> [arguments…]` (APRV-502).
|
|
598
|
+
*
|
|
599
|
+
* Words AFTER the script are the script's own arguments and are not read here:
|
|
600
|
+
* `python3 skills/agent-commons/scripts/search_forum.py --q x` is the shape.
|
|
601
|
+
* They are still scanned for protected and credential paths by
|
|
602
|
+
* `classifySegment`, which runs after the table and can only tighten.
|
|
603
|
+
*/
|
|
604
|
+
export declare function skillScriptArg(bin: string, args: readonly string[]): string | null;
|
|
548
605
|
/**
|
|
549
606
|
* The table.
|
|
550
607
|
*
|
|
@@ -2473,9 +2473,143 @@ function refineApprovalVerb(positionals, args = []) {
|
|
|
2473
2473
|
function refineApproval(ctx) {
|
|
2474
2474
|
return (refineApprovalVerb(ctx.positionals, ctx.args) ?? { class: GATE_SELF_CLASS, rule: "approval" });
|
|
2475
2475
|
}
|
|
2476
|
+
// ---------------------------------------------------------------------------
|
|
2477
|
+
// Skill scripts (APRV-502)
|
|
2478
|
+
// ---------------------------------------------------------------------------
|
|
2479
|
+
/**
|
|
2480
|
+
* The class an interpreter run of a skill script takes: SPEC.md §7's
|
|
2481
|
+
* `exec.local`, "tests, lint, build, scripts inside the workspace". No new
|
|
2482
|
+
* class is minted; the classifier simply had no row that emitted this one.
|
|
2483
|
+
*/
|
|
2484
|
+
const SKILL_SCRIPT_CLASS = "exec.local";
|
|
2485
|
+
/** The rule id for `bun` / `python` / `python3` running a skill script. */
|
|
2486
|
+
export const SKILL_SCRIPT_RULE = "skill-script";
|
|
2487
|
+
/** The rule id `refineNode` returns for `node` running a skill script. */
|
|
2488
|
+
export const NODE_SKILL_SCRIPT_RULE = "node-skill-script";
|
|
2489
|
+
/**
|
|
2490
|
+
* The flags an interpreter may carry BEFORE a skill script, per binary
|
|
2491
|
+
* (APRV-502). An ALLOWLIST: a flag outside it, a `--flag=value`, a lone `-`
|
|
2492
|
+
* (stdin) or `--` means the rule does not match at all, so the command keeps
|
|
2493
|
+
* exactly the answer it had before this rule existed (`unclassified` for
|
|
2494
|
+
* `bun --cwd /etc …` and `python3 -m x`, opaque for `python3 -c`, `node -e`).
|
|
2495
|
+
*
|
|
2496
|
+
* Every flag here is inert in the sense that matters: it changes neither WHICH
|
|
2497
|
+
* file runs nor where code comes from. A flag that takes a value (`--cwd`,
|
|
2498
|
+
* `--require`, `--import`, `-W`, `-X`) is absent on purpose, because its value
|
|
2499
|
+
* would be a second thing the interpreter loads or a different directory the
|
|
2500
|
+
* script path is read from. The same flags spelled through the environment
|
|
2501
|
+
* (`BUN_OPTIONS`, `NODE_OPTIONS`, `PYTHONPATH`, `PYTHONSTARTUP`, `HOME`) are
|
|
2502
|
+
* refused the same way: any `NAME=value` prefix on the segment declines the
|
|
2503
|
+
* shape ({@link matchRule}, `RuleContext.assigned`).
|
|
2504
|
+
*/
|
|
2505
|
+
const SKILL_SCRIPT_FLAGS = {
|
|
2506
|
+
bun: ["--bun", "--smol"],
|
|
2507
|
+
node: ["--no-warnings", "--enable-source-maps", "--experimental-strip-types"],
|
|
2508
|
+
python: [],
|
|
2509
|
+
python3: [],
|
|
2510
|
+
};
|
|
2511
|
+
/**
|
|
2512
|
+
* Python's inert single-letter flags, alone or clustered (`-I`, `-IB`, `-OO`):
|
|
2513
|
+
* isolated mode, unbuffered, no bytecode, ignore `PYTHON*` variables, no user
|
|
2514
|
+
* site, no `site` import, optimise, quiet. None of them takes a value, so a
|
|
2515
|
+
* cluster cannot hide one; `-c`, `-m`, `-W` and `-X` are not letters here.
|
|
2516
|
+
*/
|
|
2517
|
+
const PYTHON_SKILL_SCRIPT_FLAGS = /^-[IuBEsSOq]+$/u;
|
|
2518
|
+
/**
|
|
2519
|
+
* One segment of a skill-script path: non-empty, not `.` or `..`, not
|
|
2520
|
+
* flag-shaped, and free of anything a shell could expand (`$`, backtick, a
|
|
2521
|
+
* glob or brace character, a home shortcut) or a platform could read as a
|
|
2522
|
+
* separator (`\`).
|
|
2523
|
+
*/
|
|
2524
|
+
function isSkillScriptSegment(segment) {
|
|
2525
|
+
if (segment.length === 0 || segment === "." || segment === "..")
|
|
2526
|
+
return false;
|
|
2527
|
+
if (segment.startsWith("-"))
|
|
2528
|
+
return false;
|
|
2529
|
+
return !/[*?[\]{}$`~\\]/u.test(segment);
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Is `word` a skill-script path, in exactly one of the two shapes (APRV-502)?
|
|
2533
|
+
*
|
|
2534
|
+
* - Relative: `skills/<skill>/scripts/<file>`, with any number of leading
|
|
2535
|
+
* `./` normalised away. Four segments, no more: `skills/x/scripts/sub/f.ts`
|
|
2536
|
+
* and `.hermes/skills/x/scripts/f.ts` are not this shape.
|
|
2537
|
+
* - Absolute: a path ending `.hermes/skills/<skill>/scripts/<file>` or
|
|
2538
|
+
* `.hermes/profiles/<p>/skills/<skill>/scripts/<file>`, the same Hermes-home
|
|
2539
|
+
* grammar {@link protectedPathClass} reads (a profile home is a home of its
|
|
2540
|
+
* own, one level down).
|
|
2541
|
+
*
|
|
2542
|
+
* Every segment of either shape, the absolute prefix included, must pass
|
|
2543
|
+
* {@link isSkillScriptSegment}: an empty segment (`skills//x`), a `.` or `..`
|
|
2544
|
+
* anywhere, a glob, a variable or a backtick, and the shape does not match.
|
|
2545
|
+
* An unexpanded `$HERMES_HOME/skills/…` or `~/.hermes/skills/…` is therefore
|
|
2546
|
+
* not this rule; the text cannot say where it points.
|
|
2547
|
+
*
|
|
2548
|
+
* Text only, like everything in this file. Whether the file really sits in
|
|
2549
|
+
* the directory it was spelled in is `src/cli/hook.ts`'s disk pass, which can
|
|
2550
|
+
* only tighten this answer.
|
|
2551
|
+
*/
|
|
2552
|
+
export function isSkillScriptPath(word) {
|
|
2553
|
+
if (word.startsWith("/")) {
|
|
2554
|
+
const parts = word.slice(1).split("/");
|
|
2555
|
+
if (!parts.every(isSkillScriptSegment))
|
|
2556
|
+
return false;
|
|
2557
|
+
const n = parts.length;
|
|
2558
|
+
const tail = (offset) => parts[n - offset] === ".hermes" && parts[n - 4] === "skills" && parts[n - 2] === "scripts";
|
|
2559
|
+
if (n >= 5 && tail(5))
|
|
2560
|
+
return true;
|
|
2561
|
+
return n >= 7 && parts[n - 6] === "profiles" && tail(7);
|
|
2562
|
+
}
|
|
2563
|
+
let rest = word;
|
|
2564
|
+
while (rest.startsWith("./"))
|
|
2565
|
+
rest = rest.slice(2);
|
|
2566
|
+
const parts = rest.split("/");
|
|
2567
|
+
return (parts.length === 4 &&
|
|
2568
|
+
parts.every(isSkillScriptSegment) &&
|
|
2569
|
+
parts[0] === "skills" &&
|
|
2570
|
+
parts[2] === "scripts");
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* The skill script this interpreter argv runs, or `null` when the argv is not
|
|
2574
|
+
* exactly `[allowlisted flags…] <skill-script path> [arguments…]` (APRV-502).
|
|
2575
|
+
*
|
|
2576
|
+
* Words AFTER the script are the script's own arguments and are not read here:
|
|
2577
|
+
* `python3 skills/agent-commons/scripts/search_forum.py --q x` is the shape.
|
|
2578
|
+
* They are still scanned for protected and credential paths by
|
|
2579
|
+
* `classifySegment`, which runs after the table and can only tighten.
|
|
2580
|
+
*/
|
|
2581
|
+
export function skillScriptArg(bin, args) {
|
|
2582
|
+
const allowed = SKILL_SCRIPT_FLAGS[bin];
|
|
2583
|
+
if (allowed === undefined)
|
|
2584
|
+
return null;
|
|
2585
|
+
const python = bin === "python" || bin === "python3";
|
|
2586
|
+
for (const arg of args) {
|
|
2587
|
+
if (!arg.startsWith("-"))
|
|
2588
|
+
return isSkillScriptPath(arg) ? arg : null;
|
|
2589
|
+
if (allowed.includes(arg))
|
|
2590
|
+
continue;
|
|
2591
|
+
if (python && PYTHON_SKILL_SCRIPT_FLAGS.test(arg))
|
|
2592
|
+
continue;
|
|
2593
|
+
// Anything else: a value-taking flag, `--flag=value`, inline code (`-c`,
|
|
2594
|
+
// `-e`, `-m`, `-p`), a lone `-` (stdin) or `--`. Not this rule.
|
|
2595
|
+
return null;
|
|
2596
|
+
}
|
|
2597
|
+
return null;
|
|
2598
|
+
}
|
|
2599
|
+
/** `bun` / `python` / `python3` running a skill script; binds the script path. */
|
|
2600
|
+
function refineSkillScript(ctx) {
|
|
2601
|
+
// `matchRule` only lets this row match when `skillScriptArg` found a script,
|
|
2602
|
+
// so the fallback is unreachable; it is spelled as the row's own answer
|
|
2603
|
+
// rather than as a `null`, which would be read as "opaque".
|
|
2604
|
+
const script = skillScriptArg(ctx.bin, ctx.args);
|
|
2605
|
+
return script === null
|
|
2606
|
+
? { class: SKILL_SCRIPT_CLASS, rule: SKILL_SCRIPT_RULE }
|
|
2607
|
+
: { class: SKILL_SCRIPT_CLASS, rule: SKILL_SCRIPT_RULE, path: script };
|
|
2608
|
+
}
|
|
2476
2609
|
/**
|
|
2477
2610
|
* `node` — an inline script is opaque, the gate's own entry point is
|
|
2478
|
-
* pass-through,
|
|
2611
|
+
* pass-through, a skill script is `exec.local` (APRV-502), and anything else
|
|
2612
|
+
* is a workspace script.
|
|
2479
2613
|
*/
|
|
2480
2614
|
function refineNode(ctx) {
|
|
2481
2615
|
if (hasFlag(ctx.args, ["-e", "--eval", "-p", "--print"]))
|
|
@@ -2489,6 +2623,14 @@ function refineNode(ctx) {
|
|
|
2489
2623
|
rule: "node-approval-cli",
|
|
2490
2624
|
});
|
|
2491
2625
|
}
|
|
2626
|
+
// APRV-502. A substituted word is a hole in the text, and an assignment
|
|
2627
|
+
// prefix (`NODE_OPTIONS='--require …'`) is a flag spelled through the
|
|
2628
|
+
// environment, so either declines the skill-script reading and the segment
|
|
2629
|
+
// keeps the answer it had before.
|
|
2630
|
+
const skill = ctx.substituted || ctx.assigned ? null : skillScriptArg("node", ctx.args);
|
|
2631
|
+
if (skill !== null) {
|
|
2632
|
+
return { class: SKILL_SCRIPT_CLASS, rule: NODE_SKILL_SCRIPT_RULE, path: skill };
|
|
2633
|
+
}
|
|
2492
2634
|
return { class: "files.write.workspace", rule: "node-script" };
|
|
2493
2635
|
}
|
|
2494
2636
|
/**
|
|
@@ -2686,9 +2828,24 @@ export const COMMAND_RULES = [
|
|
|
2686
2828
|
id: "node",
|
|
2687
2829
|
bins: ["node"],
|
|
2688
2830
|
class: "files.write.workspace",
|
|
2689
|
-
emits: [GATE_SELF_CLASS, "log.sync", "log.advance", "policy.core"],
|
|
2831
|
+
emits: [GATE_SELF_CLASS, "log.sync", "log.advance", "policy.core", SKILL_SCRIPT_CLASS],
|
|
2690
2832
|
refine: refineNode,
|
|
2691
2833
|
},
|
|
2834
|
+
// APRV-502. An interpreter running a skill script, `exec.local`. Below every
|
|
2835
|
+
// `bun` row of the package-manager group, so `bun run x`, `bun install` and
|
|
2836
|
+
// `bun --version` keep their classes; `skillScript` makes the row match ONLY
|
|
2837
|
+
// the exact shape, so every other `bun`, `python` or `python3` argv keeps the
|
|
2838
|
+
// answer it had before the row existed. `node` reaches the same answer
|
|
2839
|
+
// inside {@link refineNode}, because its row has no `subs` to sit beside.
|
|
2840
|
+
// Named in {@link CODE_EXECUTING_RULES}: a skill script is code the runtime
|
|
2841
|
+
// did not author.
|
|
2842
|
+
{
|
|
2843
|
+
id: SKILL_SCRIPT_RULE,
|
|
2844
|
+
bins: ["bun", "python", "python3"],
|
|
2845
|
+
class: SKILL_SCRIPT_CLASS,
|
|
2846
|
+
skillScript: true,
|
|
2847
|
+
refine: refineSkillScript,
|
|
2848
|
+
},
|
|
2692
2849
|
{
|
|
2693
2850
|
id: "approval",
|
|
2694
2851
|
bins: ["approval"],
|
|
@@ -3198,6 +3355,13 @@ export const CODE_EXECUTING_RULES = [
|
|
|
3198
3355
|
"npm-script",
|
|
3199
3356
|
/** `node <script>` — the plainest spelling of "run what I just wrote". */
|
|
3200
3357
|
"node-script",
|
|
3358
|
+
/**
|
|
3359
|
+
* `bun` / `python3` / `node` running `skills/<skill>/scripts/<file>`
|
|
3360
|
+
* (APRV-502). A skill script is a file in the workspace an agent may have
|
|
3361
|
+
* written a minute ago, exactly as `node x.mjs` is.
|
|
3362
|
+
*/
|
|
3363
|
+
SKILL_SCRIPT_RULE,
|
|
3364
|
+
NODE_SKILL_SCRIPT_RULE,
|
|
3201
3365
|
/** `npx`, `tsx`, `tsc`, `vitest`, `jest`, `make`, and kin. */
|
|
3202
3366
|
"workspace-tool",
|
|
3203
3367
|
/**
|
|
@@ -3409,15 +3573,27 @@ function sandboxWrapper(words, start) {
|
|
|
3409
3573
|
/**
|
|
3410
3574
|
* Find the first table row matching this binary, subcommand and argv.
|
|
3411
3575
|
*
|
|
3412
|
-
* The argv is read for
|
|
3413
|
-
*
|
|
3576
|
+
* The argv is read for two purposes only: a `probe` row matches an argv that
|
|
3577
|
+
* is nothing but version or help flags (APRV-397), and a `skillScript` row
|
|
3578
|
+
* matches an argv that runs a skill script (APRV-502). No other row reads it.
|
|
3579
|
+
*
|
|
3580
|
+
* `substituted` and `assigned` are read by the `skillScript` row alone: a
|
|
3581
|
+
* substituted word is a hole in the text, and an assignment prefix is a flag
|
|
3582
|
+
* spelled through the environment (`BUN_OPTIONS='--cwd ../x'` makes bun read
|
|
3583
|
+
* a different directory's file), so either one means the shape is not matched
|
|
3584
|
+
* and the segment keeps the answer it had before (fail closed).
|
|
3414
3585
|
*/
|
|
3415
|
-
function matchRule(bin, sub, args) {
|
|
3586
|
+
function matchRule(bin, sub, args, substituted = false, assigned = false) {
|
|
3416
3587
|
for (const rule of COMMAND_RULES) {
|
|
3417
3588
|
if (!rule.bins.includes(bin))
|
|
3418
3589
|
continue;
|
|
3419
3590
|
if (rule.probe === true && !isVersionProbe(args))
|
|
3420
3591
|
continue;
|
|
3592
|
+
// APRV-502: see the doc comment above; any of the three declines the row.
|
|
3593
|
+
if (rule.skillScript === true &&
|
|
3594
|
+
(substituted || assigned || skillScriptArg(bin, args) === null)) {
|
|
3595
|
+
continue;
|
|
3596
|
+
}
|
|
3421
3597
|
if (rule.subs !== undefined) {
|
|
3422
3598
|
if (sub === null || !rule.subs.includes(sub))
|
|
3423
3599
|
continue;
|
|
@@ -3457,6 +3633,8 @@ function classifySegment(segment, protectedPaths, context) {
|
|
|
3457
3633
|
let cursor = 0;
|
|
3458
3634
|
while (cursor < words.length && ASSIGNMENT.test(words[cursor]))
|
|
3459
3635
|
cursor += 1;
|
|
3636
|
+
// APRV-502: read by the skill-script shape only ({@link matchRule}).
|
|
3637
|
+
const assigned = cursor > 0;
|
|
3460
3638
|
// APRV-193. A sandbox wrapper is not a command: it is a room, and what
|
|
3461
3639
|
// matters is what runs inside it. `approval sandbox -- npm install` is
|
|
3462
3640
|
// `deps.add`, and it has to be, in both directions.
|
|
@@ -3541,7 +3719,10 @@ function classifySegment(segment, protectedPaths, context) {
|
|
|
3541
3719
|
if (credential !== null)
|
|
3542
3720
|
return { ok: true, ...credential };
|
|
3543
3721
|
const sub = positionals[0] ?? null;
|
|
3544
|
-
const
|
|
3722
|
+
const substituted = segment.words
|
|
3723
|
+
.slice(cursor + 1)
|
|
3724
|
+
.some((word) => word.substitutions.length > 0);
|
|
3725
|
+
const rule = matchRule(basename, sub, args, substituted, assigned);
|
|
3545
3726
|
if (rule === null) {
|
|
3546
3727
|
return {
|
|
3547
3728
|
ok: false,
|
|
@@ -3551,10 +3732,7 @@ function classifySegment(segment, protectedPaths, context) {
|
|
|
3551
3732
|
: `no rule for ${basename} ${sub}`,
|
|
3552
3733
|
};
|
|
3553
3734
|
}
|
|
3554
|
-
const
|
|
3555
|
-
.slice(cursor + 1)
|
|
3556
|
-
.some((word) => word.substitutions.length > 0);
|
|
3557
|
-
const ctx = { bin: basename, args, positionals, sub, substituted, context };
|
|
3735
|
+
const ctx = { bin: basename, args, positionals, sub, substituted, assigned, context };
|
|
3558
3736
|
const refined = rule.refine === undefined ? null : rule.refine(ctx);
|
|
3559
3737
|
if (rule.refine !== undefined && refined === null) {
|
|
3560
3738
|
return { ok: false, code: "opaque", detail: `${basename} runs inline source` };
|