@sema-agent/core 7.41.0 → 7.42.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 (79) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/agents/subagent.js +2 -0
  3. package/dist/core/agent-definition.d.ts +1 -1
  4. package/dist/core/allow-entry.d.ts +241 -0
  5. package/dist/core/allow-entry.js +340 -0
  6. package/dist/core/allow-rule-shadow.d.ts +18 -0
  7. package/dist/core/allow-rule-shadow.js +1 -0
  8. package/dist/core/ask-origin.d.ts +5 -5
  9. package/dist/core/ask-origin.js +1 -1
  10. package/dist/core/auto-mode.d.ts +3 -2
  11. package/dist/core/checkpoint-store.d.ts +11 -19
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/decision-receipt.d.ts +4 -2
  14. package/dist/core/decision-receipt.js +1 -1
  15. package/dist/core/engine-notice.d.ts +8 -5
  16. package/dist/core/gate-fold.js +1 -1
  17. package/dist/core/gate-lanes.d.ts +1 -49
  18. package/dist/core/gate-lanes.js +85 -223
  19. package/dist/core/governance-codes.d.ts +3 -3
  20. package/dist/core/governance-codes.js +3 -3
  21. package/dist/core/hooks.d.ts +13 -10
  22. package/dist/core/hooks.js +5 -3
  23. package/dist/core/mode-policy.d.ts +3 -3
  24. package/dist/core/mode-policy.js +2 -2
  25. package/dist/core/permission-rules.d.ts +24 -23
  26. package/dist/core/permission-rules.js +37 -33
  27. package/dist/core/reminder-disclosure.d.ts +53 -2
  28. package/dist/core/reminder-disclosure.js +49 -36
  29. package/dist/core/rule-lane-reads.d.ts +84 -0
  30. package/dist/core/rule-lane-reads.js +164 -0
  31. package/dist/core/runner/contracts.d.ts +4 -2
  32. package/dist/core/runner/gate-exit.d.ts +3 -3
  33. package/dist/core/runner/image.js +4 -3
  34. package/dist/core/runner/inline-image.d.ts +14 -0
  35. package/dist/core/runner/inline-image.js +26 -0
  36. package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
  37. package/dist/core/runner/permission-rule-lanes.js +1 -1
  38. package/dist/core/runner/prepare-ask-lane.d.ts +0 -19
  39. package/dist/core/runner/prepare-ask-lane.js +2 -2
  40. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  41. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  42. package/dist/core/runner/prepare-park-ask.js +3 -3
  43. package/dist/core/runner/prepare-policy-chain.d.ts +12 -0
  44. package/dist/core/runner/prepare-policy-chain.js +10 -9
  45. package/dist/core/runner/prepare-project-context.d.ts +13 -3
  46. package/dist/core/runner/prepare-project-context.js +5 -4
  47. package/dist/core/runner/prepare-task.js +8 -5
  48. package/dist/core/runner/prepare-turn-wiring.d.ts +16 -3
  49. package/dist/core/runner/prepare-turn-wiring.js +19 -18
  50. package/dist/core/runner/run-leg.js +1 -1
  51. package/dist/core/runner/runtask.js +2 -1
  52. package/dist/core/runner-deps.d.ts +11 -0
  53. package/dist/core/skill-grant.d.ts +8 -47
  54. package/dist/core/skill-grant.js +15 -136
  55. package/dist/core/synthetic-read-verdict.d.ts +108 -0
  56. package/dist/core/synthetic-read-verdict.js +111 -0
  57. package/dist/core/task-spec.d.ts +18 -0
  58. package/dist/core/tool-name-readings.d.ts +1 -1
  59. package/dist/core/tool-name-readings.js +1 -1
  60. package/dist/core/tool-policy.d.ts +25 -24
  61. package/dist/core/tool-policy.js +26 -17
  62. package/dist/core/tool-spec.d.ts +7 -0
  63. package/dist/core/trace.d.ts +16 -0
  64. package/dist/index.d.ts +5 -1
  65. package/dist/index.js +4 -0
  66. package/dist/internal/harness-types.d.ts +1 -1
  67. package/dist/tools/fs/dir-entries.d.ts +69 -0
  68. package/dist/tools/fs/dir-entries.js +60 -0
  69. package/dist/tools/fs/fs-pdf.d.ts +7 -11
  70. package/dist/tools/fs/fs-pdf.js +5 -14
  71. package/dist/tools/fs/fs-read.d.ts +173 -2
  72. package/dist/tools/fs/fs-read.js +399 -286
  73. package/dist/tools/fs/image-admission.d.ts +51 -0
  74. package/dist/tools/fs/image-admission.js +36 -0
  75. package/dist/tools/fs/search.js +3 -7
  76. package/package.json +1 -1
  77. package/test/export-surface.snapshot.json +29 -1
  78. package/wire-changes/core@7.41.1.json +159 -0
  79. package/wire-changes/core@7.42.0.json +1107 -0
@@ -0,0 +1,164 @@
1
+ import { describeThrown } from "./tool-policy.js";
2
+ import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "./permission-rule-org.js";
3
+ import { isRuleBehavior } from "./permission-rule-model.js";
4
+ function isPlainOwnRecord(x) {
5
+ try {
6
+ if (x === null || typeof x !== "object" || Array.isArray(x))
7
+ return false;
8
+ const p = Object.getPrototypeOf(x);
9
+ return p === Object.prototype || p === null;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ function ownDataValue(obj, key) {
16
+ try {
17
+ const d = Object.getOwnPropertyDescriptor(obj, key);
18
+ return d && "value" in d ? d.value : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ export function normalizeOrgGateVerdict(answer, unreadable) {
25
+ const clean = (v) => Object.assign(Object.create(null), v);
26
+ const readable = (x) => {
27
+ try {
28
+ return x !== null && (typeof x === "object" || typeof x === "function") && !Array.isArray(x);
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ };
34
+ if (!readable(answer))
35
+ return clean({ status: "unavailable", disclosures: [unreadable] });
36
+ const status = ownDataValue(answer, "status");
37
+ if (status === "unavailable") {
38
+ const disclosures = ownDataValue(answer, "disclosures");
39
+ let lines = [];
40
+ if (Array.isArray(disclosures)) {
41
+ try {
42
+ for (let i = 0; i < disclosures.length; i++) {
43
+ const d = disclosures[i];
44
+ if (typeof d === "string")
45
+ lines.push(d);
46
+ }
47
+ }
48
+ catch {
49
+ lines = [];
50
+ }
51
+ }
52
+ return clean({ status: "unavailable", disclosures: lines.length > 0 ? lines : [unreadable] });
53
+ }
54
+ if (status !== "available")
55
+ return clean({ status: "unavailable", disclosures: [unreadable] });
56
+ const revision = ownDataValue(answer, "revision");
57
+ const revisionCell = typeof revision === "number" && Number.isFinite(revision) ? { revision } : {};
58
+ const verdict = ownDataValue(answer, "verdict");
59
+ if (verdict === undefined)
60
+ return clean({ status: "available", ...revisionCell });
61
+ if (!readable(verdict))
62
+ return clean({ status: "unavailable", disclosures: [unreadable] });
63
+ const behavior = ownDataValue(verdict, "behavior");
64
+ const rule = ownDataValue(verdict, "rule");
65
+ if ((behavior !== "deny" && behavior !== "ask") || typeof rule !== "string")
66
+ return clean({ status: "unavailable", disclosures: [unreadable] });
67
+ const reason = ownDataValue(verdict, "unreadable");
68
+ const unreadableCell = behavior === "ask" && typeof reason === "string" ? { unreadable: reason } : {};
69
+ return clean({ status: "available", verdict: clean({ behavior: behavior, rule, ...unreadableCell }), ...revisionCell });
70
+ }
71
+ function copiedUsableDots(dots) {
72
+ let copied;
73
+ try {
74
+ if (Array.isArray(dots)) {
75
+ const walked = [];
76
+ for (let i = 0; i < dots.length; i++) {
77
+ const d = dots[i];
78
+ walked.push(isPlainOwnRecord(d) ? { actor: ownDataValue(d, "actor"), counter: ownDataValue(d, "counter") } : { actor: undefined, counter: undefined });
79
+ }
80
+ copied = walked;
81
+ }
82
+ }
83
+ catch {
84
+ copied = undefined;
85
+ }
86
+ const usable = copied?.every((d) => typeof d.actor === "string" && typeof d.counter === "number" && Number.isFinite(d.counter)) === true && copied.length > 0;
87
+ return usable ? { dots: copied } : {};
88
+ }
89
+ export function normalizePersistedRuleHit(hit) {
90
+ if (!isPlainOwnRecord(hit))
91
+ return {};
92
+ if (ownDataValue(hit, "unreadable") === true) {
93
+ const reason = ownDataValue(hit, "reason");
94
+ return typeof reason === "string" ? { unreadable: true, reason } : { unreadable: true };
95
+ }
96
+ const rulesRaw = ownDataValue(hit, "rules");
97
+ if (rulesRaw === undefined) {
98
+ const coverageRaw = ownDataValue(hit, "segmentCoverage");
99
+ let rows;
100
+ try {
101
+ if (Array.isArray(coverageRaw)) {
102
+ const walked = [];
103
+ for (let i = 0; i < coverageRaw.length; i++) {
104
+ const r = coverageRaw[i];
105
+ walked.push(isPlainOwnRecord(r) ? { segment: ownDataValue(r, "segment"), covered: ownDataValue(r, "covered") } : { segment: undefined, covered: undefined });
106
+ }
107
+ rows = walked.every((r) => typeof r.segment === "string" && typeof r.covered === "boolean") ? walked : undefined;
108
+ }
109
+ }
110
+ catch {
111
+ rows = undefined;
112
+ }
113
+ return rows !== undefined && rows.length > 0 ? { coverage: rows.map((r) => ({ segment: r.segment, covered: r.covered })) } : {};
114
+ }
115
+ let members;
116
+ try {
117
+ if (Array.isArray(rulesRaw)) {
118
+ const walked = [];
119
+ for (let i = 0; i < rulesRaw.length; i++) {
120
+ const m = rulesRaw[i];
121
+ walked.push(isPlainOwnRecord(m) ? { rule: ownDataValue(m, "rule"), dots: ownDataValue(m, "dots") } : { rule: undefined, dots: undefined });
122
+ }
123
+ members = walked;
124
+ }
125
+ }
126
+ catch {
127
+ members = undefined;
128
+ }
129
+ if (members === undefined || members.length === 0)
130
+ return {};
131
+ if (!members.every((m) => typeof m.rule === "string"))
132
+ return {};
133
+ const behavior = ownDataValue(hit, "behavior");
134
+ if (!isRuleBehavior(behavior))
135
+ return {};
136
+ return { hit: { behavior, rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
137
+ }
138
+ export async function readOrgRuleLane(org, req, signal, timeoutMs = ORG_ADJUDICATION_TIMEOUT_MS) {
139
+ let pending;
140
+ try {
141
+ pending = Promise.resolve(org.adjudicate(req));
142
+ }
143
+ catch (err) {
144
+ pending = Promise.resolve({ status: "unavailable", disclosures: [`the org adjudication face threw before answering: ${describeThrown(err)}`] });
145
+ }
146
+ const raw = await settleOrgVerdictWithin(pending.catch((err) => ({
147
+ status: "unavailable",
148
+ disclosures: [`the org adjudication face threw: ${describeThrown(err)}`],
149
+ })), {
150
+ status: "unavailable",
151
+ disclosures: [`the org adjudication face did not answer within ${timeoutMs}ms (or the task ended first)`],
152
+ }, { ...(signal !== undefined ? { signal } : {}), timeoutMs });
153
+ return normalizeOrgGateVerdict(raw, "the org adjudication face answered something this gate cannot read as a verdict — treated as unreadable governance, never as an empty one");
154
+ }
155
+ export async function readPersistedRuleLane(lane, req, signal, timeoutMs = ORG_ADJUDICATION_TIMEOUT_MS) {
156
+ let pendingHit;
157
+ try {
158
+ pendingHit = Promise.resolve(lane?.adjudicate(req));
159
+ }
160
+ catch {
161
+ return { unreadable: true };
162
+ }
163
+ return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(signal !== undefined ? { signal } : {}), timeoutMs });
164
+ }
@@ -239,8 +239,8 @@ export type StopForDenialLimit = (info: {
239
239
  fallback: DenialLimitFallbackFace;
240
240
  settlement?: SettlementKind;
241
241
  }) => void;
242
- /** The gate's seat inputs for one call (the static seat or the `Skill` tool's per-call reader, and the run's skill rows). */
243
- export type GateSeatInputsFor = (calledName: string, wireName: string) => import("../mode-policy.js").PermissionSeatInputs & SkillGateSeats;
242
+ /** The gate's seat inputs for one call (the static seat or the `Skill` tool's per-call reader, the run's skill and allow-list rows). */
243
+ export type GateSeatInputsFor = (calledName: string, wireName: string) => import("../mode-policy.js").PermissionSeatInputs & SkillGateSeats & import("../allow-entry.js").AllowEntryGateSeats;
244
244
  export interface Prepared {
245
245
  harness: AgentHarness;
246
246
  /** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
@@ -1810,6 +1810,8 @@ export interface RunInternals {
1810
1810
  * seeding nothing (it carries refusals too). Absent or empty ⇒ nothing inherited.
1811
1811
  */
1812
1812
  inheritedSkillGrants?: readonly SkillGrantRecord[];
1813
+ /** The parent's request allow entries at the spawn (`ctx.activeRequestAllowedTools()`), seeded as this child's own request rows until it ends — the channel of {@link inheritedSkillGrants}. */
1814
+ inheritedAllowedTools?: readonly string[];
1813
1815
  /**
1814
1816
  * When this task runs as a SUB-AGENT spawned under a parent task's
1815
1817
  * tool call, the spawning tool's `ToolExecuteContext.toolCallId`. The Runner stamps it onto this task's
@@ -218,9 +218,9 @@ export interface GatePass {
218
218
  hookAsk: Extract<PermissionResult, {
219
219
  action: "ask";
220
220
  }> | undefined;
221
- /** borrowed-mutable — the phase-1 hook answered its own `allow` (the exact word): a statement about this call, read by
222
- * the fold's last station only while the policy left the arguments as the hook saw them. Writer: the dispatch (phase 1).
223
- * Reader: the fold's unstated station. */
221
+ /** borrowed-mutable — the phase-1 hook answered its own `allow` (the exact word): the allow layer's hook member, which
222
+ * speaks only while no policy rewrote the arguments after the hook saw them. Writer: the dispatch (phase 1). Reader: the
223
+ * lanes' allow layer (its one reader). */
224
224
  hookAllowed?: true;
225
225
  /** borrowed-mutable — set by a park attempt that FAILED (see `ParkAttemptFailed`). Read at the single deny
226
226
  * exit — whatever the fallback chain then decides to say, it says it knowing the park was tried first.
@@ -1,8 +1,9 @@
1
1
  import { isIP } from "node:net";
2
2
  import { imageMagicMatches } from "../../tools/fs/safety.js";
3
- const KNOWN_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
3
+ import { ADMITTED_IMAGE_MIME_TYPES } from "../../tools/fs/image-admission.js";
4
+ import { admitInlineImage } from "./inline-image.js";
4
5
  function sniffImageMime(bytes) {
5
- return KNOWN_IMAGE_MIME_TYPES.find((mime) => imageMagicMatches(bytes, mime));
6
+ return ADMITTED_IMAGE_MIME_TYPES.find((mime) => imageMagicMatches(bytes, mime));
6
7
  }
7
8
  function isPrivateV4(ip) {
8
9
  const o = ip.split(".").map(Number);
@@ -125,5 +126,5 @@ export async function toImageContent(img, allowUrl) {
125
126
  const { buf, mimeType } = await fetchImageCapped(img.url);
126
127
  return { type: "image", data: buf.toString("base64"), mimeType };
127
128
  }
128
- return { type: "image", data: img.data, mimeType: img.mimeType };
129
+ return admitInlineImage(img);
129
130
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `TaskSpec.images` INLINE arm's admission: the same byte admission a file the Read tool serves passes
3
+ * (image-admission.ts) — a claimed type the API accepts, within the raw cap, that type's signature, under
4
+ * the provider's base64 limit. Without it a mis-declared image rode straight to the provider and 400'd the whole
5
+ * request. Like the Read lane it checks the SIGNATURE, not decodability: a file cut short after its signature
6
+ * passes (KL-1311a-c). No downsampling here: this arm only refuses what would not survive the wire, and a
7
+ * valid image reaches the model unchanged. The block is built from the bytes that were checked.
8
+ */
9
+ import type { ImageContent } from "../../internal/llm.js";
10
+ import type { ImageInput } from "../types.js";
11
+ /** Admit one inline entry (`{ data, mimeType }`) or throw `config.image_invalid`. */
12
+ export declare function admitInlineImage(img: Extract<ImageInput, {
13
+ data: string;
14
+ }>): Promise<ImageContent>;
@@ -0,0 +1,26 @@
1
+ import { admitImageBytes, ADMITTED_IMAGE_MIME_TYPES } from "../../tools/fs/image-admission.js";
2
+ import { MCP_IMAGE_MAX_BASE64 } from "../image-downsample.js";
3
+ function inlineImageRefusal(detail) {
4
+ const e = new Error(`TaskSpec.images: ${detail}`);
5
+ e.code = "config.image_invalid";
6
+ return e;
7
+ }
8
+ export async function admitInlineImage(img) {
9
+ const data = img.data;
10
+ const mimeType = img.mimeType;
11
+ if (typeof data !== "string" || typeof mimeType !== "string")
12
+ throw inlineImageRefusal("an inline image needs a base64 string `data` and a string `mimeType`.");
13
+ const admitted = await admitImageBytes(Buffer.from(data, "base64"), mimeType, undefined);
14
+ if (admitted.ok)
15
+ return admitted.block;
16
+ switch (admitted.reason) {
17
+ case "mime_unsupported":
18
+ throw inlineImageRefusal(`an inline image declares mimeType ${JSON.stringify(mimeType.slice(0, 80))}, which the model API does not accept (accepted: ${ADMITTED_IMAGE_MIME_TYPES.join(", ")}).`);
19
+ case "magic_mismatch":
20
+ throw inlineImageRefusal(`an inline image's bytes do not begin with a ${mimeType} signature (empty, mis-declared, or not base64) — refused rather than sent.`);
21
+ case "raw_too_large":
22
+ throw inlineImageRefusal(`an inline image is ${admitted.rawBytes} bytes, over the ${admitted.cap}-byte cap.`);
23
+ case "base64_too_large":
24
+ throw inlineImageRefusal(`an inline image's base64 is ${admitted.base64.length} chars, over the ${MCP_IMAGE_MAX_BASE64}-char / 5MB API image limit — shrink it before sending.`);
25
+ }
26
+ }
@@ -175,6 +175,8 @@ export declare function createPermissionRuleLanes(cfg: {
175
175
  onStoreUnreadable: (at: "call", sources?: EffectivePermissionRules["unreadableSources"]) => void;
176
176
  /** The run's cut: a read it interrupts is abandoned quietly — a cancelled wait is no fact about the store. */
177
177
  signal?: AbortSignal;
178
+ /** The allow layer's auto strip over a stored ALLOW row (true ⇒ out of this call's table BEFORE the coverage is chosen). */
179
+ stripsAllow?: (rule: PersistedRule, req: ToolCallRequest) => boolean;
178
180
  }): PermissionRuleLanes;
179
181
  /** The closed reason set for the rule-offer factory's empty answer — the same three
180
182
  * spellings the `AskRequest` and durable-row seats declare, kept at the one factory that fills both.
@@ -229,7 +231,7 @@ interface RuleOffersAskFacts extends RuleClearanceFacts {
229
231
  * `decision.matchedAskRule === undefined` conjunct on its allow arm), so a person who wrote a
230
232
  * standing `Bash` `ask` rule and had no allow rule yet was still offered "allow, and stop asking
231
233
  * me this" — and the rule they minted came back on the very next call as a SHADOWED match
232
- * (`persistedRuleShadowed`, which this door was already refusing). The offer was only ever
234
+ * (the allow layer's disclosure, which this door was already refusing). The offer was only ever
233
235
  * suppressed one call too late, after the useless rule existed. The shadowed conjunct stays: it
234
236
  * covers the person who ALREADY has the allow rule; this one covers the person about to mint it.
235
237
  *
@@ -142,7 +142,7 @@ export function createPermissionRuleLanes(cfg) {
142
142
  if (view.unreadable !== undefined)
143
143
  skipped(view.unreadable, view.unreadableSources);
144
144
  try {
145
- const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(view.rules, { ...req, toolName: n }, grammar)));
145
+ const answer = strongestAnswer(judged.map(([n, grammar]) => judgeAgainst(view.rules.filter((r) => r.behavior !== "allow" || cfg.stripsAllow?.(r, req) !== true), { ...req, toolName: n }, grammar)));
146
146
  return view.unreadable !== undefined && (answer === undefined || "segmentCoverage" in answer) ? { unreadable: true } : answer;
147
147
  }
148
148
  catch (err) {
@@ -1,22 +1,3 @@
1
- /**
2
- * PrepareTask's IN-STREAM ASK LANE, verbatim from the driver's gate-machinery block:
3
- * the per-call signal composition bound to this task's run signal, the abort-bound `adjudicate` wrapper (the budget
4
- * snapshot and the live tracked cwd stamped onto every request), the approval-preview projection over the live roster,
5
- * the `resolveAskBound` mint (the inherited-unavailable intercept, the duplicate-frame grant reuse, the human-review
6
- * ledger, the bare-human-rejection record), the pre-wrapped deny observer and the hook-crash notifier. The three
7
- * module-scope helpers only this segment called moved with it (the deny-observer factory, the preview sanitizer and
8
- * resolver). The interface is the dependency list the segment had implicitly — the closures below
9
- * used to capture every seat here off the driver's scope.
10
- *
11
- * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (every await lives inside the closures
12
- * it builds, which run per tool call), so the phase adds no yield between the wiring-manifest station before it and
13
- * the suspend saga after it (a stretch with no await is extracted as a sync function). Read-stability
14
- * of the host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
15
- *
16
- * The lane exists exactly when the wiring-manifest phase's `gateMachineryActive` holds — the driver's `if` around the
17
- * whole block became this phase's one early return (the block's other four phases key off the lane's presence), so
18
- * the orchestrator grows no branch.
19
- */
20
1
  import type { AgentTool } from "../../internal/harness.js";
21
2
  import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
22
3
  import type { AutoModeDenialTracker } from "../auto-mode.js";
@@ -1,3 +1,4 @@
1
+ import { cardDisclosureOf } from "../allow-entry.js";
1
2
  import { primaryActivityArg } from "../arg-summary.js";
2
3
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
3
4
  import { advertisedWritableRootsSeat, isLiveApproverSeat, resolveAsk } from "../tool-policy.js";
@@ -97,12 +98,11 @@ export function prepareAskLane(input) {
97
98
  return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
98
99
  })(),
99
100
  ...ruleOffersOf(req, req.args, decision.action === "ask" ? decision : undefined),
100
- message: decision.message ?? `approval required for "${req.toolName}"`,
101
+ ...cardDisclosureOf(decision, `approval required for "${req.toolName}"`),
101
102
  ...askSourceIdentity(),
102
103
  ...riskAxesOf(req),
103
104
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
104
105
  ...(decision.action === "ask" ? askMandateMember(decision) : {}),
105
- ...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
106
106
  ...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking.current) : {}),
107
107
  ...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
108
108
  ...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
@@ -109,6 +109,8 @@ export interface PrepareInheritedGateInput {
109
109
  };
110
110
  /** borrowed-readonly — the per-task skill grant table (the ctx's `activeSkillGrants` capture and the checkpoint section read it call-time). Not written here. */
111
111
  skillGrants: SkillGrantTable;
112
+ /** borrowed-readonly — the request's allow-entry table (the ctx's `activeRequestAllowedTools` capture reads it call-time). Not written here. */
113
+ requestAllowedTools: Pick<import("../allow-entry.js").AllowEntryTable, "entries">;
112
114
  /** borrowed-readonly — the filtered display-forwarding wrapper (ctx seat; absent ⇒ no key). */
113
115
  forwardEvent: ((e: TaskEvent) => void) | undefined;
114
116
  /** borrowed-readonly — the retain ledger (ctx seat; absent ⇒ no key). */
@@ -250,6 +250,7 @@ export function prepareInheritedGate(input) {
250
250
  ...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
251
251
  ...(centerAdoption?.sourceRevision !== undefined ? { centerSourceRevision: centerAdoption.sourceRevision } : {}),
252
252
  activeSkillGrants: () => skillGrants.records(),
253
+ activeRequestAllowedTools: () => input.requestAllowedTools.entries(),
253
254
  inheritedGateForChildren,
254
255
  delegationProvenanceForChildren,
255
256
  delegationSettlement: delegationSettlementForChildren,
@@ -116,7 +116,7 @@ export function prepareParkAsk(input) {
116
116
  }
117
117
  };
118
118
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
119
- ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, carry) => {
119
+ ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, allowRuleShadowed, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, carry) => {
120
120
  const syncFirstEligible = wireOf("park.syncFirstRoute", req) === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
121
121
  if (syncFirstEligible &&
122
122
  runtimeCaps?.forceDurableGate !== true &&
@@ -238,7 +238,7 @@ export function prepareParkAsk(input) {
238
238
  toolName: req.toolName,
239
239
  args: parkedArgs,
240
240
  safety,
241
- ...(shadowedRule !== undefined ? { shadowedRule } : {}),
241
+ ...(allowRuleShadowed !== undefined ? { allowRuleShadowed } : {}),
242
242
  ...(probeReason !== undefined ? { probeReason } : {}),
243
243
  ...(probeCause !== undefined ? { probeCause } : {}),
244
244
  shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
@@ -283,7 +283,7 @@ export function prepareParkAsk(input) {
283
283
  ...(carry?.mandate !== undefined && !storedShapeMandated ? { mandate: carry.mandate } : {}),
284
284
  ...(carry?.authoredBy === "deployment" ? { authoredBy: "deployment" } : {}),
285
285
  ...(rowRealApproval !== undefined ? { requiresRealApproval: true } : {}),
286
- ...(shadowedRule !== undefined ? { persistedRuleShadowed: shadowedRule } : {}),
286
+ ...(allowRuleShadowed !== undefined ? { allowRuleShadowed } : {}),
287
287
  ...(askDecisionReason !== undefined ? { decisionReason: askDecisionReason } : {}),
288
288
  ...(rowMatchedAskRule !== undefined ? { matchedAskRule: rowMatchedAskRule } : {}),
289
289
  ...(probeMandated === true ? { probeMandated: true } : {}),
@@ -31,6 +31,7 @@ import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect } from "../types.js"
31
31
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
32
32
  import type { ReadDenyMatcher } from "../../tools/fs/read-deny.js";
33
33
  import type { SkillGrantLane, SkillGrantTable } from "../skill-grant.js";
34
+ import { type AllowEntryLane, type AllowEntryTable } from "../allow-entry.js";
34
35
  import type { InheritedGate, IrreversibilityTier, PrepareResume, RunInternals, StopForDenialLimit } from "./contracts.js";
35
36
  import { type Arbitration } from "../ask-origin.js";
36
37
  import { type ModeGateSeats, type PermissionModeController } from "../mode-policy.js";
@@ -121,6 +122,12 @@ export interface PreparePolicyChainInput {
121
122
  /** borrowed-readonly — the run's skill grant table; this phase binds its lane to the call facts the person's rule lane
122
123
  * reads (root, session, live cwd, the execution environment's home). Not written here. */
123
124
  skillGrants: SkillGrantTable;
125
+ /** borrowed-readonly — the request's and the deployment's allow-entry tables (prepare seeded them); this phase binds
126
+ * their lanes to the same call facts. Not written here. */
127
+ allowedToolTables: {
128
+ readonly request: AllowEntryTable;
129
+ readonly deployment: AllowEntryTable;
130
+ };
124
131
  /** borrowed-readonly — the FROZEN approver seat (the pass-through test's approver identity). */
125
132
  frozenOnAsk: OnAsk | undefined;
126
133
  /** borrowed-readonly — the identity of this task's hook-env source (the pass-through test's environment term). */
@@ -173,6 +180,11 @@ export interface PreparePolicyChainResult {
173
180
  denyNarrowingPolicy: ToolPolicy | undefined;
174
181
  /** owned — the skill grant lane (`Prepared.skillGrants`; the gate's skill member and deny rows read it). */
175
182
  skillGrantLane: SkillGrantLane;
183
+ /** owned — the request's and the deployment's allow-entry lanes (the gate's two entry-list members). */
184
+ allowEntryLanes: {
185
+ readonly request: AllowEntryLane;
186
+ readonly deployment: AllowEntryLane;
187
+ };
176
188
  /** owned — the caller-facing policy layers (the wiring manifest reads their COUNT). */
177
189
  policyLayers: ToolPolicy[];
178
190
  /** owned — THE effective policy the gate station adjudicates with (the fold plus its final-form re-check), or undefined. */
@@ -10,6 +10,7 @@ import { askApproverIdentity, carryEngineAttestations, carryPrecedingSettlement,
10
10
  import { judgeRuleToolName } from "../tool-name-aliases.js";
11
11
  import { NAMESPACED_NAME_SHAPES } from "../protocol-table.js";
12
12
  import { emitTrace } from "../trace.js";
13
+ import { autoStrippedRule, cardDisclosureOf } from "../allow-entry.js";
13
14
  import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
14
15
  import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
15
16
  const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
@@ -21,7 +22,7 @@ import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-
21
22
  import { thrownText } from "../thrown-value.js";
22
23
  import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
23
24
  export async function preparePolicyChain(input) {
24
- const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
25
+ const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
25
26
  const ancestorArbitrations = new Map();
26
27
  const arbitrateInherited = (ask, toolCallId, judged) => {
27
28
  const verdict = arbitrateByMode(permissionMode.current, judged.classification, { origin: judged.origin, ...askMandateMember(ancestorFacts(judged.ask.action === "ask" ? judged.ask : undefined)) });
@@ -294,7 +295,9 @@ export async function preparePolicyChain(input) {
294
295
  };
295
296
  };
296
297
  const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
297
- const skillGrantLane = skillGrants.lane({ root: taskRootFinal, sessionId, liveCwd: () => handsCwdRef?.current, home: executionEnv.homeDir });
298
+ const listFacts = { root: taskRootFinal, sessionId, liveCwd: () => handsCwdRef?.current, home: executionEnv.homeDir };
299
+ const skillGrantLane = skillGrants.lane(listFacts);
300
+ const allowEntryLanes = { request: allowedToolTables.request.lane(listFacts), deployment: allowedToolTables.deployment.lane(listFacts) };
298
301
  const { personal: permissionRuleLane, org: permissionRuleOrgLane } = createPermissionRuleLanes({
299
302
  provider: deps.permissionRuleStore,
300
303
  localOwnerDeclared: deps.localOwnerRules === true,
@@ -309,6 +312,7 @@ export async function preparePolicyChain(input) {
309
312
  onDisclosure: (message) => emitTrace(deps.tracer, () => ({ kind: "permission.rule_store_unreadable", version: 1, taskId: hostTaskId, message, ts: Date.now() })),
310
313
  onStoreUnreadable: announceRuleStoreSkipped,
311
314
  signal: abortController.signal,
315
+ stripsAllow: (rule, req) => permissionMode.current === "auto" && autoStrippedRule(rule, req),
312
316
  });
313
317
  const ruleOffersOf = createRuleOffersOf({
314
318
  laneArmed: permissionRuleLane !== undefined,
@@ -383,12 +387,11 @@ export async function preparePolicyChain(input) {
383
387
  toolCallId: creq.toolCallId,
384
388
  args: editArgs,
385
389
  ...ruleOffersOf(creq, editArgs, ancestorFacts(re)),
386
- message: re.message ?? inheritedAskFallbackMessage(creq.toolName),
390
+ ...cardDisclosureOf(re, inheritedAskFallbackMessage(creq.toolName)),
387
391
  ...askSourceIdentity(),
388
392
  ...riskAxesOf(creq),
389
393
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
390
394
  ...askMandateMember(ancestorFacts(re)),
391
- ...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
392
395
  ...inheritedAskCarry(judged, isLiveApproverSeat(onAskOf), ancestorTracker),
393
396
  ruleEvidence: inheritedAskEvidence,
394
397
  }, onAskOf, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
@@ -463,12 +466,11 @@ export async function preparePolicyChain(input) {
463
466
  toolCallId: creq.toolCallId,
464
467
  args: presentedArgs,
465
468
  ...ruleOffersOf(creq, presentedArgs, ancestorFacts(inheritedAsk.action === "ask" ? inheritedAsk : undefined)),
466
- message: inheritedAsk.message ?? inheritedAskFallbackMessage(creq.toolName),
469
+ ...cardDisclosureOf(inheritedAsk, inheritedAskFallbackMessage(creq.toolName)),
467
470
  ...askSourceIdentity(),
468
471
  ...riskAxesOf(creq),
469
472
  ...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
470
473
  ...askMandateMember(ancestorFacts(inheritedAsk.action === "ask" ? inheritedAsk : undefined)),
471
- ...(inheritedAsk.action === "ask" && inheritedAsk.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: inheritedAsk.persistedRuleShadowed } : {}),
472
474
  ...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
473
475
  ruleEvidence: inheritedAskEvidence,
474
476
  }, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
@@ -536,12 +538,11 @@ export async function preparePolicyChain(input) {
536
538
  toolCallId: creq.toolCallId,
537
539
  args: presentedArgs,
538
540
  ...ruleOffersOf(creq, presentedArgs, ancestorFacts(inheritedAsk.action === "ask" ? inheritedAsk : undefined)),
539
- message: inheritedAsk.message ?? inheritedAskFallbackMessage(creq.toolName),
541
+ ...cardDisclosureOf(inheritedAsk, inheritedAskFallbackMessage(creq.toolName)),
540
542
  ...askSourceIdentity(),
541
543
  ...riskAxesOf(creq),
542
544
  ...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
543
545
  ...askMandateMember(ancestorFacts(inheritedAsk.action === "ask" ? inheritedAsk : undefined)),
544
- ...(inheritedAsk.action === "ask" && inheritedAsk.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: inheritedAsk.persistedRuleShadowed } : {}),
545
546
  ...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
546
547
  ruleEvidence: inheritedAskEvidence,
547
548
  }, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
@@ -666,7 +667,7 @@ export async function preparePolicyChain(input) {
666
667
  return decision;
667
668
  })
668
669
  : foldedPolicy;
669
- return { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, skillGrantLane, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations };
670
+ return { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, skillGrantLane, allowEntryLanes, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations };
670
671
  }
671
672
  const advisedPolicyNames = new WeakMap();
672
673
  const ADVISED_KEYS_CAP = 64;
@@ -2,6 +2,7 @@ import type { ResolvedSkillListingBudget } from "../skill-listing-budget.js";
2
2
  import { type StoredSession } from "../session.js";
3
3
  import type { RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
4
4
  import { type SkillGrantTable, type SkillSeatReader } from "../skill-grant.js";
5
+ import { type AllowEntryTable } from "../allow-entry.js";
5
6
  import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, ToolFaceSnapshot } from "./contracts.js";
6
7
  import { normalizeSkills } from "./synthetic-tools.js";
7
8
  import { RosterBuilder } from "../tool-roster.js";
@@ -9,14 +10,23 @@ export interface PrepareProjectContextInput {
9
10
  /** borrowed-readonly — the deployment seams this phase reads: `loadProjectMemory` (the snapshot),
10
11
  * `onError` (the operator lane for a throwing seam / seeding / oversize skill), `sharedMemoryStores`
11
12
  * (the shared-memory pair's switch: wired ⇒ mount). Receiver preserved (`deps.onError?.(…)`). */
12
- deps: Pick<RunnerDeps, "loadProjectMemory" | "onError" | "sharedMemoryStores" | "onNotice">;
13
+ deps: Pick<RunnerDeps, "loadProjectMemory" | "onError" | "sharedMemoryStores" | "onNotice" | "allowedTools">;
13
14
  /** borrowed-readonly — the REBOUND spec (`spec′`). Read: `sessionId` (phase derivation + the
14
15
  * continuation projections), `skills` (the roster), `userInvokedSkills` (the person's invoked skills — their
15
16
  * rows are written here), `principal` (the shared-memory tools' trusted identity). Never mutated. */
16
17
  spec: Pick<TaskSpec, "sessionId" | "skills" | "userInvokedSkills" | "principal">;
17
18
  /** borrowed-readonly — trusted internals: `agentName` (seam ctx), `inheritedSkillGrants` (the parent's captured
18
- * skill grant rows, seeded as `inherited`). `undefined` on a standalone prepare. */
19
- internals: Pick<RunInternals, "agentName" | "inheritedSkillGrants"> | undefined;
19
+ * skill grant rows, seeded as `inherited`), `inheritedAllowedTools` (the parent's request entries, seeded as this leg's
20
+ * own). `undefined` on a standalone prepare. */
21
+ internals: Pick<RunInternals, "agentName" | "inheritedSkillGrants" | "inheritedAllowedTools"> | undefined;
22
+ /** borrowed-readonly — the request's `allowedTools`, screened and snapshotted at the config door (before the first await). */
23
+ allowedTools: readonly string[] | undefined;
24
+ /** borrowed-mutable — the request's and the deployment's allow-entry tables (identity is the contract: the gate's lanes and
25
+ * the delegation capture alias these objects). Seeded here, once; nothing writes them after this phase. */
26
+ allowedToolTables: {
27
+ readonly request: AllowEntryTable;
28
+ readonly deployment: AllowEntryTable;
29
+ };
20
30
  /** borrowed-readonly — the acquired session; only `getBranch()` is read (phase derivation walk).
21
31
  * The read is best-effort: a throw keeps the provisional phase. */
22
32
  session: Pick<StoredSession, "getBranch">;
@@ -5,14 +5,15 @@ import { hasConversationContent } from "../session.js";
5
5
  import { createSharedMemoryTools } from "../shared-memory/tools.js";
6
6
  import { SHARED_MEMORY_TOOL_NAMES } from "../shared-memory/types.js";
7
7
  import { defineTool } from "../tools.js";
8
- import { readFrozenSkillGrants, seedSkillGrants, skillEntriesNotGrantedNotice, skillSeatReader } from "../skill-grant.js";
8
+ import { readFrozenSkillGrants, seedSkillGrants, skillSeatReader } from "../skill-grant.js";
9
+ import { allowedToolsNotGrantedNotice, seedAllowEntryTables } from "../allow-entry.js";
9
10
  import { deliverEngineNotice } from "../engine-notice.js";
10
11
  import { SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
11
12
  import { skillLoadReading } from "./skill-base-dir.js";
12
13
  import { RosterBuilder } from "../tool-roster.js";
13
14
  import { asThrownError } from "../thrown-value.js";
14
15
  export async function prepareProjectContext(input) {
15
- const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillGrants, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume, skillListingBudget } = input;
16
+ const { deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillGrants, allowedTools, allowedToolTables, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume, skillListingBudget } = input;
16
17
  const tools = roster.tools;
17
18
  let memoryBlock = memoryBlockFromEngine;
18
19
  let instructionSources;
@@ -87,9 +88,9 @@ export async function prepareProjectContext(input) {
87
88
  const frozenSkillGrants = readFrozenSkillGrants(resume?.seed.skillGrants);
88
89
  if (frozenSkillGrants === undefined)
89
90
  throw new Error("the resumed checkpoint's skill-grant section does not read as grant rows — refusing to seed the run without the refusals it carries (the resume screen refuses this row before the claim)");
90
- const inert = seedSkillGrants(skillGrants, { skills: skillSpecs, userInvokedSkills: spec.userInvokedSkills, inherited: [...(internals?.inheritedSkillGrants ?? []), ...frozenSkillGrants] });
91
+ const inert = seedAllowEntryTables(allowedToolTables, { skillInert: seedSkillGrants(skillGrants, { skills: skillSpecs, userInvokedSkills: spec.userInvokedSkills, inherited: [...(internals?.inheritedSkillGrants ?? []), ...frozenSkillGrants] }), inherited: internals?.inheritedAllowedTools, request: allowedTools, deployment: deps.allowedTools });
91
92
  if (inert.length > 0)
92
- deliverEngineNotice(deps.onNotice, skillEntriesNotGrantedNotice(inert, sessionId));
93
+ deliverEngineNotice(deps.onNotice, allowedToolsNotGrantedNotice(inert, sessionId));
93
94
  if (skillSpecs.length > 0) {
94
95
  if (tools.some((t) => t.name === SKILL_TOOL_NAME)) {
95
96
  const e = new Error(`Tool name "${SKILL_TOOL_NAME}" is reserved when spec.skills is present.`);