@sema-agent/core 7.42.0 → 7.43.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 (89) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -3
  5. package/dist/agents/teacher.js +1 -1
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/allow-entry.d.ts +50 -31
  8. package/dist/core/allow-entry.js +100 -37
  9. package/dist/core/ask-origin.d.ts +16 -5
  10. package/dist/core/ask-origin.js +11 -3
  11. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  12. package/dist/core/ask-unresolvable-notice.js +1 -1
  13. package/dist/core/auto-mode.d.ts +5 -4
  14. package/dist/core/auto-mode.js +3 -1
  15. package/dist/core/checkpoint-store.d.ts +19 -8
  16. package/dist/core/checkpoint-store.js +8 -4
  17. package/dist/core/child-clamp-seats.d.ts +15 -1
  18. package/dist/core/child-clamp-seats.js +6 -2
  19. package/dist/core/classifier-unavailable-arm.d.ts +42 -23
  20. package/dist/core/classifier-unavailable-arm.js +14 -3
  21. package/dist/core/decision-receipt.js +2 -0
  22. package/dist/core/gate-lanes.js +37 -29
  23. package/dist/core/hooks.d.ts +6 -1
  24. package/dist/core/hooks.js +3 -0
  25. package/dist/core/mode-policy.d.ts +5 -4
  26. package/dist/core/mode-policy.js +4 -0
  27. package/dist/core/permission-rule-consent.d.ts +3 -2
  28. package/dist/core/permission-rule-consent.js +25 -20
  29. package/dist/core/permission-rule-model.d.ts +47 -7
  30. package/dist/core/permission-rule-model.js +80 -6
  31. package/dist/core/permission-rules.d.ts +8 -2
  32. package/dist/core/permission-rules.js +26 -9
  33. package/dist/core/runner/classifier-ladder.d.ts +10 -8
  34. package/dist/core/runner/classifier-ladder.js +3 -1
  35. package/dist/core/runner/denial-limit-arms.d.ts +25 -8
  36. package/dist/core/runner/denial-limit-arms.js +24 -10
  37. package/dist/core/runner/gate-exit.js +2 -1
  38. package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
  39. package/dist/core/runner/permission-rule-lanes.js +8 -2
  40. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  41. package/dist/core/runner/prepare-ask-lane.js +2 -1
  42. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
  43. package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
  44. package/dist/core/runner/prepare-config-doors.d.ts +3 -0
  45. package/dist/core/runner/prepare-config-doors.js +6 -5
  46. package/dist/core/runner/prepare-gate-stations.js +1 -1
  47. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  48. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  49. package/dist/core/runner/prepare-park-ask.d.ts +4 -4
  50. package/dist/core/runner/prepare-park-ask.js +2 -4
  51. package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
  52. package/dist/core/runner/prepare-policy-chain.js +59 -38
  53. package/dist/core/runner/prepare-project-context.js +6 -2
  54. package/dist/core/runner/prepare-task.js +6 -6
  55. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
  56. package/dist/core/runner/prepare-wiring-manifest.js +8 -9
  57. package/dist/core/runner/resume-checkpoint-screen.js +1 -1
  58. package/dist/core/runner/steer-admission.js +2 -1
  59. package/dist/core/runner/stream-steer-verb.js +2 -2
  60. package/dist/core/runner/synthetic-tools.d.ts +3 -2
  61. package/dist/core/runner/synthetic-tools.js +1 -1
  62. package/dist/core/session-policy-store.d.ts +3 -3
  63. package/dist/core/session-policy-store.js +18 -4
  64. package/dist/core/skill-grant-binding.d.ts +44 -0
  65. package/dist/core/skill-grant-binding.js +22 -0
  66. package/dist/core/skill-grant.d.ts +59 -28
  67. package/dist/core/skill-grant.js +96 -23
  68. package/dist/core/task-notification.d.ts +6 -0
  69. package/dist/core/task-notification.js +3 -0
  70. package/dist/core/task-spec.d.ts +14 -1
  71. package/dist/core/tighten-task-spec.d.ts +2 -0
  72. package/dist/core/tighten-task-spec.js +3 -0
  73. package/dist/core/tool-name-aliases.d.ts +35 -5
  74. package/dist/core/tool-name-aliases.js +29 -4
  75. package/dist/core/tool-policy.d.ts +10 -0
  76. package/dist/core/tool-policy.js +9 -2
  77. package/dist/core/tool-spec.d.ts +7 -0
  78. package/dist/core/wiring-manifest.d.ts +56 -14
  79. package/dist/core/wiring-manifest.js +29 -4
  80. package/dist/index.d.ts +4 -3
  81. package/dist/index.js +3 -3
  82. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  83. package/dist/orchestration/run-workflow-tool.js +1 -1
  84. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  85. package/dist/orchestration/workflow-primitives.js +6 -6
  86. package/package.json +1 -1
  87. package/test/export-surface.snapshot.json +13 -1
  88. package/wire-changes/core@7.42.1.json +56 -0
  89. package/wire-changes/core@7.43.0.json +985 -0
@@ -3,25 +3,32 @@ import { parsePermissionRule } from "./permission-rules.js";
3
3
  import { mountedToolPermissionSeat } from "./mode-policy.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { boundInputHashOf } from "./canonical-json.js";
6
- import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, judgeAllowEntries } from "./allow-entry.js";
6
+ import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries } from "./allow-entry.js";
7
7
  export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
8
8
  const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
9
9
  export function skillNamedBy(args, skills) {
10
10
  const named = args?.skill;
11
11
  return typeof named === "string" ? skills.find((s) => s.name === named) : undefined;
12
12
  }
13
- export function skillGrantDigest(skill) {
13
+ function skillGrantBinding(skill) {
14
14
  const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
15
15
  const p = parsePermissionRule(e);
16
16
  return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
17
17
  }))].sort();
18
- return boundInputHashOf({ skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) });
18
+ const boundGrants = { skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) };
19
+ return { boundGrantDigest: boundInputHashOf(boundGrants), boundGrants };
20
+ }
21
+ export function skillGrantDigest(skill) {
22
+ return skillGrantBinding(skill).boundGrantDigest;
19
23
  }
20
- export function skillCallGrantDigest(call, args, skills, skillToolName) {
21
- return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName ? skillGrantDigest(skillNamedBy(args, skills)) : undefined;
24
+ export function skillGrantBindingMembers(call, args, skills, skillToolName) {
25
+ return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : {};
26
+ }
27
+ function isSkillActivationCall(call, skills, skillToolName) {
28
+ return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName;
22
29
  }
23
30
  export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
24
- if (row.boundGrantDigest === undefined && skillCallGrantDigest(row, args, skills, skillToolName) === undefined)
31
+ if (row.boundGrantDigest === undefined && skillGrantBindingMembers(row, args, skills, skillToolName).boundGrantDigest === undefined)
25
32
  return undefined;
26
33
  if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
27
34
  return undefined;
@@ -43,18 +50,18 @@ export function detachSkillLists(skills) {
43
50
  export function declaresSkillLists(skill) {
44
51
  return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
45
52
  }
46
- export function compileSkillGrant(skill, origin) {
53
+ export function compileSkillGrant(skill, origin, roster) {
47
54
  const rows = [];
48
55
  const inert = [];
49
56
  for (const [list, behavior, entries] of [
50
57
  ["allowed-tools", "allow", skill.allowedTools ?? []],
51
58
  ["disallowed-tools", "deny", skill.disallowedTools ?? []],
52
59
  ]) {
53
- const compiled = compileAllowList(entries, behavior);
60
+ const compiled = compileAllowList(entries, behavior, roster);
54
61
  for (const r of compiled.rows)
55
62
  rows.push({ skill: skill.name, origin, behavior, entry: r.entry, compiled: r.compiled });
56
63
  for (const i of compiled.inert)
57
- inert.push({ skill: skill.name, list, entry: i.entry, reason: i.reason });
64
+ inert.push({ skill: skill.name, list, ...i });
58
65
  }
59
66
  return { rows, inert };
60
67
  }
@@ -65,10 +72,15 @@ function judge(rows, req, behavior, ctx) {
65
72
  export function skillDenyDecision(hit) {
66
73
  return {
67
74
  action: "deny",
68
- message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
75
+ message: hit.readsAsNothing !== undefined
76
+ ? skillDenyUnreadable(hit.skill, hit.readsAsNothing, "activation").message
77
+ : `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
69
78
  decisionReason: "skill",
70
79
  };
71
80
  }
81
+ function unreadDenyOf(inert) {
82
+ return inert.filter((i) => i.list === "disallowed-tools");
83
+ }
72
84
  export function skillRowRefusal(lane, req) {
73
85
  const hit = lane?.denying(req);
74
86
  return hit === undefined ? undefined : skillDenyDecision(hit);
@@ -83,21 +95,21 @@ export function skillAllowDecision(hit, updatedInput) {
83
95
  }
84
96
  const CARD_ENTRIES_MAX = 10;
85
97
  const CARD_ENTRY_MAX_CHARS = 120;
86
- export function skillActivationQuestion(skill, mode) {
98
+ export function skillActivationQuestion(skill, mode, roster) {
87
99
  const head = `Execute skill: ${inlineUntrusted(skill.name, 200)}`;
88
- const granting = compileSkillGrant(skill, "model").rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
100
+ const granting = compileSkillGrant(skill, "model", roster).rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
89
101
  if (granting.length === 0)
90
102
  return head;
91
103
  const shown = granting.slice(0, CARD_ENTRIES_MAX).map((r) => inlineUntrusted(r.entry, CARD_ENTRY_MAX_CHARS));
92
104
  const rest = granting.length - shown.length;
93
105
  return `${head}\nApproving also lets these tools be used without asking for the rest of this run: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
94
106
  }
95
- export function skillSeatReader(skills) {
107
+ export function skillSeatReader(skills, mounted) {
96
108
  if (skills.length === 0)
97
109
  return undefined;
98
110
  return (args, mode) => {
99
111
  const skill = skillNamedBy(args, skills);
100
- return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode) } : undefined;
112
+ return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode, mounted?.()) } : undefined;
101
113
  };
102
114
  }
103
115
  export function skillGateWiring(opts) {
@@ -123,27 +135,68 @@ export function seedSkillGrants(table, opts) {
123
135
  for (const name of invoked)
124
136
  table.activate(named(name), "user");
125
137
  }
126
- return opts.skills.flatMap((s) => compileSkillGrant(s, "model").inert);
138
+ const roster = table.mountedNames();
139
+ return opts.skills.flatMap((s) => compileSkillGrant(s, "model", roster).inert);
140
+ }
141
+ export function screenUserInvokedSkills(config) {
142
+ if (config.userInvokedSkills === undefined)
143
+ return;
144
+ try {
145
+ seedSkillGrants(new SkillGrantTable(), { skills: config.skills ?? [], userInvokedSkills: config.userInvokedSkills, inherited: [] });
146
+ }
147
+ catch (e) {
148
+ const code = e?.code;
149
+ if (code === USER_INVOKED_SKILL_UNKNOWN_CODE || code === SKILL_DENY_UNREADABLE_CODE)
150
+ throw e;
151
+ }
127
152
  }
128
153
  export class SkillGrantTable {
129
154
  rowsHeld = [];
155
+ rowKeys = new Set();
156
+ mounted;
157
+ activations;
158
+ bindMountedNames(mounted) {
159
+ this.mounted = mounted;
160
+ }
161
+ bindSkills(skills, skillToolName) {
162
+ this.activations = { skills, skillToolName };
163
+ }
164
+ mountedNames() {
165
+ return this.mounted?.();
166
+ }
130
167
  hold(row) {
131
- if (!this.rowsHeld.some((r) => r.skill === row.skill && r.origin === row.origin && r.behavior === row.behavior && r.entry === row.entry))
132
- this.rowsHeld.push(row);
168
+ const key = JSON.stringify([row.skill, row.origin, row.behavior, row.entry]);
169
+ if (this.rowKeys.has(key))
170
+ return;
171
+ this.rowKeys.add(key);
172
+ this.rowsHeld.push(row);
133
173
  }
134
174
  activate(skill, origin) {
135
- const { rows, inert } = compileSkillGrant(skill, origin);
175
+ const { rows, inert } = compileSkillGrant(skill, origin, this.mountedNames());
176
+ const unreadDeny = unreadDenyOf(inert);
177
+ if (unreadDeny.length > 0)
178
+ throw skillDenyUnreadable(skill.name, unreadDeny, "activation");
136
179
  for (const row of rows)
137
180
  this.hold(row);
138
181
  return inert;
139
182
  }
140
183
  seed(records, origin) {
184
+ const roster = this.mountedNames();
185
+ const rows = [];
186
+ const unreadDeny = [];
141
187
  for (const r of records) {
142
- const compiled = compileAllowEntry(r.entry, r.behavior);
143
- if (typeof compiled === "string")
144
- continue;
145
- this.hold({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled });
188
+ const read = compileAllowList([r.entry], r.behavior, roster);
189
+ const row = read.rows[0];
190
+ if (row !== undefined)
191
+ rows.push({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled: row.compiled });
192
+ else if (r.behavior === "deny")
193
+ unreadDeny.push(...read.inert.map((i) => ({ skill: r.skill, ...i })));
146
194
  }
195
+ const first = unreadDeny[0];
196
+ if (first !== undefined)
197
+ throw skillDenyUnreadable(first.skill, unreadDeny.filter((i) => i.skill === first.skill), "carried");
198
+ for (const row of rows)
199
+ this.hold(row);
147
200
  }
148
201
  records() {
149
202
  return this.rowsHeld.map((r) => ({ skill: r.skill, behavior: r.behavior, entry: r.entry, origin: r.origin }));
@@ -151,9 +204,17 @@ export class SkillGrantTable {
151
204
  lane(ctx) {
152
205
  return {
153
206
  allowing: (req, mode) => judge(allowingRows(this.rowsHeld, req, mode), req, "allow", ctx),
154
- denying: (req) => judge(this.rowsHeld, req, "deny", ctx),
207
+ denying: (req) => judge(this.rowsHeld, req, "deny", ctx) ?? this.activationRefused(req),
155
208
  };
156
209
  }
210
+ activationRefused(req) {
211
+ const at = this.activations;
212
+ if (at === undefined || !isSkillActivationCall(req, at.skills, at.skillToolName))
213
+ return undefined;
214
+ const skill = skillNamedBy(req.args, at.skills);
215
+ const unread = skill === undefined ? [] : unreadDenyOf(compileSkillGrant(skill, "model", this.mountedNames()).inert);
216
+ return skill === undefined || unread.length === 0 ? undefined : { skill: skill.name, entries: unread.map((i) => i.entry), readsAsNothing: unread };
217
+ }
157
218
  }
158
219
  function readCarriedRecords(raw, carrier) {
159
220
  if (raw === undefined)
@@ -190,3 +251,15 @@ export function readFrozenSkillGrants(section) {
190
251
  return readCarriedRecords(section, "frozen");
191
252
  }
192
253
  export const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
254
+ export const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
255
+ function skillDenyUnreadable(skill, entries, site) {
256
+ const listed = entries.map(inertEntryPhrase).join(", ");
257
+ const one = entries.length === 1;
258
+ const name = inlineUntrusted(skill, 120);
259
+ const message = site === "activation"
260
+ ? `Skill "${name}" was not activated: its disallowed-tools entr${one ? `y ${listed} reads` : `ies ${listed} read`} as nothing — a refusal this engine cannot read is not dropped, so the activation does not happen (its instructions are not loaded and nothing it lists is granted). Spell each entry as the tool is mounted, or remove it, in its definition.`
261
+ : `the grant rows carried for Skill "${name}" (a park's frozen rows, or a parent's capture) hold the disallowed-tools entr${one ? `y ${listed}, which reads` : `ies ${listed}, which read`} as nothing for this run — a refusal this engine cannot read is not dropped, so this leg does not start without it. A parked task an earlier release wrote can be finished on a worker of that release; otherwise correct the entry in its disallowed-tools and submit the task again.`;
262
+ const e = new Error(message);
263
+ e.code = SKILL_DENY_UNREADABLE_CODE;
264
+ return e;
265
+ }
@@ -30,6 +30,12 @@ export declare const SYSTEM_INJECTION_PRIORITIES: readonly SystemInjectionPriori
30
30
  * no-subscriber retain path indexes `queues[priority]` and the park path stores it verbatim, so an
31
31
  * unknown value used to travel until something downstream failed on it (or didn't, and lied). */
32
32
  export declare function isSystemInjectionPriority(value: unknown): value is SystemInjectionPriority;
33
+ /** The ONE reading of a steer's priority: a parked row or an accepted input that carries none means `"next"`
34
+ * (the default the live admission has always applied) — so a retry that spells the default and a row parked
35
+ * without it are the same instruction, whichever side normalized. */
36
+ export declare function steerPriorityOf(x: {
37
+ readonly priority?: SystemInjectionPriority;
38
+ }): SystemInjectionPriority;
33
39
  export interface TaskNotificationPayload {
34
40
  task_id: string;
35
41
  /** design/144 §2: `"external"` is CORE-MINTED ONLY — `TaskStream.notify()` casts it onto every
@@ -10,6 +10,9 @@ export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
10
10
  export function isSystemInjectionPriority(value) {
11
11
  return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
12
12
  }
13
+ export function steerPriorityOf(x) {
14
+ return x.priority ?? "next";
15
+ }
13
16
  const TASK_NOTIFICATION_TAGS = [
14
17
  "task-notification",
15
18
  "task-id",
@@ -110,6 +110,18 @@ export interface TaskSpec {
110
110
  * explicit opt-out, which covers both arms.
111
111
  */
112
112
  interactionPosture?: "interactive" | "headless";
113
+ /**
114
+ * The run's declaration that NOBODY on it can be asked to approve a call — read by the approval seat's one resolution
115
+ * ({@link import("./wiring-manifest.js").resolveAskSeat}). `"none"`: an approver callback the spec or the deployment wires
116
+ * is not this run's approver (never called; the manifest reports `ask.form: "declared_none"`); with the deployment's park
117
+ * facility armed an ask parks for a person to answer later (`park_only`), without one it is refused as nobody's. It
118
+ * approves and releases nothing; a blanket `"allow"` / `"deny"` seat stays a policy; under `dontAsk` the mode refuses
119
+ * first. Absent = today's reading. Not checkpointed (a resumed leg holds it only when its taskConfig supplies it again);
120
+ * children inherit it (the clamp seats). `interactionPosture: "interactive"` beside it is refused
121
+ * (`config.interaction_posture`); a value outside the closed set too (`config.approval_reach_invalid`). Independent of
122
+ * {@link interactiveTools} — a content question still goes to `onQuestion`.
123
+ */
124
+ approvalReach?: import("./wiring-manifest.js").ApprovalReach;
113
125
  /**
114
126
  * design/173 §8.2 — the EXPLICIT knob (default off) letting a posture-`"interactive"` run
115
127
  * CONTINUE with the synthetic self-answer instruction after all on BOTH hardened question arms
@@ -562,7 +574,8 @@ export interface TaskSpec {
562
574
  * projections and the deny-narrowing layers still apply regardless).
563
575
  */
564
576
  basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
565
- /** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk`. */
577
+ /** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk` when present.
578
+ * A callback here is set aside when the run declares {@link approvalReach} `"none"`. */
566
579
  onAsk?: import("./tool-policy.js").OnAsk;
567
580
  /** Content-ask seam (design/64 §5): routes an AskUserQuestion tool call to a real human/UI. When set, the
568
581
  * AskUserQuestion tool is mounted (one of the delivery faces in the criterion — see
@@ -13,6 +13,8 @@ export declare class TaskSpecTightenError extends Error {
13
13
  * would drop the base policy entirely.
14
14
  * - **`handsReadOnly`** — `true` is stricter. `base:true` + `override:false` ⇒ **throw** (loosening). Otherwise
15
15
  * the stricter (`true` if either is true) wins.
16
+ * - **`approvalReach`** — `"none"` (nobody can be asked) is the only word; a base declaration survives an override that
17
+ * omits it or writes `undefined`.
16
18
  * - **`shellGate`** — one value (`"always"`, the operator floor). Either side set ⇒ the floor stands; it cannot
17
19
  * be loosened because absence is the only other state and an override cannot unset a base value.
18
20
  * - **`onAsk`** — no safe automatic merge (the approver and the policy must be coordinated). Both set to
@@ -47,6 +47,9 @@ export function tightenTaskSpec(base, overrides) {
47
47
  if (base.handsReadOnly !== undefined || overrides.handsReadOnly !== undefined) {
48
48
  merged.handsReadOnly = base.handsReadOnly === true || overrides.handsReadOnly === true;
49
49
  }
50
+ if (base.approvalReach !== undefined || overrides.approvalReach !== undefined) {
51
+ merged.approvalReach = base.approvalReach === "none" || overrides.approvalReach === "none" ? "none" : (overrides.approvalReach ?? base.approvalReach);
52
+ }
50
53
  if (base.permissionMode !== undefined && overrides.permissionMode !== undefined) {
51
54
  const { effective, clamped } = clampPermissionMode(overrides.permissionMode, base.permissionMode);
52
55
  if (clamped) {
@@ -1,3 +1,4 @@
1
+ import { type ProtocolNamespace } from "./protocol-table.js";
1
2
  /**
2
3
  * RB-476-A stage 2 (ruling 2026-08-02, RB-476 "no compatibility surface survives", replay
3
4
  * resolution included) — the durable-name machinery this module used to hold is RETIRED:
@@ -26,17 +27,23 @@
26
27
  * wider tool: writing it would deny more than the entry did, so it is no current name), or `removed — `.
27
28
  */
28
29
  export declare const RETIRED_TOOL_NAMES: ReadonlyMap<string, string>;
29
- /** The three answers {@link judgeRuleToolName} gives — a closed set. */
30
- export declare const RULE_TOOL_NAME_VERDICTS: readonly ["current", "retired", "unprefixed_namespace"];
30
+ /** The four answers {@link judgeRuleToolName} gives — a closed set. */
31
+ export declare const RULE_TOOL_NAME_VERDICTS: readonly ["current", "retired", "unprefixed_namespace", "unmintable_segment"];
31
32
  /**
32
33
  * Whether a tool name written in a rule is a stale name — one that, against `roster`, names no tool and never can:
33
- * - `current` — not a stale name: the roster mounts it, or it is neither retired nor a prefix-less `__` name. (Whether a
34
- * current name matches anything THIS run is a different question — an unknown name is the prepare audit's advisory.)
34
+ * - `current` — not a stale name: the roster mounts it, or it is none of the three below. (Whether a current name matches
35
+ * anything THIS run is a different question — an unknown name is the prepare audit's advisory.)
35
36
  * - `retired` — a name in {@link RETIRED_TOOL_NAMES} the roster does not mount; `guidance` is that table's line, and
36
37
  * `currentName` the 1:1 successor when there is one (see the module header's grammar).
37
38
  * - `unprefixed_namespace` — carries the `__` namespace separator with no protocol prefix. The roster cannot clear it:
38
39
  * a caller tool name or alias carrying `__` is refused at prepare, every engine tool name is `__`-free, and every
39
40
  * protocol tool is minted with its prefix.
41
+ * - `unmintable_segment` — carries a protocol prefix, but its peer segment (the server / remote agent) is not a spelling
42
+ * the mint produces for any peer: the name is read as written (peers compare exactly), and every mounted tool carries
43
+ * the minted spelling, so the name reaches none of them. `guidance` says so in one line; `currentName` is the whole name
44
+ * rewritten with the spelling the mint produces (`mcp__github.com__*` → `mcp__github_com__*`), absent when the peer
45
+ * segment is empty and there is nothing to rewrite. The tool segment after the peer is never judged: it is a literal
46
+ * tool name or a glob over one, and an unmatched one is the audit's advisory.
40
47
  *
41
48
  * `roster` = the tool names (aliases included) the reader knows are mounted: a run's assembled roster at prepare, the
42
49
  * names a deployment DECLARES it mounts its own tools under at a session-rule store (an empty set when it declares none).
@@ -49,6 +56,29 @@ export type RuleToolNameVerdict = {
49
56
  readonly currentName?: string;
50
57
  } | {
51
58
  readonly kind: "unprefixed_namespace";
59
+ } | {
60
+ readonly kind: "unmintable_segment";
61
+ readonly guidance: string;
62
+ readonly currentName?: string;
52
63
  };
53
- /** See {@link RuleToolNameVerdict}. Pure; the one answer to "is this rule's tool name a retired or prefix-less name". */
64
+ /**
65
+ * A name in ANY protocol namespace split into its owning namespace, its peer segment and its optional tool segment —
66
+ * CC `rP` (:70710) verbatim, re-anchored on the {@link protocolOf} lookup instead of CC's literal `"mcp"` first split element.
67
+ * `undefined` for a name that is not namespaced (no protocol prefix). The peer is the first segment — EMPTY for `mcp____x` and
68
+ * for the bare prefix, which name no peer; the tool segment is the REST of the split rejoined, so a tool whose minted segment
69
+ * itself carries the separator (charset normalization maps `get..issue` → `get__issue`) decomposes back to one tool name, and
70
+ * is absent when the name stops at the peer (`mcp__srv`). The one decomposition: the judgment, the covering matcher and the
71
+ * allow-entry grammar all read a namespaced name through it.
72
+ */
73
+ export declare function splitNamespacedName(name: string): {
74
+ readonly ns: ProtocolNamespace;
75
+ readonly peer: string;
76
+ readonly tool?: string;
77
+ } | undefined;
78
+ /** See {@link RuleToolNameVerdict}. Pure; the one answer to "is this rule's tool name a name that reaches no tool". */
54
79
  export declare function judgeRuleToolName(name: string, roster: ReadonlySet<string>): RuleToolNameVerdict;
80
+ /** The names a run mounts — every tool's primary name and its aliases: the roster {@link judgeRuleToolName} is given. */
81
+ export declare function mountedToolNames(tools: ReadonlyArray<{
82
+ readonly name: string;
83
+ readonly aliases?: readonly string[];
84
+ }>): Set<string>;
@@ -1,4 +1,5 @@
1
1
  import { carriesNamespaceSeparator, protocolOf } from "./protocol-table.js";
2
+ import { mintNamespacePrefix } from "./protocol-naming.js";
2
3
  export const RETIRED_TOOL_NAMES = new Map([
3
4
  ["read_file", 'renamed "Read"'],
4
5
  ["edit_file", 'renamed "Edit"'],
@@ -41,9 +42,29 @@ export const RETIRED_TOOL_NAMES = new Map([
41
42
  ["Recall", "removed — memory is file-based; read the memory directory with the ordinary file tools"],
42
43
  ["recall", "removed — memory is file-based; read the memory directory with the ordinary file tools"],
43
44
  ]);
44
- export const RULE_TOOL_NAME_VERDICTS = Object.freeze(["current", "retired", "unprefixed_namespace"]);
45
+ export const RULE_TOOL_NAME_VERDICTS = Object.freeze(["current", "retired", "unprefixed_namespace", "unmintable_segment"]);
45
46
  const CURRENT = Object.freeze({ kind: "current" });
46
47
  const UNPREFIXED = Object.freeze({ kind: "unprefixed_namespace" });
48
+ const NAMESPACE_SEPARATOR = "__";
49
+ export function splitNamespacedName(name) {
50
+ const ns = protocolOf(name);
51
+ if (ns === undefined)
52
+ return undefined;
53
+ const [peer = "", ...rest] = name.slice(ns.prefix.length).split(NAMESPACE_SEPARATOR);
54
+ return rest.length > 0 ? { ns, peer, tool: rest.join(NAMESPACE_SEPARATOR) } : { ns, peer };
55
+ }
56
+ function unmintableSegmentOf({ ns, peer, tool }) {
57
+ if (peer === "")
58
+ return { kind: "unmintable_segment", guidance: `the ${ns.id} name has an empty peer segment — no tool is minted under it` };
59
+ const minted = mintNamespacePrefix(ns, peer).slice(ns.prefix.length, -NAMESPACE_SEPARATOR.length);
60
+ if (minted === peer)
61
+ return undefined;
62
+ return {
63
+ kind: "unmintable_segment",
64
+ guidance: `the ${ns.id} peer segment ${JSON.stringify(peer)} is not a spelling the engine mints — its tools are named ${ns.prefix}${minted}${NAMESPACE_SEPARATOR}<tool>, and a rule's peer segment must be spelled the way its tools are`,
65
+ currentName: `${ns.prefix}${minted}${tool !== undefined ? `${NAMESPACE_SEPARATOR}${tool}` : ""}`,
66
+ };
67
+ }
47
68
  export function judgeRuleToolName(name, roster) {
48
69
  if (roster.has(name))
49
70
  return CURRENT;
@@ -52,7 +73,11 @@ export function judgeRuleToolName(name, roster) {
52
73
  const currentName = /^renamed "([^"]+)"/.exec(guidance)?.[1];
53
74
  return currentName !== undefined ? { kind: "retired", guidance, currentName } : { kind: "retired", guidance };
54
75
  }
55
- if (carriesNamespaceSeparator(name) && protocolOf(name) === undefined)
56
- return UNPREFIXED;
57
- return CURRENT;
76
+ const split = splitNamespacedName(name);
77
+ if (split === undefined)
78
+ return carriesNamespaceSeparator(name) ? UNPREFIXED : CURRENT;
79
+ return unmintableSegmentOf(split) ?? CURRENT;
80
+ }
81
+ export function mountedToolNames(tools) {
82
+ return new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
58
83
  }
@@ -303,6 +303,9 @@ export type PermissionResult = {
303
303
  * same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
304
304
  * route it would be a window for has not been chosen yet. */
305
305
  denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
306
+ /** WHY a classifier station handed this call to a person: the classification did not rule (`error` / `timeout` — it could
307
+ * not run; `parse_error` — no verdict). Engine-stamped with `requiresRealApproval`; class `classifier_no_ruling`. */
308
+ classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
306
309
  /** The allow layer's DISCLOSURE: the members that MATCHED this call and could not clear the ask (it is a class
307
310
  * no allow rule clears — a mandated, governance, hook, ask-rule or mode question — or the call reached the layer
308
311
  * marked), one record per member ({@link AllowRuleShadow}). ENGINE-STAMPED by the allow layer, overwriting a
@@ -1201,6 +1204,10 @@ export interface AskRequest {
1201
1204
  * mint site added later is covered by construction); optional on the type because a deployment may
1202
1205
  * invoke its approver function directly. */
1203
1206
  readonly boundInputHash?: string;
1207
+ /** A call to the engine's `Skill` tool: the hash of {@link boundGrants} — the digest a parked approval of the same card is judged by, so a live card reconciles against its parked row by `(toolCallId, boundGrantDigest)` and an answer is remembered by "this tool + this binding". Present exactly on that call (whoever asked, however the question was worded), absent on every other card; `message` renders the same lists narrowed to what releases in the leg's mode. Reconciliation metadata, never adjudication input. */
1208
+ readonly boundGrantDigest?: string;
1209
+ /** The value {@link boundGrantDigest} hashes: `{ skill, allowedTools, disallowedTools }` in canonical spelling (de-duplicated, sorted), `skill: null` when no loaded skill is named; a fresh copy per ask; same presence. Display metadata (deployment text — render with contextual escaping). */
1210
+ readonly boundGrants?: import("./skill-grant-binding.js").BoundSkillGrants;
1204
1211
  /**
1205
1212
  * design/179 §4, re-formed by design/375 §3.1 (BREAKING — this seat REPLACES the retired
1206
1213
  * `ruleSuggestions` key, which is never emitted again; the new name is the safe failure posture for
@@ -1396,6 +1403,9 @@ export interface AskRequest {
1396
1403
  * that forwards a decision's member here does not compile, which is the point: the window is the
1397
1404
  * route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
1398
1405
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1406
+ /** The {@link PermissionResult} ask-arm member of the same name (class `classifier_no_ruling`), carried by the four mint
1407
+ * stations; the `message`'s second paragraph says the same for a client that does not read this. */
1408
+ readonly classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
1399
1409
  /** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
1400
1410
  * member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
1401
1411
  * The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
@@ -259,7 +259,7 @@ const ALLOW = Object.freeze({ action: "allow" });
259
259
  const ALLOW_BY_RULE = Object.freeze({ action: "allow", decisionReason: "rule" });
260
260
  const RETIRED_TEXT_FIELD = "reason";
261
261
  export const DECISION_MEMBERS = Object.keys({
262
- action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, allowRuleShadowed: 0, probeReason: 0,
262
+ action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, classifierNoRuling: 0, allowRuleShadowed: 0, probeReason: 0,
263
263
  probeCause: 0, probeMandated: 0, mandated: 0, mandate: 0, probeReadRootCandidate: 0, matchedAskRule: 0, ruleEvidence: 0, segmentCoverage: 0,
264
264
  origin: 0, askClass: 0, classes: 0, guard: 0, ruleUnreadable: 0, authoredBy: 0, foldedAskClasses: 0,
265
265
  });
@@ -551,6 +551,7 @@ export function combinePolicies(...policies) {
551
551
  let probeMandateSeen = false;
552
552
  let mandateSeen = false;
553
553
  let fallbackSeen;
554
+ let noRulingSeen;
554
555
  let authoredBySeen = false;
555
556
  const contributors = [];
556
557
  const settleDeny = (d) => {
@@ -597,6 +598,8 @@ export function combinePolicies(...policies) {
597
598
  mandateSeen = true;
598
599
  if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
599
600
  fallbackSeen = d.denialLimitFallback;
601
+ if (d.action === "ask" && d.classifierNoRuling !== undefined && noRulingSeen === undefined)
602
+ noRulingSeen = d.classifierNoRuling;
600
603
  if (d.action === "ask" && d.authoredBy === "deployment")
601
604
  authoredBySeen = true;
602
605
  if (d.action === "ask")
@@ -610,12 +613,16 @@ export function combinePolicies(...policies) {
610
613
  const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
611
614
  const withMandate = mandateSeen && withMark.mandated !== true ? { ...withMark, mandated: true } : withMark;
612
615
  const withFallback = fallbackSeen !== undefined && withMandate.denialLimitFallback === undefined ? { ...withMandate, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMandate;
613
- const withAuthor = authoredBySeen && withFallback.authoredBy === undefined ? { ...withFallback, authoredBy: "deployment" } : withFallback;
616
+ const withNoRuling = noRulingSeen !== undefined && withFallback.classifierNoRuling === undefined ? { ...withFallback, classifierNoRuling: noRulingSeen, requiresRealApproval: true } : withFallback;
617
+ const withAuthor = authoredBySeen && withNoRuling.authoredBy === undefined ? { ...withNoRuling, authoredBy: "deployment" } : withNoRuling;
614
618
  const folded = new Set(withAuthor.foldedAskClasses ?? []);
615
619
  for (const c of contributors)
616
620
  if (c !== asked)
617
621
  for (const k of decisionAskClassesOf(c))
618
622
  folded.add(k);
623
+ if (withNoRuling !== withMandate)
624
+ for (const k of decisionAskClassesOf(asked))
625
+ folded.add(k);
619
626
  const withClasses = folded.size > 0 ? { ...withAuthor, foldedAskClasses: [...folded] } : withAuthor;
620
627
  const out = merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
621
628
  if (folded.size > 0)
@@ -952,6 +952,13 @@ export interface ToolExecuteContext {
952
952
  * is deliberately NOT inherited).
953
953
  */
954
954
  interactiveTools?: false;
955
+ /**
956
+ * The run's declaration that nobody on it can be asked to approve ({@link TaskSpec.approvalReach} `"none"`),
957
+ * Runner-filled read-only on the same trusted seat, present ONLY when declared. A delegation tool copies it onto every
958
+ * child spec (the clamp carrier): the child's own seat resolution would otherwise fall back to the deployment's live
959
+ * approver, because a declared-none parent hands down no approver of its own ({@link onAsk} is absent on its ctx).
960
+ */
961
+ approvalReach?: "none";
955
962
  /**
956
963
  * Read-only mirror of {@link "./types.js".TaskSpec.oneShot} (same trust tier/seat as
957
964
  * `principal`): this RUN has no later turn for an async background notification to land in (a
@@ -26,6 +26,7 @@ import { type StoreDurability } from "./checkpoint-store.js";
26
26
  import type { WiringManifestHookEntry } from "./hook-seats.js";
27
27
  import type { McpDelivered, McpFailureKind, McpServerLiveness } from "./mcp-failure.js";
28
28
  import type { WriteProtectionTargetView } from "./write-protect.js";
29
+ import type { OnAsk } from "./tool-policy.js";
29
30
  export type { StoreDurability };
30
31
  /** The manifest-face projection of {@link StoreDurability}: `"declared_durable"` (the name says it —
31
32
  * a DECLARATION relayed, not a verification) vs `"process_local"` (declared so, or fail-closed
@@ -37,11 +38,29 @@ export type ManifestDurability = "declared_durable" | "process_local";
37
38
  * child's manifest rides the CHILD's event stream and is deliberately not in the parent-forwarding
38
39
  * whitelist — a host that wants a child's assembly evidence subscribes to the child. */
39
40
  export type WiringLegKind = "root" | "child" | "resume";
40
- /** The shape of the resolved permission-ask seat (`spec.onAsk ?? deps.onAsk`): a live approver
41
- * callback, a blanket `"allow"`/`"deny"` policy setting, or nothing at all. `"deny"` (a deliberate
42
- * policy) and `"absent"` (a degrade to the headless auto-deny default) are DISTINCT on purpose —
43
- * design/173 */
44
- export type AskSeamForm = "callback" | "allow" | "deny" | "absent";
41
+ /** The shape of the resolved permission-ask seat ({@link resolveAskSeat}): a live approver callback, a blanket
42
+ * `"allow"`/`"deny"` policy setting, nothing at all, or `"declared_none"` — the run declared that nobody on it can be
43
+ * asked (`TaskSpec.approvalReach: "none"`), so a callback beside the declaration is not this run's approver. `"deny"` (a
44
+ * deliberate policy) and `"absent"` (no seat supplied a value — a degrade to the headless auto-deny default) are DISTINCT
45
+ * on purpose — design/173 — and so are `"absent"` and `"declared_none"`: one says nothing was wired, the other that
46
+ * the run said nobody can answer. */
47
+ export type AskSeamForm = "callback" | "allow" | "deny" | "absent" | "declared_none";
48
+ /**
49
+ * The closed set of a run's approval-reach declaration (`TaskSpec.approvalReach`): `"none"` — nobody on this run can be
50
+ * asked to approve. It removes the one seat that could be a person (a callback); it never approves or releases anything.
51
+ * There is no positive word: "a person is reachable" is the `interactionPosture: "interactive"` promise, which has its own
52
+ * door.
53
+ */
54
+ export declare const APPROVAL_REACH: readonly ["none"];
55
+ export type ApprovalReach = (typeof APPROVAL_REACH)[number];
56
+ /** Membership test for {@link ApprovalReach}. */
57
+ export declare function isApprovalReach(v: unknown): v is ApprovalReach;
58
+ /**
59
+ * The declaration's door: absent passes as absent, a member passes, anything else present is refused loudly
60
+ * (`config.approval_reach_invalid`) — a declaration nobody can read must not fold to "a person may be asked" (or to
61
+ * "nobody may"). Prepare, the static half and a resume's pre-claim screen call this one function.
62
+ */
63
+ export declare function screenApprovalReach(v: unknown): ApprovalReach | undefined;
45
64
  /** What a policy `ask` actually reaches under this assembly. `"unresolved"` = not computable from
46
65
  * the facts at hand (the static half's park-dependent arm). */
47
66
  export type AskEffective = "human_reachable" | "auto_allow" | "auto_deny" | "park_only" | "unresolved";
@@ -178,8 +197,8 @@ export interface WiringManifest {
178
197
  peerPresence?: import("../agents/peer-directory.js").PeerPresence;
179
198
  ask: {
180
199
  form: AskSeamForm;
181
- /** Absent iff `form === "absent"` (no seat supplied a value — a fabricated provenance would be
182
- * a false fact). */
200
+ /** Absent iff no seat supplied a value (`"absent"`, or `"declared_none"` with nothing wired) — a fabricated provenance
201
+ * would be a false fact. */
183
202
  provenance?: SeamProvenance;
184
203
  /** OMITTED on the static half (design/173 §8.4): the absent-form arm depends on the park lane,
185
204
  * which the static half cannot always resolve. Always present on the effective half. */
@@ -556,7 +575,7 @@ export interface WiringFacts {
556
575
  * single-source shapes, no parallel hand-copied interface). */
557
576
  export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore" | "permissionRuleSyncWired">;
558
577
  /** Named view of the spec seats the static half reads (a `Pick` of the real {@link TaskSpec}). */
559
- export type StaticWiringSpec = Pick<TaskSpec, "onAsk" | "onQuestion" | "checkpointStore" | "durableApproval" | "mcp" | "interactiveTools" | "interactionPosture">;
578
+ export type StaticWiringSpec = Pick<TaskSpec, "onAsk" | "approvalReach" | "onQuestion" | "checkpointStore" | "durableApproval" | "mcp" | "interactiveTools" | "interactionPosture">;
560
579
  /**
561
580
  * Read a store's {@link StoreDurability} declaration: absent ⇒ `"process-local"` (fail-closed —
562
581
  * under-promise, never over-promise), a declared value passes through, and an UNPARSEABLE value is
@@ -613,8 +632,9 @@ export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean,
613
632
  * so on an armed park lane the ask parks and the honest report is `park_only` — reporting the
614
633
  * blanket as what the ask "reaches" would be a false fact of exactly the kind this module refuses.
615
634
  * Off the park lane the blanket is what happens: the deliberate auto policies;
616
- * - no seat at all ⇒ the park lane decides: armed ⇒ `park_only`, unresolved ⇒ `unresolved`,
617
- * else the headless degrade `auto_deny`.
635
+ * - no seat at all, or a run that declared nobody can be asked (`declared_none`) ⇒ the park lane decides: armed ⇒
636
+ * `park_only` (the deployment's park is its own statement that an answer can wait for a person), unresolved ⇒
637
+ * `unresolved`, else the headless degrade `auto_deny`.
618
638
  */
619
639
  export declare function deriveAskEffective(form: AskSeamForm, parkEffective: boolean | "unresolved", mode?: PermissionMode): AskEffective;
620
640
  /**
@@ -637,10 +657,32 @@ export declare function askReachesPerson(ask: {
637
657
  readonly effective?: AskEffective;
638
658
  }): boolean;
639
659
  export declare function deriveWiringManifest(facts: WiringFacts): WiringManifest;
640
- /** The one ask-seat resolution (`spec` wins over `deps`), shared with the effective half so the
641
- * form/provenance read cannot fork. Junk values (a JS caller passing something outside the OnAsk
642
- * union) are refused loudly — a manifest must not classify what it cannot name. */
643
- export declare function resolveAskSeamForm(spec: Pick<TaskSpec, "onAsk">, deps: Pick<RunnerDeps, "onAsk">): {
660
+ /** A run's approval seat as its one resolution froze it: the value every reader that needs the approver itself reads
661
+ * (`undefined` when nothing on this run can be asked), the form the manifest reports and every reader of the word derives
662
+ * from, the seat that supplied the value, and the declaration when the run made one. */
663
+ export interface AskSeat {
664
+ readonly value: OnAsk | undefined;
665
+ readonly form: AskSeamForm;
666
+ /** Absent iff no seat supplied a value. A callback the declaration set aside still names its seat. */
667
+ readonly provenance?: SeamProvenance;
668
+ readonly reach?: ApprovalReach;
669
+ }
670
+ /**
671
+ * THE approval-seat resolution — the one place a run's `spec.onAsk` / `deps.onAsk` are read. Prepare calls it once, at
672
+ * its config doors (before any resource is taken), and hands the frozen result down; the static half calls it for its
673
+ * own self-check. The seat is the spec's when the spec's key is PRESENT, else the deployment's (presence, not `??`: a
674
+ * spec `null` is a junk value, never a fall-through to the deployment). A value outside the `OnAsk` union is refused
675
+ * loudly (`config.interaction_wiring`), a declaration outside {@link APPROVAL_REACH} too (`config.approval_reach_invalid`).
676
+ *
677
+ * `approvalReach: "none"` is one sentence here: a blanket `"allow"` / `"deny"` stays what it is (a policy setting never
678
+ * claimed to be a person); a callback, or no seat, becomes `declared_none` with NO value — so the readers of the value
679
+ * (the approver lane, the park's sync-first decline, the durable-mandate source, a child's inherited seat) and the readers
680
+ * of the word ({@link deriveAskEffective}) cannot disagree about whether a person is there.
681
+ */
682
+ export declare function resolveAskSeat(spec: Pick<TaskSpec, "onAsk" | "approvalReach">, deps: Pick<RunnerDeps, "onAsk">): AskSeat;
683
+ /** The form and provenance half of {@link resolveAskSeat} — the projection the manifest's two halves and a server's
684
+ * per-leg judgment read (hand it the request's `approvalReach` beside the seat). */
685
+ export declare function resolveAskSeamForm(spec: Pick<TaskSpec, "onAsk" | "approvalReach">, deps: Pick<RunnerDeps, "onAsk">): {
644
686
  form: AskSeamForm;
645
687
  provenance?: SeamProvenance;
646
688
  };