@sema-agent/core 7.45.0 → 7.47.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 (139) hide show
  1. package/CHANGELOG.md +298 -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 +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The roster a host declares for a delegation tool's `extraTools` factory (`SubagentToolOptions.extraToolNames`): every tool
3
+ * the factory may inject at a spawn, by the names it answers to and its content class. Declared, the delegation tool's pool can
4
+ * be listed again — the judgment of a denied-tools list and the memory content-origin reader read it exactly as they read the
5
+ * static pool. Each spawn then checks the factory's products against it: a product the roster does not declare (a name or an
6
+ * alias no entry carries), or declares nearer to home than the product says it is (its content class further out than the
7
+ * entry's), refuses that spawn. The roster is an upper bound: a declared tool the factory did not inject this time is fine.
8
+ *
9
+ * One code for every way the declaration fails, `config.extra_tool_names`, with the arm on `detail.reason`: `shape` (the value
10
+ * is not a roster — refused where the delegation tool is built), `undeclared_name` / `undeclared_origin` (a product the roster
11
+ * does not cover — refused at the spawn). A host value is never printed or converted: a sentence names an entry's place and
12
+ * the TYPE of what it found there.
13
+ */
14
+ import type { ToolContentOrigin, ToolSpec } from "../core/types.js";
15
+ export declare const EXTRA_TOOL_NAMES_CODE = "config.extra_tool_names";
16
+ /** Which way a declaration failed (`detail.reason`). */
17
+ export type ExtraToolNamesReason = "shape" | "undeclared_name" | "undeclared_origin";
18
+ /** One declared tool, as the delegation tool keeps it (a frozen copy of the host's entry). */
19
+ export interface ExtraToolDeclaration {
20
+ readonly name: string;
21
+ readonly aliases?: readonly string[];
22
+ readonly contentOrigin?: ToolContentOrigin;
23
+ }
24
+ /** The error a failed declaration is refused with: the code, and the arm on `detail.reason`. */
25
+ export declare function extraToolNamesError(message: string, reason: ExtraToolNamesReason): Error & {
26
+ code: string;
27
+ detail: {
28
+ reason: ExtraToolNamesReason;
29
+ };
30
+ };
31
+ /**
32
+ * The host's declaration, read ONCE into frozen copies (a later change to the host's array does not reach the pool), or a
33
+ * refusal (`shape`): a roster on a delegation tool with no factory, a value that is not a list, an entry that is not an object, a
34
+ * name that is not a non-empty string, aliases that are not a list of non-empty strings, a content class outside the vocabulary,
35
+ * or one spelling in two entries.
36
+ */
37
+ export declare function readExtraToolNames(value: unknown, hasFactory: boolean): readonly ExtraToolDeclaration[];
38
+ /**
39
+ * Whether one factory product is a tool the roster declares, as declared — answering to exactly one entry's names, its class no
40
+ * further out than the entry's: undefined when it is, else the arm and the sentence (the delegation tool's name, the product's
41
+ * offending name or class, the roster). A product's content class is its own
42
+ * declaration when that is a vocabulary member, else external — the class an undeclared host tool reads as; an entry that
43
+ * states no class allows any.
44
+ */
45
+ export declare function undeclaredExtraTool(roster: readonly ExtraToolDeclaration[], product: ToolSpec, toolName: string): {
46
+ reason: Exclude<ExtraToolNamesReason, "shape">;
47
+ sentence: string;
48
+ } | undefined;
49
+ /** One row of a delegation tool's pool face (`ToolSpec.agentToolPool`): a tool's names and its declared content class. */
50
+ export interface DelegationPoolRow {
51
+ name: string;
52
+ aliases?: readonly string[];
53
+ contentOrigin?: ToolContentOrigin;
54
+ }
55
+ /**
56
+ * The pool a delegation tool's agent faces narrow FROM (`ToolSpec.agentToolPool`, read by the judgment of a denied-tools list and
57
+ * by the memory content-origin reader): what the tool hands its children — its own captured set, not the parent's roster (the two
58
+ * may differ, and a parent mounting only the delegation tool can still offer children a network tool) — plus the tools its factory
59
+ * is declared to inject. Absent when a factory has no declared roster: that pool does not exist until the call happens, and the
60
+ * readers take an absent pool as unknowable (the memory reader: external). Without an explicit pool the inherited surface is the
61
+ * face — its spawn-time narrowing only removes entries.
62
+ */
63
+ export declare function delegationPoolFace(staticPool: readonly ToolSpec[], hasFactory: boolean, roster: readonly ExtraToolDeclaration[] | undefined): DelegationPoolRow[] | undefined;
64
+ /** The first factory product the roster does not declare (see {@link undeclaredExtraTool}), or undefined when every one is declared. */
65
+ export declare function firstUndeclaredExtraTool(roster: readonly ExtraToolDeclaration[], products: readonly ToolSpec[], toolName: string): ReturnType<typeof undeclaredExtraTool>;
@@ -0,0 +1,124 @@
1
+ import { isToolContentOrigin } from "../core/memory-engine/content-origin.js";
2
+ import { inlineUntrusted } from "../core/untrusted-text.js";
3
+ import { readToolNameList, valueTypeLabel } from "./tool-filter.js";
4
+ export const EXTRA_TOOL_NAMES_CODE = "config.extra_tool_names";
5
+ export function extraToolNamesError(message, reason) {
6
+ const e = new Error(message);
7
+ e.code = EXTRA_TOOL_NAMES_CODE;
8
+ e.detail = { reason };
9
+ return e;
10
+ }
11
+ const shape = (what) => extraToolNamesError(`createSubagentTool: extraToolNames ${what} — it declares the tools the extraTools factory may inject, each { name, aliases?, contentOrigin? }. Fix the declaration.`, "shape");
12
+ function own(entry, key) {
13
+ try {
14
+ return { ok: true, value: entry[key] };
15
+ }
16
+ catch {
17
+ return { ok: false };
18
+ }
19
+ }
20
+ export function readExtraToolNames(value, hasFactory) {
21
+ if (!hasFactory)
22
+ throw extraToolNamesError("createSubagentTool: extraToolNames declares the tools an extraTools factory may inject, and this delegation tool has no extraTools factory. Remove the declaration or add the factory.", "shape");
23
+ const list = readToolNameList(value);
24
+ if (list.kind === "not_a_list")
25
+ throw shape(`is a value that is ${list.type}, not a list`);
26
+ if (list.kind === "unreadable")
27
+ throw shape("is a list whose entries cannot be read (an accessor or a proxy trap threw)");
28
+ const out = [];
29
+ const owner = new Map();
30
+ list.entries.forEach((entry, index) => {
31
+ const at = `entry ${index + 1}`;
32
+ if (entry === null || typeof entry !== "object" || valueTypeLabel(entry) !== "an object")
33
+ throw shape(`${at} is a value that is ${valueTypeLabel(entry)}, not an object`);
34
+ const name = own(entry, "name");
35
+ if (!name.ok)
36
+ throw shape(`${at}: its name cannot be read`);
37
+ if (typeof name.value !== "string" || name.value === "")
38
+ throw shape(`${at}: its name is ${typeof name.value === "string" ? "empty" : `a value that is ${valueTypeLabel(name.value)}`}, not a tool name`);
39
+ const aliasesRead = own(entry, "aliases");
40
+ if (!aliasesRead.ok)
41
+ throw shape(`${at}: its aliases cannot be read`);
42
+ let aliases;
43
+ if (aliasesRead.value !== undefined) {
44
+ const al = readToolNameList(aliasesRead.value);
45
+ if (al.kind !== "entries")
46
+ throw shape(`${at}: its aliases are ${al.kind === "not_a_list" ? `a value that is ${al.type}` : "a list that cannot be read"}, not a list of names`);
47
+ aliases = [];
48
+ for (const a of al.entries) {
49
+ if (typeof a !== "string" || a === "")
50
+ throw shape(`${at}: an alias is ${typeof a === "string" ? "empty" : `a value that is ${valueTypeLabel(a)}`}, not a tool name`);
51
+ aliases.push(a);
52
+ }
53
+ }
54
+ const originRead = own(entry, "contentOrigin");
55
+ if (!originRead.ok)
56
+ throw shape(`${at}: its contentOrigin cannot be read`);
57
+ if (originRead.value !== undefined && !isToolContentOrigin(originRead.value))
58
+ throw shape(`${at}: its contentOrigin is not one of "local", "execution", "external"`);
59
+ for (const spelling of new Set([name.value, ...(aliases ?? [])])) {
60
+ const first = owner.get(spelling);
61
+ if (first !== undefined)
62
+ throw shape(`entries ${first + 1} and ${index + 1} both answer to ${JSON.stringify(inlineUntrusted(spelling, 80))}`);
63
+ owner.set(spelling, index);
64
+ }
65
+ out.push(Object.freeze({
66
+ name: name.value,
67
+ ...(aliases !== undefined ? { aliases: Object.freeze(aliases) } : {}),
68
+ ...(originRead.value !== undefined ? { contentOrigin: originRead.value } : {}),
69
+ }));
70
+ });
71
+ return Object.freeze(out);
72
+ }
73
+ const REACH = { local: 0, execution: 1, external: 2 };
74
+ const quoted = (name) => `"${inlineUntrusted(name, 80)}"`;
75
+ function rosterPhrase(roster) {
76
+ const shown = roster.slice(0, 8).map((e) => quoted(e.name));
77
+ return roster.length === 0 ? "none" : shown.join(", ") + (roster.length > shown.length ? `, … (${roster.length - shown.length} more)` : "");
78
+ }
79
+ export function undeclaredExtraTool(roster, product, toolName) {
80
+ const where = `the extraTools factory of delegation tool ${quoted(toolName)}`;
81
+ const tail = "the declaration is the upper bound of what the factory may inject. Fix the declaration or the factory.";
82
+ const name = product.name;
83
+ if (typeof name !== "string")
84
+ return { reason: "undeclared_name", sentence: `${where} injected a tool whose name is a value that is ${valueTypeLabel(name)}, which its declared extra tools (extraToolNames: ${rosterPhrase(roster)}) cannot name — ${tail}` };
85
+ const entry = roster.find((e) => e.name === name || (e.aliases?.includes(name) ?? false));
86
+ if (entry === undefined)
87
+ return { reason: "undeclared_name", sentence: `${where} injected tool ${quoted(name)}, which its declared extra tools (extraToolNames: ${rosterPhrase(roster)}) do not name — ${tail}` };
88
+ const spellings = new Set([entry.name, ...(entry.aliases ?? [])]);
89
+ const aliases = product.aliases;
90
+ const aliasList = Array.isArray(aliases) ? aliases : [];
91
+ for (const alias of aliasList) {
92
+ if (typeof alias !== "string" || !spellings.has(alias)) {
93
+ const said = typeof alias === "string" ? quoted(alias) : `a value that is ${valueTypeLabel(alias)}`;
94
+ return { reason: "undeclared_name", sentence: `${where} injected tool ${quoted(name)} answering to ${said}, which its declared extra tools (extraToolNames: ${rosterPhrase(roster)}) do not name — ${tail}` };
95
+ }
96
+ }
97
+ const answers = new Set([name, ...aliasList]);
98
+ const missing = [...spellings].find((n) => !answers.has(n));
99
+ if (missing !== undefined)
100
+ return { reason: "undeclared_name", sentence: `${where} injected tool ${quoted(name)}, which does not answer to ${quoted(missing)} as its declared extra tools (extraToolNames: ${rosterPhrase(roster)}) say it does — ${tail}` };
101
+ const stated = product.contentOrigin;
102
+ const productClass = isToolContentOrigin(stated) ? stated : "external";
103
+ const declaredClass = entry.contentOrigin ?? "external";
104
+ if (REACH[productClass] > REACH[declaredClass]) {
105
+ return { reason: "undeclared_origin", sentence: `${where} injected tool ${quoted(name)} of content class "${productClass}", which its declared extra tools (extraToolNames: ${rosterPhrase(roster)}) state as "${declaredClass}" — ${tail}` };
106
+ }
107
+ return undefined;
108
+ }
109
+ export function delegationPoolFace(staticPool, hasFactory, roster) {
110
+ if (hasFactory && roster === undefined)
111
+ return undefined;
112
+ return [...staticPool, ...(roster ?? [])].map((t) => {
113
+ const origin = t.contentOrigin;
114
+ return { name: t.name, ...(t.aliases !== undefined ? { aliases: t.aliases } : {}), ...(origin !== undefined ? { contentOrigin: origin } : {}) };
115
+ });
116
+ }
117
+ export function firstUndeclaredExtraTool(roster, products, toolName) {
118
+ for (const product of products) {
119
+ const flaw = undeclaredExtraTool(roster, product, toolName);
120
+ if (flaw !== undefined)
121
+ return flaw;
122
+ }
123
+ return undefined;
124
+ }
@@ -704,13 +704,13 @@ export interface SubagentToolOptions {
704
704
  inheritedTools?: ToolSpec[];
705
705
  /**
706
706
  * Tool-level allowlist (design/38 1A), used when NO `agent` is selected (a selected agent definition's
707
- * own `allowTools`/`denyTools` REPLACE these). When set, the child sees only these tools from `tools`
708
- * (`["*"]` = all). Omit for the whole `tools` pool. Allowlist is the security-recommended shape.
707
+ * own `allowTools`/`denyTools` REPLACE these). When set, the child sees only these of its pool and of the engine's hand
708
+ * tools its run mounts (Read / Write / Bash …) (`["*"]` = all). Omit for everything. Allowlist is the security-recommended shape.
709
709
  * NOTE: this filters only the WORK tools — the nested delegation tool is governed by `maxDepth`, not
710
710
  * allow/deny. To deny the child any further delegation, set `maxDepth: 1` (not a `denyTools` entry).
711
711
  */
712
712
  allowTools?: string[];
713
- /** Tool-level denylist (design/38 1A), used when no `agent` is selected: names removed (a deny wins). */
713
+ /** Tool-level denylist (design/38 1A), used when no `agent` is selected: names removed from the pool and the hand tools (a deny wins). */
714
714
  denyTools?: string[];
715
715
  /** System prompt / persona for the child. Default (1.259.0, CC 198 parity): the LEAN
716
716
  * {@link SUBAGENT_PROMPT} (agent persona + return contract), NOT the full default constitution role
@@ -764,10 +764,11 @@ export interface SubagentToolOptions {
764
764
  * call's parent coordinates, to produce tool instances that cannot sit in the mount-time static
765
765
  * `tools` pool (the deployment case: per-task tools issued by a task-env registry — the static pool
766
766
  * is fixed at `createSubagentTool({...})` time, before any task exists). The products are MERGED
767
- * INTO the `tools` pool and then flow through the EXACT SAME `resolveToolSubset` whitelist as the
767
+ * INTO the `tools` pool and then flow through the EXACT SAME subset reading as the
768
768
  * static tools — an agent definition's `allowTools`/`denyTools` (or the tool-level ones when no
769
769
  * agent is selected) govern them verbatim: an explicit allowlist must NAME an injected tool for the
770
- * child to see it; `"*"`/absent allowlist = all. Nested delegation threads the SAME factory down
770
+ * child to see it; `"*"`/absent allowlist = all. Without {@link extraToolNames} the pool cannot be listed before a spawn.
771
+ * Nested delegation threads the SAME factory down
771
772
  * (`createSubagentToolNode(opts, depth+1)` carries it), so a grandchild spawn re-evaluates it — per-spawn,
772
773
  * at every level; each child gets its own instances bound to its own spawn moment (the
773
774
  * server-side factory is execute-time late-bound, so per-spawn evaluation is the confirmed shape).
@@ -788,6 +789,17 @@ export interface SubagentToolOptions {
788
789
  * tightening layer on the supported paths, never the only line of defense.
789
790
  */
790
791
  extraTools?: (ctx: SubagentSpawnContext) => ToolSpec[] | Promise<ToolSpec[]>;
792
+ /**
793
+ * The roster of {@link extraTools} — every tool it may inject: names, aliases, content class (absent = external). The declared
794
+ * tools join the pool, so the pool is listable before a spawn; a product outside the roster refuses that spawn
795
+ * (`config.extra_tool_names`, `detail.reason` `undeclared_name` / `undeclared_origin`, also to {@link onExtraToolsError});
796
+ * a malformed roster, or one with no factory, throws where the tool is built (`shape`). Absent ⇒ as before. See extra-tool-names.ts.
797
+ */
798
+ extraToolNames?: ReadonlyArray<{
799
+ name: string;
800
+ aliases?: readonly string[];
801
+ contentOrigin?: import("../core/types.js").ToolContentOrigin;
802
+ }>;
791
803
  /**
792
804
  * extraTools 快审 S3 — host-side sink for the ORIGINAL `extraTools` factory error (and the S4
793
805
  * budget-exceeded signal). The model-facing report deliberately carries only the fixed
@@ -15,7 +15,8 @@ export { childThinkingSeat, deploymentSubagentThinking, deploymentSubagentTierMo
15
15
  import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
16
16
  import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
17
17
  import { OFFLOAD_TOOL_NAME } from "../core/tool-result-store.js";
18
- import { readToolNameList, resolveToolSubset, snapshotDefinitionLists, snapshotToolNameList, toolNameAllowed, toolNameListFlaw } from "./tool-filter.js";
18
+ import { readToolNameList, resolveAgentToolSubset, snapshotDefinitionLists, snapshotToolNameList, toolNameAllowed, toolNameListFlaw } from "./tool-filter.js";
19
+ import { EXTRA_TOOL_NAMES_CODE, delegationPoolFace, extraToolNamesError, firstUndeclaredExtraTool, readExtraToolNames } from "./extra-tool-names.js";
19
20
  import { builtinAgentDefinitions } from "./builtin-agents.js";
20
21
  import { SUBAGENT_PROMPT } from "../prompts/default.js";
21
22
  import { hasSessionFork } from "../core/session.js";
@@ -271,8 +272,10 @@ const worktreeUnavailableSentence = (reason) => `Sub-agent not started: isolatio
271
272
  const WORKTREE_UNAVAILABLE = {
272
273
  readOnly: worktreeUnavailableSentence("no worktree capability — a read-only run mints no worktree"),
273
274
  needsFactory: worktreeUnavailableSentence("this deployment has no executionEnvFactory — a static execution environment has one shared working directory, so the child would run in this run's own root instead of a worktree; deploy an executionEnvFactory to enable worktree isolation"),
275
+ notHonored: worktreeUnavailableSentence("the execution environment factory placed the child inside this run's own tree instead of the worktree it was offered — it would have read and written this checkout; the worktree was removed and the child was not started"),
274
276
  seatSilent: worktreeUnavailableSentence("the runner this tool runs children on does not declare whether it can place a child in a tree other than the caller's (its seat has no canRerootChild), and an undeclared answer is read as no; use a Runner, or add that method to the seat"),
275
277
  };
278
+ const placementRefused = (terminal) => terminal?.kind === "failed" && terminal.code === "isolation.unavailable";
276
279
  const isolationUnavailable = (content) => ({
277
280
  isError: true,
278
281
  content,
@@ -1270,6 +1273,8 @@ export function createSubagentTool(opts) {
1270
1273
  opts = { ...opts, builtinAgents: catalog.builtinAgents };
1271
1274
  }
1272
1275
  const assemblyModels = opts.models ?? catalog?.models;
1276
+ if (opts.extraToolNames !== undefined)
1277
+ opts = { ...opts, extraToolNames: readExtraToolNames(opts.extraToolNames, opts.extraTools !== undefined) };
1273
1278
  if (opts.denyTools !== undefined)
1274
1279
  opts = { ...opts, denyTools: snapshotToolNameList(opts.denyTools) };
1275
1280
  if (opts.allowTools !== undefined)
@@ -1386,15 +1391,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1386
1391
  })),
1387
1392
  ...(forkOffered ? [{ name: FORK_SUBAGENT_TYPE, ...(depthPermitsNesting ? { canRedelegate: true } : {}) }] : []),
1388
1393
  ];
1389
- const agentToolPool = opts.extraTools !== undefined
1390
- ? undefined
1391
- : (opts.tools ?? opts.inheritedTools ?? []).map((t) => ({
1392
- name: t.name,
1393
- ...(t.aliases !== undefined ? { aliases: t.aliases } : {}),
1394
- ...(t.contentOrigin !== undefined
1395
- ? { contentOrigin: t.contentOrigin }
1396
- : {}),
1397
- }));
1394
+ const agentToolPool = delegationPoolFace(opts.tools ?? opts.inheritedTools ?? [], opts.extraTools !== undefined, opts.extraToolNames);
1398
1395
  return {
1399
1396
  name: opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME,
1400
1397
  ...toolFace("agent"),
@@ -1755,11 +1752,16 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1755
1752
  }
1756
1753
  }
1757
1754
  let worktreeKeptPath;
1758
- const finishWorktree = async () => {
1755
+ let childPlacement;
1756
+ const finishWorktree = async (terminal) => {
1759
1757
  if (worktreeDir === undefined)
1760
1758
  return undefined;
1761
1759
  try {
1762
1760
  const outcome = await ctx.worktreeIsolation.finish(worktreeDir);
1761
+ if (placementRefused(terminal))
1762
+ return undefined;
1763
+ if (childPlacement?.placement === "elsewhere")
1764
+ return `worktree: none — the execution environment ran the agent at ${inlineUntrusted(childPlacement.cwd, REPORT_FIELD_MAX)}, not in the worktree it was offered (${outcome === "pruned" ? "removed unused" : `kept at ${worktreeDir}: it changed though the agent never worked in it`})`;
1763
1765
  if (outcome === "kept")
1764
1766
  worktreeKeptPath = worktreeDir;
1765
1767
  return outcome === "pruned"
@@ -1808,14 +1810,15 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1808
1810
  reportExtraToolsError(e);
1809
1811
  }
1810
1812
  }
1811
- if (ctx.signal?.aborted) {
1813
+ const notStarted = async (why, details) => {
1812
1814
  const wt = await finishWorktree();
1813
- return {
1814
- isError: true,
1815
- content: `Sub-agent not started: the parent task was aborted while extraTools was being evaluated.${wt ? `\n${wt}` : ""}`,
1816
- details: { error: "aborted" },
1817
- };
1818
- }
1815
+ return { isError: true, content: `Sub-agent not started: ${why.slice(0, 1600)}${wt ? `\n${wt}` : ""}`, details };
1816
+ };
1817
+ if (ctx.signal?.aborted)
1818
+ return notStarted("the parent task was aborted while extraTools was being evaluated.", { error: "aborted" });
1819
+ const undeclared = opts.extraToolNames !== undefined ? firstUndeclaredExtraTool(opts.extraToolNames, injectedTools, toolName) : undefined;
1820
+ if (undeclared !== undefined)
1821
+ return (reportExtraToolsError(extraToolNamesError(undeclared.sentence, undeclared.reason)), notStarted(undeclared.sentence, { error: EXTRA_TOOL_NAMES_CODE, reason: undeclared.reason }));
1819
1822
  }
1820
1823
  const inherited = opts.tools === undefined && extraToolsNote === undefined ? inheritedChildPool(opts.inheritedTools ?? [], ctx.mountsCallerTool, wantsBackground && agentName === undefined && !wantsFork) : [];
1821
1824
  if (extraToolsNote !== undefined && opts.tools === undefined && (opts.inheritedTools?.length ?? 0) > 0)
@@ -1842,8 +1845,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1842
1845
  }
1843
1846
  childPool = [...(opts.tools ?? inherited.filter((t) => ![t.name, ...(t.aliases ?? [])].some((n) => taken.has(n)))), ...accepted];
1844
1847
  }
1845
- const allowed = new Set([...resolveToolSubset(childPool, def ? def.allowTools : opts.allowTools, def ? def.denyTools : opts.denyTools), ...(agentName !== undefined ? teammateInheritedFamily(childPool.filter((t) => inherited.includes(t)), def ? def.denyTools : opts.denyTools) : [])]);
1848
+ const subset = resolveAgentToolSubset(childPool, def ? def.allowTools : opts.allowTools, def ? def.denyTools : opts.denyTools);
1849
+ const allowed = new Set([...subset.handed, ...(agentName !== undefined ? teammateInheritedFamily(childPool.filter((t) => inherited.includes(t)), def ? def.denyTools : opts.denyTools) : [])]);
1846
1850
  const childTools = childPool.filter((t) => allowed.has(t));
1851
+ const childExcludeTools = ctx.excludeTools !== undefined || subset.withheldHands.length > 0 ? [...new Set([...(ctx.excludeTools ?? []), ...subset.withheldHands])] : undefined;
1847
1852
  const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
1848
1853
  const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
1849
1854
  const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
@@ -1921,7 +1926,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1921
1926
  ...(ctx.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: ctx.getApiKeyAndHeaders } : {}),
1922
1927
  ...(ctx.checkpointStoreForChildren !== undefined ? { checkpointStore: ctx.checkpointStoreForChildren } : {}),
1923
1928
  tools: [reportTool],
1924
- ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
1929
+ ...(childExcludeTools !== undefined ? { excludeTools: [...childExcludeTools] } : {}),
1925
1930
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
1926
1931
  ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
1927
1932
  ...(ctx.restoreGatedTools !== undefined ? { restoreGatedTools: ctx.restoreGatedTools === true ? true : [...ctx.restoreGatedTools] } : {}),
@@ -2167,8 +2172,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2167
2172
  })(),
2168
2173
  ...(ctx.subagentRetain !== undefined ? { parentRetainLedger: ctx.subagentRetain } : {}),
2169
2174
  onForwardEvent: recordingForward,
2175
+ ...(ctx.parentCwd !== undefined ? { callerRoot: ctx.parentCwd } : {}),
2170
2176
  ...(worktreeDir !== undefined
2171
- ? { parentCwd: worktreeDir }
2177
+ ? { parentCwd: worktreeDir, worktreeOffered: true, onWorkspaceResolved: (ws) => void (childPlacement = ws) }
2172
2178
  : childCwd !== undefined
2173
2179
  ?
2174
2180
  { parentCwd: childCwd, requestedCwd: childCwd }
@@ -2201,7 +2207,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2201
2207
  ...(ctx.checkpointStoreForChildren !== undefined ? { checkpointStore: ctx.checkpointStoreForChildren } : {}),
2202
2208
  ...(childOnAsk !== undefined ? { onAsk: childOnAsk } : {}),
2203
2209
  ...childClampSeats({ handsReadOnly: ctx.handsReadOnly, interactiveTools: ctx.interactiveTools, approvalReach: ctx.approvalReach, readFace: ctx.readFace, writeFace: ctx.writeFace, readDenyPatterns: ctx.readDenyPatterns }),
2204
- ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
2210
+ ...(childExcludeTools !== undefined ? { excludeTools: [...childExcludeTools] } : {}),
2205
2211
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
2206
2212
  ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
2207
2213
  ...(ctx.restoreGatedTools !== undefined ? { restoreGatedTools: ctx.restoreGatedTools === true ? true : [...ctx.restoreGatedTools] } : {}),
@@ -2686,7 +2692,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2686
2692
  const childFace = terminalProjection(child.terminal);
2687
2693
  settleForkRow(child.terminal);
2688
2694
  dropHostAbortListener();
2689
- await finishWorktree();
2695
+ await finishWorktree(child.terminal);
2690
2696
  try {
2691
2697
  const nsb = child.stats.nested;
2692
2698
  ctx.reportUsage?.({
@@ -3464,7 +3470,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3464
3470
  settleDelegationRow(child.terminal);
3465
3471
  closeObserverWindow(childFace.status);
3466
3472
  dropHostAbortListener();
3467
- await finishWorktree();
3473
+ await finishWorktree(child.terminal);
3468
3474
  try {
3469
3475
  const ns2 = child.stats.nested;
3470
3476
  ctx.reportUsage?.({
@@ -3869,7 +3875,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3869
3875
  }
3870
3876
  }
3871
3877
  }
3872
- const worktreeLine = await finishWorktree();
3878
+ const worktreeLine = await finishWorktree(child.terminal);
3879
+ if (worktreeDir !== undefined && placementRefused(child.terminal))
3880
+ return isolationUnavailable(WORKTREE_UNAVAILABLE.notHonored);
3873
3881
  const errClass = classifySubagentError(child.terminal);
3874
3882
  const lines = [
3875
3883
  ...(handbackWarning ? [handbackWarning, ""] : []),
@@ -93,4 +93,20 @@ export declare function allowedToolNames(allowTools: unknown): ReadonlySet<strin
93
93
  * TOOL-level allow/deny keeps its own semantics (delegation governed by `maxDepth`).
94
94
  */
95
95
  export declare function toolNameAllowed(name: string, allowTools?: ReadonlyArray<string>, denyTools?: ReadonlyArray<string>): boolean;
96
- export declare function resolveToolSubset(pool: ReadonlyArray<ToolSpec>, allowTools?: ReadonlyArray<string>, denyTools?: ReadonlyArray<string>): ToolSpec[];
96
+ export declare function resolveToolSubset<T extends {
97
+ readonly name: string;
98
+ readonly aliases?: readonly string[];
99
+ } = ToolSpec>(pool: ReadonlyArray<T>, allowTools?: ReadonlyArray<string>, denyTools?: ReadonlyArray<string>): T[];
100
+ /**
101
+ * What ONE agent's tool-name lists leave it, read once over everything it can get: the tools the delegation tool hands it
102
+ * (`handed` — its pool and the factory's products, the subset in pool order) and the engine's hand tools its own run mounts
103
+ * over an execution environment ({@link HAND_TOOLS} — `withheldHands`, the wire names the lists take away, which the spawn
104
+ * adds to the child's exclusion list: the one fact every reader of "is this tool mounted" consults). One
105
+ * {@link resolveToolSubset} call over both halves, so a name reads the same whichever half carries it. The exclusion list
106
+ * unmounts by wire name, so a withheld hand name that a handed tool is mounted under stays off it (it would take the kept
107
+ * tool with it); a handed tool's alias does not count — the exclusion never reaches an alias, so the hand tool stays withheld.
108
+ */
109
+ export declare function resolveAgentToolSubset(pool: ReadonlyArray<ToolSpec>, allowTools?: ReadonlyArray<string>, denyTools?: ReadonlyArray<string>): {
110
+ handed: ToolSpec[];
111
+ withheldHands: string[];
112
+ };
@@ -1,4 +1,5 @@
1
1
  import { parsePermissionRule } from "../core/permission-rule-syntax.js";
2
+ import { HAND_TOOLS } from "../core/tool-catalog-entries.js";
2
3
  export function readDeniedEntry(entry) {
3
4
  if (typeof entry !== "string")
4
5
  return { kind: "not_a_string", type: valueTypeLabel(entry) };
@@ -137,3 +138,9 @@ export function resolveToolSubset(pool, allowTools, denyTools) {
137
138
  return (!allow || matches(allow)) && (!deny || !matches(deny));
138
139
  });
139
140
  }
141
+ export function resolveAgentToolSubset(pool, allowTools, denyTools) {
142
+ const kept = new Set(resolveToolSubset([...pool, ...HAND_TOOLS], allowTools, denyTools));
143
+ const handed = pool.filter((t) => kept.has(t));
144
+ const mountedUnder = new Set(handed.map((t) => t.name));
145
+ return { handed, withheldHands: HAND_TOOLS.filter((t) => !kept.has(t) && !mountedUnder.has(t.name)).map((t) => t.name) };
146
+ }
@@ -0,0 +1,20 @@
1
+ /** The configuration directory's name below a home: the one segment the deny tables spell the agent's own settings under. */
2
+ export declare const AGENT_CONFIG_DIR_SEGMENT = ".sema";
3
+ /** The code a declared root that names no directory is refused with, wherever it is declared. */
4
+ export declare const AGENT_CONFIG_DIR_INVALID_CODE = "config.agent_config_dir_invalid";
5
+ /** A run (the `/`-separated segments of a deny row, home word already removed) read below the configuration directory:
6
+ * `.sema/settings.json` ⇒ `settings.json`, `.sema` ⇒ `""`, and `undefined` for a run that does not open with the
7
+ * directory's name. The first segment is compared as the row compares it (ASCII-case-insensitive unless the row opts out).
8
+ *
9
+ * @contract agent_config_dir.one_reading — which rows a declared configuration root relocates is decided HERE and nowhere
10
+ * else: a row that opens with the `.sema` segment names its tail below the root, in the read placement, in the always-on
11
+ * write-protection table and in the opt-in deny set alike; the root adds a place and never moves the default one, a row that
12
+ * does not open with that segment keeps its place, and a face with a list of its own of "the rows that relocate" is a fork
13
+ * of this sentence, not a variant of it. */
14
+ export declare function agentConfigDirTail(run: string, caseSensitive?: boolean): string | undefined;
15
+ /** The segments of a declared root in lexical normal form — what a path's leading segments are compared with. */
16
+ export declare function agentConfigDirSegments(dir: string): string[];
17
+ /** The declared configuration root as given (`undefined` = none declared, the default location), or a refusal naming the
18
+ * seat and the shape of the value: not a string, empty, not absolute, no such directory, not a directory, not readable.
19
+ * The directory is looked at on the host's own file system — the host that declares the root is the host that keeps it. */
20
+ export declare function checkedAgentConfigDir(value: string | undefined, seat: string): string | undefined;
@@ -0,0 +1,49 @@
1
+ import { statSync } from "node:fs";
2
+ import { isAbsolutePathForm, normalizeAbsPathLexically } from "../tools/fs/safety.js";
3
+ export const AGENT_CONFIG_DIR_SEGMENT = ".sema";
4
+ export const AGENT_CONFIG_DIR_INVALID_CODE = "config.agent_config_dir_invalid";
5
+ export function agentConfigDirTail(run, caseSensitive = false) {
6
+ const [first, ...rest] = run.split("/").filter((segment) => segment.length > 0);
7
+ if (first === undefined)
8
+ return undefined;
9
+ const folded = caseSensitive ? first : first.replace(/[A-Z]/g, (c) => c.toLowerCase());
10
+ return folded === AGENT_CONFIG_DIR_SEGMENT ? rest.join("/") : undefined;
11
+ }
12
+ export function agentConfigDirSegments(dir) {
13
+ return normalizeAbsPathLexically(dir).split(/[\\/]/).filter((segment) => segment.length > 0);
14
+ }
15
+ function shown(value) {
16
+ try {
17
+ return JSON.stringify(value) ?? String(value);
18
+ }
19
+ catch {
20
+ return typeof value;
21
+ }
22
+ }
23
+ function invalidAgentConfigDir(seat, value, why) {
24
+ const e = new Error(`${seat} ${shown(value)} ${why} — it must be the absolute path of an existing directory (the host's own configuration root); ` +
25
+ `a value that names no directory would guard nothing, so it is refused rather than read as the default \`~/${AGENT_CONFIG_DIR_SEGMENT}\` location.`);
26
+ e.code = AGENT_CONFIG_DIR_INVALID_CODE;
27
+ return e;
28
+ }
29
+ export function checkedAgentConfigDir(value, seat) {
30
+ if (value === undefined)
31
+ return undefined;
32
+ if (typeof value !== "string")
33
+ throw invalidAgentConfigDir(seat, value, `is a ${value === null ? "null" : typeof value}, not a string`);
34
+ if (value.length === 0)
35
+ throw invalidAgentConfigDir(seat, value, "is an empty string");
36
+ if (!isAbsolutePathForm(value))
37
+ throw invalidAgentConfigDir(seat, value, "is not an absolute path");
38
+ let isDirectory;
39
+ try {
40
+ isDirectory = statSync(value).isDirectory();
41
+ }
42
+ catch (e) {
43
+ const code = typeof e === "object" && e !== null && "code" in e && typeof e.code === "string" ? e.code : "unreadable";
44
+ throw invalidAgentConfigDir(seat, value, code === "ENOENT" || code === "ENOTDIR" ? "names no existing directory" : `could not be examined (${code})`);
45
+ }
46
+ if (!isDirectory)
47
+ throw invalidAgentConfigDir(seat, value, "names a file, not a directory");
48
+ return value;
49
+ }
@@ -1,7 +1,7 @@
1
1
  import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
2
- import { type ToolCallRequest } from "./tool-policy.js";
2
+ import type { ToolCallRequest } from "./tool-policy.js";
3
3
  import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
4
- import { type PersistedRule } from "./permission-rule-model.js";
4
+ import { type PathRuleBases, type PersistedRule } from "./permission-rule-model.js";
5
5
  import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
6
6
  import { type ToolNameListReading } from "../agents/tool-filter.js";
7
7
  import { type EngineNotice } from "./engine-notice.js";
@@ -94,12 +94,14 @@ export interface AllowEntryRow {
94
94
  readonly entry: string;
95
95
  readonly compiled: CompiledAllowEntry;
96
96
  }
97
- /** The call facts a content row is judged against — the persisted lane's own bases. */
97
+ /** The facts a list's content row is judged under: the leg's root and session (which rows apply), the live working
98
+ * directory (where a command's `cd` lands), and the ANCHORS of the list's path rows — the leg the list was installed
99
+ * on (its root and environment home): a path row of a list never moves with a `cd`. */
98
100
  export interface AllowEntryJudgeContext {
99
101
  readonly root: string | undefined;
100
102
  readonly sessionId: string | undefined;
101
103
  readonly liveCwd: () => string | undefined;
102
- readonly home: string | undefined;
104
+ readonly anchors: PathRuleBases;
103
105
  }
104
106
  /**
105
107
  * The ONE list matcher, both directions — the rows that speak for the call, or undefined. ALLOW: the first name row that
@@ -1,11 +1,11 @@
1
1
  import { heldBy } from "./tool-name-readings.js";
2
2
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
3
3
  import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
4
- import { catalogToolNames } from "./tool-registry.js";
5
- import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
4
+ import { catalogToolNames, isShellFaceToolName } from "./tool-registry.js";
6
5
  import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
7
6
  import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
8
7
  import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
8
+ import { callTargetReadingOf } from "./effective-path-target.js";
9
9
  import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
10
10
  import { mandateDisclosureOf } from "./ask-origin.js";
11
11
  import { inlineUntrusted } from "./untrusted-text.js";
@@ -67,7 +67,7 @@ function readAllowEntry(raw, behavior, roster) {
67
67
  return row({ kind: "namespace", pattern: name });
68
68
  return row({ kind: "tool", name });
69
69
  }
70
- return row(COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name });
70
+ return row(isShellFaceToolName(name) ? { kind: "shell", name } : { kind: "tool", name });
71
71
  }
72
72
  if (name === COMMAND_RULE_TOOL) {
73
73
  const allowRead = parseRuleText(raw, behavior);
@@ -146,7 +146,7 @@ export function autoStripped(compiled, call) {
146
146
  case "shell":
147
147
  return true;
148
148
  case "tool":
149
- return call !== undefined && answersTo(call, (n) => COARSE_SHELL_TOOLS.includes(n));
149
+ return call !== undefined && answersTo(call, isShellFaceToolName);
150
150
  case "command":
151
151
  return shellContentStripped(shellRuleContent(compiled.rule));
152
152
  case "namespace":
@@ -162,6 +162,7 @@ export function autoStripped(compiled, call) {
162
162
  export function autoStrippedRule(rule, call) {
163
163
  return rule.behavior === "allow" && rule.tool === COMMAND_RULE_TOOL && autoStripped({ kind: "command", rule }, call);
164
164
  }
165
+ const NO_LEADS = new Map();
165
166
  function nameRowReaches(compiled, req) {
166
167
  const covers = compiled.kind === "namespace" ? (n) => namespacedRuleNameCovers(compiled.pattern, n) : compiled.kind === "tool" || compiled.kind === "shell" ? (n) => n === compiled.name : undefined;
167
168
  return covers !== undefined && answersTo(req, covers);
@@ -203,7 +204,7 @@ export function judgeAllowEntries(rows, req, behavior, ctx) {
203
204
  if (pathRows.length > 0) {
204
205
  const table = pathRows.map((r) => r.compiled.rule);
205
206
  for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === "path")) {
206
- const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, liveCwd, home: ctx.home });
207
+ const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, anchorsOf: () => ctx.anchors, leads: callTargetReadingOf(req)?.leads ?? NO_LEADS });
207
208
  const spoke = answer !== undefined && ("unreadable" in answer ? behavior === "deny" : "behavior" in answer && answer.behavior === behavior);
208
209
  if (spoke) {
209
210
  const rule = answer !== undefined && "rules" in answer ? answer.rules[0]?.rule : undefined;