@sema-agent/core 7.45.0 → 7.46.0

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 (127) hide show
  1. package/CHANGELOG.md +246 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +5 -3
  11. package/dist/core/allow-entry.js +3 -1
  12. package/dist/core/ask-origin.js +1 -1
  13. package/dist/core/checkpoint-store.d.ts +39 -2
  14. package/dist/core/effective-path-target.d.ts +136 -4
  15. package/dist/core/effective-path-target.js +110 -2
  16. package/dist/core/engine-notice.d.ts +5 -0
  17. package/dist/core/gate-fold.js +3 -0
  18. package/dist/core/gate-lanes.js +9 -3
  19. package/dist/core/governance-codes.d.ts +3 -1
  20. package/dist/core/governance-codes.js +3 -0
  21. package/dist/core/hooks.d.ts +3 -0
  22. package/dist/core/path-rule-binding.d.ts +65 -0
  23. package/dist/core/path-rule-binding.js +99 -0
  24. package/dist/core/permission-rule-model.d.ts +47 -18
  25. package/dist/core/permission-rule-model.js +38 -42
  26. package/dist/core/permission-rule-provider.d.ts +7 -0
  27. package/dist/core/permission-rule-provider.js +10 -3
  28. package/dist/core/permission-rule-store.d.ts +8 -0
  29. package/dist/core/permission-rule-store.js +6 -2
  30. package/dist/core/permission-rule-sync.js +2 -2
  31. package/dist/core/permission-rules.d.ts +22 -40
  32. package/dist/core/permission-rules.js +50 -43
  33. package/dist/core/runner/child-tree.d.ts +97 -0
  34. package/dist/core/runner/child-tree.js +104 -0
  35. package/dist/core/runner/construction-doors.d.ts +5 -0
  36. package/dist/core/runner/construction-doors.js +6 -0
  37. package/dist/core/runner/contracts.d.ts +46 -0
  38. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  39. package/dist/core/runner/permission-rule-lanes.js +18 -12
  40. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  41. package/dist/core/runner/persisted-path-rows.js +31 -12
  42. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  43. package/dist/core/runner/prepare-ask-lane.js +4 -2
  44. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  45. package/dist/core/runner/prepare-execution-env.js +10 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  47. package/dist/core/runner/prepare-gate-stations.js +2 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  49. package/dist/core/runner/prepare-hands-readface.js +6 -6
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  51. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  52. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  53. package/dist/core/runner/prepare-park-ask.js +5 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  55. package/dist/core/runner/prepare-policy-chain.js +11 -4
  56. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  57. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  58. package/dist/core/runner/prepare-task.js +37 -18
  59. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  60. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  61. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  62. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  63. package/dist/core/runner/resume-apply.js +17 -3
  64. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  65. package/dist/core/runner/run-leg.js +5 -1
  66. package/dist/core/runner/runtask.js +2 -2
  67. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  68. package/dist/core/runner/write-protection-station.d.ts +3 -2
  69. package/dist/core/runner/write-protection-station.js +1 -0
  70. package/dist/core/runner-deps.d.ts +14 -0
  71. package/dist/core/sensitive-path-policy.d.ts +8 -0
  72. package/dist/core/sensitive-path-policy.js +18 -2
  73. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  74. package/dist/core/shell-wrapper-projection.js +56 -0
  75. package/dist/core/shell-wrapper-table.d.ts +144 -34
  76. package/dist/core/shell-wrapper-table.js +40 -20
  77. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  78. package/dist/core/synthetic-read-verdict.js +3 -1
  79. package/dist/core/tool-catalog-entries.d.ts +21 -0
  80. package/dist/core/tool-catalog-entries.js +9 -0
  81. package/dist/core/tool-policy.d.ts +30 -8
  82. package/dist/core/tool-policy.js +9 -7
  83. package/dist/core/tool-spec.d.ts +3 -2
  84. package/dist/core/write-protect.d.ts +4 -0
  85. package/dist/core/write-protect.js +30 -9
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.js +4 -2
  88. package/dist/orchestration/run-spec.js +8 -7
  89. package/dist/orchestration/workflow.js +1 -1
  90. package/dist/shell/lex.d.ts +7 -0
  91. package/dist/shell/lex.js +17 -0
  92. package/dist/tools/fs/bash-program-position.d.ts +8 -87
  93. package/dist/tools/fs/bash-program-position.js +33 -69
  94. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  95. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  96. package/dist/tools/fs/index.d.ts +3 -2
  97. package/dist/tools/fs/program-read-model.d.ts +694 -0
  98. package/dist/tools/fs/program-read-model.js +1252 -0
  99. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  100. package/dist/tools/fs/program-read-model.rows.js +613 -0
  101. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  102. package/dist/tools/fs/read-deny-home.js +62 -24
  103. package/dist/tools/fs/read-deny.d.ts +54 -14
  104. package/dist/tools/fs/read-deny.js +104 -50
  105. package/dist/tools/fs/removal-targets.d.ts +7 -6
  106. package/dist/tools/fs/removal-targets.js +22 -22
  107. package/dist/tools/fs/safety.d.ts +3 -0
  108. package/dist/tools/fs/safety.js +10 -3
  109. package/dist/tools/fs/shell-read-argv.d.ts +85 -0
  110. package/dist/tools/fs/shell-read-argv.js +386 -0
  111. package/dist/tools/fs/shell-read-bindings.d.ts +273 -0
  112. package/dist/tools/fs/shell-read-bindings.js +746 -0
  113. package/dist/tools/fs/shell-read-check.js +23 -15
  114. package/dist/tools/fs/shell-read-establish.js +79 -5
  115. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  116. package/dist/tools/fs/shell-read-judge.js +9 -1
  117. package/dist/tools/fs/shell-read-plan.d.ts +31 -7
  118. package/dist/tools/fs/shell-read-plan.js +299 -393
  119. package/dist/tools/fs/shell-read-word.d.ts +29 -26
  120. package/dist/tools/fs/shell-read-word.js +30 -38
  121. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  122. package/dist/tools/fs/shell-read-write-targets.js +83 -136
  123. package/dist/tools/fs/shell-reentry-text.d.ts +38 -6
  124. package/dist/tools/fs/shell-reentry-text.js +90 -53
  125. package/package.json +1 -1
  126. package/test/export-surface.snapshot.json +17 -3
  127. package/wire-changes/core@7.46.0.json +1522 -0
@@ -47,3 +47,24 @@ export declare const ENGINE_CARDS: Readonly<Record<string, string>>;
47
47
  export declare function toolFace(id: string): ToolFaceDeclaration;
48
48
  /** Definition-id → face, for the modules that spread one (`...TOOL_FACES.read`). Frozen: a face is data. */
49
49
  export declare const TOOL_FACES: Readonly<Record<string, ToolFaceDeclaration>>;
50
+ /**
51
+ * The engine's own tool names, once each: every catalog entry the engine mounts itself (`source: "builtin"` — the hands,
52
+ * the scaffolds, the memory tools, the background-task tools), whatever face or contract it is mounted under. A host that
53
+ * must hand the engine a tool face with NO built-ins (`TaskSpec.excludeTools`, a shell's `--tools ""`) reads this set
54
+ * instead of keeping a list of its own: the catalog is the one source, and a name the engine adds later lands here with it.
55
+ */
56
+ export declare const ENGINE_BUILTIN_TOOL_NAMES: readonly string[];
57
+ /**
58
+ * The engine's HAND tools, once each, with the other spellings they answer to: the catalog rows mounted by `hands` — what a run
59
+ * mounts for itself over an execution environment (the file and shell band, the worktree pair, the band's background-shell
60
+ * pair). A delegated child's tool-name lists govern these beside the tools the delegation tool hands it, so the spawn reads
61
+ * this set rather than a list of its own.
62
+ */
63
+ export declare const HAND_TOOLS: ReadonlyArray<{
64
+ readonly name: string;
65
+ readonly aliases?: readonly string[];
66
+ }>;
67
+ /** The wire names of {@link HAND_TOOLS}. */
68
+ export declare const HAND_TOOL_NAMES: readonly string[];
69
+ /** Is this name one the engine mounts itself ({@link ENGINE_BUILTIN_TOOL_NAMES})? A caller tool, an MCP tool or a skill tool is not. */
70
+ export declare function isEngineBuiltinToolName(name: string): boolean;
@@ -180,3 +180,12 @@ function deepFreeze(value) {
180
180
  }
181
181
  deepFreeze(TOOL_CATALOG_ENTRIES);
182
182
  export const TOOL_FACES = Object.freeze(Object.fromEntries(TOOL_CATALOG_ENTRIES.map((e) => [e.id, e.face])));
183
+ export const ENGINE_BUILTIN_TOOL_NAMES = Object.freeze([...new Set(TOOL_CATALOG_ENTRIES.filter((e) => e.source === "builtin").map((e) => e.name))]);
184
+ export const HAND_TOOLS = Object.freeze([...new Set(TOOL_CATALOG_ENTRIES.filter((e) => e.mountedBy.includes("hands")).map((e) => e.name))].map((name) => {
185
+ const aliases = [...new Set(TOOL_CATALOG_ENTRIES.filter((e) => e.name === name && e.mountedBy.includes("hands")).flatMap((e) => e.face.aliases ?? []))];
186
+ return Object.freeze(aliases.length > 0 ? { name, aliases: Object.freeze(aliases) } : { name });
187
+ }));
188
+ export const HAND_TOOL_NAMES = Object.freeze(HAND_TOOLS.map((t) => t.name));
189
+ export function isEngineBuiltinToolName(name) {
190
+ return ENGINE_BUILTIN_TOOL_NAMES.includes(name);
191
+ }
@@ -79,6 +79,17 @@ export interface ToolCallRequest {
79
79
  * Read-only, like `cwd`.
80
80
  */
81
81
  root?: string;
82
+ /** The call's target reading (`readCallTarget`: lexical / canonical / hop views per reading of the path slot), stamped by the Runner on a
83
+ * leg whose lanes can hold a path rule; it describes only the `args` object it was taken for (`callTargetReadingOf`). Read-only. */
84
+ targetReading?: import("./effective-path-target.js").CallTargetReading;
85
+ /**
86
+ * An ENGINE-INTERNAL seat, not a fact of the call: the anchors of the policy now judging this request — the leg its
87
+ * path rules were installed on (`installPolicyAnchors`). Present only on the copy an installed policy hands its own
88
+ * inner `check` (where `createPermissionRulePolicy` reads it; an inner installation's anchors win over an outer one's);
89
+ * the request the gate holds, an `AskRequest`, a hook's input and a parked row never carry it. A host neither sets nor
90
+ * reads it.
91
+ */
92
+ pathRuleAnchors?: import("./permission-rule-model.js").PathRuleBases;
82
93
  /**
83
94
  * The CANONICAL directories THIS RUN'S PROMPT ADVERTISES AS WRITABLE — the admitted list of the root
84
95
  * fence's advertised-writable-directory seat (the memory engine's write home, the deployment's
@@ -537,11 +548,14 @@ export interface ToolPolicy {
537
548
  * describes the policy must read that bit, not the field's presence.
538
549
  */
539
550
  readonly projection?: ToolPolicyProjection;
540
- /** The policy's path DENY rules that speak for a face of the engine's deny table (a `Read(<pattern>)` / `Edit(<pattern>)`
541
- * deny compiled by `createPermissionRulePolicy`), as rows the prepared leg joins to its read and write tables — so the
542
- * shell's reads and writes and the traversals meet the rule, not only the tool it names. Carried through
543
- * {@link combinePolicies}; read with {@link toolPolicyPathDenyRules}. Deny-only: a row can only refuse more. */
544
- readonly pathDenyRules?: readonly import("../tools/fs/read-deny.js").PathRuleDenyRow[];
551
+ /** The policy's PATH rules as a function of the ANCHORS of the leg they are installed on (`createPermissionRulePolicy`
552
+ * sets it; present exactly when the policy judges a path target): given the leg's root and home, every path rule
553
+ * bound there — the face-keyed deny rows the prepared leg joins to its read and write tables (so the shell's reads and
554
+ * writes and the traversals meet the rule, not only the tool it names), and the rows its own `check` judges at the
555
+ * gate. A policy the Runner already installed answers with its own installation's rows whatever it is given. Carried
556
+ * through {@link combinePolicies}; read with {@link toolPolicyPathRules}. Its presence also tells the Runner to read
557
+ * each call's target ({@link ToolCallRequest.targetReading}). */
558
+ readonly pathRules?: (anchors: import("./permission-rule-model.js").PathRuleBases) => readonly import("./path-rule-binding.js").BoundPathRule[];
545
559
  /** Does this policy guard a write to this ABSOLUTE target — would it refuse the write if a write tool made it? The
546
560
  * policy's answer for a writer it cannot see as a call (the shell's file commands), read by the shell station's
547
561
  * `writesPlaced` fact alone: a guarded target is not placed, so no permission mode answers the command and its ask
@@ -558,9 +572,15 @@ export interface ToolPolicy {
558
572
  * each by name off the wrapped policy, {@link combinePolicies} folds each. The fence makes a new {@link ToolPolicy} member
559
573
  * a compile error until the table names it.
560
574
  */
561
- declare const TOOL_POLICY_CARRIED_MEMBERS: readonly ["projection", "pathDenyRules", "writeTargetGuard", "nameSets"];
575
+ declare const TOOL_POLICY_CARRIED_MEMBERS: readonly ["projection", "pathRules", "writeTargetGuard", "nameSets"];
562
576
  type ToolPolicyCarriedMember = (typeof TOOL_POLICY_CARRIED_MEMBERS)[number];
563
577
  export type ToolPolicyCarriesEveryMember = AssertAllKeysHandled<Exclude<Exclude<keyof ToolPolicy, "check">, ToolPolicyCarriedMember>>;
578
+ /** The carried members that are set on `p`, each read by name — an accessor or a non-enumerable member included. */
579
+ export declare function carriedMembersOf(p: {
580
+ readonly [K in ToolPolicyCarriedMember]?: ToolPolicy[K];
581
+ }): {
582
+ -readonly [K in ToolPolicyCarriedMember]?: ToolPolicy[K];
583
+ };
564
584
  /**
565
585
  * The ONE way to wrap a policy: the wrapper decides through `check`; every member {@link ToolPolicy} declares beside it
566
586
  * (projection, name sets, path deny rows, write-target guard — the carried-member table) rides as `inner` holds it, read
@@ -570,8 +590,10 @@ export type ToolPolicyCarriesEveryMember = AssertAllKeysHandled<Exclude<Exclude<
570
590
  * The ask-class brand is not carried: it attests the replaced `check`.
571
591
  */
572
592
  export declare function wrapToolPolicy<P extends ToolPolicy>(inner: P, check: ToolPolicy["check"]): Pick<P, ToolPolicyCarriedMember> & ToolPolicy;
573
- /** Every path deny row the given policies carry (combined policies included), in order — the deny tables' rule rows. */
574
- export declare function toolPolicyPathDenyRules(policies: readonly (ToolPolicy | undefined)[]): readonly import("../tools/fs/read-deny.js").PathRuleDenyRow[];
593
+ /** Every path rule the given policies carry (combined policies included), in order, bound at `anchors` — the anchors of
594
+ * the leg the policies are installed on (a policy already installed answers with its own installation's rows). The deny
595
+ * tables take the rows that guard a face (`faces` non-empty) of a deny rule. */
596
+ export declare function toolPolicyPathRules(policies: readonly (ToolPolicy | undefined)[], anchors: import("./permission-rule-model.js").PathRuleBases): readonly import("./path-rule-binding.js").BoundPathRule[];
575
597
  /** The one write-target guard the given policies carry together (combined policies included): `true` when any member
576
598
  * guards the target, a member that throws counting as guarding it. `undefined` when none carries one. */
577
599
  export declare function toolPolicyWriteTargetGuard(policies: readonly (ToolPolicy | undefined)[]): ToolPolicy["writeTargetGuard"];
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { homedir } from "node:os";
3
+ import { withArgsReading } from "./effective-path-target.js";
3
4
  import { brandPolicyAskClass } from "./ask-class.js";
4
5
  import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
5
6
  import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
@@ -137,8 +138,8 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
137
138
  export function decisionText(d) {
138
139
  return d.message;
139
140
  }
140
- const TOOL_POLICY_CARRIED_MEMBERS = ["projection", "pathDenyRules", "writeTargetGuard", "nameSets"];
141
- function carriedMembersOf(p) {
141
+ const TOOL_POLICY_CARRIED_MEMBERS = ["projection", "pathRules", "writeTargetGuard", "nameSets"];
142
+ export function carriedMembersOf(p) {
142
143
  const out = {};
143
144
  const carry = (k) => {
144
145
  const v = p[k];
@@ -152,8 +153,8 @@ function carriedMembersOf(p) {
152
153
  export function wrapToolPolicy(inner, check) {
153
154
  return { ...carriedMembersOf(inner), check };
154
155
  }
155
- export function toolPolicyPathDenyRules(policies) {
156
- return policies.flatMap((p) => (Array.isArray(p?.pathDenyRules) ? p.pathDenyRules : []));
156
+ export function toolPolicyPathRules(policies, anchors) {
157
+ return policies.flatMap((p) => (typeof p?.pathRules === "function" ? p.pathRules(anchors) : []));
157
158
  }
158
159
  export function toolPolicyWriteTargetGuard(policies) {
159
160
  const guards = policies.flatMap((p) => (typeof p?.writeTargetGuard === "function" ? [p.writeTargetGuard] : []));
@@ -514,7 +515,7 @@ export function createApprovalPolicy(opts) {
514
515
  }
515
516
  export function combinePolicies(...policies) {
516
517
  const nameSets = policies.flatMap(toolPolicyNameSets);
517
- const pathDenyRules = toolPolicyPathDenyRules(policies);
518
+ const pathRuled = policies.filter((p) => typeof p?.pathRules === "function");
518
519
  const writeTargetGuard = toolPolicyWriteTargetGuard(policies);
519
520
  const childProjections = policies.map((p) => p.projection);
520
521
  const projectedComponents = [];
@@ -536,7 +537,7 @@ export function combinePolicies(...policies) {
536
537
  const folded = {
537
538
  projection: combinedProjection,
538
539
  nameSets: nameSets.length > 0 ? nameSets : undefined,
539
- pathDenyRules: pathDenyRules.length > 0 ? pathDenyRules : undefined,
540
+ pathRules: pathRuled.length > 0 ? (anchors) => toolPolicyPathRules(pathRuled, anchors) : undefined,
540
541
  writeTargetGuard,
541
542
  };
542
543
  return {
@@ -576,7 +577,8 @@ export function combinePolicies(...policies) {
576
577
  continue;
577
578
  }
578
579
  if (d.updatedInput !== undefined) {
579
- current = { ...current, args: d.updatedInput };
580
+ const reread = withArgsReading(current, d.updatedInput, signal);
581
+ current = reread !== undefined ? await reread : { ...current, args: d.updatedInput };
580
582
  rewrite = d;
581
583
  stated = d.action === "allow" ? d.decisionReason : undefined;
582
584
  }
@@ -212,8 +212,9 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
212
212
  * carries its own tool set, and a parent that mounts nothing but the delegation tool can still hand
213
213
  * its children a network tool, so the parent's roster is the wrong thing to look at.
214
214
  *
215
- * ABSENT ⇔ the pool is not statically knowable — a spawn-time tool factory can inject tools whose
216
- * identity does not exist until the call happens — and the reader fails closed on that.
215
+ * ABSENT ⇔ the pool is not statically knowable — a spawn-time tool factory with no declared roster
216
+ * (`SubagentToolOptions.extraToolNames`) can inject tools whose identity does not exist until the call
217
+ * happens — and the reader fails closed on that. A declared roster's tools are rows of the pool.
217
218
  */
218
219
  agentToolPool?: ReadonlyArray<{
219
220
  name: string;
@@ -173,6 +173,10 @@ export interface WriteProtectionCompileOptions {
173
173
  * path-family source when the call carries no cwd. Omitted ⇒ relative spellings resolve against the
174
174
  * cwd, then the environment's own working directory. */
175
175
  readonly rootPath?: string;
176
+ /** The host's configuration root when it keeps the agent's configuration directory somewhere other than the home's
177
+ * `.sema` (`RunnerDeps.agentConfigDir`, absolute, already screened): every row that names the `.sema` directory also
178
+ * names the same tail below this root, and a hit there cites the row. Omitted ⇒ the table is judged as it stands. */
179
+ readonly agentConfigDir?: string;
176
180
  /**
177
181
  * Called at most ONCE per judgment, at the moment the ENVIRONMENT half is found unable to contribute
178
182
  * ({@link WriteProtectionViewAbsence}) — before the judgment returns, and on the hit arm as well as the
@@ -4,6 +4,7 @@ import { pathTargetBaseOf } from "./effective-path-target.js";
4
4
  import { canonicalDataRootKeys, dataRootSpellings, relativeUnderRoot } from "./sensitive-path-policy.js";
5
5
  import { symlinkChainHops } from "./path-identity.js";
6
6
  import { readDenyPatternRun } from "../tools/fs/read-deny.js";
7
+ import { agentConfigDirSegments, agentConfigDirTail } from "./agent-config-dir.js";
7
8
  import { thrownText } from "./thrown-value.js";
8
9
  const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
9
10
  export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
@@ -196,6 +197,7 @@ export function compileWriteProtection(entries, opts) {
196
197
  const basenames = new Map();
197
198
  const segments = new Map();
198
199
  const runs = [];
200
+ const configTails = [];
199
201
  for (const row of rows) {
200
202
  const folded = foldWriteProtectName(row.name);
201
203
  if (row.kind === "basename") {
@@ -209,8 +211,23 @@ export function compileWriteProtection(entries, opts) {
209
211
  else {
210
212
  runs.push({ name: row.name, parts: folded.split("/").filter((s) => s.length > 0) });
211
213
  }
214
+ const tail = row.kind === "basename" ? undefined : agentConfigDirTail(row.name);
215
+ if (tail !== undefined && opts?.agentConfigDir !== undefined)
216
+ configTails.push({ hit: { name: row.name, kind: row.kind }, tail: foldWriteProtectName(tail).split("/").filter((s) => s.length > 0) });
212
217
  }
213
- const judgeSegments = (raw) => {
218
+ const configRootSpelled = opts?.agentConfigDir === undefined ? [] : [agentConfigDirSegments(opts.agentConfigDir)];
219
+ const judgeAtConfigRoots = (segs, roots) => {
220
+ for (const root of roots) {
221
+ const at = root.map(foldWriteProtectSegment);
222
+ if (segs.length < at.length || !at.every((a, i) => segs[i] === a))
223
+ continue;
224
+ for (const { hit, tail } of configTails)
225
+ if (segs.length >= at.length + tail.length && tail.every((t, i) => segs[at.length + i] === t))
226
+ return hit;
227
+ }
228
+ return null;
229
+ };
230
+ const judgeSegments = (raw, configRoots) => {
214
231
  if (raw.length === 0)
215
232
  return null;
216
233
  const segs = raw.map(foldWriteProtectSegment);
@@ -229,23 +246,26 @@ export function compileWriteProtection(entries, opts) {
229
246
  return { name: run.name, kind: "segment-run" };
230
247
  }
231
248
  }
232
- return null;
249
+ return configTails.length === 0 ? null : judgeAtConfigRoots(segs, configRoots);
233
250
  };
234
- const judgeViews = (spelling) => {
235
- const posix = judgeSegments(pathViewSegments(spelling, "literal", POSIX_SEPARATOR_FAMILY, uncAnchorSegments(spelling)));
251
+ const judgeViews = (spelling, configRoots = configRootSpelled) => {
252
+ const posix = judgeSegments(pathViewSegments(spelling, "literal", POSIX_SEPARATOR_FAMILY, uncAnchorSegments(spelling)), configRoots);
236
253
  if (posix !== null)
237
254
  return posix;
238
- const bothSeparators = judgeSegments(pathViewSegments(spelling, "literal"));
255
+ const bothSeparators = judgeSegments(pathViewSegments(spelling, "literal"), configRoots);
239
256
  if (bothSeparators !== null)
240
257
  return bothSeparators;
241
258
  for (const normalization of WIN32_NORMALIZATIONS) {
242
- const hit = judgeSegments(pathViewSegments(spelling, normalization));
259
+ const hit = judgeSegments(pathViewSegments(spelling, normalization), configRoots);
243
260
  if (hit !== null)
244
261
  return hit;
245
262
  }
246
263
  return null;
247
264
  };
248
- const judgeSpelled = (spelled) => judgeViews(relativeUnderDataRoot(spelled, rootViews) ?? spelled);
265
+ const judgeSpelled = (spelled) => {
266
+ const remainder = relativeUnderDataRoot(spelled, rootViews);
267
+ return remainder === null ? judgeViews(spelled) : judgeViews(remainder, []);
268
+ };
249
269
  const judgeSpelling = (path, cwd) => {
250
270
  const base = typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
251
271
  const family = pathFamilyOf({ cwd: base });
@@ -291,7 +311,8 @@ export function compileWriteProtection(entries, opts) {
291
311
  }
292
312
  try {
293
313
  if (canonKey !== undefined) {
294
- const byTarget = judgeViews(canonKey);
314
+ const canonicalRoots = configTails.length === 0 || opts?.agentConfigDir === undefined ? [] : (await canonicalDataRootKeys(env, [opts.agentConfigDir], ctx.signal)).map(agentConfigDirSegments);
315
+ const byTarget = judgeViews(canonKey, [...configRootSpelled, ...canonicalRoots]);
295
316
  if (byTarget !== null) {
296
317
  for (const rootKey of await canonicalDataRootKeys(env, dataRootPaths, ctx.signal)) {
297
318
  const relative = relativeUnderRoot(canonKey, rootKey);
@@ -299,7 +320,7 @@ export function compileWriteProtection(entries, opts) {
299
320
  continue;
300
321
  if (relative === "")
301
322
  return { kind: "clear" };
302
- const remainder = judgeViews(relative);
323
+ const remainder = judgeViews(relative, []);
303
324
  return remainder === null ? { kind: "clear" } : { kind: "hit", hit: remainder, via: "target" };
304
325
  }
305
326
  return { kind: "hit", hit: byTarget, via: "target" };
package/dist/index.d.ts CHANGED
@@ -151,7 +151,8 @@ export { TOOL_FAMILIES, TOOL_MOUNT_TAGS, TOOL_APPROVAL_CARDS, TOOL_PATH_ACCESSES
151
151
  export { describeToolCatalog, catalogEntriesNamed, catalogToolNames, catalogRuleFaceOf, pathTargetOf, pathTargetValue, protectivePathTargetOf, declaredPathTargetOf, BARE_REQUEST_CODE, isProtectedWrite, isDeclaredWrite, fileOpAccessOf, type ToolCallLike, pathConfinableWriteToolNames, pathTargetingToolNames, compactableToolNames, budgetExemptToolNames, handBandEffects, engineCardTypes, predictMountedToolNames, HANDS_BAND_TAGS, MOUNT_TAG_IN_HANDS_BAND, } from "./core/tool-registry.js";
152
152
  export { ToolRosterEntry, ToolRoster, ToolRosterDelta, ToolRosterRenderHints, ROSTER_SOURCES, type RosterSource, type ToolIdentity, toolIdentityOf, mintCapabilityId, diffToolRoster, judgeParkedToolIdentity, ruleFacesFromRoster, toolCallFaceOf, rosterToolName, ruleNamesOf, inputKeysOf, ruleFaceOfInstance, rosterMemberBoundProblem, type RosterBoundMembers, type RosterBoundCode, ToolRosterDeltaSeat, } from "./core/tool-roster.js";
153
153
  export { type ToolCallFace } from "./core/tool-policy.js";
154
- export { effectivePathTargetOf, pathTargetBaseOf, type ToolPathBases, type PathTargetReading } from "./core/effective-path-target.js";
154
+ export { effectivePathTargetOf, pathTargetBaseOf, type ToolPathBases, type PathTargetReading, type CallTargetReading, type CallTargetView } from "./core/effective-path-target.js";
155
+ export { type BoundPathRule, type PathRuleSpec } from "./core/path-rule-binding.js";
155
156
  export { pathFamilyOf, isAbsoluteForFamily, pathIdentityOf, win32NamespaceScreen, WIN32_NAMESPACE_REFUSALS, type PathFamily, type PathIdentity, type PathIdentityReading, type Win32NamespaceRefusal } from "./tools/fs/safety.js";
156
157
  export { judgeShellRead, type ShellReadJudgeInput, type ShellReadJudgment, type ShellReadVerdict, type ShellReadStationSeat, type ShellReadAdmission, } from "./tools/fs/shell-read-check.js";
157
158
  export { SHELL_READ_FINDINGS, SHELL_READ_SEATS, SHELL_READ_DECISION, SHELL_READ_FINDING_RANK, type ShellReadFinding, type ShellReadSeat, type ShellReadDisposition, type ShellReadBoundary, type ReadEstablishment, type EstablishedRead, type ReadFinding, } from "./tools/fs/shell-read-judge.js";
@@ -193,7 +194,7 @@ export { createAllowDenyPolicy, createApprovalPolicy,
193
194
  * members that seat has no lane for (`"unavailable"`, a routing answer; `updatedInput`, an operator edit a
194
195
  * policy verdict cannot get re-adjudicated). Exported so a host can NAME it instead of deriving it through
195
196
  * `Parameters<typeof createApprovalPolicy>[0]["approve"]`. */
196
- type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, abortedSeatDeny, receiveHookResult, isEngineRefusal, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathDenyRules, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
197
+ type ApprovalSeatOutcome, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, abortedSeatDeny, receiveHookResult, isEngineRefusal, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathRules, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
197
198
  export { type ReadRootGrantScreen } from "./core/read-root-grant-screen.js";
198
199
  export { type AskOrigin, ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, type PersistedRuleMandate, isPersistedRuleMandate, screenAskClassMembers } from "./core/ask-origin.js";
199
200
  export { PERMISSION_MODES, type PermissionMode, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, type AskClass, isAskClass, MODE_ARBITRATION, type ModeArbitrationRow, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, type ModeReleaseReason, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, type AskClassification, type AskClassFacts, type ToolMarkSource, arbitrateByMode, type Arbitration, fileCommandOptionsSentence } from "./core/ask-origin.js";
@@ -202,12 +203,13 @@ export type { PermissionSeatInputs, PermissionSeatReading } from "./core/mode-po
202
203
  export type { SkillGateSeats, SkillGrantHit, SkillGrantLane, SkillGrantOrigin, SkillGrantRecord } from "./core/skill-grant.js";
203
204
  export { SKILL_GRANT_ORIGINS } from "./core/skill-grant.js";
204
205
  export type { BoundSkillGrants } from "./core/skill-grant-binding.js";
205
- export { ALLOW_ENTRY_INERT_REASONS, compileAllowEntry, compileAllowList, type AllowEntryInertReason, type CompiledAllowEntry, type AllowEntryGateSeats, type AllowEntryLane, type AllowEntryHit } from "./core/allow-entry.js";
206
+ export { ALLOW_ENTRY_INERT_REASONS, compileAllowEntry, compileAllowList, renderListEntry, type AllowEntryInertReason, type CompiledAllowEntry, type AllowEntryGateSeats, type AllowEntryLane, type AllowEntryHit } from "./core/allow-entry.js";
206
207
  export { ALLOW_RULE_MEMBERS, type AllowRuleMember, type AllowRuleShadow } from "./core/allow-rule-shadow.js";
207
208
  export { mintNamespacePrefix } from "./core/protocol-naming.js";
208
209
  export { readDenyClassifierRules, renderClassifierDenyRules, type DerivedDenyRule } from "./core/auto-mode-prompt.js";
209
210
  export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource, type AutoModeDenySource } from "./core/runtime-caps.js";
210
211
  export { SETTLEMENT_KINDS, type SettlementKind, isSettlementKind, type Settlement, SETTLEMENT_PARTIES, type SettlementParty, isSettlementParty, HOST_DECIDERS, type HostDecider, isHostDecider, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, type DeniedBy, isDeniedBy, DENIED_BY_MAY_VETO, CLASSIFIER_DENY_CAUSES, type ClassifierDenyCause, isClassifierDenyCause, DENIED_BY_MAY_CARRY_CAUSE, type GateDisposition, type GateOutcome, type ModeRelease, type ModeRefusal, screenGateOutcome } from "./core/gate-outcome.js";
212
+ export { ENGINE_BUILTIN_TOOL_NAMES, isEngineBuiltinToolName } from "./core/tool-catalog-entries.js";
211
213
  export { policyCannotAnswer, type HeldQuestion } from "./core/real-approval.js";
212
214
  export { type AskCarry } from "./core/hooks.js";
213
215
  export { SWAPPABLE_DEP_SEATS, SWAP_SEAT_NOTICE, isSwappableDepSeat, screenSwappableDeps, type SwappableDepSeat, type SwappableDeps } from "./core/swappable-deps.js";
package/dist/index.js CHANGED
@@ -124,6 +124,7 @@ export { describeToolCatalog, catalogEntriesNamed, catalogToolNames, catalogRule
124
124
  export { ToolRosterEntry, ToolRoster, ToolRosterDelta, ToolRosterRenderHints, ROSTER_SOURCES, toolIdentityOf, mintCapabilityId, diffToolRoster, judgeParkedToolIdentity, ruleFacesFromRoster, toolCallFaceOf, rosterToolName, ruleNamesOf, inputKeysOf, ruleFaceOfInstance, rosterMemberBoundProblem, ToolRosterDeltaSeat, } from "./core/tool-roster.js";
125
125
  export {} from "./core/tool-policy.js";
126
126
  export { effectivePathTargetOf, pathTargetBaseOf } from "./core/effective-path-target.js";
127
+ export {} from "./core/path-rule-binding.js";
127
128
  export { pathFamilyOf, isAbsoluteForFamily, pathIdentityOf, win32NamespaceScreen, WIN32_NAMESPACE_REFUSALS } from "./tools/fs/safety.js";
128
129
  export { judgeShellRead, } from "./tools/fs/shell-read-check.js";
129
130
  export { SHELL_READ_FINDINGS, SHELL_READ_SEATS, SHELL_READ_DECISION, SHELL_READ_FINDING_RANK, } from "./tools/fs/shell-read-judge.js";
@@ -158,18 +159,19 @@ export { hasScheduler, isValidCronExpr, SchedulerError } from "./core/scheduler.
158
159
  export { createSchedulerTools, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTINEL, AUTONOMOUS_LOOP_DYNAMIC_SENTINEL, } from "./tools/scheduler-tools.js";
159
160
  export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, } from "./tools/loop-tick.js";
160
161
  export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
161
- export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, abortedSeatDeny, receiveHookResult, isEngineRefusal, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathDenyRules, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
162
+ export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, abortedSeatDeny, receiveHookResult, isEngineRefusal, wrapToolPolicy, decisionText, resolveAsk, toolPolicyNameSets, toolPolicyPathRules, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
162
163
  export {} from "./core/read-root-grant-screen.js";
163
164
  export { ASK_ORIGINS, isAskOrigin, ORIGIN_IMPLIES_REAL_APPROVAL, PERSISTED_RULE_MANDATES, isPersistedRuleMandate, screenAskClassMembers } from "./core/ask-origin.js";
164
165
  export { PERMISSION_MODES, isPermissionMode, PERMISSION_MODE_RANK, clampPermissionMode, ASK_CLASSES, isAskClass, MODE_ARBITRATION, RELEASABLE_ASK_CLASSES, MODE_RELEASE_REASONS, isModeReleaseReason, RRA_EXPLAINERS, classifyAsk, foldArbitrationRow, arbitrateByMode, fileCommandOptionsSentence } from "./core/ask-origin.js";
165
166
  export { modePolicyOf, PERMISSION_MODE_FOLD_REASONS, ACCEPT_EDITS_FILE_COMMANDS, modeAcceptsFileCommand } from "./core/mode-policy.js";
166
167
  export { SKILL_GRANT_ORIGINS } from "./core/skill-grant.js";
167
- export { ALLOW_ENTRY_INERT_REASONS, compileAllowEntry, compileAllowList } from "./core/allow-entry.js";
168
+ export { ALLOW_ENTRY_INERT_REASONS, compileAllowEntry, compileAllowList, renderListEntry } from "./core/allow-entry.js";
168
169
  export { ALLOW_RULE_MEMBERS } from "./core/allow-rule-shadow.js";
169
170
  export { mintNamespacePrefix } from "./core/protocol-naming.js";
170
171
  export { readDenyClassifierRules, renderClassifierDenyRules } from "./core/auto-mode-prompt.js";
171
172
  export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./core/runtime-caps.js";
172
173
  export { SETTLEMENT_KINDS, isSettlementKind, SETTLEMENT_PARTIES, isSettlementParty, HOST_DECIDERS, isHostDecider, SETTLEMENT_IS_REFUSAL, DENIED_BY_VALUES, isDeniedBy, DENIED_BY_MAY_VETO, CLASSIFIER_DENY_CAUSES, isClassifierDenyCause, DENIED_BY_MAY_CARRY_CAUSE, screenGateOutcome } from "./core/gate-outcome.js";
174
+ export { ENGINE_BUILTIN_TOOL_NAMES, isEngineBuiltinToolName } from "./core/tool-catalog-entries.js";
173
175
  export { policyCannotAnswer } from "./core/real-approval.js";
174
176
  export {} from "./core/hooks.js";
175
177
  export { SWAPPABLE_DEP_SEATS, SWAP_SEAT_NOTICE, isSwappableDepSeat, screenSwappableDeps } from "./core/swappable-deps.js";
@@ -5,7 +5,8 @@ import { combinePolicies } from "../core/tool-policy.js";
5
5
  import { delimitUntrusted } from "../core/untrusted-text.js";
6
6
  import { isDurablePause } from "../agents/suspend-guard.js";
7
7
  import { terminalProjection } from "../core/runner/terminal-projection.js";
8
- import { isAbsoluteForFamily, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
8
+ import { writeTargetPath } from "../tools/fs/safety.js";
9
+ import { callTargetViewsOf } from "../core/effective-path-target.js";
9
10
  import { captureBaseline, resolveFrozenPaths, restoreFrozenPaths, runOracle, snapshotFrozenPaths, specError, } from "../core/spec-contract.js";
10
11
  import { isProtectedWrite } from "../core/tool-registry.js";
11
12
  const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
@@ -21,13 +22,13 @@ export function frozenDenyPolicy(rootDir, frozenResolved) {
21
22
  check(req) {
22
23
  if (!isProtectedWrite(req))
23
24
  return { action: "allow" };
24
- const raw = writeTargetPath(req);
25
- if (typeof raw !== "string" || raw.length === 0)
25
+ const views = callTargetViewsOf(req, "tighten", { root: rootDir });
26
+ if (views === undefined || views.spellings.length === 0)
26
27
  return { action: "allow" };
27
- const absForm = isAbsoluteForFamily(pathFamilyOf({ root: rootDir, cwd: req.cwd }), raw);
28
- const abs = pathKey(absForm ? raw : resolve(req.cwd || rootDir, raw));
29
- if (frozen.has(abs)) {
30
- return { action: "deny", message: `${raw} is part of the frozen specification surface (read-only).` };
28
+ const raw = writeTargetPath(req);
29
+ const hit = views.spellings.some((v) => frozen.has(pathKey(v)));
30
+ if (hit) {
31
+ return { action: "deny", message: `${typeof raw === "string" && raw.length > 0 ? raw : views.spellings[0]} is part of the frozen specification surface (read-only).` };
31
32
  }
32
33
  return { action: "allow" };
33
34
  },
@@ -1579,7 +1579,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1579
1579
  opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
1580
1580
  }
1581
1581
  : undefined;
1582
- const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(await captureFloorSeatsNow()), ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
1582
+ const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd, callerRoot: opts.parentCwd } : {}), ...spawnAttribution, ...(await captureFloorSeatsNow()), ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), delegationTaskType: "workflow", agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
1583
1583
  let attempts = 0;
1584
1584
  let throttleRetried = false;
1585
1585
  let lastAttemptReason;
@@ -248,6 +248,13 @@ export declare function substitutionsInHeredocBody(body: string): {
248
248
  readonly body: string;
249
249
  readonly kind: "command" | "arithmetic";
250
250
  }[] | undefined;
251
+ /** The text a word's pieces hand the program when every piece is literal; `undefined` when one is an expansion or
252
+ * unresolved. */
253
+ export declare function literalTextOf(pieces: readonly WordPiece[]): string | undefined;
254
+ /** The text an UNQUOTED here-document body delivers when the shell expands nothing in it: a backslash before `$`, a
255
+ * backquote or a backslash is removed (a line continuation was joined when the body was read), every other character
256
+ * stands. `undefined` when an unescaped `$` or backquote stands in it: the program reads what the expansion makes. */
257
+ export declare function literalHeredocBody(body: string): string | undefined;
251
258
  /** Lex a command: its segments, or the one reason bash (or this lexer) cannot delimit them. */
252
259
  export declare function lexShell(source: string): ShellLex;
253
260
  /**
package/dist/shell/lex.js CHANGED
@@ -319,6 +319,23 @@ export function substitutionsInHeredocBody(body) {
319
319
  }
320
320
  return out;
321
321
  }
322
+ export function literalTextOf(pieces) {
323
+ return pieces.every((q) => q.literal !== undefined) ? pieces.map((q) => q.literal).join("") : undefined;
324
+ }
325
+ export function literalHeredocBody(body) {
326
+ let out = "";
327
+ for (let i = 0; i < body.length; i++) {
328
+ const ch = body[i];
329
+ if (ch === "$" || ch === "`")
330
+ return undefined;
331
+ if (ch === "\\" && i + 1 < body.length && "$`\\".includes(body[i + 1])) {
332
+ out += body[++i];
333
+ continue;
334
+ }
335
+ out += ch;
336
+ }
337
+ return out;
338
+ }
322
339
  const BLANK = /[ \t]/;
323
340
  const fail = (reason, at) => ({ parseFailure: { reason, at } });
324
341
  const publicPiece = (p) => ({
@@ -31,8 +31,6 @@ export declare function writtenAssignmentPrefix(token: string): {
31
31
  readonly settlesWholeName: boolean;
32
32
  } | undefined;
33
33
  export { SHELL_CONTROL_PREFIXES };
34
- /** Shell block words that name no command of their own (a loop head, a block close, a case head). */
35
- export declare const SHELL_BLOCK_WORDS: ReadonlySet<string>;
36
34
  /** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
37
35
  * looked up on PATH, whose failure changes nothing), so a quoted or escaped spelling never counts as one. */
38
36
  export declare function isBareControlWord(words: readonly LexedWord[], i: number, set: ReadonlySet<string>): boolean;
@@ -43,88 +41,6 @@ export declare function isBareControlWord(words: readonly LexedWord[], i: number
43
41
  * over control words and earlier assignments re-traces). One fact, two readers that must agree.
44
42
  */
45
43
  export declare function isLeadingAssignmentWord(words: readonly LexedWord[], i: number): boolean;
46
- /**
47
- * Programs that turn their STANDARD INPUT into the OPERANDS of another program, and EXEC that program directly.
48
- *
49
- * The same class as the shell re-entry programs, one level down: there the unseen text is COMMANDS; here the
50
- * command's name IS visible and its argv is not — `echo <path> | xargs cat` opens a file no token of this walk
51
- * ever named, because the operand arrives on a stream. The membership criterion is that behaviour and only that
52
- * behaviour; a program that consumes stdin as DATA (`sort`, `wc`, a reader the deployment never listed) is NOT a
53
- * member, because its stdin never becomes a filename. Spelled as a pattern — these are program names, never tool
54
- * names.
55
- *
56
- * "EXECS DIRECTLY" is what separates this class from the shell re-entry class, and it is why GNU `parallel` is
57
- * NOT a member: parallel BUILDS A COMMAND LINE and hands it to a shell, and with no command word at all it runs
58
- * each input line as a command — so parallel belongs to the shell re-entry class and is refused whole with no
59
- * exemption at all. A spelling this file has no name for (an aliased or wrapped `xargs`) is judged as any other
60
- * unlisted program is — the name-table floor the re-entry class records for itself.
61
- */
62
- export declare const ARGV_REENTRY_PROGRAM_RE: RegExp;
63
- /** The program an {@link ARGV_REENTRY_PROGRAM_RE} launcher runs when the chain names none: `xargs` defaults to
64
- * `echo`, which is why a bare one reads nothing. */
65
- export declare const ARGV_REENTRY_DEFAULT_PROGRAM = "echo";
66
- /**
67
- * What a launcher declares about its OWN options: each exact spelling with its ARITY — `0` for a switch, `1` for an
68
- * option whose value is the next token. Every spelling not in the map falls to the refusal (the conservative side,
69
- * and the verdict those spellings always had); a `1` with no token left to take is not readable either.
70
- *
71
- * THREE SPELLINGS OF ONE DECLARATION, because getopt has three: an arity-1 entry is honoured separated (`-k 5`),
72
- * fused to its long name (`--signal=TERM`) and fused to its short letter (`-k5`), and an arity-0 short entry inside
73
- * a cluster of arity-0 letters (`-pv`). Reading only the exact spelling made one program two.
74
- *
75
- * WHAT A ROW MAY NOT GET WRONG: declaring an arity-1 option as arity 0 steps onto its VALUE and reads the value as
76
- * the program — the one direction that turns a refusal into a wrong answer rather than a lost one. A row carries
77
- * only options whose arity every supported implementation agrees on; a flag no row declares still refuses.
78
- */
79
- export interface LauncherRow {
80
- /** Consulted ONLY for the launcher the command line itself names — a bare keyword in front of the command
81
- * (`time -p …`) or the written command word (`"time" -p …`, `X=1 time -p …`, `/usr/bin/time -p …`). A launcher
82
- * nested behind another (`env time -p …`) is that launcher's operand, and its options refuse: resolving past
83
- * them would turn a whole refusal into an ordinary walked segment — a widening, where every other read of this
84
- * table only ever finds MORE program to judge. */
85
- readonly optionArity: ReadonlyMap<string, 0 | 1>;
86
- /** A bare NEGATIVE integer in this launcher's option section is its OWN argument (`nice -5 <cmd>`), not a flag
87
- * the table cannot read. Per row: elsewhere a dash-leading token is an option, and reading one as a number
88
- * would step over a flag whose meaning this table does not know. */
89
- readonly negativeIntegerIsOwnArgument?: true;
90
- /** Positional operands the launcher takes BEFORE the program, consumed whatever they spell (`timeout DURATION
91
- * COMMAND`); a spelling-based reading under-accepted, and every spelling it missed became the program word. */
92
- readonly requiredOperands?: number;
93
- /** The launcher reads NAME=VALUE operands with ITS OWN name grammar (`env 1=x …`), wider than the shell's. */
94
- readonly assignmentOperands?: true;
95
- /** The launcher hands its operands to a SHELL as text (`watch "cat /x"` runs `sh -c`): no program word to read. */
96
- readonly shellText?: true;
97
- /** With no program word the launcher runs a SHELL: piped text would reach it, so an empty program position refuses. */
98
- readonly whenNoProgram?: "shell";
99
- /** Options whose value is a WHOLE COMMAND LINE the launcher splits and runs (`env -S "cmd arg"`,
100
- * `--split-string=…`). Not an arity this reader steps over — the program is inside the value, so the
101
- * program-position walk still refuses here — but the candidate extraction reads the value as nested shell
102
- * text instead of as one opaque word. */
103
- readonly commandString?: readonly string[];
104
- }
105
- /**
106
- * The ONE launcher truth: programs that RUN ANOTHER PROGRAM named in ordinary argv tokens, so a declared reader can
107
- * sit behind one (`env cat <path>`, `sudo cat <path>`, `timeout 5 cat <path>`), each with the option grammar this
108
- * file reads for it.
109
- *
110
- * Membership is the key set. A row with no declared options means every option-shaped word after that launcher
111
- * refuses ("which program runs is not readable without the launcher's option table") — the answer every launcher
112
- * but `time` has always had, now stated per row rather than by omission. Declaring a row's options is a behaviour
113
- * decision with its own criteria; adding a launcher is a row.
114
- *
115
- * `time` carries both spellings a deployment can meet: the shell builtin takes `-p` alone, and the standalone
116
- * program adds a format, an output file, and three switches.
117
- *
118
- * A deliberate SUBSET of the rule lane's own `BARE_INTERPRETER_NAMES`, mirrored rather than imported because that
119
- * module imports the classifier — the dependency runs that way, and a cycle is a worse problem than a mirror whose
120
- * subset relation is pinned by test. GNU `parallel` is absent for the reason the shells are absent: it does not run
121
- * a program named in token-visible argv — it BUILDS a command line and hands it to a shell — so it is a shell
122
- * re-entry member instead. Language interpreters and shells are excluded as a scope statement: their program
123
- * argument is a script path or a quoted code string, not a token-visible program with token-visible operands.
124
- */
125
- export declare const LAUNCHER_TABLE: ReadonlyMap<string, LauncherRow>;
126
- /** The launcher names — derived from the table, never kept beside it. */
127
- export declare const COMMAND_LAUNCHERS: ReadonlySet<string>;
128
44
  export declare const PROGRAM_POSITION_REFUSAL_KINDS: readonly ["option_at_program_position", "control_syntax_at_program_position"];
129
45
  export type ProgramPositionRefusalKind = (typeof PROGRAM_POSITION_REFUSAL_KINDS)[number];
130
46
  /** Why the program could not be read — a closed set, each member minted into one sentence. `owner` is the word
@@ -140,6 +56,9 @@ export type ProgramPositionRefusal = {
140
56
  /** Not a DECLINE ({@link programPositionRunsShell}): the row WAS read and a SHELL stands at the program
141
57
  * position. Absent ⇒ genuine control syntax, which IS one. */
142
58
  readonly runsShell?: true;
59
+ /** The launcher hands the shell its operands as text (`watch "S"`); absent beside `runsShell`, the shell is handed
60
+ * nothing (`sudo -s`, `chroot DIR`) and runs its standard input. */
61
+ readonly handsText?: true;
143
62
  };
144
63
  export interface ProgramPosition {
145
64
  /** Indices of the bare control words stripped in front (`!`, `if`, `time`, …), in order; their declared options
@@ -159,6 +78,8 @@ export interface ProgramPosition {
159
78
  readonly argvFromStdin: boolean;
160
79
  /** The chain ends ON the re-entry program (nothing follows it), so it runs its default program. */
161
80
  readonly runsDefaultProgram: boolean;
81
+ /** The program the stream-fed chain member runs when the chain names none (`xargs` runs `echo`). */
82
+ readonly defaultProgram?: string;
162
83
  /** The program bash RUNS: the first word past the chain. `name` is the folded word as written, `basename` what
163
84
  * bash resolves (`/usr/bin/cat` is `cat`). `undefined` when the chain names none or the read was refused. */
164
85
  readonly program: {
@@ -184,7 +105,7 @@ export declare function readProgramPosition(words: readonly LexedWord[]): Progra
184
105
  /** Does this program position hand a program operands the walk cannot see? True when the chain re-enters through
185
106
  * stdin AND the program it runs is not one the caller accepts as a stream target. ONE reading, shared by every
186
107
  * consumer; WHICH targets are accepted is the caller's declaration, not this reader's. */
187
- export declare function argvReentryUnjudged(pos: ProgramPosition, acceptedTargets: ReadonlySet<string>): boolean;
108
+ export declare function argvReentryUnjudged(pos: ProgramPosition, acceptsTarget: (basename: string) => boolean): boolean;
188
109
  /**
189
110
  * The two ways this reader DECLINES rather than judges — a CLOSED set (`docs/CLOSED-SETS.md`), because what a
190
111
  * seat does with a declined segment is a ROW of the shell read decision table (`shell-read-judge.ts`, the
@@ -197,9 +118,9 @@ export type ShellScanDecline = (typeof SHELL_SCAN_DECLINES)[number];
197
118
  /** Does a SHELL stand where this segment's program would be — as the program itself (`sh -c`), or as a launcher
198
119
  * ROW handing its operands to one (`watch "…"`)? ONE predicate for the re-entry class, so the two spellings
199
120
  * cannot be answered differently; which names are shells is the caller's declaration. Not a decline. */
200
- export declare function programPositionRunsShell(pos: ProgramPosition, shellProgram: RegExp): boolean;
121
+ export declare function programPositionRunsShell(pos: ProgramPosition, runsShell: (basename: string) => boolean): boolean;
201
122
  /** Which decline, if any, this program position raises — ONE reading of the two facts above, so no consumer
202
123
  * re-derives either. A refusal that says a SHELL runs is NOT one: it names what runs. */
203
- export declare function shellScanDeclineOf(pos: ProgramPosition, acceptedStreamTargets: ReadonlySet<string>): ShellScanDecline | undefined;
124
+ export declare function shellScanDeclineOf(pos: ProgramPosition, acceptsStreamTarget: (basename: string) => boolean): ShellScanDecline | undefined;
204
125
  /** ONE sentence per refusal kind, minted here and nowhere else. */
205
126
  export declare function programPositionRefusalSentence(refusal: ProgramPositionRefusal): string;