@sema-agent/core 7.42.1 → 7.44.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 (115) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +3 -1
  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/brain/errors.d.ts +23 -2
  8. package/dist/brain/errors.js +26 -2
  9. package/dist/brain/stream-engine.d.ts +2 -1
  10. package/dist/brain/stream-engine.js +10 -9
  11. package/dist/core/allow-entry.d.ts +50 -31
  12. package/dist/core/allow-entry.js +100 -37
  13. package/dist/core/ask-origin.d.ts +16 -5
  14. package/dist/core/ask-origin.js +11 -3
  15. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  16. package/dist/core/ask-unresolvable-notice.js +1 -1
  17. package/dist/core/auto-mode.d.ts +5 -4
  18. package/dist/core/auto-mode.js +3 -1
  19. package/dist/core/checkpoint-store.d.ts +44 -20
  20. package/dist/core/checkpoint-store.js +10 -16
  21. package/dist/core/child-clamp-seats.d.ts +15 -1
  22. package/dist/core/child-clamp-seats.js +6 -2
  23. package/dist/core/classifier-unavailable-arm.d.ts +42 -23
  24. package/dist/core/classifier-unavailable-arm.js +14 -3
  25. package/dist/core/decision-receipt.js +2 -0
  26. package/dist/core/engine-notice.d.ts +16 -14
  27. package/dist/core/engine-notice.js +4 -3
  28. package/dist/core/gate-lanes.js +37 -29
  29. package/dist/core/governance-codes.d.ts +4 -3
  30. package/dist/core/governance-codes.js +1 -0
  31. package/dist/core/hooks.d.ts +6 -1
  32. package/dist/core/hooks.js +3 -0
  33. package/dist/core/memory-engine/engine.d.ts +2 -2
  34. package/dist/core/memory-engine/engine.js +7 -14
  35. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  36. package/dist/core/memory-engine/file-backend.js +5 -7
  37. package/dist/core/memory-engine/layout.d.ts +37 -4
  38. package/dist/core/memory-engine/layout.js +34 -5
  39. package/dist/core/mode-policy.d.ts +5 -4
  40. package/dist/core/mode-policy.js +4 -0
  41. package/dist/core/permission-rule-consent.d.ts +3 -2
  42. package/dist/core/permission-rule-consent.js +25 -20
  43. package/dist/core/permission-rule-model.d.ts +47 -7
  44. package/dist/core/permission-rule-model.js +80 -6
  45. package/dist/core/permission-rules.d.ts +8 -2
  46. package/dist/core/permission-rules.js +26 -9
  47. package/dist/core/runner/classifier-ladder.d.ts +10 -8
  48. package/dist/core/runner/classifier-ladder.js +3 -1
  49. package/dist/core/runner/contracts.d.ts +1 -16
  50. package/dist/core/runner/denial-limit-arms.d.ts +25 -8
  51. package/dist/core/runner/denial-limit-arms.js +24 -10
  52. package/dist/core/runner/gate-exit.js +2 -1
  53. package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
  54. package/dist/core/runner/permission-rule-lanes.js +8 -2
  55. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  56. package/dist/core/runner/prepare-ask-lane.js +2 -1
  57. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
  58. package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
  59. package/dist/core/runner/prepare-config-doors.d.ts +3 -0
  60. package/dist/core/runner/prepare-config-doors.js +6 -5
  61. package/dist/core/runner/prepare-gate-stations.js +1 -1
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  63. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  64. package/dist/core/runner/prepare-park-ask.d.ts +4 -4
  65. package/dist/core/runner/prepare-park-ask.js +2 -4
  66. package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
  67. package/dist/core/runner/prepare-policy-chain.js +59 -38
  68. package/dist/core/runner/prepare-project-context.js +6 -2
  69. package/dist/core/runner/prepare-task.js +6 -6
  70. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
  71. package/dist/core/runner/prepare-wiring-manifest.js +8 -9
  72. package/dist/core/runner/resume-checkpoint-screen.js +1 -1
  73. package/dist/core/runner/runtask.js +1 -5
  74. package/dist/core/runner/steer-admission.d.ts +10 -11
  75. package/dist/core/runner/steer-admission.js +6 -8
  76. package/dist/core/runner/stream-steer-verb.d.ts +5 -4
  77. package/dist/core/runner/stream-steer-verb.js +6 -8
  78. package/dist/core/runner/synthetic-tools.d.ts +3 -2
  79. package/dist/core/runner/synthetic-tools.js +1 -1
  80. package/dist/core/session-policy-store.d.ts +3 -3
  81. package/dist/core/session-policy-store.js +18 -4
  82. package/dist/core/skill-grant-binding.d.ts +44 -0
  83. package/dist/core/skill-grant-binding.js +22 -0
  84. package/dist/core/skill-grant.d.ts +59 -28
  85. package/dist/core/skill-grant.js +96 -23
  86. package/dist/core/steer-replay.d.ts +46 -0
  87. package/dist/core/steer-replay.js +22 -0
  88. package/dist/core/task-event.d.ts +3 -2
  89. package/dist/core/task-spec.d.ts +14 -1
  90. package/dist/core/task-stream.d.ts +12 -6
  91. package/dist/core/tighten-task-spec.d.ts +2 -0
  92. package/dist/core/tighten-task-spec.js +3 -0
  93. package/dist/core/tool-errors.d.ts +2 -2
  94. package/dist/core/tool-name-aliases.d.ts +35 -5
  95. package/dist/core/tool-name-aliases.js +29 -4
  96. package/dist/core/tool-policy.d.ts +10 -0
  97. package/dist/core/tool-policy.js +9 -2
  98. package/dist/core/tool-spec.d.ts +7 -0
  99. package/dist/core/types.d.ts +2 -2
  100. package/dist/core/types.js +1 -1
  101. package/dist/core/wiring-manifest.d.ts +56 -14
  102. package/dist/core/wiring-manifest.js +29 -4
  103. package/dist/index.d.ts +6 -4
  104. package/dist/index.js +5 -4
  105. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  106. package/dist/orchestration/run-workflow-tool.js +1 -1
  107. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  108. package/dist/orchestration/workflow-primitives.js +6 -6
  109. package/dist/orchestration/workflow-types.d.ts +5 -0
  110. package/dist/orchestration/workflow.d.ts +3 -2
  111. package/dist/orchestration/workflow.js +14 -6
  112. package/package.json +1 -1
  113. package/test/export-surface.snapshot.json +21 -5
  114. package/wire-changes/core@7.43.0.json +985 -0
  115. package/wire-changes/core@7.44.0.json +469 -0
@@ -156,14 +156,27 @@ export declare function callPathFaceOf(call: ToolCallLike): DenyFace | undefined
156
156
  /** Does some {@link PATH_RULE_FACES} ask column hold `face` — may a face-keyed ask row speak for a call on it, whatever
157
157
  * the call's tool is called (a caller's tool declaring a read path target is on the read face too)? */
158
158
  export declare function faceAskSpeaksFor(face: DenyFace | undefined): boolean;
159
+ /**
160
+ * The tools whose rule content NAMES THE VALUE of one parameter instead of a command or a path — the reference's `Skill(<name>)`
161
+ * form, one row per tool: `param` is the argument the rule's content is read against, `params` every scalar argument the tool
162
+ * carries (the parameter spelling `Tool(<param>:<pattern>)` those names used to reach is retired for such a tool, see
163
+ * {@link parseNameRuleContent}). The tool's real schema keys are pinned equal to `params` (test/backlog1318).
164
+ */
165
+ export declare const NAME_RULE_FACES: Readonly<Record<string, {
166
+ readonly param: string;
167
+ readonly params: readonly string[];
168
+ }>>;
159
169
  /**
160
170
  * Which tools a rule may name — the model's tool set is DERIVED, never a hand-kept list: the shell tool
161
171
  * takes the COMMAND grammar; every tool the catalog declares a path target for (Read / Edit / Write /
162
- * Glob / NotebookEdit / …, `pathTargetOf`) takes the PATH grammar. A rule on any other tool has no
163
- * content this model can judge and is refused (`unsupported.tool`). The tool set therefore moves with the
164
- * catalog's declarations, exactly as the protective fences' covered set does.
172
+ * Glob / NotebookEdit / …, `pathTargetOf`) takes the PATH grammar; the tools of {@link NAME_RULE_FACES} take the NAME grammar.
173
+ * A rule on any other tool has no content this model can judge and is refused (`unsupported.tool`). The tool set therefore
174
+ * moves with the catalog's declarations, exactly as the protective fences' covered set does.
165
175
  */
166
- export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | undefined;
176
+ export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | "name" | undefined;
177
+ /** The value a name-grammar tool's call carries in its name argument — the string every NAME rule is read against, the one a
178
+ * `Skill` call is resolved by — or undefined (not a name-grammar tool; no such argument; not a string). */
179
+ export declare function nameRuleSubjectOf(toolName: string, args: unknown): string | undefined;
167
180
  /** The names the persisted grammar judges a call under: every name the call answers to (the model's spelling, the wire
168
181
  * name, the other aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
169
182
  * tool that answers to that name under another wire name, as a policy's rule does. Empty ⇒ the grammar speaks for none. */
@@ -191,7 +204,7 @@ export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").Too
191
204
  * otherwise the working directory). Its predicate is {@link pathRuleReaches}. Not an allow form: a
192
205
  * standing approval by path pattern would be a wider grant than any card ever shows a person.
193
206
  */
194
- export type PersistedRuleMatch = "exact" | "prefix" | "wildcard" | "subpath" | "path";
207
+ export type PersistedRuleMatch = "exact" | "prefix" | "wildcard" | "subpath" | "path" | "name" | "name_prefix";
195
208
  /**
196
209
  * Where a rule applies — design/382 §4.1, the THREE consent dimensions.
197
210
  *
@@ -525,6 +538,9 @@ export declare function parseRuleText(text: string, behavior: RuleBehavior): {
525
538
  * `subpath` the command is the lexical-normal absolute directory (one leading slash), re-spelled in the
526
539
  * double-slash directory form; for `path` it is the pattern as spelled. */
527
540
  export declare function formatRuleText(command: string, match: PersistedRuleMatch, tool: PersistedRuleTool): string;
541
+ /** Does a name rule reach this name? `name`: equal; `name_prefix`: begins with it (no boundary — the reference's `startsWith`).
542
+ * A rule of any other family reaches no name. */
543
+ export declare function nameRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, name: string): boolean;
528
544
  /**
529
545
  * The closed set of BASES a path-form pattern resolves against — facts of the CALL (its execution
530
546
  * environment and its task), never of the rule:
@@ -675,8 +691,8 @@ export type ProgramRunReachOutcome = {
675
691
  * other segment already reached.
676
692
  *
677
693
  * A rule body the lexer cannot read (foreign store bytes; every entrance validates against exactly
678
- * this) reaches nothing. A path-family rule (`subpath` / `path`) reaches no command: its predicates are
679
- * {@link directoryRuleAdmits} and {@link pathRuleReaches}.
694
+ * this) reaches nothing. A rule of another family (path: `subpath` / `path`; name: `name` / `name_prefix`) reaches no
695
+ * command: its predicates are {@link directoryRuleAdmits}, {@link pathRuleReachOf} and {@link nameRuleAdmits}.
680
696
  */
681
697
  export declare function programRunReachOf(rule: Pick<PersistedRule, "match" | "command">, command: string): ProgramRunReachOutcome;
682
698
  /** The boolean face of {@link programRunReachOf}: `reached` only. The shadow view and the pins that
@@ -926,6 +942,30 @@ export declare function adjudicatePersistedPathRules(rules: readonly PersistedRu
926
942
  readonly tighten?: PathTargetReading;
927
943
  readonly allow?: PathTargetReading;
928
944
  }, bases: PathRuleBases): PersistedRuleVerdict | undefined;
945
+ /**
946
+ * The name-family sibling of {@link adjudicatePersistedRules}: the same precedence walk over the rules of one name tool
947
+ * ({@link NAME_RULE_FACES}), judged on the one string the call names (`nameRuleSubjectOf`). A rule of the tool speaks for the call
948
+ * when it is live, of this behavior, in scope ({@link eligiblePersisted}, the one predicate) and {@link nameRuleAdmits} the name; the
949
+ * first behavior in {@link RULE_BEHAVIORS_BY_PRECEDENCE} whose rules speak decides, the first table-order rule of it is the answer.
950
+ * Every behavior is judged with the one predicate — a name has no "program run" the tightening direction could read wider — and
951
+ * there is nothing unreadable: a name is a string.
952
+ */
953
+ export declare function adjudicatePersistedNameRules(rules: readonly PersistedRule[], call: {
954
+ tool: string;
955
+ name: string | undefined;
956
+ cwd: string | undefined;
957
+ sessionId?: string;
958
+ }): PersistedRuleVerdict | undefined;
959
+ /**
960
+ * The ONE offer a name tool's approval card carries: the exact rule for the name the call carries (`Skill(<name>)`, the reference's
961
+ * "don't ask again for <skill>" row), or undefined when no rule spells exactly that name (the round-trip law: the canonical text,
962
+ * parsed back as an allow, must be the exact name rule over the very name — a leading slash, an interior star or an unreadable
963
+ * spelling offers nothing rather than a rule that would not clear the ask it was minted on). The reference's second row, the first
964
+ * word as a prefix, is offered only for a name containing a space; it is not minted here (it would widen past the name shown).
965
+ */
966
+ export declare function suggestRuleForName(tool: string, name: string): Extract<RuleOffer, {
967
+ kind: "single";
968
+ }> | undefined;
929
969
  /**
930
970
  * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
931
971
  * already admits, judged with the SAME split and the same eligibility predicates as the gate's
@@ -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."
@@ -33,7 +33,7 @@ import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
33
33
  import type { AutoModeDecider, AutoModeDenialTracker, DenialLimitFallbackFace } from "../auto-mode.js";
34
34
  import type { SettlementKind } from "../gate-outcome.js";
35
35
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
36
- import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
36
+ import type { Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
37
37
  import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
38
38
  import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } from "../context-edit.js";
39
39
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
@@ -2494,21 +2494,6 @@ export interface RunState {
2494
2494
  attachmentsInjected: number;
2495
2495
  };
2496
2496
  }
2497
- /** One accepted live steer's REPLAY identity, the twin of the parked
2498
- * queue's `samePendingSteerPayload`. `payload` is the exact string handed to the harness, so the text,
2499
- * the speaker envelope and the trust framing all fold into it; `trusted` is kept SEPARATELY from those
2500
- * framed bytes so a text that spells its own `<system-reminder>` wrapper can never read as the trusted
2501
- * arm's byte-equal twin. `actor` carries the attribution the ledger frame reports. */
2502
- export interface AcceptedSteerInput {
2503
- payload: string;
2504
- trusted: boolean;
2505
- /** The NORMALIZED priority the call delivered under (absent input ⇒ "next"),
2506
- * the replay identity's sixth axis: same id + same text at a DIFFERENT tier is not a replay —
2507
- * a `next` input re-sent as `now` answering "idempotent success" without the interrupt would be
2508
- * a disposition lie, so it refuses `duplicate_input_id` like any other identity mismatch. */
2509
- priority: SystemInjectionPriority;
2510
- actor?: ActorAssertion;
2511
- }
2512
2497
  /**
2513
2498
  * Config re-supplied to {@link Runner.resume}. A suspended task's tools / model / policy /
2514
2499
  * hooks cannot be reconstructed from a checkpoint token (the session stores neither tool implementations
@@ -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.