@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
@@ -37,14 +37,29 @@ export function callPathFaceOf(call) {
37
37
  export function faceAskSpeaksFor(face) {
38
38
  return face !== undefined && Object.values(PATH_RULE_FACES).some((columns) => columns.ask?.includes(face) === true);
39
39
  }
40
+ export const NAME_RULE_FACES = Object.freeze({
41
+ Skill: Object.freeze({ param: "skill", params: Object.freeze(["skill", "args"]) }),
42
+ });
40
43
  export function ruleToolGrammarOf(toolName) {
41
44
  if (toolName === COMMAND_RULE_TOOL)
42
45
  return "command";
46
+ if (Object.hasOwn(NAME_RULE_FACES, toolName))
47
+ return "name";
43
48
  return pathTargetOf(toolName) !== undefined ? "path" : undefined;
44
49
  }
50
+ export function nameRuleSubjectOf(toolName, args) {
51
+ const face = Object.hasOwn(NAME_RULE_FACES, toolName) ? NAME_RULE_FACES[toolName] : undefined;
52
+ if (face === undefined || typeof args !== "object" || args === null || Array.isArray(args) || !Object.hasOwn(args, face.param))
53
+ return undefined;
54
+ const value = args[face.param];
55
+ return typeof value === "string" ? value : undefined;
56
+ }
45
57
  export function laneToolNamesOf(req) {
46
58
  return heldValues("policy.persistedGrammar", { get: (n) => (ruleToolGrammarOf(n) !== undefined ? n : undefined) }, req);
47
59
  }
60
+ function isCommandMatch(match) {
61
+ return match === "exact" || match === "prefix" || match === "wildcard";
62
+ }
48
63
  export const MAX_RULE_TEXT_CHARS = 512;
49
64
  export const BARE_INTERPRETER_NAMES = new Set([
50
65
  "node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
@@ -247,8 +262,11 @@ export function parseRuleText(text, behavior) {
247
262
  return reject("invalid.grammar", `"${text}" is not a Tool(content) rule — a bare tool name claims the whole tool and is not a command rule`);
248
263
  }
249
264
  if (parsed.toolName !== COMMAND_RULE_TOOL) {
250
- if (ruleToolGrammarOf(parsed.toolName) !== "path") {
251
- return reject("unsupported.tool", `only Bash command rules and rules on path-targeting tools (Read, Edit, Write, …) have a rule form in this version (got "${parsed.toolName}")`);
265
+ const grammar = ruleToolGrammarOf(parsed.toolName);
266
+ if (grammar === "name")
267
+ return parseNameRuleContent(text, parsed.toolName, parsed.ruleContent, behavior);
268
+ if (grammar !== "path") {
269
+ return reject("unsupported.tool", `only Bash command rules, rules on path-targeting tools (Read, Edit, Write, …) and Skill name rules have a rule form in this version (got "${parsed.toolName}")`);
252
270
  }
253
271
  return parsePathRuleContent(text, parsed.toolName, parsed.ruleContent, behavior);
254
272
  }
@@ -303,9 +321,46 @@ export function parseRuleText(text, behavior) {
303
321
  export function formatRuleText(command, match, tool) {
304
322
  if (match === "subpath")
305
323
  return `${tool}(//${command.startsWith("/") ? command.slice(1) : command}/**)`;
306
- if (match === "path")
324
+ if (match === "path" || match === "name")
307
325
  return `${tool}(${command})`;
308
- return `${tool}(${command}${match === "prefix" ? ":*" : match === "wildcard" ? " *" : ""})`;
326
+ return `${tool}(${command}${match === "prefix" || match === "name_prefix" ? ":*" : match === "wildcard" ? " *" : ""})`;
327
+ }
328
+ function parseNameRuleContent(text, tool, content, behavior, verifyCanonical = true) {
329
+ const face = NAME_RULE_FACES[tool];
330
+ const bare = content.startsWith("/") ? content.slice(1) : content;
331
+ const colon = bare.indexOf(":");
332
+ if (face !== undefined && colon > 0 && face.params.includes(bare.slice(0, colon).trim())) {
333
+ return reject("invalid.grammar", `"${text}" is the parameter spelling, which a ${tool} rule no longer has — a ${tool} rule names the ${face.param} itself: ${tool}(<name>) for one, ${tool}(<name>:*) for every ${face.param} whose name begins with <name>`);
334
+ }
335
+ const open = bare.endsWith(":*") || bare.endsWith(" *");
336
+ const body = open ? bare.slice(0, -2) : bare;
337
+ const match = open ? "name_prefix" : "name";
338
+ if (body === "" || body.trim() !== body) {
339
+ return reject("invalid.grammar", `the name in "${text}" is empty or carries surrounding whitespace — a ${tool} rule names the ${face?.param ?? "name"} exactly as it is spelled, so the spelling must be the one meant`);
340
+ }
341
+ if (body.includes("*")) {
342
+ return reject("unsupported.wildcard", `a \`*\` inside the name of "${text}" matches anything at that position, which has no rule form — only the trailing \`<name>:*\` prefix form does`);
343
+ }
344
+ const canonical = formatRuleText(body, match, tool);
345
+ const back = parsePermissionRule(canonical);
346
+ if (back.toolName !== tool || back.ruleContent !== (open ? `${body}:*` : body)) {
347
+ return reject("invalid.grammar", `the name in "${text}" has no spelling the rule syntax reads back (a backslash or an unbalanced parenthesis) — no ${tool} rule can name it`);
348
+ }
349
+ const rule = { behavior, rule: canonical, tool, match, command: body };
350
+ if (verifyCanonical) {
351
+ const again = parseNameRuleContent(canonical, tool, back.ruleContent, behavior, false);
352
+ if (!("rule" in again) || again.rule.rule !== canonical || again.rule.match !== match || again.rule.command !== body) {
353
+ return reject("invalid.grammar", `"${text}" has no canonical spelling that reads back as itself ("${canonical}" does not) — no ${tool} rule can name it exactly`);
354
+ }
355
+ }
356
+ return { rule };
357
+ }
358
+ export function nameRuleAdmits(rule, name) {
359
+ if (rule.match === "name")
360
+ return name === rule.command;
361
+ if (rule.match === "name_prefix")
362
+ return name.startsWith(rule.command);
363
+ return false;
309
364
  }
310
365
  function parsePathRuleContent(text, tool, content, behavior) {
311
366
  const dirForm = /^\/\/(.+)\/\*\*$/.exec(content);
@@ -508,7 +563,7 @@ export const PROGRAM_RUN_REACHES = Object.freeze(["reached", "not_reached", "unr
508
563
  const REACHED = { reach: "reached" };
509
564
  const NOT_REACHED = { reach: "not_reached" };
510
565
  export function programRunReachOf(rule, command) {
511
- if (rule.match === "subpath" || rule.match === "path")
566
+ if (!isCommandMatch(rule.match))
512
567
  return NOT_REACHED;
513
568
  const body = readRuleBody(rule.command);
514
569
  if (body === undefined)
@@ -594,7 +649,7 @@ function readCommandMemo(command) {
594
649
  return lastRead.shape;
595
650
  }
596
651
  function admitsUnder(rule, command, reading) {
597
- if (rule.match === "subpath" || rule.match === "path")
652
+ if (!isCommandMatch(rule.match))
598
653
  return false;
599
654
  const shape = ruleLaneShapeOf(command, reading);
600
655
  if ("reject" in shape)
@@ -894,6 +949,25 @@ export function adjudicatePersistedPathRules(rules, call, targets, bases) {
894
949
  }
895
950
  return undefined;
896
951
  }
952
+ export function adjudicatePersistedNameRules(rules, call) {
953
+ if (call.name === undefined)
954
+ return undefined;
955
+ for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
956
+ for (const rule of rules) {
957
+ if (eligiblePersisted(rule, call, behavior) && nameRuleAdmits(rule, call.name))
958
+ return { behavior, rules: [rule] };
959
+ }
960
+ }
961
+ return undefined;
962
+ }
963
+ export function suggestRuleForName(tool, name) {
964
+ if (ruleToolGrammarOf(tool) !== "name")
965
+ return undefined;
966
+ const parsed = parseRuleText(formatRuleText(name, "name", tool), "allow");
967
+ if (!("rule" in parsed) || parsed.rule.match !== "name" || parsed.rule.command !== name)
968
+ return undefined;
969
+ return { kind: "single", rule: parsed.rule.rule, match: "name", command: name };
970
+ }
897
971
  export function segmentCoverageOf(command, rules, call) {
898
972
  const folded = foldSpacing(command);
899
973
  if (folded === undefined)
@@ -38,6 +38,11 @@
38
38
  * A `Read(...)` / `Edit(...)` deny or ask reaches the calls on its behavior's FACES (`PATH_RULE_FACES`), not only
39
39
  * its own tool: a `Read` ask asks for a Grep / Glob rooted in its path, judged on that call's own target.
40
40
  *
41
+ * The NAME lane is the same shape for the tools of `NAME_RULE_FACES` (the `Skill` tool): a `Skill(<name>)` / `Skill(<prefix>:*)`
42
+ * deny or ask — the reference's own spelling — compiles through the same validator (the model's name grammar) and is judged on the
43
+ * one string the call names (`nameRuleSubjectOf`) with `nameRuleAdmits`, the reader the persisted rule store uses. The parameter
44
+ * spelling `Skill(skill:<name>)` those tools used to take is retired: refused loudly, its sentence naming the CC form.
45
+ *
41
46
  * 🔴 与 CC 的有意差(design/127 r2):
42
47
  * - CC 参数级 generic 道无 allow lane(gnn 只有 deny/ask)—— 我们同样,且整个 policy 无 allow 臂。
43
48
  * - CC 对尾缀垃圾静默降级为裸名(Lh `n !== len-1`)—— `parsePermissionRule` 保持 CC parity,但
@@ -75,8 +80,9 @@ export interface PermissionRuleIssue {
75
80
  /** A `behavior` outside the closed set `deny | ask` (a JS caller's typo, an undefined member, a boxed string): refused —
76
81
  * a rule that matched nothing because its verb was misspelled would be a deny that silently never fires. */
77
82
  | "invalid.behavior"
78
- /** A content-form Bash deny/ask rule the ONE rule validator refuses (a redirection, a substitution, a
79
- * mid-pattern star, control characters…) — the message carries the validator's own code and sentence. */
83
+ /** A content-form deny/ask rule the ONE rule validator refuses — the shell's command grammar (a redirection, a substitution,
84
+ * a mid-pattern star, control characters…), a path tool's path grammar, a name tool's name grammar (the retired parameter
85
+ * spelling `Skill(skill:<name>)`, an empty name…) — the message carries the validator's own code and sentence. */
80
86
  | "invalid.command_rule"
81
87
  /** An `allowedTools` value (the request's `TaskSpec.allowedTools`, the deployment's `RunnerDeps.allowedTools`) that is
82
88
  * not a list of strings: one issue for a non-list, one per element that is not a string (`rule` is empty — there is
@@ -1,7 +1,8 @@
1
1
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
2
+ import { splitNamespacedName } from "./tool-name-aliases.js";
2
3
  import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
3
4
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
4
- import { PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
5
+ import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
5
6
  import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
6
7
  import { effectivePathTargetOf } from "./effective-path-target.js";
7
8
  import { protectivePathTargetOf } from "./tool-registry.js";
@@ -58,15 +59,10 @@ export function wildcardMatch(pattern, value) {
58
59
  return p === pattern.length;
59
60
  }
60
61
  function parseNamespacedRuleName(name) {
61
- const ns = protocolOf(name);
62
- if (ns === undefined)
62
+ const split = splitNamespacedName(name);
63
+ if (split === undefined || split.peer === "")
63
64
  return undefined;
64
- const segments = name.slice(ns.prefix.length).split("__");
65
- const peer = segments[0];
66
- if (peer === undefined || peer === "")
67
- return undefined;
68
- const rest = segments.slice(1);
69
- return rest.length > 0 ? { protocol: ns.id, peer, tool: rest.join("__") } : { protocol: ns.id, peer };
65
+ return split.tool !== undefined ? { protocol: split.ns.id, peer: split.peer, tool: split.tool } : { protocol: split.ns.id, peer: split.peer };
70
66
  }
71
67
  export function namespacedRuleNameCovers(ruleName, toolName) {
72
68
  const rule = parseNamespacedRuleName(ruleName);
@@ -215,6 +211,21 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
215
211
  continue;
216
212
  }
217
213
  const face = resolved.face;
214
+ if (ruleToolGrammarOf(toolName) === "name") {
215
+ const parsedName = parseRuleText(text, verb);
216
+ if ("reject" in parsedName) {
217
+ bad(text, "invalid.command_rule", `${parsedName.reject.code}: ${parsedName.reject.message}`);
218
+ continue;
219
+ }
220
+ const param = NAME_RULE_FACES[toolName].param;
221
+ if (face?.params !== undefined && !face.params.includes(param)) {
222
+ bad(text, "unsupported.param_unknown", `"${param}" is not a parameter of ${toolName} on this leg (it carries ${[...face.params].sort().join(", ")}) — a ${toolName} rule is read against it, so the rule would compile and never match`);
223
+ continue;
224
+ }
225
+ entry.content[verb].push({ parsed: parsedName.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
226
+ byTool.set(toolName, entry);
227
+ continue;
228
+ }
218
229
  if (face !== undefined && face.pathTargeting === true && colon <= 0) {
219
230
  const parsedPath = parseRuleText(text, verb);
220
231
  if ("reject" in parsedPath) {
@@ -362,6 +373,12 @@ export function createPermissionRulePolicy(rules, opts) {
362
373
  const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
363
374
  let unreadable;
364
375
  for (const r of lane) {
376
+ if (r.parsed.match === "name" || r.parsed.match === "name_prefix") {
377
+ const named = nameRuleSubjectOf(r.parsed.tool, req.args);
378
+ if (named !== undefined && nameRuleAdmits(r.parsed, named))
379
+ return { rule: r };
380
+ continue;
381
+ }
365
382
  if (r.parsed.match === "subpath" || r.parsed.match === "path") {
366
383
  if (target === undefined || target.kind === "refused")
367
384
  continue;
@@ -110,17 +110,19 @@ export declare function announceClassifierRoleDefaulted(args: {
110
110
  runId?: string;
111
111
  }): void;
112
112
  /**
113
- * The `classifier.unavailable` notice — a classification that could not RULE (the classifier leg failed, or its
114
- * per-level budget ran out) and where the call went: `"asked"` = on down the ask chain to this leg's approval seat (a
115
- * person or a durable park decides; the handed-on ask carries `requiresRealApproval`, so no automatic lane answers it),
116
- * `"denied"` = refused (no approval seat: nobody to ask). Audience `user`: the refusal is the session user's own
113
+ * The `classifier.unavailable` notice — a classification that could not RULE (the classifier leg failed, its per-level
114
+ * budget ran out, or it replied with no verdict — `parse_error`; the code name predates that third cause) and where the
115
+ * call went: `"asked"` = on down the ask chain to this leg's approval seat (a person or a durable park decides; the
116
+ * handed-on ask carries `requiresRealApproval` and `classifierNoRuling`, so no automatic lane answers it), `"denied"` =
117
+ * refused (no approval seat: nobody to ask — a reply with no verdict is then also counted on the denial limit). Audience `user`: the refusal is the session user's own
117
118
  * call, and on a lane with no approval face (`-p`) this line is the only place they hear why it did not run. Once
118
119
  * per classification that could not answer, from both classifier stations (the leg's gate and an ancestor's frozen
119
- * classifier in the policy fold) — a call judged by more than one classifier can carry one line per classifier. It is never a block: the denial limit does not count it, so this line is not a step toward the
120
- * limit's card either.
120
+ * classifier in the policy fold) — a call judged by more than one classifier can carry one line per classifier. The line
121
+ * itself is never the count: the unavailable form is never counted; a reply with no verdict refused for want of a seat is
122
+ * counted by the station's ruling path, not by this line.
121
123
  *
122
- * `detail: { toolName, toolCallId, cause, disposition, sessionId?, runId? }` — `cause` is the
123
- * `AutoModeUnavailableCause` word (`timeout` / `error`). The sentence names the seat the station forked on — `leg` (this
124
+ * `detail: { toolName, toolCallId, cause, disposition, sessionId?, runId? }` — `cause` is why the classification did not rule,
125
+ * an `AutoModeFallbackCause` word (`timeout` / `error`, or `parse_error` for a reply with no verdict). The sentence names the seat the station forked on — `leg` (this
124
126
  * run's own approval seat) or `ancestor` (the frozen approver of the ancestor layer whose classifier it was) — and, on a
125
127
  * timeout, the budget the classifier actually held (`budgetMs`, the decider's {@link AutoModeDecider.timeoutMs}; a
126
128
  * hand-built decider states none, and the sentence names the knob alone).
@@ -98,7 +98,9 @@ export function classifierUnavailableNotice(args) {
98
98
  const whose = args.station === "ancestor" ? "the classifier of the ancestor layer that asked" : "the classifier";
99
99
  const why = args.cause === "timeout"
100
100
  ? `${whose} did not answer within its per-level budget (${args.budgetMs !== undefined ? `${args.budgetMs} ms, ` : ""}RunnerDeps.autoMode.timeoutMs)`
101
- : `${whose} could not run (error)`;
101
+ : args.cause === "parse_error"
102
+ ? args.station === "ancestor" ? `${whose} replied without a verdict` : "the classifier's reply carried no verdict"
103
+ : `${whose} could not run (error)`;
102
104
  const next = args.disposition === "asked"
103
105
  ? args.station === "ancestor"
104
106
  ? "so the call was handed on to that layer's approver rather than refused by the classifier."
@@ -1,4 +1,4 @@
1
- import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback, type UnarmedDenialLimitFallback } from "../auto-mode.js";
1
+ import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type AutoModeFallbackCause, type DenialLimitFallback, type UnarmedDenialLimitFallback } from "../auto-mode.js";
2
2
  import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
3
3
  import { type ClassifierUnavailableSeat } from "../classifier-unavailable-arm.js";
4
4
  import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
@@ -114,6 +114,7 @@ export interface AskRequestCarry {
114
114
  askClass?: AskClass;
115
115
  classes?: readonly AskClass[];
116
116
  denialLimitFallback?: DenialLimitFallback;
117
+ classifierNoRuling?: AutoModeFallbackCause;
117
118
  }
118
119
  /** The GATE's own mint station: every member is read off the surviving decision, which is the gate's
119
120
  * OWN snapshot (its stamp point spreads the policy's object and stamps the word and the class onto the copy),
@@ -154,8 +155,9 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
154
155
  * `classification` is the class set of the ask handed on, under the same station facts: on the two unchanged-ask
155
156
  * branches the one read the exclusion judged by, on the unavailable and bound branches a read of the object this
156
157
  * station minted (each adds a real-approval bit, which the set must reflect). */
157
- /** `ask` is the incoming ask unchanged (the arm's receipt snapshot of the ancestor's decision), or — an UNAVAILABLE classification with a live frozen
158
- * approver — that ask with the classifier sentence appended; without one it is a `deny` whose attested cause reads `unavailable`. */
158
+ /** `ask` is the incoming ask unchanged (the arm's receipt snapshot of the ancestor's decision), or — a classification that did not rule, with a
159
+ * live frozen approver — that ask carrying why (`classifierNoRuling`, the sentence appended); without one the unavailable form is a `deny`
160
+ * whose attested cause reads `unavailable`. */
159
161
  | {
160
162
  kind: "resolve";
161
163
  ask: A;
@@ -163,6 +165,7 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
163
165
  mintedHere: boolean;
164
166
  origin: AskOrigin;
165
167
  classification: AskClassification;
168
+ noRulingHere?: true;
166
169
  };
167
170
  /**
168
171
  * The ancestor's frozen classifier, run BEFORE its frozen approver in the ancestor's own gate order,
@@ -170,10 +173,11 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
170
173
  * auto-allow (and the streak resets); `block` below the bound ⇒ deny, the exact verdict the ancestor's
171
174
  * gate would have produced; `block` AT the bound ⇒ the ask is re-spoken in the classifier's voice as
172
175
  * the fallback (`decisionReason:"classifier"`, `requiresRealApproval`, the member carrying counts +
173
- * window) for the arm to resolve at the frozen approver; `parse_error` ⇒ the same block path with the CC
174
- * parse-failure sentence (counted); `unavailable` ⇒ the gate's own exit (`classifierUnavailableArm`) on the
175
- * frozen approver — a live one resolves the incoming ask, none ⇒ a DENY that says so (CC 2.1.250 form), never
176
- * counted; excluded / no classifier ⇒ the incoming ask, unchanged. The member may ALREADY be on the incoming
176
+ * window) for the arm to resolve at the frozen approver; a classification that did not rule (`unavailable`, or a reply
177
+ * with no verdict) ⇒ the gate's own exit (`classifierNoRulingArm`) on the frozen approver — a live one resolves the incoming
178
+ * ask carrying why (never counted); none ⇒ the unavailable form is a DENY that says so (CC 2.1.250 form, never counted) and
179
+ * a reply with no verdict takes the block path with the CC parse-failure sentence (counted); a CONFLICTING reply takes the
180
+ * block path too; excluded / no classifier ⇒ the incoming ask, unchanged. The member may ALREADY be on the incoming
177
181
  * ask (a deeper ancestor's fallback, minted inside the nested fold — the case the exclusion names):
178
182
  * it rides to the approver and the headless arm unchanged, whichever tracker minted it; only a
179
183
  * tracker that counted THIS streak is reset by the person's later allow (`mintedHere`).
@@ -203,11 +207,23 @@ export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
203
207
  /** The rewrite the arm's allow would have carried (the ask's own `updatedInput`, the re-check's edited
204
208
  * args) — handed IN so the allow is minted here, attested, once; absent ⇒ a bare allow. */
205
209
  allowRewrite?: unknown;
206
- /** An `unavailable` verdict's fork facts (`classifierUnavailableArm`): is the frozen approver live; who hears it. */
210
+ /** An `unavailable` verdict's fork facts (`classifierNoRulingArm`): is the frozen approver live; who hears it. */
207
211
  unavailableSeat?: ClassifierUnavailableSeat;
208
212
  /** The station's facts for this call (the base with this task's tool seat); absent ⇒ {@link INHERITED_STATION_FACTS}. */
209
213
  facts?: AskClassFacts;
210
214
  }): Promise<InheritedClassifierJudgment<A>>;
215
+ /** The fallback an inherited station hands its frozen approver, minted ONCE for both ways a bound is reached (a counted ruling
216
+ * refusal; a no-ruling card the approver's POLICY refused, {@link settleNoRulingAsk}) — its own object, word and class set. */
217
+ type ResolveJudgment<A extends InheritedAsk> = Extract<InheritedClassifierJudgment<A>, {
218
+ kind: "resolve";
219
+ }>;
220
+ export declare function inheritedFallbackJudgment<A extends InheritedAsk>(ask: A, fallback: UnarmedDenialLimitFallback, latestBlockedAction: string, facts: AskClassFacts): ResolveJudgment<A>;
221
+ /** One resolution of an inherited ask at its frozen approver (the three arms): a no-ruling card its POLICY refused counts
222
+ * ({@link settleNoRulingAsk}); at the bound the call is resolved again there as the fallback. Returns the judgment it answered. */
223
+ export declare function resolveInheritedAsk<A extends InheritedAsk, R extends Pick<ResolvedAsk, "action" | "settlement">>(judged: ResolveJudgment<A>, resolveWith: (j: ResolveJudgment<A>) => Promise<R>, tracker: AutoModeDenialTracker | undefined, toolName: string, facts: AskClassFacts): Promise<{
224
+ resolved: R;
225
+ judged: ResolveJudgment<A>;
226
+ }>;
211
227
  /** The resolver's answer as the settlement reads it. */
212
228
  export type FallbackResolution = Pick<ResolvedAsk, "action" | "settlement" | "approverUnavailable">;
213
229
  /** A deny nobody JUDGED, at a wrapper arm — read off the one table over the settlement words
@@ -258,3 +274,4 @@ export declare function lateAskSettlementObserver(args: {
258
274
  kind: "error";
259
275
  error: unknown;
260
276
  }) => void;
277
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { markTightenMessage } from "../safety-axis-vocab.js";
2
- import { classifierParseFailureDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
2
+ import { classifierParseFailureDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, isAutoModeFallbackCause, rungDidNotRule, unarmedWindow } from "../auto-mode.js";
3
3
  import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
4
- import { classifierUnavailableArm } from "../classifier-unavailable-arm.js";
4
+ import { classifierNoRulingArm, settleNoRulingAsk } from "../classifier-unavailable-arm.js";
5
5
  import { attestClassifierDenyCause, attestClassifierRound, carryEngineAttestations } from "../tool-policy.js";
6
6
  import { SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js";
7
7
  import { deliverEngineNotice } from "../types.js";
@@ -44,7 +44,7 @@ export function createDenialLimitStop(opts) {
44
44
  if (gateStopRef.terminal !== undefined)
45
45
  return;
46
46
  const lane = info.settlement === undefined
47
- ? `no approver is wired to fall back to`
47
+ ? `nobody in this run can be asked to answer it`
48
48
  : `the durable approval it parked was settled with nobody judging it ("${info.settlement}")`;
49
49
  const message = `too many classifier denials in headless mode — ${denialLimitSentence(info.fallback)} The run was stopped: ` +
50
50
  `the auto-mode classifier's denial limit falls back to a person, and ${lane} ` +
@@ -61,6 +61,7 @@ export function createDenialLimitStop(opts) {
61
61
  return { gateStopRef, stopForDenialLimit };
62
62
  }
63
63
  export const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} };
64
+ const noRulingCarry = (why) => (isAutoModeFallbackCause(why) ? { classifierNoRuling: why } : {});
64
65
  function fallbackCarry(fallback, liveApprover, tracker) {
65
66
  if (fallback === undefined)
66
67
  return {};
@@ -71,6 +72,7 @@ export function gateAskCarry(decision, liveApprover, tracker) {
71
72
  ...(decision.origin !== undefined ? { origin: decision.origin } : {}),
72
73
  ...(decision.askClass !== undefined && decision.classes !== undefined ? { askClass: decision.askClass, classes: decision.classes } : {}),
73
74
  ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker),
75
+ ...noRulingCarry(decision.classifierNoRuling),
74
76
  };
75
77
  }
76
78
  export function inheritedAskCarry(judged, liveApprover, tracker) {
@@ -78,6 +80,7 @@ export function inheritedAskCarry(judged, liveApprover, tracker) {
78
80
  origin: judged.origin,
79
81
  ...askClassMembersOf(judged.classification),
80
82
  ...fallbackCarry(judged.fallback, liveApprover, tracker),
83
+ ...noRulingCarry(judged.ask.classifierNoRuling),
81
84
  };
82
85
  }
83
86
  export const inheritedAskFallbackMessage = (toolName) => `approval required for "${toolName}" (inherited parent policy)`;
@@ -105,12 +108,12 @@ export async function judgeInheritedClassifier(opts) {
105
108
  autoMode.denialTracking?.recordAllow();
106
109
  return { kind: "allow", result: attributed({ action: "allow", decisionReason: "classifier", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) };
107
110
  }
108
- if (verdict.kind === "unavailable") {
109
- const arm = classifierUnavailableArm({ ask, req, cause: verdict.cause, seat: opts.unavailableSeat, aborted: opts.signal.aborted });
110
- if (arm.kind === "asked")
111
- return { kind: "resolve", ask: arm.ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification: classifyAsk(arm.ask, facts) };
111
+ const noRuling = rungDidNotRule(verdict);
112
+ const arm = noRuling !== undefined ? classifierNoRulingArm({ ask, req, cause: noRuling, seat: opts.unavailableSeat, aborted: opts.signal.aborted }) : undefined;
113
+ if (arm?.kind === "asked")
114
+ return { kind: "resolve", ask: arm.ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification: classifyAsk(arm.ask, facts), noRulingHere: true };
115
+ if (arm?.kind === "denied")
112
116
  return { kind: "deny", result: attributed(arm.deny) };
113
- }
114
117
  const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
115
118
  const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
116
119
  const tracked = autoMode.denialTracking?.recordBlock();
@@ -122,10 +125,21 @@ export async function judgeInheritedClassifier(opts) {
122
125
  };
123
126
  return { kind: "deny", result: attributed(verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", req) : denied) };
124
127
  }
125
- const fallback = tracked.fallback;
126
- const minted = carryEngineAttestations(ask, { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback });
128
+ return inheritedFallbackJudgment(ask, tracked.fallback, reason || category || req.toolName, facts);
129
+ }
130
+ export function inheritedFallbackJudgment(ask, fallback, latestBlockedAction, facts) {
131
+ const { classifierNoRuling: _why, ...standing } = ask;
132
+ const minted = carryEngineAttestations(ask, { ...standing, message: denialLimitFallbackMessage(fallback, latestBlockedAction), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback });
127
133
  return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, facts), classification: classifyAsk(minted, facts) };
128
134
  }
135
+ export async function resolveInheritedAsk(judged, resolveWith, tracker, toolName, facts) {
136
+ const resolved = await resolveWith(judged);
137
+ const refallback = settleNoRulingAsk({ ask: judged.ask, mintedHere: judged.noRulingHere === true, resolved, tracker });
138
+ if (refallback === undefined)
139
+ return { resolved, judged };
140
+ const again = inheritedFallbackJudgment(judged.ask, refallback, toolName, facts);
141
+ return { resolved: await resolveWith(again), judged: again };
142
+ }
129
143
  export function headlessDenyAtFold(r) {
130
144
  return r.settlement !== undefined && SETTLEMENT_NOBODY_JUDGED[r.settlement.kind];
131
145
  }
@@ -139,7 +139,8 @@ export async function exitGate(pass) {
139
139
  const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(pass.denyCause !== undefined ? { cause: pass.denyCause } : {}), ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}), ...(pass.classifierRound !== undefined ? { classifier: pass.classifierRound } : {}), ...(modeRefusal !== undefined ? { modeRefusal: { mode: modeRefusal.mode, askClass: modeRefusal.askClass, classes: modeRefusal.classes } } : {}) });
140
140
  const foldCause = askUnresolvableCauseOf(pass.decision, { toolCallId, toolName });
141
141
  const unresolvable = foldCause ?? unresolvableCauseOfSettlement(gate);
142
- const remedySeat = unresolvable !== undefined ? askUnresolvableRemedySeat(unresolvable) : undefined;
142
+ const tableSeat = unresolvable !== undefined ? askUnresolvableRemedySeat(unresolvable) : undefined;
143
+ const remedySeat = tableSeat === "absent" && input.askSeatForm === "declared_none" && pass.askClassification?.classes.has("content_question") !== true ? "declared_none" : tableSeat;
143
144
  const remedy = remedySeat !== undefined ? headlessApprovalRemedy({ mode: input.permissionMode?.(), classification: foldCause !== undefined ? ANCESTOR_UNRESOLVED_CLASSIFICATION : pass.askClassification, approverSeat: remedySeat, coveredWrite: isProtectedWrite(pass.req), ...(pass.shellUnplacedOptions !== undefined ? { unplacedOptions: pass.shellUnplacedOptions } : {}) }) : undefined;
144
145
  const denyReason = withParkFailureCause(`${decisionText(pass.decision) ?? `tool "${toolName}" denied by policy`}${remedy !== undefined ? ` — ${remedy}` : ""}`, pass.parkFailed);
145
146
  if (unresolvable !== undefined) {
@@ -32,8 +32,8 @@ import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage }
32
32
  import { type ToolCallLike } from "../tool-registry.js";
33
33
  import { type OrgRuleResolution } from "../permission-rule-org.js";
34
34
  import { type EffectivePermissionRules, type PermissionRuleStoreProvider } from "../permission-rule-provider.js";
35
- /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for
36
- * it alone). The PATH grammar's tools are derived from the catalog (`ruleToolGrammarOf`). */
35
+ /** The one SHELL tool the persisted-rule lane's COMMAND grammar speaks for (the rule-offer face mints for it and the
36
+ * name tools). The PATH and NAME grammars' tools are derived (`ruleToolGrammarOf`). */
37
37
  export declare const PERSISTED_RULE_TOOL = "Bash";
38
38
  /**
39
39
  * The evidence an INHERITED-lane ask carries.
@@ -1,6 +1,6 @@
1
1
  import { persistedRuleMandateOf, ruleClearanceOf } from "../ask-origin.js";
2
2
  import {} from "../hooks.js";
3
- import { adjudicatePersistedPathRules, adjudicatePersistedRules, callPathFaceOf, faceAskSpeaksFor, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
3
+ import { adjudicatePersistedNameRules, adjudicatePersistedPathRules, adjudicatePersistedRules, callPathFaceOf, faceAskSpeaksFor, laneToolNamesOf, nameRuleSubjectOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
6
6
  import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
@@ -155,6 +155,8 @@ export function createPermissionRuleLanes(cfg) {
155
155
  const liveCwd = cfg.liveCwd();
156
156
  if (grammar === "path")
157
157
  return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
158
+ if (grammar === "name")
159
+ return persistedRuleAnswerOf(adjudicatePersistedNameRules(table, { tool: req.toolName, name: nameRuleSubjectOf(req.toolName, req.args), cwd: cfg.root, sessionId: cfg.sessionId }));
158
160
  const command = req.args?.command;
159
161
  if (typeof command !== "string")
160
162
  return undefined;
@@ -172,7 +174,7 @@ export function createPermissionRuleLanes(cfg) {
172
174
  export function createRuleOffersOf(cfg) {
173
175
  return (call, args, ask) => {
174
176
  const toolName = wireOf("offer.laneTool", call);
175
- if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
177
+ if (!cfg.laneArmed || (toolName !== PERSISTED_RULE_TOOL && ruleToolGrammarOf(toolName) !== "name"))
176
178
  return {};
177
179
  if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
178
180
  return {};
@@ -184,6 +186,10 @@ export function createRuleOffersOf(cfg) {
184
186
  }).word;
185
187
  if (closedDoor !== undefined)
186
188
  return { ruleOffersAbsence: closedDoor };
189
+ if (ruleToolGrammarOf(toolName) === "name") {
190
+ const offer = suggestRuleForName(toolName, nameRuleSubjectOf(toolName, args) ?? "");
191
+ return offer !== undefined ? { ruleOffers: [offer] } : { ruleOffersAbsence: "lane_cannot_speak" };
192
+ }
187
193
  const command = args?.command;
188
194
  if (typeof command !== "string")
189
195
  return { ruleOffersAbsence: "lane_cannot_speak" };
@@ -6,6 +6,7 @@ import { type OnAsk, type ToolCallRequest, type ToolPolicy } from "../tool-polic
6
6
  import type { RunnerDeps, TaskSpec } from "../types.js";
7
7
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
8
8
  import type { AskLane, Prepared } from "./contracts.js";
9
+ import type { GrantBindingOf } from "../skill-grant-binding.js";
9
10
  import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
10
11
  import { type GrantReadRoot } from "./read-root-grant.js";
11
12
  import type { createRuleOffersOf } from "./permission-rule-lanes.js";
@@ -49,6 +50,8 @@ export interface PrepareAskLaneInput {
49
50
  now: () => number;
50
51
  /** borrowed-readonly — the policy-chain phase's rule-offer factory (the suggestion members the ask mint threads). */
51
52
  ruleOffersOf: ReturnType<typeof createRuleOffersOf>;
53
+ /** borrowed-readonly — the policy-chain phase's Skill-card binding factory (`boundGrants` / `boundGrantDigest` on the ask). */
54
+ grantBindingOf: GrantBindingOf;
52
55
  /** borrowed-readonly — the policy-chain phase's Runner-filled source identity (one factory, every ask mint site). */
53
56
  askSourceIdentity: () => {
54
57
  principal?: string;
@@ -59,7 +59,7 @@ function resolveApprovalPreview(tools, toolName, args) {
59
59
  }
60
60
  }
61
61
  export function prepareAskLane(input) {
62
- const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
62
+ const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
63
63
  if (!gateMachineryActive)
64
64
  return { askLane: undefined };
65
65
  const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
@@ -98,6 +98,7 @@ export function prepareAskLane(input) {
98
98
  return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
99
99
  })(),
100
100
  ...ruleOffersOf(req, req.args, decision.action === "ask" ? decision : undefined),
101
+ ...grantBindingOf(req, req.args),
101
102
  ...cardDisclosureOf(decision, `approval required for "${req.toolName}"`),
102
103
  ...askSourceIdentity(),
103
104
  ...riskAxesOf(req),
@@ -30,7 +30,6 @@ import { type CheckpointSeat } from "../checkpoint-seat.js";
30
30
  import { type ComplianceCapability } from "../compliance.js";
31
31
  import type { LockedPreflight } from "../locked-config.js";
32
32
  import type { StoredSession } from "../session.js";
33
- import type { OnAsk } from "../tool-policy.js";
34
33
  import type { RunnerDeps, RuntimeCaps, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
35
34
  import type { AutoModeArmFact } from "../wiring-manifest.js";
36
35
  import type { AssertAllKeysHandled } from "../ask-origin.js";
@@ -88,8 +87,8 @@ export interface PrepareCapsAndWorkflowInput {
88
87
  harnessRef: {
89
88
  current?: AgentHarness;
90
89
  };
91
- /** borrowed-readonly — the FROZEN approver seat (the Workflow mount's parentOnAsk). */
92
- frozenOnAsk: OnAsk | undefined;
90
+ /** borrowed-readonly — the run's approval seat as the config doors froze it (the Workflow mount's parentOnAsk / parentApprovalReach). */
91
+ askSeat: import("../wiring-manifest.js").AskSeat;
93
92
  /** borrowed-readonly — the parent SPEC seat the children inherit (minted once by the inherited-gate phase; the Workflow mount's dep, the same value the delegation ctx carries). */ parentSpecSeat: ParentSpecSeat;
94
93
  /** borrowed-readonly — the ONE read-face carrier expression (the Workflow mount's parentReadFace getter calls it). */
95
94
  carrierReadFace: () => ContainmentFace | undefined;
@@ -114,7 +114,7 @@ function autoModeArmReasonOf(intent, facePresent, caps, capsFaulted) {
114
114
  return { armed: true, reason: "armed" };
115
115
  }
116
116
  export async function prepareCapsAndWorkflow(input) {
117
- const { spec, deps, parentSpecSeat, checkpointSeat, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, carrierWriteFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, permissionMode, readDenyEntries, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
117
+ const { spec, deps, parentSpecSeat, checkpointSeat, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, askSeat, carrierReadFace, carrierWriteFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, permissionMode, readDenyEntries, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
118
118
  const roster = new RosterBuilder(toolEffects);
119
119
  for (const t of spec.tools ?? []) {
120
120
  const mounted = mountCallerTool(t);
@@ -326,7 +326,8 @@ export async function prepareCapsAndWorkflow(input) {
326
326
  return model === undefined ? undefined : { model, ...(typeof ref === "string" ? { modelRef: ref } : {}) };
327
327
  },
328
328
  ...(spec.getApiKeyAndHeaders !== undefined ? { parentGetApiKeyAndHeaders: spec.getApiKeyAndHeaders } : {}),
329
- ...(frozenOnAsk !== undefined ? { parentOnAsk: frozenOnAsk } : {}),
329
+ ...(askSeat.value !== undefined ? { parentOnAsk: askSeat.value } : {}),
330
+ ...(askSeat.reach !== undefined ? { parentApprovalReach: askSeat.reach } : {}),
330
331
  ...(spec.durableApproval !== undefined ? { parentDurableApproval: { ...spec.durableApproval } } : {}),
331
332
  ...(internals?.workflowParkedResume !== undefined
332
333
  ? { parkedResume: (resumeFromRunId) => {
@@ -29,6 +29,7 @@ import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
29
29
  import type { SessionStore } from "../session.js";
30
30
  import type { RunnerDeps, TaskLimits, TaskSpec } from "../types.js";
31
31
  import { type UsageWindow } from "../usage-window-store.js";
32
+ import { type AskSeat } from "../wiring-manifest.js";
32
33
  import type { PrepareResume, RunInternals, ToolFaceSnapshot } from "./contracts.js";
33
34
  export declare function limitConfigError(code: string, message: string): Error & {
34
35
  code?: string;
@@ -147,6 +148,8 @@ export interface PrepareConfigDoorsResult {
147
148
  /** owned — the resolved interaction posture (spec > spawning run > deployment); the
148
149
  * AskUserQuestion mount and the child-ctx injection read this same value. */
149
150
  resolvedInteractionPosture: "interactive" | "headless" | undefined;
151
+ /** owned, frozen — the run's approval seat ({@link resolveAskSeat}), the one read of the seats; later phases read only this. */
152
+ askSeat: AskSeat;
150
153
  /** owned — the validated/resolved microCompact knob ({@link resolveMicroCompactKnob}
151
154
  * for the door + placement reasoning). The per-run state builder consumes exactly these values —
152
155
  * the deps bag is never re-read after this door. */