@sema-agent/core 7.24.4 → 7.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/agents/peer-session-drain.d.ts +16 -1
  3. package/dist/agents/peer-session-drain.js +16 -1
  4. package/dist/agents/send-message-tool.d.ts +10 -2
  5. package/dist/agents/send-message-tool.js +18 -7
  6. package/dist/agents/subagent.js +13 -10
  7. package/dist/core/arg-summary.d.ts +0 -7
  8. package/dist/core/arg-summary.js +2 -1
  9. package/dist/core/background-agent-store.d.ts +3 -3
  10. package/dist/core/checkpoint-store.d.ts +1 -0
  11. package/dist/core/delegation-frames.d.ts +5 -0
  12. package/dist/core/gate-fold.js +85 -0
  13. package/dist/core/gate-lanes.js +23 -1
  14. package/dist/core/gate-outcome.d.ts +8 -2
  15. package/dist/core/gate-outcome.js +3 -1
  16. package/dist/core/hooks.d.ts +29 -0
  17. package/dist/core/hooks.js +2 -0
  18. package/dist/core/mailbox-store.d.ts +5 -0
  19. package/dist/core/mailbox-store.js +1 -1
  20. package/dist/core/parent-spec-seat.d.ts +7 -1
  21. package/dist/core/parent-spec-seat.js +2 -0
  22. package/dist/core/permission-rule-consent.js +29 -47
  23. package/dist/core/permission-rule-provider.js +4 -3
  24. package/dist/core/permission-rule-store.d.ts +143 -7
  25. package/dist/core/permission-rule-store.js +165 -15
  26. package/dist/core/runner/contracts.d.ts +36 -2
  27. package/dist/core/runner/gate-exit.d.ts +14 -0
  28. package/dist/core/runner/gate-exit.js +4 -0
  29. package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
  30. package/dist/core/runner/prepare-delegation-surface.js +2 -1
  31. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  32. package/dist/core/runner/prepare-gate-stations.js +7 -4
  33. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  34. package/dist/core/runner/prepare-hands-readface.js +25 -10
  35. package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
  36. package/dist/core/runner/prepare-inherited-gate.js +11 -6
  37. package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
  38. package/dist/core/runner/prepare-policy-chain.js +14 -4
  39. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
  40. package/dist/core/runner/prepare-suspend-saga.js +4 -3
  41. package/dist/core/runner/prepare-task.js +10 -9
  42. package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
  43. package/dist/core/runner/prepare-wiring-manifest.js +8 -4
  44. package/dist/core/runner/resume-apply.js +2 -0
  45. package/dist/core/runner/run-harness-handlers.js +1 -0
  46. package/dist/core/runner/run-identity-wiring.d.ts +2 -2
  47. package/dist/core/runner/run-leg.js +26 -0
  48. package/dist/core/runner/run-notification-lane.d.ts +2 -2
  49. package/dist/core/runner/run-notification-lane.js +2 -1
  50. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  51. package/dist/core/runner/session-rule-policy.d.ts +3 -4
  52. package/dist/core/runner/session-rule-policy.js +11 -3
  53. package/dist/core/session-policy-store.d.ts +27 -3
  54. package/dist/core/session-policy-store.js +12 -4
  55. package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
  56. package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
  57. package/dist/core/store-contracts/peer-directory-contract.js +82 -0
  58. package/dist/core/task-event.d.ts +6 -0
  59. package/dist/core/task-notification.d.ts +79 -4
  60. package/dist/core/task-notification.js +90 -1
  61. package/dist/core/task-registry-agent.js +1 -2
  62. package/dist/core/task-registry-monitor.js +2 -1
  63. package/dist/core/task-registry-shared.d.ts +53 -5
  64. package/dist/core/task-registry-shared.js +13 -2
  65. package/dist/core/task-registry.js +8 -5
  66. package/dist/core/task-spec.d.ts +12 -5
  67. package/dist/core/tool-catalog-entries.js +1 -0
  68. package/dist/core/tool-policy.js +30 -23
  69. package/dist/core/tool-spec.d.ts +1 -1
  70. package/dist/core/wiring-manifest.d.ts +2 -2
  71. package/dist/engine/loop/agent-loop.js +1 -1
  72. package/dist/engine/loop/types.d.ts +4 -0
  73. package/dist/index.d.ts +6 -3
  74. package/dist/index.js +4 -3
  75. package/dist/prompts/coordinator.d.ts +2 -1
  76. package/dist/stores/file/adoption/adopt.js +23 -1
  77. package/dist/stores/file/permission-rule-store.js +3 -1
  78. package/dist/stores/file/session-policy-store.d.ts +31 -0
  79. package/dist/stores/file/session-policy-store.js +87 -7
  80. package/dist/tools/fs/bash-lexer.d.ts +13 -0
  81. package/dist/tools/fs/bash-lexer.js +61 -0
  82. package/dist/tools/fs/bash-program-position.d.ts +36 -29
  83. package/dist/tools/fs/bash-program-position.js +33 -13
  84. package/dist/tools/fs/bash-readonly-classifier.d.ts +26 -313
  85. package/dist/tools/fs/bash-readonly-classifier.js +81 -782
  86. package/dist/tools/fs/fs-bash.d.ts +43 -207
  87. package/dist/tools/fs/fs-bash.js +20 -341
  88. package/dist/tools/fs/index.d.ts +12 -13
  89. package/dist/tools/fs/index.js +3 -4
  90. package/dist/tools/fs/read-deny.d.ts +25 -0
  91. package/dist/tools/fs/read-deny.js +19 -0
  92. package/dist/tools/fs/safety.js +2 -6
  93. package/dist/tools/fs/shell-read-check.d.ts +99 -0
  94. package/dist/tools/fs/shell-read-check.js +229 -0
  95. package/dist/tools/fs/shell-read-establish.d.ts +70 -115
  96. package/dist/tools/fs/shell-read-establish.js +216 -102
  97. package/dist/tools/fs/shell-read-judge.d.ts +231 -0
  98. package/dist/tools/fs/shell-read-judge.js +170 -0
  99. package/dist/tools/fs/shell-read-plan.d.ts +3 -0
  100. package/dist/tools/fs/shell-read-plan.js +88 -27
  101. package/dist/tools/monitor.d.ts +5 -5
  102. package/dist/tools/monitor.js +3 -5
  103. package/dist/tools/task-list.d.ts +4 -0
  104. package/package.json +1 -1
  105. package/test/export-surface.snapshot.json +19 -3
  106. package/wire-changes/core@7.25.0.json +671 -0
  107. package/wire-changes/core@7.25.1.json +64 -0
@@ -1,5 +1,4 @@
1
1
  import { type SegmentTokens, type ShellSegmentConnector } from "./bash-lexer.js";
2
- import { type ShellScanDecline } from "./bash-program-position.js";
3
2
  /**
4
3
  * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
5
4
  * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
@@ -314,148 +313,10 @@ export interface BashReadonlyRootBoundary {
314
313
  * is the read-only verdict; everything else is a demotion.
315
314
  */
316
315
  export interface CompoundReadonlyVerdict {
317
- /** Demotion reason (the same string {@link classifyCompoundReadonly} returns), or undefined. */
316
+ /** Demotion reason (the same string {@link classifyCompoundReadonly} returns), or undefined: the
317
+ * command's SHAPE is not one the classify doctrine auto-allows. WHERE the command reads is not this
318
+ * verdict's business — that is the gate's shell read station (`shell-read-check.ts`). */
318
319
  reason?: string;
319
- /** Set when a listed reader's statically resolved path operand lies outside the roots. With `reason` the
320
- * containment sentence itself, the roots were the SOLE obstacle (every segment a listed reader, no
321
- * blocking/device/guarded-form arm fired, every operand resolved) and the narrow "allow reading from <dir>"
322
- * approval would make the command auto-allowable; with `reason` a SHAPE refusal (follow mode, a device, a
323
- * blocking stdin read, a guarded verb form, multi-cd) or an UNRESOLVABLE-operand refusal (another operand
324
- * the walk could not read) the finding rides beside the sentence — the read of that path is the same read,
325
- * so the boundary's objection is stated either way, while the command stays refused after any grant. */
326
- outOfRootRead?: true;
327
- /** The resolved out-of-root paths, de-duplicated, in first-seen order. Present iff `outOfRootRead`. */
328
- outOfRootPaths?: readonly string[];
329
- /** B-057 — an operand matched the deployment's read deny judge (`BashReadonlyRootBoundary.denyMatch`):
330
- * the demotion was raised by a DECLARED read boundary, not by the classifier's own caution. The probe
331
- * reads it to mint `mandated` — a stored allow rule or the read-only shell arm may retire a classify-
332
- * tier question, never one the read face itself asked. Structural sibling of `outOfRootRead`. */
333
- readDenied?: true;
334
- /** The matched operand (resolved spelling) and the pattern — present iff {@link readDenied} (the
335
- * `outOfRootRead`/`outOfRootPaths` idiom), so a refusal CARD names the target without parsing prose. */
336
- readDeniedPath?: string;
337
- readDeniedPattern?: string;
338
- /**
339
- * The paths this scan resolved INSIDE the roots (de-duplicated, first-seen order), i.e. the
340
- * candidates a caller holding a filesystem should re-check with symlinks resolved. Absent when there
341
- * are none.
342
- *
343
- * Why the ALLOW side is reported at all: containment here is LEXICAL (see
344
- * {@link resolveOperandLexically}), so "inside the roots" means the written spelling has an in-root
345
- * prefix — a symlink inside a root that points out of it (`<root>/vendor` → `/etc`) passes. Closing
346
- * that needs `realpath`, which this function deliberately cannot do (synchronous, pure, no
347
- * ExecutionEnv). Carrying the candidates is the seam: the ENFORCING caller canonicalizes them and
348
- * re-compares (`createBashReadonlyTool` does exactly this — a target that resolves out of root is
349
- * refused there). Typically 0-2 entries per command.
350
- *
351
- * Present on BOTH arms, not only the allow arm: a command can name one out-of-root path AND one
352
- * in-root candidate (`cat /etc/passwd inroot.txt`), and a caller that EXEMPTS the out-of-root path
353
- * (the engine's own overflow spool, or a non-canonical spelling that resolves back inside) then goes
354
- * on to execute — with the other candidate still unverified. The field is what that caller checks
355
- * before running, whichever way the lexical verdict came out.
356
- *
357
- * Includes bare-word operands (`cat linkfile.txt`), which are NOT lexical out-of-root candidates —
358
- * the "a name with no separator can only reach the working directory" argument is true lexically and
359
- * false under symlinks, so those are exactly the entries a filesystem-holding caller must resolve.
360
- * They never affect {@link reason}: a bare word cannot demote and does not here either.
361
- */
362
- checkedPaths?: readonly string[];
363
- /**
364
- * Operands whose spelling carries an UNEXPANDED glob (`*`, `?`, `[…]`, unquoted). Lexical
365
- * containment is not decidable for them: the shell expands the pattern AFTER this verdict is made, and
366
- * inside a root an expansion can land on a symlink that points out of it (`cat *` where the directory
367
- * holds a link to an out-of-root file — reproduced end-to-end, not a theoretical shape).
368
- *
369
- * They are NOT in {@link checkedPaths}: a filesystem-holding resolver answers the unexpanded spelling
370
- * with its deepest EXISTING ancestor — the directory itself — and reports nothing escaping, which is
371
- * precisely the false "resolved, inside" answer this field exists to prevent.
372
- *
373
- * CONSUMER CONTRACT. A gate that auto-allows on containment must treat a verdict carrying this field as
374
- * UNDECIDED: ask, or expand the patterns itself against the filesystem and judge every match. The
375
- * read-only judgement itself is unaffected — `ls *` genuinely writes nothing, and {@link reason} stays
376
- * absent — because a hard refusal here would take a routine idiom away from every caller, including the
377
- * ones that CAN resolve it. The engine's own `bash_readonly` leg is such a caller: it expands and
378
- * resolves each match before executing, so it keeps running these commands unprompted.
379
- *
380
- * `cd` is the one exception, and it fails closed: a glob there cannot be expanded into the single
381
- * directory the compound face must track as the new working directory, so it demotes (see {@link reason}).
382
- *
383
- * ALSO carries the {@link recursiveReadPaths} entries (a subset): both families are operands whose
384
- * real read set is not in the command text, and the consumer contract above ("non-empty ⇒ ask, or
385
- * resolve yourself") is stated over this one field so an auto-allow gate cannot honour one family
386
- * and miss the other.
387
- */
388
- undecidedPaths?: readonly string[];
389
- /** The KERNEL spellings of the {@link checkedPaths} / {@link undecidedPaths} entries whose operand
390
- * traverses `..`: the same absolute path, `..` UNCOLLAPSED ({@link OperandSpellings.spelled}). A fold
391
- * names a different file whenever a parent step follows a symlink — `<root>/link/../x` is
392
- * `dirname(realpath(link))/x` to the kernel, `<root>/x` to the text. Absent when nothing traverses
393
- * `..`, and never a replacement: a resolving caller judges these BESIDE the folded entries, list for
394
- * list. Containment, deny judgment and every `reason` here stay decided on the FOLDED form — this file
395
- * is zero-I/O, and only a caller holding the filesystem can tell the two apart. */
396
- checkedSpellings?: readonly string[];
397
- /** The kernel spellings of {@link undecidedPaths} — see {@link checkedSpellings}. */
398
- undecidedSpellings?: readonly string[];
399
- /** The kernel spellings of {@link outOfRootPaths} — see {@link checkedSpellings}. A caller GRANTING
400
- * anything off a resolved out-of-root path judges this beside it: the two can disagree about whether
401
- * the target is back inside. */
402
- outOfRootSpellings?: readonly string[];
403
- /**
404
- * Operands (written spellings) the walk could NOT resolve at all — a `~user` form, a `~` under no declared
405
- * home, an argument the shell rewrites (brace/variable/command expansion), a `cd` target it cannot follow
406
- * (`cd -`, bare `cd`, a pattern). The STRUCTURAL form of the unresolvable sentence: `reason` carries the
407
- * first one's sentence, this member every one of them, and both survive whatever other sentence (a shape
408
- * refusal) takes precedence. CONSUMER CONTRACT: non-empty ⇒ the boundary could not read where the command
409
- * reads — a fail-closed ask, never a vouch (plain on the classify seat, whose classifier asks beside it; MANDATED on the
410
- * boundary-only seat, where nothing else would ask).
411
- */
412
- unresolvedOperands?: readonly string[];
413
- /**
414
- * The grammar gate refused the WHOLE command before the walk ran (an escape, a substitution, a subshell, a
415
- * redirection, a line break — see {@link rejectedSansRedirection} and the compound gate): no operand was judged,
416
- * and what the shell would run is not knowable from the text (a reader can be spelled `ca\t`, fed by `<`, or hidden
417
- * in `$(…)`). CONSUMER CONTRACT: present ⇒ the boundary judged nothing — MANDATED on both probe seats (a name-reading
418
- * arm or a stored allow rule must not retire a read nobody judged). Structure, never the sentence.
419
- */
420
- refusedWhole?: true;
421
- /**
422
- * Operands of a RECURSIVE/EXPANDING read form (`grep -r`, `ls -R`, `du`, … — see
423
- * {@link RECURSIVE_READ_FORMS}) judged with a {@link BashReadonlyRootBoundary.denyMatch} seat wired.
424
- * The judge sees only the operand's own resolved spelling, but a recursive verb reads the operand's whole
425
- * SUBTREE — `grep -r x /home/user` touches `/home/user/.ssh/*` while the judged spelling matches nothing.
426
- * The traversal's reach set is therefore not covered by the lexical check at all, and with zero I/O
427
- * "provably not a directory" does not exist — so every such operand is UNDECIDED (no directory guessing,
428
- * no bounded pre-check: both would be a false "resolved, inside" of exactly the kind
429
- * {@link undecidedPaths} exists to prevent).
430
- *
431
- * Subset of {@link undecidedPaths} (same consumer contract: ask, never auto-allow); carried separately so
432
- * a consumer minting prose can name the recursive-reach cause rather than the glob one. Minted ONLY when
433
- * `denyMatch` is wired: without a judge there is nothing the traversal bypasses. EVERY face wiring the
434
- * seat sees this field, the `bash_readonly` shell included, and answers it by its OWN rule for a demotion
435
- * — that face has no approval channel, so what it cannot establish it REFUSES (an unbounded traversal is
436
- * the same "the read set is not in the text" finding as a pattern the expander cannot enumerate).
437
- */
438
- recursiveReadPaths?: readonly string[];
439
- /**
440
- * The ONE path whose addition to the declared roots would make every objection THIS WALK raised go away,
441
- * MEASURED not argued: the walk runs again with the proposed root in place and the value is offered only
442
- * if that walk has no objection left (`..` spellings are excluded on their own account — a folded-vs-kernel
443
- * disagreement no replay of the text settles). It rides the containment sentence, and a per-segment NAME
444
- * refusal ({@link segmentNameRefusal}), which is not a refusal of the READ; a SHAPE refusal takes it with
445
- * it. VALUE: the deepest directory PROPERLY containing every {@link outOfRootPaths} entry, folded (zero
446
- * I/O, so a directory contributes its PARENT); where that would contain the declared home or a root, the
447
- * single path read is offered VERBATIM instead ({@link outOfRootGrantCoversExactPath}) — `cat
448
- * ~/.gitconfig` is one file and the alternative is a card with no way out. Contract:
449
- * {@link import("../../core/tool-spec.js").ReadRootGrantCandidate}.
450
- */
451
- outOfRootGrantCandidate?: string;
452
- /** {@link outOfRootGrantCandidate} is the path the command NAMED, offered verbatim, rather than a
453
- * directory containing it — minted where that directory would have handed out the home or the workspace.
454
- * A consumer adds the string ITSELF; expanding it into a directory pattern is what this prevents. */
455
- outOfRootGrantCoversExactPath?: true;
456
- /** The verdict's {@link reason} is a per-segment NAME refusal (an unlisted program, a path-prefixed name,
457
- * a control word before a reader) rather than the walk's own sentence or a refused SHAPE. */
458
- segmentNameRefusal?: true;
459
320
  }
460
321
  /**
461
322
  * The single minting point for the out-of-root-read approval option text, so a gate rendering
@@ -468,18 +329,6 @@ export interface CompoundReadonlyVerdict {
468
329
  * uses the host's native separator.
469
330
  */
470
331
  export declare function formatOutOfRootReadApprovalOption(directory: string): string;
471
- /**
472
- * The deepest directory containing both, or `undefined` when they share none (two path families).
473
- *
474
- * LINEAR, and that is the point: both paths are already folded, so the answer is their common prefix cut
475
- * at a separator. Climbing one directory at a time re-folds the whole remaining path each step — the same
476
- * answer at quadratic cost, measured at 4.9 s of synchronous work for one 32 KB command naming a
477
- * 16 000-segment operand, on a walk that runs BEFORE the gate arms its probe deadline. The family-aware
478
- * comparator keeps the last word: a prefix it cannot confirm contains both paths yields no candidate.
479
- * Exported for its unit tests — the family confirmation is reachable from a command only through shapes
480
- * the compound grammar refuses first, so a command-level pin cannot tell a live guard from an absent one.
481
- */
482
- export declare function deepestCommonDir(a: string, b: string): string | undefined;
483
332
  /** Programs whose operands are NEVER filesystem paths, so no operand of theirs is checked:
484
333
  * `echo`'s words are literal text, `which`'s are PATH lookups, `tr`'s are character sets, and
485
334
  * `basename`/`dirname` do pure string surgery on a path without ever opening it. Everything else —
@@ -550,27 +399,6 @@ export declare function resolveOperandSpellings(base: string | undefined, operan
550
399
  * exactly as `sh` does, and a deployment that meets one of them meets the whole member. GNU `parallel` is a member
551
400
  * for the reason it is not a launcher: it hands a BUILT command line to a shell. */
552
401
  export declare const SHELL_REENTRY_PROGRAM_RE: RegExp;
553
- /** Whether a command the grammar gate REFUSED WHOLE may READ a file the boundary never judged. The answer is the
554
- * complement of a WHITELIST: the command is exempt only when every part of it is a shape the scan fully understands
555
- * and none of those shapes reads a file outside the walk's reach — everything else is read evidence, so a token the
556
- * scan does not understand mandates rather than passes. Exempt shapes, per segment: a program that reads no path
557
- * (`echo`, `python3 -`, `tee out.txt`, an unlisted program); `cat` / `wc` with payload-free flags whose input is a
558
- * here-document, a here-string or a literal in-root stdin file; a directory change (which then makes every relative
559
- * stdin literal in the command unjudged). A substitution body (`$(…)`, backticks, `<(…)`) is judged on its own: green
560
- * under the compound walk, or exempt under this whitelist; a here-document body is text, not commands. */
561
- export declare function refusedCommandMayRead(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary | undefined,
562
- /** The {@link ShellScanDecline} members THIS SEAT reads as evidence (tables and argument: `fs-bash.ts`); one
563
- * the table omits ends the scan with "exempt". */
564
- declinesAreEvidence: ReadonlySet<ShellScanDecline>): RefusedCommandScan;
565
- /** What ONE scan of a refused command found — TWO facts, because a seat's table answers only the first. `mayRead`
566
- * is the whitelist's verdict ("may read a file the boundary never judged"), which a seat turns into a MANDATE.
567
- * `declined` is the scan's own report that it could not judge where a segment reads AT ALL
568
- * ({@link SHELL_SCAN_DECLINES}), recorded BEFORE the seat's table and so the SAME on every seat: a seat omitting
569
- * that decline has ruled only that an ANSWERER may retire the ask, never that nothing happened. */
570
- export interface RefusedCommandScan {
571
- readonly mayRead: boolean;
572
- readonly declined: boolean;
573
- }
574
402
  /**
575
403
  * Does this RAW token carry a metacharacter the shell EXPANDS, unquoted?
576
404
  *
@@ -866,6 +694,9 @@ export interface SedScriptNames {
866
694
  readonly writes: readonly string[];
867
695
  readonly commands: readonly string[];
868
696
  readonly executesPatternSpace: boolean;
697
+ /** Why the script's names are NOT all here, when they are not: the script is supplied from a FILE whose text is not
698
+ * in the command (one reading), or no reading could parse it to its end. Absent when every name is listed. */
699
+ readonly unread?: string;
869
700
  }
870
701
  /**
871
702
  * Does this verb's SCRIPT SLOT put a file name or a command line somewhere the walk's operand reading
@@ -980,12 +811,14 @@ export declare function inlineProgramClause(model: InlineProgramModel, args: rea
980
811
  * command name. Names-only traversals (`find`, `ls -R`, `du`, `tree`, an archive write) are in the table
981
812
  * and answer `false` here — see {@link RecursiveReadFormModel.discloses}. */
982
813
  export declare function segmentSelectsRecursiveRead(name: string, args: readonly string[]): boolean;
983
- export declare function assignmentWordPayload(word: string): {
814
+ /** `raw` is the word as WRITTEN; it defaults to the folded spelling, which is the reading that expands most. */
815
+ export declare function assignmentWordPayload(word: string, raw?: string): {
984
816
  kind: "value";
985
817
  value: string;
986
818
  } | {
987
819
  kind: "unresolved";
988
820
  reason: string;
821
+ value?: string;
989
822
  } | undefined;
990
823
  /** WHICH programs are DECLARED not to open the value of a `NAME=value` ARGUMENT — an allow-list, because
991
824
  * the other polarity was measured false. `make PREFIX=/usr/local`, `cmake -D…`, `docker run -e HOME=/root`
@@ -1003,9 +836,9 @@ export declare function assignmentWordPayload(word: string): {
1003
836
  export declare const ASSIGNMENT_VALUE_NOT_PATHS: ReadonlySet<string>;
1004
837
  /** The program the shell will RUN here and where its OWN argv begins — the two seats
1005
838
  * {@link HeadedSegment.argvProgram} / {@link HeadedSegment.argvFrom} carry, from one reader so they cannot
1006
- * disagree. Both assignment spellings are stripped BEFORE the program-position reader runs: it does not
1007
- * read `NAME+=value` as an assignment (quoting makes `'A+=x'` a command NAME on folded text), so a leading
1008
- * append word arrives AS the program and hides any launcher — measured on `A+=x env B=<outside> <prog>`. */
839
+ * disagree. The LEADING assignments (both spellings) are the program-position reader's own step, so this
840
+ * seat no longer strips them a second time: a second strip read the quoted `'A+=x'` as an assignment the
841
+ * one reader calls a program name, which is two readers with two answers. */
1009
842
  export declare function argvProgramOf(tokens: SegmentTokens): {
1010
843
  program: string;
1011
844
  from: number;
@@ -1037,83 +870,12 @@ export declare function foldHomeVariableSpelling(command: string, homeDir: strin
1037
870
  * out-of-root signal; the boundary scan below only ever converts a would-be READ-ONLY verdict into a
1038
871
  * demotion, which is why omitting `boundary` reproduces the name-only behaviour exactly.
1039
872
  */
1040
- export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary,
1041
- /** The poll-loop face passes `iterated:true` over a body REPLICATED
1042
- * `beats` times — a single cd repeated per iteration is a legitimate accumulating shift, not the
1043
- * ">1 cd = ask for clarity" compound CC refuses (a `for` loop is a sema-only face; CC asks for any
1044
- * loop). It suppresses ONLY that multi-cd refuse; every read is still threaded + boundary-judged
1045
- * against its real per-iteration base, so the escape a climbing loop opens is still caught. */
1046
- opts?: {
1047
- readonly iterated?: boolean;
873
+ export declare function classifyCompoundReadonlyDetailed(command: string, allow: ReadonlySet<string>,
874
+ /** The environment's declared home, so the `$HOME` / `${HOME}` word spellings fold the way the read station
875
+ * folds them before the shape is read. Nothing else is read off it: WHERE a command reads is the station's. */
876
+ boundary?: {
877
+ readonly homeDir?: string;
1048
878
  }): CompoundReadonlyVerdict;
1049
- /**
1050
- * The read boundary ALONE, for the `bash_readonly` face.
1051
- *
1052
- * `bash_readonly` declares `effect:"read"` and is admitted by {@link coarseReadonlyCheck}, an argv[0]-NAME
1053
- * filter: path operands were invisible to it, so an allowlisted reader could name any file on the host
1054
- * (`cat /etc/passwd`) and — with no approval gate on the read effect — get it. The compound face above
1055
- * answers a different question for a different consumer (a gate that can ASK); this face answers only
1056
- * "does this command read outside the roots", leaving the availability arms (blocking stdin, `tail -f`,
1057
- * device reads) untouched so a caller wiring it gains the boundary and nothing else.
1058
- *
1059
- * PRECONDITION: `command` has already passed {@link coarseReadonlyCheck}, i.e. it is ONE simple command
1060
- * with no connectors, redirects, substitutions or escapes anywhere. That is what makes a whitespace split
1061
- * exact here. Feeding it an unvetted compound is not unsafe (the connector ends up glued to a token, which
1062
- * only ever produces MORE path candidates and a stricter verdict) but the reason text would name a
1063
- * nonsense command, so callers vet first.
1064
- *
1065
- * Returns the same {@link CompoundReadonlyVerdict} the compound face returns: `reason === undefined` means
1066
- * every path operand resolved inside the roots. A caller with no approval channel treats BOTH demotion
1067
- * kinds — resolved-and-outside (`outOfRootRead`, with `outOfRootPaths`) and could-not-resolve (reason
1068
- * only) — as a refusal; there is nowhere to degrade to.
1069
- *
1070
- * SCOPE OF THE GUARANTEE (both directions): "inside the roots" is LEXICAL containment — see
1071
- * {@link resolveOperandLexically}. A symlink inside a root that points out of it reads as in-root here,
1072
- * so this face bounds where a command may NAME a file, not where the kernel will ultimately take it.
1073
- * A deployment that needs filesystem-true containment resolves the operands itself, and this
1074
- * verdict hands it the list to resolve ({@link CompoundReadonlyVerdict.checkedPaths}, on both arms).
1075
- * The engine's own shell legs are that caller on a local env — the read-only face before it runs an
1076
- * allowlisted command, the full face after its gate allowed one.
1077
- */
1078
- export declare function classifySimpleCommandReadBoundary(command: string, boundary: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
1079
- /**
1080
- * What {@link classifyOutOfRootReadGate} answers. Two members and one rule between them:
1081
- * `paths` is EVIDENCE for a `gated` answer, never the answer itself. A caller decides on `gated`.
1082
- */
1083
- export interface OutOfRootReadGateVerdict {
1084
- /** True ⇔ this command is structurally gated on the read boundary. */
1085
- readonly gated: boolean;
1086
- /** The RESOLVED out-of-root paths behind a `gated` answer. EMPTY when the gate fired for a reason
1087
- * that resolves none — a working-directory move this walk cannot follow, after which every relative
1088
- * operand is placed against a base bash has already left, so no path can be named honestly. Read it
1089
- * as display/triage evidence; emptiness is not "not gated". */
1090
- readonly paths: readonly string[];
1091
- /** Did the deployment's DENY judge speak about an operand this command names? The other half of the same
1092
- * question, over the very same readers, in a second pass WITH the deny judge — which is what lets the
1093
- * containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing.
1094
- *
1095
- * It carries the FINDING, not a boolean: this is the arm a command the grammar gate refused WHOLE
1096
- * (`cat .env > out.txt`, a subshell, a substitution) reaches, and there the compound verdict carries no
1097
- * deny fields at all — so a caller disclosing the hit has nowhere else to read the path and the pattern
1098
- * from, and said nothing. The second pass already computed both; throwing them away was the whole bug. */
1099
- readonly denied?: {
1100
- readonly path: string;
1101
- readonly pattern: string;
1102
- };
1103
- /** The reader operands this pass RESOLVED and placed inside the roots, plus the expansions it parsed but
1104
- * cannot enumerate — the candidates an execute-time recheck must canonicalize.
1105
- *
1106
- * Published because this pass reads SHAPES the compound walk hands nothing out for: a launcher whose
1107
- * program it cannot name (`env -- cat f`, `sudo -u root cat f`), a stream-fed one (`… | xargs cat f`).
1108
- * There the compound verdict carries no candidates at all, so the recheck had nothing to resolve and an
1109
- * in-root symlink to a deny-listed target RAN after an approval — while the same read spelled without
1110
- * the launcher was refused. Same operands, same judge, one more pass allowed to hand them over.
1111
- *
1112
- * NOT the out-of-root ones: those ride {@link paths}, they are what the boundary RAISED, and a leg that
1113
- * re-refused them would be a second adjudicator of a decision already made. */
1114
- readonly checked: readonly string[];
1115
- readonly undecided: readonly string[];
1116
- }
1117
879
  /** Working-directory moves {@link evaluateReadBoundary} does NOT model. `cd` is deliberately absent —
1118
880
  * it IS modelled (the walk threads the base through it), which is the whole difference: a move the
1119
881
  * walk follows keeps the verdict decidable, and one it cannot follow does not. */
@@ -1124,61 +886,6 @@ export declare const UNMODELLED_BASE_MOVERS: ReadonlySet<string>;
1124
886
  * does NOT look through a launcher: there every segment whose program is not a listed reader is judged whole,
1125
887
  * launcher or not, so looking through one would only narrow what it reads. */
1126
888
  export declare const COMMAND_LAUNCHERS_FOR_TEST: ReadonlySet<string>;
1127
- /**
1128
- * The OUT-OF-ROOT READ GATE asked as its OWN question: does any listed reader in this command
1129
- * name a path outside the allowed directories?
1130
- *
1131
- * {@link classifyCompoundReadonlyDetailed} answers a different question — "is this whole command
1132
- * auto-allowable?" — and it stops at the FIRST obstacle. That makes its `outOfRootRead` signal precise
1133
- * for the shape it was built for and silent for three shapes where the containment fact is just as
1134
- * true: a compound whose OTHER segment is not a listed reader (`cat <outside> | curl …` refuses on the
1135
- * unlisted name and never reaches the boundary walk at all), an operand that ALSO matches the
1136
- * sensitive-path deny judge (whose more-specific reason short-circuits the walk), and a recursive form
1137
- * whose traversal root sits outside. This face answers the containment question in all of them, so a
1138
- * consumer that must treat "reads outside the roots" as a STRUCTURAL gate rather than as one possible
1139
- * reason for a demotion has one predicate to read.
1140
- *
1141
- * What it judges and what it does not:
1142
- * · EVERY segment, under one of two readings. A segment whose PROGRAM is in `allow` — the deployment's
1143
- * own read-command table, the analogue of the reference implementation's path-restricted command
1144
- * list — is read with that verb's own slots (a pattern operand, a recursive form). The program is
1145
- * read after the two decorations the shell itself strips before choosing it (leading `NAME=value`
1146
- * assignments, a path prefix), so `/bin/cat` is judged as `cat`: recognizing readers by their literal
1147
- * spelling alone let a rule the persisted grammar admits (`Bash(/bin/cat:*)`) carry the exact read
1148
- * this gate exists for. Every OTHER segment is judged whole and verb-blind — its tokens are offered
1149
- * as path candidates under a head with no per-verb behaviour — because a program not on the table is
1150
- * not a program known to read nothing: skipping it made the gate silent for every reader the
1151
- * deployment had not enumerated, which is the one thing a name table cannot enumerate.
1152
- * · `cd` is judged whether or not the deployment listed it, because the walk MODELS it (it threads the
1153
- * base across segments) and dropping it would place every later relative operand against a base bash
1154
- * has already left. A move the walk cannot model (`pushd`/`popd`) is not silently tolerated either —
1155
- * see the `paths`-less gated answer below.
1156
- * · BOTH halves from ONE extraction, in TWO passes: the CONTAINMENT pass alone is deny-free, because
1157
- * {@link BashReadonlyRootBoundary.denyMatch} outranks containment inside the walk and returns early,
1158
- * which would let a deny-listed operand HIDE the fact that it also sits outside the roots. A SECOND
1159
- * pass over the SAME readers does carry that judge, and is what answers `denied` (see below).
1160
- * · `face: "open"` is honoured: under the open read face containment is structurally satisfied, an
1161
- * out-of-roots operand is not a finding, and this gate is silent — a full-read deployment sees no
1162
- * gate here, by the same one mechanism the face already defines.
1163
- *
1164
- * Answers `gated` with the resolved paths behind it, and NOT gated for every whole-string refusal (an
1165
- * unparseable grammar, a substitution, a redirection) and for an operand that cannot be resolved
1166
- * statically. That silence is the honest answer, not a miss: those shapes name no concrete directory,
1167
- * so there is nothing for a path gate to be about.
1168
- *
1169
- * It deliberately does NOT reuse {@link CompoundReadonlyVerdict}: one arm of this face is gated while
1170
- * resolving no path at all (the unmodelled working-directory move), and that arm cannot fill an
1171
- * `outOfRootPaths` whose contract says the entries ARE resolved out-of-root paths. A result type that
1172
- * can say "gated, and here is nothing" is the one that lets the arm exist without lying.
1173
- *
1174
- * The bounded poll-loop grammar is judged through its BODY, the same text that face re-classifies —
1175
- * without it, the one control structure this file auto-allows would be the one shape whose reads no
1176
- * gate looks at. The body is judged in a single pass (the per-iteration `cd` accumulation the
1177
- * auto-allow face models is not replicated here): a loop that climbs out of the roots over its beats
1178
- * is refused by that face on its own terms, and under-reporting here can only cost a gate, never grant
1179
- * one.
1180
- */
1181
- export declare function classifyOutOfRootReadGate(command: string, allow: ReadonlySet<string>, boundary: BashReadonlyRootBoundary): OutOfRootReadGateVerdict;
1182
889
  /**
1183
890
  * Compound read-only classification, reason-only face. Returns the demotion reason, or
1184
891
  * undefined when the command classifies read-only. ⚠️ `undefined` is NOT "safe to auto-execute":
@@ -1190,7 +897,9 @@ export declare function classifyOutOfRootReadGate(command: string, allow: Readon
1190
897
  * detailed face when the caller wants to know WHY, e.g. to offer the narrow "allow reading from
1191
898
  * <dir>" approval); without it the verdict is exactly what it always was.
1192
899
  */
1193
- export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
900
+ export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>, boundary?: {
901
+ readonly homeDir?: string;
902
+ }): string | undefined;
1194
903
  /**
1195
904
  * Iteration cap for {@link classifyBoundedReadonlyPollLoop}. The bound must be a LITERAL in the
1196
905
  * command text (that is what makes "bounded" provable without executing anything); this cap keeps a
@@ -1287,7 +996,9 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
1287
996
  * consequence case by case, the whole command is rejected up front if it carries any such character
1288
997
  * ({@link NON_ASCII_WHITESPACE}), so the grammar's tokenization and bash's stay identical.
1289
998
  */
1290
- export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): string | undefined;
999
+ export declare function classifyBoundedReadonlyPollLoop(command: string, allow: ReadonlySet<string>, boundary?: {
1000
+ readonly homeDir?: string;
1001
+ }): string | undefined;
1291
1002
  /**
1292
1003
  * The poll-loop face's STRUCTURED verdict — the same {@link CompoundReadonlyVerdict} the compound face
1293
1004
  * returns, so a consumer reads the boundary's structural findings (deny row, containment, recursive or
@@ -1296,4 +1007,6 @@ export declare function classifyBoundedReadonlyPollLoop(command: string, allow:
1296
1007
  * projection. A refusal of the LOOP GRAMMAR itself (not a bounded loop, an unlisted body program, an
1297
1008
  * unvetted sleep) carries no structure: no reader segment was walked.
1298
1009
  */
1299
- export declare function classifyBoundedReadonlyPollLoopDetailed(command: string, allow: ReadonlySet<string>, boundary?: BashReadonlyRootBoundary): CompoundReadonlyVerdict;
1010
+ export declare function classifyBoundedReadonlyPollLoopDetailed(command: string, allow: ReadonlySet<string>, boundary?: {
1011
+ readonly homeDir?: string;
1012
+ }): CompoundReadonlyVerdict;