approval-md 0.4.2 → 0.4.5

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.
@@ -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.2` and no decoration at
13
+ * terminal or an ASCII locale gets `approval.md v0.4.5` 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.2";
19
+ export declare const VERSION = "0.4.5";
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;
@@ -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.2` and no decoration at
13
+ * terminal or an ASCII locale gets `approval.md v0.4.5` 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.2";
19
+ export const VERSION = "0.4.5";
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
@@ -401,6 +401,12 @@ interface LexSegment {
401
401
  text: string;
402
402
  words: LexWord[];
403
403
  redirects: LexRedirect[];
404
+ /**
405
+ * The `$(…)` found in the bodies of unquoted heredocs consumed on this
406
+ * segment's line (APRV-504, folded into APRV-508). The shell expands them
407
+ * when the body is read, so they are classified like any other.
408
+ */
409
+ bodySubstitutions: string[];
404
410
  /** Set when the segment contains a construct whose effect cannot be read. */
405
411
  opaque: string | null;
406
412
  }
@@ -481,6 +487,15 @@ interface RuleContext {
481
487
  * (APRV-114's fetch refinement) needs to know that one of them is a hole.
482
488
  */
483
489
  substituted: boolean;
490
+ /**
491
+ * Did the segment carry a `NAME=value` assignment before the binary (or
492
+ * before a sandbox wrapper around it)? The classifier strips assignments to
493
+ * find the binary, so a rule never sees them; a rule whose answer rests on
494
+ * the argv saying WHICH file runs needs to know the environment may say
495
+ * otherwise (APRV-502: `BUN_OPTIONS='--cwd ../x'`, `NODE_OPTIONS`,
496
+ * `PYTHONPATH`, `PYTHONSTARTUP`, `HOME`).
497
+ */
498
+ assigned: boolean;
484
499
  /** What the caller knows about the machine (APRV-267). Never read from here. */
485
500
  context: ClassifierContext;
486
501
  }
@@ -531,6 +546,31 @@ export interface CommandRule {
531
546
  * answer it has today.
532
547
  */
533
548
  probe?: boolean;
549
+ /**
550
+ * Match only an argv that runs a skill script (APRV-502): allowlisted inert
551
+ * flags, then a `skills/<skill>/scripts/<file>` path, with no word of the
552
+ * segment carrying a command substitution and no `NAME=value` assignment
553
+ * before the binary ({@link skillScriptArg}, {@link matchRule}).
554
+ *
555
+ * The same device as `probe`, for the same reason: the row is a SHAPE, and
556
+ * an argv outside the shape must not match it at all, so `bun --cwd /etc x`
557
+ * and `python3 -m x` keep the `unclassified` answer they have today rather
558
+ * than being refined into some other refusal.
559
+ */
560
+ skillScript?: boolean;
561
+ /**
562
+ * Match only an argv this predicate accepts (APRV-508).
563
+ *
564
+ * The general form of `probe` and `skillScript`: the row is a SHAPE, and an
565
+ * argv outside it does not match the row at all, so the segment keeps the
566
+ * answer it had before the row existed (`unclassified`, for a binary no other
567
+ * row names). Before the predicate is asked, a shape row is declined by a
568
+ * substituted word (its text is a hole) and by a binary spelled with a path
569
+ * outside the system directories ({@link isSystemBinWord}: `./date` is
570
+ * whatever program sits in the working directory). `assignments` are the
571
+ * `NAME=value` words in front of the binary, verbatim.
572
+ */
573
+ shape?: (args: readonly string[], assignments: readonly string[]) => boolean;
534
574
  }
535
575
  /**
536
576
  * The suffix that marks a Muse model as training on what it is shown.
@@ -545,6 +585,42 @@ export declare const CONTRIBUTOR_SUFFIX = "-contributor";
545
585
  export declare const READ_OUT_OF_SCOPE_RULE = "read-out-of-scope";
546
586
  /** The rule id for a read target whose expansion the text cannot show. */
547
587
  export declare const READ_UNREADABLE_TARGET_RULE = "read-unreadable-path";
588
+ /** The rule id for `bun` / `python` / `python3` running a skill script. */
589
+ export declare const SKILL_SCRIPT_RULE = "skill-script";
590
+ /** The rule id `refineNode` returns for `node` running a skill script. */
591
+ export declare const NODE_SKILL_SCRIPT_RULE = "node-skill-script";
592
+ /**
593
+ * Is `word` a skill-script path, in exactly one of the two shapes (APRV-502)?
594
+ *
595
+ * - Relative: `skills/<skill>/scripts/<file>`, with any number of leading
596
+ * `./` normalised away. Four segments, no more: `skills/x/scripts/sub/f.ts`
597
+ * and `.hermes/skills/x/scripts/f.ts` are not this shape.
598
+ * - Absolute: a path ending `.hermes/skills/<skill>/scripts/<file>` or
599
+ * `.hermes/profiles/<p>/skills/<skill>/scripts/<file>`, the same Hermes-home
600
+ * grammar {@link protectedPathClass} reads (a profile home is a home of its
601
+ * own, one level down).
602
+ *
603
+ * Every segment of either shape, the absolute prefix included, must pass
604
+ * {@link isSkillScriptSegment}: an empty segment (`skills//x`), a `.` or `..`
605
+ * anywhere, a glob, a variable or a backtick, and the shape does not match.
606
+ * An unexpanded `$HERMES_HOME/skills/…` or `~/.hermes/skills/…` is therefore
607
+ * not this rule; the text cannot say where it points.
608
+ *
609
+ * Text only, like everything in this file. Whether the file really sits in
610
+ * the directory it was spelled in is `src/cli/hook.ts`'s disk pass, which can
611
+ * only tighten this answer.
612
+ */
613
+ export declare function isSkillScriptPath(word: string): boolean;
614
+ /**
615
+ * The skill script this interpreter argv runs, or `null` when the argv is not
616
+ * exactly `[allowlisted flags…] <skill-script path> [arguments…]` (APRV-502).
617
+ *
618
+ * Words AFTER the script are the script's own arguments and are not read here:
619
+ * `python3 skills/agent-commons/scripts/search_forum.py --q x` is the shape.
620
+ * They are still scanned for protected and credential paths by
621
+ * `classifySegment`, which runs after the table and can only tighten.
622
+ */
623
+ export declare function skillScriptArg(bin: string, args: readonly string[]): string | null;
548
624
  /**
549
625
  * The table.
550
626
  *