@sema-agent/core 7.42.1 → 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 (84) hide show
  1. package/CHANGELOG.md +100 -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 +18 -7
  16. package/dist/core/checkpoint-store.js +6 -3
  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/synthetic-tools.d.ts +3 -2
  59. package/dist/core/runner/synthetic-tools.js +1 -1
  60. package/dist/core/session-policy-store.d.ts +3 -3
  61. package/dist/core/session-policy-store.js +18 -4
  62. package/dist/core/skill-grant-binding.d.ts +44 -0
  63. package/dist/core/skill-grant-binding.js +22 -0
  64. package/dist/core/skill-grant.d.ts +59 -28
  65. package/dist/core/skill-grant.js +96 -23
  66. package/dist/core/task-spec.d.ts +14 -1
  67. package/dist/core/tighten-task-spec.d.ts +2 -0
  68. package/dist/core/tighten-task-spec.js +3 -0
  69. package/dist/core/tool-name-aliases.d.ts +35 -5
  70. package/dist/core/tool-name-aliases.js +29 -4
  71. package/dist/core/tool-policy.d.ts +10 -0
  72. package/dist/core/tool-policy.js +9 -2
  73. package/dist/core/tool-spec.d.ts +7 -0
  74. package/dist/core/wiring-manifest.d.ts +56 -14
  75. package/dist/core/wiring-manifest.js +29 -4
  76. package/dist/index.d.ts +3 -2
  77. package/dist/index.js +2 -2
  78. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  79. package/dist/orchestration/run-workflow-tool.js +1 -1
  80. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  81. package/dist/orchestration/workflow-primitives.js +6 -6
  82. package/package.json +1 -1
  83. package/test/export-surface.snapshot.json +11 -1
  84. package/wire-changes/core@7.43.0.json +985 -0
@@ -1,6 +1,7 @@
1
1
  import { heldBy } from "./tool-name-readings.js";
2
2
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
3
- import { protocolOf } from "./protocol-table.js";
3
+ import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
4
+ import { catalogToolNames } from "./tool-registry.js";
4
5
  import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
5
6
  import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
6
7
  import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
@@ -9,66 +10,95 @@ import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier
9
10
  import { mandateDisclosureOf } from "./ask-origin.js";
10
11
  import { inlineUntrusted } from "./untrusted-text.js";
11
12
  import { deliverEngineNotice } from "./engine-notice.js";
12
- export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]);
13
+ export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
13
14
  function ruleRowOf(parsed) {
14
15
  return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
15
16
  }
16
17
  const TOOL_NAME_SPELLING = /^[^\s()]+$/;
17
- export function compileAllowEntry(raw, behavior) {
18
+ function mountedSpellingOf(name, roster) {
19
+ const folded = name.toLowerCase();
20
+ for (const mounted of roster)
21
+ if (mounted.toLowerCase() === folded)
22
+ return mounted;
23
+ return undefined;
24
+ }
25
+ function toolNameReading(name, roster) {
26
+ const verdict = judgeRuleToolName(name, roster);
27
+ if (verdict.kind !== "current") {
28
+ const currentName = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
29
+ return { inert: "tool_name", detail: { verdict: verdict.kind, ...(currentName !== undefined ? { currentName } : {}) } };
30
+ }
31
+ if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
32
+ return undefined;
33
+ const mounted = mountedSpellingOf(name, roster);
34
+ return mounted === undefined ? undefined : { inert: "near_miss", detail: { currentName: mounted } };
35
+ }
36
+ function readAllowEntry(raw, behavior, roster) {
37
+ const inert = (reason) => ({ inert: reason });
38
+ const row = (compiled) => ({ compiled });
18
39
  if (raw !== raw.trim())
19
- return "grammar";
40
+ return inert("grammar");
20
41
  const parsed = parsePermissionRule(raw);
21
42
  const name = parsed.toolName;
22
43
  if (!TOOL_NAME_SPELLING.test(name))
23
- return "grammar";
44
+ return inert("grammar");
24
45
  if (name === "*")
25
- return "wildcard_all";
46
+ return inert("wildcard_all");
47
+ if (roster !== undefined) {
48
+ const stale = toolNameReading(name, roster);
49
+ if (stale !== undefined)
50
+ return stale;
51
+ }
26
52
  if (parsed.ruleContent === undefined) {
27
- const ns = protocolOf(name);
28
- if (ns !== undefined) {
29
- if (name.slice(ns.prefix.length).split("__")[0]?.includes("*") === true)
30
- return "namespace_wildcard";
53
+ const split = splitNamespacedName(name);
54
+ if (split !== undefined) {
55
+ if (split.peer.includes("*"))
56
+ return inert("namespace_wildcard");
31
57
  if (isNamespacedCoveringRuleName(name))
32
- return { kind: "namespace", pattern: name };
33
- return { kind: "tool", name };
58
+ return row({ kind: "namespace", pattern: name });
59
+ return row({ kind: "tool", name });
34
60
  }
35
- return COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name };
61
+ return row(COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name });
36
62
  }
37
63
  if (name === COMMAND_RULE_TOOL) {
38
64
  const allowRead = parseRuleText(raw, behavior);
39
65
  if ("rule" in allowRead)
40
- return { kind: "command", rule: ruleRowOf(allowRead.rule) };
66
+ return row({ kind: "command", rule: ruleRowOf(allowRead.rule) });
41
67
  if (behavior === "allow" && allowRead.reject.code === "invalid.bare_interpreter_prefix") {
42
68
  const wide = parseRuleText(raw, "deny");
43
69
  if ("rule" in wide)
44
- return { kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) };
70
+ return row({ kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) });
45
71
  }
46
- return "grammar";
72
+ return inert("grammar");
47
73
  }
48
74
  if (ruleToolGrammarOf(name) === "path") {
49
75
  const read = parseRuleText(raw, behavior);
50
76
  if ("rule" in read) {
51
77
  if (read.rule.match === "subpath" && read.rule.tool === READ_RULE_TOOL && behavior === "allow")
52
- return { kind: "read_dir", rule: ruleRowOf(read.rule) };
78
+ return row({ kind: "read_dir", rule: ruleRowOf(read.rule) });
53
79
  if (behavior === "deny")
54
- return { kind: "path", rule: ruleRowOf(read.rule) };
80
+ return row({ kind: "path", rule: ruleRowOf(read.rule) });
55
81
  }
56
- return "path_pattern";
82
+ return inert("path_pattern");
57
83
  }
58
84
  if (/^domain:/.test(parsed.ruleContent))
59
- return "domain";
60
- return "content_unsupported";
85
+ return inert("domain");
86
+ return inert("content_unsupported");
61
87
  }
62
- export function compileAllowList(entries, behavior) {
88
+ export function compileAllowEntry(raw, behavior, roster) {
89
+ const read = readAllowEntry(raw, behavior, roster);
90
+ return "compiled" in read ? read.compiled : read.inert;
91
+ }
92
+ export function compileAllowList(entries, behavior, roster) {
63
93
  const voided = behavior === "allow" && entries.some((e) => compileAllowEntry(e, behavior) === "wildcard_all");
64
94
  const rows = [];
65
95
  const inert = [];
66
96
  for (const entry of entries) {
67
- const compiled = voided ? "wildcard_all" : compileAllowEntry(entry, behavior);
68
- if (typeof compiled === "string")
69
- inert.push({ entry, reason: compiled });
97
+ const read = voided ? { inert: "wildcard_all" } : readAllowEntry(entry, behavior, roster);
98
+ if ("compiled" in read)
99
+ rows.push({ entry, compiled: read.compiled });
70
100
  else
71
- rows.push({ entry, compiled });
101
+ inert.push({ entry, reason: read.inert, ...read.detail });
72
102
  }
73
103
  return { rows, inert };
74
104
  }
@@ -179,11 +209,15 @@ export function allowingRows(rows, req, mode) {
179
209
  }
180
210
  export class AllowEntryTable {
181
211
  held = [];
182
- seed(entries) {
183
- const { rows, inert } = compileAllowList(entries, "allow");
184
- for (const r of rows)
185
- if (!this.held.some((h) => h.entry === r.entry))
186
- this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
212
+ heldEntries = new Set();
213
+ seed(entries, roster) {
214
+ const { rows, inert } = compileAllowList(entries, "allow", roster);
215
+ for (const r of rows) {
216
+ if (this.heldEntries.has(r.entry))
217
+ continue;
218
+ this.heldEntries.add(r.entry);
219
+ this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
220
+ }
187
221
  return inert;
188
222
  }
189
223
  entries() {
@@ -198,11 +232,15 @@ export class AllowEntryTable {
198
232
  };
199
233
  }
200
234
  }
201
- export function seedAllowEntryTables(tables, lists) {
235
+ export function seedAllowEntryTables(tables, lists, roster) {
202
236
  tables.request.seed(lists.inherited ?? []);
203
- const requestInert = tables.request.seed(lists.request ?? []);
204
- tables.deployment.seed(lists.deployment ?? []);
205
- return [...lists.skillInert.map((e) => ({ source: "skill", ...e })), ...requestInert.map((e) => ({ source: "request", ...e }))];
237
+ const requestInert = tables.request.seed(lists.request ?? [], roster);
238
+ const deploymentInert = tables.deployment.seed(lists.deployment ?? [], roster).filter((e) => e.reason === "tool_name" || e.reason === "near_miss");
239
+ return [
240
+ ...lists.skillInert.map((e) => ({ source: "skill", ...e })),
241
+ ...requestInert.map((e) => ({ source: "request", ...e })),
242
+ ...deploymentInert.map((e) => ({ source: "deployment", ...e })),
243
+ ];
206
244
  }
207
245
  export function withDeploymentAllowedTools(deps) {
208
246
  if (deps.allowedTools === undefined)
@@ -276,6 +314,7 @@ const SHADOW_REASON = {
276
314
  rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
277
315
  ask_rule: mandateDisclosureOf(undefined),
278
316
  denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
317
+ classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides",
279
318
  requires_user_interaction: "this tool's server declares that a call must be put to a person",
280
319
  operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
281
320
  shell_floor: mandateDisclosureOf("operator_always"),
@@ -327,14 +366,38 @@ export function cardDisclosureOf(decision, fallbackMessage) {
327
366
  }
328
367
  export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
329
368
  const NOTICE_ENTRIES_SHOWN = 5;
369
+ export function inertEntryPhrase(e) {
370
+ const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
371
+ const why = (() => {
372
+ if (e.reason === "near_miss")
373
+ return `near_miss: the mounted spelling is "${spelling}"`;
374
+ if (e.reason !== "tool_name" || e.verdict === undefined)
375
+ return e.reason;
376
+ switch (e.verdict) {
377
+ case "retired":
378
+ return `tool_name: a retired tool name${spelling !== undefined ? `, now "${spelling}"` : ""}`;
379
+ case "unprefixed_namespace":
380
+ return 'tool_name: a "__" name with no protocol prefix';
381
+ case "unmintable_segment":
382
+ return `tool_name: a peer segment the engine never mints${spelling !== undefined ? `, write "${spelling}"` : ""}`;
383
+ default: {
384
+ const exhaustive = e.verdict;
385
+ return exhaustive;
386
+ }
387
+ }
388
+ })();
389
+ return `${inlineUntrusted(JSON.stringify(e.entry), 122)} (${why})`;
390
+ }
330
391
  export function allowedToolsNotGrantedNotice(inert, sessionId) {
331
392
  const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
332
- const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} "${inlineUntrusted(e.entry, 120)}" (${e.reason})`);
393
+ const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} ${inertEntryPhrase(e)}`);
333
394
  const rest = inert.length - shown.length;
395
+ const refusing = inert.some((e) => "list" in e && e.list === "disallowed-tools");
334
396
  return {
335
397
  code: ALLOWED_TOOL_NOT_GRANTED_CODE,
336
398
  message: `${inert.length} allowed-tools entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
337
- "A list holding `*` voids the list, `mcp__*` names no server, and a write-path or domain entry has no allow reading; name the tools (or `mcp__<server>`) instead.",
399
+ "A list holding `*` voids the list, `mcp__*` names no server, a write-path or domain entry has no allow reading, and a tool name must be spelled as the tool is mounted; name the tools (or `mcp__<server>`) instead." +
400
+ (refusing ? " Such an entry in a disallowed-tools list keeps its Skill from activating: a refusal that reads as nothing is not dropped." : ""),
338
401
  detail: { ...(sessionId !== undefined ? { sessionId } : {}), entries: inert.map((e) => ({ ...e })) },
339
402
  };
340
403
  }
@@ -445,8 +445,11 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
445
445
  * row cell by cell, because the reference implementation judges the declaration ahead of its allow-rule lookup and its bypass release.
446
446
  * `unstated` is the mode's answer for a call nobody stated: a tool whose permission seat is `passthrough` (it states no
447
447
  * permission of its own — a remote peer's tool) reached the end of the fold with an allow no layer stated.
448
+ * `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
449
+ * (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
450
+ * answers it; the classifier is never asked again).
448
451
  */
449
- export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
452
+ export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
450
453
  export type AskClass = (typeof ASK_CLASSES)[number];
451
454
  /** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
452
455
  export declare function isAskClass(v: unknown): v is AskClass;
@@ -549,6 +552,13 @@ export declare const MODE_ARBITRATION: {
549
552
  readonly ruleMayClear: false;
550
553
  readonly hookAskFloor: false;
551
554
  };
555
+ readonly classifier_no_ruling: {
556
+ readonly bypassImmune: true;
557
+ readonly classifierEligible: false;
558
+ readonly sandboxAdmission: false;
559
+ readonly ruleMayClear: false;
560
+ readonly hookAskFloor: false;
561
+ };
552
562
  readonly requires_user_interaction: {
553
563
  readonly bypassImmune: true;
554
564
  readonly classifierEligible: false;
@@ -725,9 +735,10 @@ export declare function classifyAsk(d: Extract<PermissionResult, {
725
735
  * and the ask's prompt as what was requested. The gate appends the caller-side remedy. */
726
736
  export declare function headlessDenialMessage(toolName: string, askMessage: string): string;
727
737
  /** The state of the approver seat a remedy is written for: `absent` — no approver is wired for the leg; `unavailable` — one is
728
- * wired and answered that nobody is reachable for this ask. Absent in the input reads as `absent` (the posture the
729
- * function was written for). */
730
- export type HeadlessApproverSeat = "absent" | "unavailable";
738
+ * wired and answered that nobody is reachable for this ask; `declared_none` — the run declared that nobody on it can be
739
+ * asked (`approvalReach: "none"`), whatever is wired. Absent in the input reads as `absent` (the posture the function was
740
+ * written for). */
741
+ export type HeadlessApproverSeat = "absent" | "unavailable" | "declared_none";
731
742
  /**
732
743
  * The ONE sentence a headless auto-deny (an ask that reached its final deny because no approver is wired) adds
733
744
  * after its refusal: the caller-side change that would have let this call run. Read off the effective mode, the
@@ -741,7 +752,7 @@ export type HeadlessApproverSeat = "absent" | "unavailable";
741
752
  * · any other releasable ask — `bypassPermissions` releases it (disclosed on the call).
742
753
  * Every arm also names the approver, in the clause of the seat's own state ({@link HeadlessApproverSeat}): none wired ⇒
743
754
  * wire one; one wired that answered `unavailable` for this ask ⇒ make a person reachable through it (a wired approver is
744
- * never told to be wired). The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
755
+ * never told to be wired); the run declared nobody can be asked ⇒ run it where a person can be, or park it. The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
745
756
  * they call this function, neither spells it.
746
757
  */
747
758
  export declare function headlessApprovalRemedy(input: {
@@ -177,6 +177,7 @@ export const ASK_CLASSES = Object.freeze([
177
177
  "rule_unreadable",
178
178
  "ask_rule",
179
179
  "denial_limit_fallback",
180
+ "classifier_no_ruling",
180
181
  "requires_user_interaction",
181
182
  "operator_forced",
182
183
  "shell_floor",
@@ -206,6 +207,7 @@ export const MODE_ARBITRATION = {
206
207
  rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
207
208
  ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
208
209
  denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
210
+ classifier_no_ruling: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
209
211
  requires_user_interaction: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
210
212
  operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
211
213
  shell_floor: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
@@ -220,7 +222,7 @@ export const MODE_ARBITRATION = {
220
222
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
221
223
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
222
224
  };
223
- export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
225
+ export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
224
226
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
225
227
  export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
226
228
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
@@ -253,6 +255,8 @@ export function decisionAskClassesOf(d) {
253
255
  out.add("ask_rule");
254
256
  if (d.denialLimitFallback !== undefined)
255
257
  out.add("denial_limit_fallback");
258
+ if (d.classifierNoRuling !== undefined)
259
+ out.add("classifier_no_ruling");
256
260
  if (d.authoredBy === "deployment")
257
261
  out.add("operator_forced");
258
262
  if (d.probeMandated === true)
@@ -333,10 +337,14 @@ export function classifyAsk(d, facts) {
333
337
  return { classes, askClass, row: foldArbitrationRow(classes) };
334
338
  }
335
339
  export function headlessDenialMessage(toolName, askMessage) {
336
- return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (no approver is wired for "${toolName}" — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
340
+ return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
337
341
  }
338
342
  export function headlessApprovalRemedy(input) {
339
- const approver = input.approverSeat === "unavailable" ? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)" : "wire an approver (`onAsk`) to put it to a person";
343
+ const approver = input.approverSeat === "unavailable"
344
+ ? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)"
345
+ : input.approverSeat === "declared_none"
346
+ ? 'run it where a person can be asked — this run declares `approvalReach: "none"` (nobody on it can be asked) — or arm a durable approval park so the ask waits for a person'
347
+ : "wire an approver (`onAsk`) to put it to a person";
340
348
  const c = input.classification;
341
349
  if (input.mode === "acceptEdits" && input.unplacedOptions !== undefined && (c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"))))
342
350
  return `${fileCommandOptionsSentence(input.unplacedOptions.program)} Spell the ${input.unplacedOptions.program} without option words for the mode to answer it, or ${approver}`;
@@ -5,8 +5,9 @@
5
5
  * · `approver_unavailable` — the approver consulted for the call (the run's own seat, or an ancestor's
6
6
  * frozen seat on a delegated child) answered the ROUTING question `unavailable` and no durable park
7
7
  * took the call afterwards (`tool_end.gate.settlement.kind:"approver_unavailable"`);
8
- * · `no_approver_wired` — no approver (or, for a question, no question face) is wired for this run, or
9
- * the seat is the deny posture string: the headless auto-deny (`settlement.kind:"no_approver"`);
8
+ * · `no_approver_wired` — nobody in this run can be asked: no approver (or, for a question, no question face) is
9
+ * wired, the seat is the deny posture string, or the run declared that nobody on it can be asked
10
+ * (`approvalReach: "none"`): the headless auto-deny (`settlement.kind:"no_approver"`);
10
11
  * · `mandate_unreconstructible` — an inherited durable-approval mandate binds the ask and THIS leg has no
11
12
  * park facility to honor it, so the delegated child refuses in-fold rather than let a synchronous layer
12
13
  * resolve what its ancestor bound to a park (an in-fold deny; no settlement rides the frame).
@@ -6,7 +6,7 @@ export function isAskUnresolvableCause(v) {
6
6
  }
7
7
  const CAUSE_CLAUSE = {
8
8
  approver_unavailable: "the approver reported that nobody is reachable for this ask",
9
- no_approver_wired: "no approver (or question face) is wired for this run — the headless posture",
9
+ no_approver_wired: "nobody in this run can be asked to answer it — the headless posture",
10
10
  mandate_unreconstructible: "an inherited durable-approval mandate binds this ask and this leg has no park facility to honor it",
11
11
  };
12
12
  const CAUSE_REMEDY_SEAT = {
@@ -166,11 +166,12 @@ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCaus
166
166
  */
167
167
  export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
168
168
  /**
169
- * The text an ask carries to a person when the classifier could not rule on it and the leg has an approval seat
170
- * (the gate's classifier station and the inherited station): the ask's own text first — the card still says what
171
- * is being asked — then why auto mode is asking at all. One writer for both stations.
169
+ * The text an ask carries to a person when the classifier did not rule on it ({@link rungDidNotRule}) and the leg has an
170
+ * approval seat (the gate's classifier station and the inherited station): the ask's own text first — the card still
171
+ * says what is being asked — then why auto mode is asking at all, read off the same cause the ask's `classifierNoRuling`
172
+ * member carries (a client that does not read the member reads this). One writer for both stations.
172
173
  */
173
- export declare function classifierUnavailableAskMessage(askText: string, cause: AutoModeUnavailableCause): string;
174
+ export declare function classifierNoRulingAskMessage(askText: string, cause: AutoModeFallbackCause): string;
174
175
  /**
175
176
  * The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate
176
177
  * this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict,
@@ -84,7 +84,9 @@ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
84
84
  "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
85
85
  "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
86
86
  }
87
- export function classifierUnavailableAskMessage(askText, cause) {
87
+ export function classifierNoRulingAskMessage(askText, cause) {
88
+ if (cause === "parse_error")
89
+ return `${askText}\n\nThe auto-mode classifier replied without a verdict, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
88
90
  const parenthetical = cause === "timeout" ? " (timed out)" : "";
89
91
  return `${askText}\n\nThe auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
90
92
  }
@@ -816,6 +816,8 @@ export type PendingAction = {
816
816
  * its settlement never reads as the fallback's auto-deny (see `tool_end.autoDenied`). A durable
817
817
  * card renders the counts and the limit; it has no countdown to render. Echo-only. */
818
818
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
819
+ /** (additive) The PARK twin of `AskRequest.classifierNoRuling`, written only as a member of its closed set. Display metadata. */
820
+ classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
819
821
  /** (additive, same precedent) — the PARK twin of `AskRequest.origin`: WHICH AUTHORITY raised the
820
822
  * parked ask, one word from the closed {@link import("./ask-origin.js").AskOrigin} set, the same
821
823
  * word the synchronous card is rendered by (engine-stamped at the gate, never a policy's claim).
@@ -843,6 +845,8 @@ export type PendingAction = {
843
845
  boundInputHash: string;
844
846
  /** A parked `Skill` call's binding to its card — the skill's name and its two lists ({@link import("./skill-grant.js").skillGrantDigest}, judged by {@link import("./skill-grant.js").parkedSkillApprovalRefusal}); only that row carries it. */
845
847
  boundGrantDigest?: string;
848
+ /** The value {@link boundGrantDigest} hashes — the card's twin of `AskRequest.boundGrants`, minted by the same factory; written exactly when the digest is. Display metadata (additive, no version bump): the resume judges the digest alone. */
849
+ boundGrants?: import("./skill-grant-binding.js").BoundSkillGrants;
846
850
  /**
847
851
  * design/388 §2.5 boundary ② (B18; additive — no checkpoint-version bump, the `previewWithheld`
848
852
  * precedent: an optional group an older reader ignores): the IDENTITY of the row the park minted
@@ -1955,6 +1959,12 @@ export interface CheckpointSummary {
1955
1959
  * the row to learn them (`autoDenyAfterMs` is `0` on every row — see the row member). Pure echo;
1956
1960
  * OMITTED when the row carries none. */
1957
1961
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1962
+ /** The row's `PendingAction.tool_approval.classifierNoRuling`, echoed ONLY as a member of its closed set (else omitted). */
1963
+ classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
1964
+ /** The row's `boundGrantDigest`, echoed (a string; else omitted): the hash of {@link boundGrants}. ECHO-ONLY. */
1965
+ boundGrantDigest?: string;
1966
+ /** The row's `boundGrants` — a fresh copy of the skill and its two lists, published only when it hashes to {@link boundGrantDigest} (never a list beside a digest that binds another). Deployment text: render with contextual escaping. ECHO-ONLY. */
1967
+ boundGrants?: import("./skill-grant-binding.js").BoundSkillGrants;
1958
1968
  /** The row's `PendingAction.tool_approval.origin`, projected for the same one-call reason: a card
1959
1969
  * rendered by the ask's origin word must not re-open the row to learn it. Echoed ONLY when the stored
1960
1970
  * value is a member of the closed set ({@link import("./ask-origin.js").isAskOrigin}) — a bad value
@@ -2034,14 +2044,15 @@ export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "g
2034
2044
  * fabricated "open" still needs the LIVE resolution to say open too. */
2035
2045
  | "write_face_damaged" | "skill_grants_damaged" | "constraint_chain_missing";
2036
2046
  /**
2037
- * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the four
2038
- * words prepare's mode, output-style and recorded-mode doors throw, held unchanged so one refusal reads the same on a fresh
2039
- * run (`TaskResult.errorCode`) and on a resume (this member). `config.spec_retired_key`: a retired spec key is present;
2040
- * `config.output_style_invalid`: the output-style seat is malformed; `config.permission_mode_invalid`: a mode word is outside
2041
- * the closed set (the config's `permissionMode`, or a mode the row recorded); `config.shell_gate_value_retired`: the shell
2042
- * floor is a retired doctrine word.
2047
+ * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the seven
2048
+ * words prepare's mode, output-style, approval-reach, invoked-skill and recorded-mode doors throw, held unchanged so one refusal
2049
+ * reads the same on a fresh run (`TaskResult.errorCode`) and on a resume. `config.spec_retired_key`: a retired spec key;
2050
+ * `config.output_style_invalid`: a malformed output-style seat; `config.permission_mode_invalid`: a mode word outside the closed
2051
+ * set (the config's `permissionMode`, or one the row recorded); `config.shell_gate_value_retired`: a retired shell-floor word;
2052
+ * `config.approval_reach_invalid`: an `approvalReach` outside its set; `config.user_invoked_skill_unknown` / `skill_deny_unreadable`:
2053
+ * `userInvokedSkills` names a skill the config does not carry / one whose `disallowed-tools` entry the grammar reads as nothing.
2043
2054
  */
2044
- export declare const CHECKPOINT_CONFIG_REFUSAL_CODES: readonly ["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired"];
2055
+ export declare const CHECKPOINT_CONFIG_REFUSAL_CODES: readonly ["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable"];
2045
2056
  export type CheckpointConfigRefusalCode = (typeof CHECKPOINT_CONFIG_REFUSAL_CODES)[number];
2046
2057
  export declare const isCheckpointConfigRefusalCode: (word: unknown) => word is CheckpointConfigRefusalCode;
2047
2058
  /** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
@@ -2,8 +2,9 @@ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
3
  import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
- import { readDenialLimitFallback } from "./auto-mode.js";
5
+ import { isAutoModeFallbackCause, readDenialLimitFallback } from "./auto-mode.js";
6
6
  import { isAskOrigin, isPersistedRuleMandate } from "./ask-origin.js";
7
+ import { readBoundBinding } from "./skill-grant-binding.js";
7
8
  import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
8
9
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
9
10
  import { redactSecrets } from "./untrusted-egress.js";
@@ -309,12 +310,14 @@ export function summarizeCheckpoint(cp) {
309
310
  ...(() => {
310
311
  const origin = tool?.origin;
311
312
  const mandate = tool?.mandate;
312
- return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isPersistedRuleMandate(mandate) ? { mandate } : {}) };
313
+ const why = tool?.classifierNoRuling;
314
+ return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isPersistedRuleMandate(mandate) ? { mandate } : {}), ...(isAutoModeFallbackCause(why) ? { classifierNoRuling: why } : {}) };
313
315
  })(),
316
+ ...readBoundBinding(tool),
314
317
  ...(hasBidi ? { hasBidiControls: true } : {}),
315
318
  };
316
319
  }
317
- export const CHECKPOINT_CONFIG_REFUSAL_CODES = Object.freeze(["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired"]);
320
+ export const CHECKPOINT_CONFIG_REFUSAL_CODES = Object.freeze(["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable"]);
318
321
  export const isCheckpointConfigRefusalCode = (word) => CHECKPOINT_CONFIG_REFUSAL_CODES.some((w) => w === word);
319
322
  export class CheckpointError extends Error {
320
323
  code;
@@ -28,7 +28,7 @@ import type { TaskSpec } from "./types.js";
28
28
  * The clamp seats, as string literals (the closed set the scan and the wire-manifest reader both parse —
29
29
  * a spread or an identifier here reads as an empty set).
30
30
  */
31
- export declare const CHILD_CLAMP_SEATS: readonly ["handsReadOnly", "interactiveTools", "readFace", "writeFace", "readDenyPatterns"];
31
+ export declare const CHILD_CLAMP_SEATS: readonly ["handsReadOnly", "interactiveTools", "approvalReach", "readFace", "writeFace", "readDenyPatterns"];
32
32
  /** One clamp seat's name. */
33
33
  export type ChildClampSeat = (typeof CHILD_CLAMP_SEATS)[number];
34
34
  /**
@@ -58,6 +58,8 @@ export interface ChildClampSource {
58
58
  handsReadOnly: boolean | undefined;
59
59
  /** The hard-headless clamp: only `false` travels. */
60
60
  interactiveTools: boolean | undefined;
61
+ /** The declaration that nobody on the run can be asked to approve: only `"none"` travels. */
62
+ approvalReach: TaskSpec["approvalReach"] | undefined;
61
63
  /** The read containment face: only `"roots"` travels. */
62
64
  readFace: TaskSpec["readFace"] | undefined;
63
65
  /** The write containment face: only `"roots"` travels. */
@@ -77,6 +79,18 @@ export interface ChildClampSource {
77
79
  * — so a clamp can never end one level deep, and a new seat cannot reach some constructors and not others.
78
80
  */
79
81
  export declare function childClampSeats(source: ChildClampSource): Partial<TaskSpec>;
82
+ /**
83
+ * The interaction posture a leg keeps under the clamps it inherited: `"interactive"` promises a person on the leg, and a
84
+ * leg that inherited the hard-headless clamp (`interactiveTools: false`) or the declaration that nobody can be asked
85
+ * (`approvalReach: "none"`) cannot keep that promise — the clamp is the tightening value, so the posture becomes
86
+ * `"headless"` (stated, so the leg's own resolution does not fall through to a deployment default of `"interactive"`);
87
+ * every other posture is returned as it is. Read where an inherited posture meets inherited clamps: the workflow lane
88
+ * (a baseline's posture beside the host's clamps) and the revival fold (the spawn's posture beside the waker's clamps).
89
+ */
90
+ export declare function postureUnderClamps(posture: "interactive" | "headless" | undefined, clamps: {
91
+ readonly interactiveTools: boolean | undefined;
92
+ readonly approvalReach: TaskSpec["approvalReach"] | undefined;
93
+ }): "interactive" | "headless" | undefined;
80
94
  /**
81
95
  * The OWN posture of an engine-built leg that never asks the person (the observer sidecar, the verifier): no
82
96
  * human-facing tool is mounted AND the leg declares itself headless. Both, always together — the spec's posture
@@ -1,9 +1,9 @@
1
- export const CHILD_CLAMP_SEATS = Object.freeze(["handsReadOnly", "interactiveTools", "readFace", "writeFace", "readDenyPatterns"]);
1
+ export const CHILD_CLAMP_SEATS = Object.freeze(["handsReadOnly", "interactiveTools", "approvalReach", "readFace", "writeFace", "readDenyPatterns"]);
2
2
  export const CHILD_CLAMP_SEATS_NOT_CARRIED = Object.freeze({
3
3
  toolPolicy: "not a clamp a child spec copies: a parent's denies reach its children through the ancestor-constraint chain the child's own prepare folds, not by putting the parent's policy object on the child seat",
4
4
  shellGate: "travels on the inherited-gate lane (`InheritedGate.shellGate`, a trusted internals seat), not on the child spec — a spec-level copy would be a second source for the same clamp",
5
5
  permissionMode: "travels on the inherited-gate lane (`InheritedGate.permissionMode` / `permissionModeCeiling` — the parent's declared word and its LIVE seat): a child with no word of its own inherits that ceiling on every read, and a word it does declare is clamped under it; a spec-level copy would freeze the parent's word at spawn and be a second source for the same clamp",
6
- onAsk: "per-lane by construction: the delegation lane wraps the parent's approver with spawn provenance before it becomes the child's seat, and a lane with no approver must hand down none",
6
+ onAsk: "per-lane by construction: the delegation lane wraps the parent's approver with spawn provenance before it becomes the child's seat, and a lane with no approver must hand down none (a parent that declared nobody can be asked hands down no approver — its frozen seat is empty — and the declaration rides `approvalReach`)",
7
7
  hooks: "a deployment seat, not an inherited clamp — `tightenTaskSpec` refuses to merge two hook sets rather than composing them, so there is no tightening value to carry",
8
8
  excludeTools: "roster control rather than containment: the lanes that copy a parent's roster do so beside these seats, and the workflow lane authors its child roster from the governance baseline instead of the host's",
9
9
  deferTools: "roster control, same reading as `excludeTools` above",
@@ -21,6 +21,7 @@ export function childClampSeats(source) {
21
21
  return {
22
22
  ...(source.handsReadOnly === true ? { handsReadOnly: true } : {}),
23
23
  ...(source.interactiveTools === false ? { interactiveTools: false } : {}),
24
+ ...(source.approvalReach === "none" ? { approvalReach: "none" } : {}),
24
25
  ...(source.readFace === "roots" ? { readFace: "roots" } : {}),
25
26
  ...(source.writeFace === "roots" ? { writeFace: "roots" } : {}),
26
27
  ...(source.readDenyPatterns !== undefined && source.readDenyPatterns.length > 0
@@ -28,6 +29,9 @@ export function childClampSeats(source) {
28
29
  : {}),
29
30
  };
30
31
  }
32
+ export function postureUnderClamps(posture, clamps) {
33
+ return posture === "interactive" && (clamps.interactiveTools === false || clamps.approvalReach === "none") ? "headless" : posture;
34
+ }
31
35
  export function engineHeadlessLegSeats() {
32
36
  return { interactiveTools: false, interactionPosture: "headless" };
33
37
  }