@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
@@ -1,11 +1,24 @@
1
+ import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
1
2
  import { type ToolCallRequest } from "./tool-policy.js";
2
3
  import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
3
4
  import { type PersistedRule } from "./permission-rule-model.js";
4
5
  import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
5
6
  import { type EngineNotice } from "./engine-notice.js";
6
- /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. */
7
- export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"];
7
+ /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
8
+ * entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
9
+ * run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
10
+ export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
8
11
  export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
12
+ /** The facts beside a `tool_name` / `near_miss` reason: the judgment's word, and the spelling to write instead when there is one. */
13
+ export interface AllowEntryInertDetail {
14
+ readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
15
+ readonly currentName?: string;
16
+ }
17
+ /** One entry a list reads as nothing. */
18
+ export type AllowEntryInert = {
19
+ readonly entry: string;
20
+ readonly reason: AllowEntryInertReason;
21
+ } & AllowEntryInertDetail;
9
22
  /** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell
10
23
  * command rule; the Read directory form; a deny-side path pattern (a skill's `disallowed-tools`). */
11
24
  export type CompiledAllowEntry = {
@@ -27,22 +40,25 @@ export type CompiledAllowEntry = {
27
40
  readonly kind: "path";
28
41
  readonly rule: PersistedRule;
29
42
  };
30
- /** Read one entry under one direction. */
31
- export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny"): CompiledAllowEntry | AllowEntryInertReason;
32
43
  /**
33
- * Compile one list WHOLE: the entries it reads and the ones it reads as nothing. An allow list holding `*` voids the list
34
- * (every other entry on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads
35
- * as nothing by itself and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it.
44
+ * Read one entry under one direction. `roster` = the tool names the run mounts (aliases included): given, the entry's tool name
45
+ * is also read against it (a stale name, or one that differs from a mounted tool's only by case, reads as `tool_name` /
46
+ * `near_miss`); absent, the grammar alone is read — what a resume's pre-claim screen can read before the leg has a roster.
47
+ */
48
+ export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny", roster?: ReadonlySet<string>): CompiledAllowEntry | AllowEntryInertReason;
49
+ /**
50
+ * Compile one list WHOLE: the entries it reads and the ones it reads as nothing (each with its reason and, for a name the
51
+ * roster refuses, the judgment's word and the spelling to write). An allow list holding `*` voids the list (every other entry
52
+ * on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads as nothing by itself
53
+ * and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it. `roster` as in
54
+ * {@link compileAllowEntry}.
36
55
  */
37
- export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny"): {
56
+ export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny", roster?: ReadonlySet<string>): {
38
57
  rows: Array<{
39
58
  entry: string;
40
59
  compiled: CompiledAllowEntry;
41
60
  }>;
42
- inert: Array<{
43
- entry: string;
44
- reason: AllowEntryInertReason;
45
- }>;
61
+ inert: AllowEntryInert[];
46
62
  };
47
63
  /** The programs whose open allow rule the reference implementation drops in auto mode — its interpreter / launcher table
48
64
  * plus the shells and the run-anything builtins (2.1.280). A program that only reaches the network is not here. */
@@ -117,11 +133,10 @@ export interface AllowEntryGateSeats {
117
133
  */
118
134
  export declare class AllowEntryTable {
119
135
  private held;
120
- /** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. */
121
- seed(entries: readonly string[]): ReadonlyArray<{
122
- entry: string;
123
- reason: AllowEntryInertReason;
124
- }>;
136
+ private readonly heldEntries;
137
+ /** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. `roster` as in
138
+ * {@link compileAllowEntry}: a list the leg itself wrote is read against it, a carried one (a delegating parent's capture) is not. */
139
+ seed(entries: readonly string[], roster?: ReadonlySet<string>): readonly AllowEntryInert[];
125
140
  /** The entries held, as written (a copy) — what a delegated child is handed. */
126
141
  entries(): readonly string[];
127
142
  /** The lane over this table, bound to the leg's call facts. */
@@ -129,8 +144,10 @@ export declare class AllowEntryTable {
129
144
  }
130
145
  /**
131
146
  * Seed one leg's request and deployment tables — a delegated child's captured request entries first (an entry that reads as
132
- * nothing was announced where it was written), then the leg's own list; the deployment's (announced once, at construction) —
133
- * and return the dead entries of the leg's OWN lists, the skills' beside the request's, for its one start-of-run notice.
147
+ * nothing was announced where it was written, and is read without the roster for the same reason), then the leg's own list
148
+ * against `roster`; the deployment's grammar reading is announced once, at construction, so only its name readings
149
+ * (`tool_name`, `near_miss` — facts about THIS leg's roster) are announced here — and return the dead entries of the leg's
150
+ * OWN lists, the skills' beside the request's, for its one start-of-run notice.
134
151
  */
135
152
  export declare function seedAllowEntryTables(tables: {
136
153
  readonly request: AllowEntryTable;
@@ -139,13 +156,11 @@ export declare function seedAllowEntryTables(tables: {
139
156
  skillInert: ReadonlyArray<{
140
157
  skill: string;
141
158
  list: "allowed-tools" | "disallowed-tools";
142
- entry: string;
143
- reason: AllowEntryInertReason;
144
- }>;
159
+ } & AllowEntryInert>;
145
160
  inherited: readonly string[] | undefined;
146
161
  request: readonly string[] | undefined;
147
162
  deployment: readonly string[] | undefined;
148
- }): AllowEntryInertNote[];
163
+ }, roster: ReadonlySet<string>): AllowEntryInertNote[];
149
164
  /** The deployment's allow rules, read ONCE at construction: screened (a value that is not a list of strings refuses), frozen,
150
165
  * and its dead entries announced once (no session). Returns the deps with the snapshot in place of the caller's list. */
151
166
  export declare function withDeploymentAllowedTools<D extends {
@@ -181,6 +196,7 @@ declare const SHADOW_REASON: {
181
196
  readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown";
182
197
  readonly ask_rule: string;
183
198
  readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides";
199
+ readonly classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides";
184
200
  readonly requires_user_interaction: "this tool's server declares that a call must be put to a person";
185
201
  readonly operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
186
202
  readonly shell_floor: string;
@@ -221,21 +237,24 @@ export declare function cardDisclosureOf(decision: {
221
237
  /** The notice for list entries that grant (or refuse) nothing — one code for every list. */
222
238
  export declare const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
223
239
  /** One entry that reads as nothing, and the list it came from (a skill's names the skill and which of its two lists). */
224
- export type AllowEntryInertNote = {
240
+ export type AllowEntryInertNote = ({
225
241
  readonly source: "skill";
226
242
  readonly skill: string;
227
243
  readonly list: "allowed-tools" | "disallowed-tools";
228
- readonly entry: string;
229
- readonly reason: AllowEntryInertReason;
230
- } | {
244
+ } & AllowEntryInert) | ({
231
245
  readonly source: AllowEntrySource;
232
- readonly entry: string;
233
- readonly reason: AllowEntryInertReason;
234
- };
246
+ } & AllowEntryInert);
247
+ /**
248
+ * One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
249
+ * — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
250
+ * skill's refused activation share.
251
+ */
252
+ export declare function inertEntryPhrase(e: AllowEntryInert): string;
235
253
  /**
236
254
  * The notice for entries this grammar reads as nothing — delivered only when one exists: once per prepared task for the
237
255
  * skill and request lists (`sessionId` given), once per Runner for the deployment's list. The person whose list it is
238
- * reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more).
256
+ * reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more). A skill whose
257
+ * `disallowed-tools` holds such an entry is named as one that does not activate.
239
258
  */
240
259
  export declare function allowedToolsNotGrantedNotice(inert: readonly AllowEntryInertNote[], sessionId: string | undefined): EngineNotice;
241
260
  export {};
@@ -1,6 +1,7 @@
1
1
  import { heldBy } from "./tool-name-readings.js";
2
2
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
3
- import { protocolOf } from "./protocol-table.js";
3
+ import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
4
+ import { catalogToolNames } from "./tool-registry.js";
4
5
  import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
5
6
  import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
6
7
  import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
@@ -9,66 +10,95 @@ import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier
9
10
  import { mandateDisclosureOf } from "./ask-origin.js";
10
11
  import { inlineUntrusted } from "./untrusted-text.js";
11
12
  import { deliverEngineNotice } from "./engine-notice.js";
12
- export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]);
13
+ export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
13
14
  function ruleRowOf(parsed) {
14
15
  return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
15
16
  }
16
17
  const TOOL_NAME_SPELLING = /^[^\s()]+$/;
17
- export function compileAllowEntry(raw, behavior) {
18
+ function mountedSpellingOf(name, roster) {
19
+ const folded = name.toLowerCase();
20
+ for (const mounted of roster)
21
+ if (mounted.toLowerCase() === folded)
22
+ return mounted;
23
+ return undefined;
24
+ }
25
+ function toolNameReading(name, roster) {
26
+ const verdict = judgeRuleToolName(name, roster);
27
+ if (verdict.kind !== "current") {
28
+ const currentName = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
29
+ return { inert: "tool_name", detail: { verdict: verdict.kind, ...(currentName !== undefined ? { currentName } : {}) } };
30
+ }
31
+ if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
32
+ return undefined;
33
+ const mounted = mountedSpellingOf(name, roster);
34
+ return mounted === undefined ? undefined : { inert: "near_miss", detail: { currentName: mounted } };
35
+ }
36
+ function readAllowEntry(raw, behavior, roster) {
37
+ const inert = (reason) => ({ inert: reason });
38
+ const row = (compiled) => ({ compiled });
18
39
  if (raw !== raw.trim())
19
- return "grammar";
40
+ return inert("grammar");
20
41
  const parsed = parsePermissionRule(raw);
21
42
  const name = parsed.toolName;
22
43
  if (!TOOL_NAME_SPELLING.test(name))
23
- return "grammar";
44
+ return inert("grammar");
24
45
  if (name === "*")
25
- return "wildcard_all";
46
+ return inert("wildcard_all");
47
+ if (roster !== undefined) {
48
+ const stale = toolNameReading(name, roster);
49
+ if (stale !== undefined)
50
+ return stale;
51
+ }
26
52
  if (parsed.ruleContent === undefined) {
27
- const ns = protocolOf(name);
28
- if (ns !== undefined) {
29
- if (name.slice(ns.prefix.length).split("__")[0]?.includes("*") === true)
30
- return "namespace_wildcard";
53
+ const split = splitNamespacedName(name);
54
+ if (split !== undefined) {
55
+ if (split.peer.includes("*"))
56
+ return inert("namespace_wildcard");
31
57
  if (isNamespacedCoveringRuleName(name))
32
- return { kind: "namespace", pattern: name };
33
- return { kind: "tool", name };
58
+ return row({ kind: "namespace", pattern: name });
59
+ return row({ kind: "tool", name });
34
60
  }
35
- return COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name };
61
+ return row(COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name });
36
62
  }
37
63
  if (name === COMMAND_RULE_TOOL) {
38
64
  const allowRead = parseRuleText(raw, behavior);
39
65
  if ("rule" in allowRead)
40
- return { kind: "command", rule: ruleRowOf(allowRead.rule) };
66
+ return row({ kind: "command", rule: ruleRowOf(allowRead.rule) });
41
67
  if (behavior === "allow" && allowRead.reject.code === "invalid.bare_interpreter_prefix") {
42
68
  const wide = parseRuleText(raw, "deny");
43
69
  if ("rule" in wide)
44
- return { kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) };
70
+ return row({ kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) });
45
71
  }
46
- return "grammar";
72
+ return inert("grammar");
47
73
  }
48
74
  if (ruleToolGrammarOf(name) === "path") {
49
75
  const read = parseRuleText(raw, behavior);
50
76
  if ("rule" in read) {
51
77
  if (read.rule.match === "subpath" && read.rule.tool === READ_RULE_TOOL && behavior === "allow")
52
- return { kind: "read_dir", rule: ruleRowOf(read.rule) };
78
+ return row({ kind: "read_dir", rule: ruleRowOf(read.rule) });
53
79
  if (behavior === "deny")
54
- return { kind: "path", rule: ruleRowOf(read.rule) };
80
+ return row({ kind: "path", rule: ruleRowOf(read.rule) });
55
81
  }
56
- return "path_pattern";
82
+ return inert("path_pattern");
57
83
  }
58
84
  if (/^domain:/.test(parsed.ruleContent))
59
- return "domain";
60
- return "content_unsupported";
85
+ return inert("domain");
86
+ return inert("content_unsupported");
61
87
  }
62
- export function compileAllowList(entries, behavior) {
88
+ export function compileAllowEntry(raw, behavior, roster) {
89
+ const read = readAllowEntry(raw, behavior, roster);
90
+ return "compiled" in read ? read.compiled : read.inert;
91
+ }
92
+ export function compileAllowList(entries, behavior, roster) {
63
93
  const voided = behavior === "allow" && entries.some((e) => compileAllowEntry(e, behavior) === "wildcard_all");
64
94
  const rows = [];
65
95
  const inert = [];
66
96
  for (const entry of entries) {
67
- const compiled = voided ? "wildcard_all" : compileAllowEntry(entry, behavior);
68
- if (typeof compiled === "string")
69
- inert.push({ entry, reason: compiled });
97
+ const read = voided ? { inert: "wildcard_all" } : readAllowEntry(entry, behavior, roster);
98
+ if ("compiled" in read)
99
+ rows.push({ entry, compiled: read.compiled });
70
100
  else
71
- rows.push({ entry, compiled });
101
+ inert.push({ entry, reason: read.inert, ...read.detail });
72
102
  }
73
103
  return { rows, inert };
74
104
  }
@@ -179,11 +209,15 @@ export function allowingRows(rows, req, mode) {
179
209
  }
180
210
  export class AllowEntryTable {
181
211
  held = [];
182
- seed(entries) {
183
- const { rows, inert } = compileAllowList(entries, "allow");
184
- for (const r of rows)
185
- if (!this.held.some((h) => h.entry === r.entry))
186
- this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
212
+ heldEntries = new Set();
213
+ seed(entries, roster) {
214
+ const { rows, inert } = compileAllowList(entries, "allow", roster);
215
+ for (const r of rows) {
216
+ if (this.heldEntries.has(r.entry))
217
+ continue;
218
+ this.heldEntries.add(r.entry);
219
+ this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
220
+ }
187
221
  return inert;
188
222
  }
189
223
  entries() {
@@ -198,11 +232,15 @@ export class AllowEntryTable {
198
232
  };
199
233
  }
200
234
  }
201
- export function seedAllowEntryTables(tables, lists) {
235
+ export function seedAllowEntryTables(tables, lists, roster) {
202
236
  tables.request.seed(lists.inherited ?? []);
203
- const requestInert = tables.request.seed(lists.request ?? []);
204
- tables.deployment.seed(lists.deployment ?? []);
205
- return [...lists.skillInert.map((e) => ({ source: "skill", ...e })), ...requestInert.map((e) => ({ source: "request", ...e }))];
237
+ const requestInert = tables.request.seed(lists.request ?? [], roster);
238
+ const deploymentInert = tables.deployment.seed(lists.deployment ?? [], roster).filter((e) => e.reason === "tool_name" || e.reason === "near_miss");
239
+ return [
240
+ ...lists.skillInert.map((e) => ({ source: "skill", ...e })),
241
+ ...requestInert.map((e) => ({ source: "request", ...e })),
242
+ ...deploymentInert.map((e) => ({ source: "deployment", ...e })),
243
+ ];
206
244
  }
207
245
  export function withDeploymentAllowedTools(deps) {
208
246
  if (deps.allowedTools === undefined)
@@ -276,6 +314,7 @@ const SHADOW_REASON = {
276
314
  rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
277
315
  ask_rule: mandateDisclosureOf(undefined),
278
316
  denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
317
+ classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides",
279
318
  requires_user_interaction: "this tool's server declares that a call must be put to a person",
280
319
  operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
281
320
  shell_floor: mandateDisclosureOf("operator_always"),
@@ -327,14 +366,38 @@ export function cardDisclosureOf(decision, fallbackMessage) {
327
366
  }
328
367
  export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
329
368
  const NOTICE_ENTRIES_SHOWN = 5;
369
+ export function inertEntryPhrase(e) {
370
+ const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
371
+ const why = (() => {
372
+ if (e.reason === "near_miss")
373
+ return `near_miss: the mounted spelling is "${spelling}"`;
374
+ if (e.reason !== "tool_name" || e.verdict === undefined)
375
+ return e.reason;
376
+ switch (e.verdict) {
377
+ case "retired":
378
+ return `tool_name: a retired tool name${spelling !== undefined ? `, now "${spelling}"` : ""}`;
379
+ case "unprefixed_namespace":
380
+ return 'tool_name: a "__" name with no protocol prefix';
381
+ case "unmintable_segment":
382
+ return `tool_name: a peer segment the engine never mints${spelling !== undefined ? `, write "${spelling}"` : ""}`;
383
+ default: {
384
+ const exhaustive = e.verdict;
385
+ return exhaustive;
386
+ }
387
+ }
388
+ })();
389
+ return `${inlineUntrusted(JSON.stringify(e.entry), 122)} (${why})`;
390
+ }
330
391
  export function allowedToolsNotGrantedNotice(inert, sessionId) {
331
392
  const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
332
- const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} "${inlineUntrusted(e.entry, 120)}" (${e.reason})`);
393
+ const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} ${inertEntryPhrase(e)}`);
333
394
  const rest = inert.length - shown.length;
395
+ const refusing = inert.some((e) => "list" in e && e.list === "disallowed-tools");
334
396
  return {
335
397
  code: ALLOWED_TOOL_NOT_GRANTED_CODE,
336
398
  message: `${inert.length} allowed-tools entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
337
- "A list holding `*` voids the list, `mcp__*` names no server, and a write-path or domain entry has no allow reading; name the tools (or `mcp__<server>`) instead.",
399
+ "A list holding `*` voids the list, `mcp__*` names no server, a write-path or domain entry has no allow reading, and a tool name must be spelled as the tool is mounted; name the tools (or `mcp__<server>`) instead." +
400
+ (refusing ? " Such an entry in a disallowed-tools list keeps its Skill from activating: a refusal that reads as nothing is not dropped." : ""),
338
401
  detail: { ...(sessionId !== undefined ? { sessionId } : {}), entries: inert.map((e) => ({ ...e })) },
339
402
  };
340
403
  }
@@ -445,8 +445,11 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
445
445
  * row cell by cell, because the reference implementation judges the declaration ahead of its allow-rule lookup and its bypass release.
446
446
  * `unstated` is the mode's answer for a call nobody stated: a tool whose permission seat is `passthrough` (it states no
447
447
  * permission of its own — a remote peer's tool) reached the end of the fold with an allow no layer stated.
448
+ * `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
449
+ * (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
450
+ * answers it; the classifier is never asked again).
448
451
  */
449
- export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
452
+ export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
450
453
  export type AskClass = (typeof ASK_CLASSES)[number];
451
454
  /** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
452
455
  export declare function isAskClass(v: unknown): v is AskClass;
@@ -549,6 +552,13 @@ export declare const MODE_ARBITRATION: {
549
552
  readonly ruleMayClear: false;
550
553
  readonly hookAskFloor: false;
551
554
  };
555
+ readonly classifier_no_ruling: {
556
+ readonly bypassImmune: true;
557
+ readonly classifierEligible: false;
558
+ readonly sandboxAdmission: false;
559
+ readonly ruleMayClear: false;
560
+ readonly hookAskFloor: false;
561
+ };
552
562
  readonly requires_user_interaction: {
553
563
  readonly bypassImmune: true;
554
564
  readonly classifierEligible: false;
@@ -725,9 +735,10 @@ export declare function classifyAsk(d: Extract<PermissionResult, {
725
735
  * and the ask's prompt as what was requested. The gate appends the caller-side remedy. */
726
736
  export declare function headlessDenialMessage(toolName: string, askMessage: string): string;
727
737
  /** The state of the approver seat a remedy is written for: `absent` — no approver is wired for the leg; `unavailable` — one is
728
- * wired and answered that nobody is reachable for this ask. Absent in the input reads as `absent` (the posture the
729
- * function was written for). */
730
- export type HeadlessApproverSeat = "absent" | "unavailable";
738
+ * wired and answered that nobody is reachable for this ask; `declared_none` — the run declared that nobody on it can be
739
+ * asked (`approvalReach: "none"`), whatever is wired. Absent in the input reads as `absent` (the posture the function was
740
+ * written for). */
741
+ export type HeadlessApproverSeat = "absent" | "unavailable" | "declared_none";
731
742
  /**
732
743
  * The ONE sentence a headless auto-deny (an ask that reached its final deny because no approver is wired) adds
733
744
  * after its refusal: the caller-side change that would have let this call run. Read off the effective mode, the
@@ -741,7 +752,7 @@ export type HeadlessApproverSeat = "absent" | "unavailable";
741
752
  * · any other releasable ask — `bypassPermissions` releases it (disclosed on the call).
742
753
  * Every arm also names the approver, in the clause of the seat's own state ({@link HeadlessApproverSeat}): none wired ⇒
743
754
  * wire one; one wired that answered `unavailable` for this ask ⇒ make a person reachable through it (a wired approver is
744
- * never told to be wired). The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
755
+ * never told to be wired); the run declared nobody can be asked ⇒ run it where a person can be, or park it. The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
745
756
  * they call this function, neither spells it.
746
757
  */
747
758
  export declare function headlessApprovalRemedy(input: {
@@ -177,6 +177,7 @@ export const ASK_CLASSES = Object.freeze([
177
177
  "rule_unreadable",
178
178
  "ask_rule",
179
179
  "denial_limit_fallback",
180
+ "classifier_no_ruling",
180
181
  "requires_user_interaction",
181
182
  "operator_forced",
182
183
  "shell_floor",
@@ -206,6 +207,7 @@ export const MODE_ARBITRATION = {
206
207
  rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
207
208
  ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
208
209
  denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
210
+ classifier_no_ruling: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
209
211
  requires_user_interaction: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
210
212
  operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
211
213
  shell_floor: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
@@ -220,7 +222,7 @@ export const MODE_ARBITRATION = {
220
222
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
221
223
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
222
224
  };
223
- export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
225
+ export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
224
226
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
225
227
  export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
226
228
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
@@ -253,6 +255,8 @@ export function decisionAskClassesOf(d) {
253
255
  out.add("ask_rule");
254
256
  if (d.denialLimitFallback !== undefined)
255
257
  out.add("denial_limit_fallback");
258
+ if (d.classifierNoRuling !== undefined)
259
+ out.add("classifier_no_ruling");
256
260
  if (d.authoredBy === "deployment")
257
261
  out.add("operator_forced");
258
262
  if (d.probeMandated === true)
@@ -333,10 +337,14 @@ export function classifyAsk(d, facts) {
333
337
  return { classes, askClass, row: foldArbitrationRow(classes) };
334
338
  }
335
339
  export function headlessDenialMessage(toolName, askMessage) {
336
- return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (no approver is wired for "${toolName}" — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
340
+ return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
337
341
  }
338
342
  export function headlessApprovalRemedy(input) {
339
- const approver = input.approverSeat === "unavailable" ? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)" : "wire an approver (`onAsk`) to put it to a person";
343
+ const approver = input.approverSeat === "unavailable"
344
+ ? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)"
345
+ : input.approverSeat === "declared_none"
346
+ ? 'run it where a person can be asked — this run declares `approvalReach: "none"` (nobody on it can be asked) — or arm a durable approval park so the ask waits for a person'
347
+ : "wire an approver (`onAsk`) to put it to a person";
340
348
  const c = input.classification;
341
349
  if (input.mode === "acceptEdits" && input.unplacedOptions !== undefined && (c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"))))
342
350
  return `${fileCommandOptionsSentence(input.unplacedOptions.program)} Spell the ${input.unplacedOptions.program} without option words for the mode to answer it, or ${approver}`;
@@ -5,8 +5,9 @@
5
5
  * · `approver_unavailable` — the approver consulted for the call (the run's own seat, or an ancestor's
6
6
  * frozen seat on a delegated child) answered the ROUTING question `unavailable` and no durable park
7
7
  * took the call afterwards (`tool_end.gate.settlement.kind:"approver_unavailable"`);
8
- * · `no_approver_wired` — no approver (or, for a question, no question face) is wired for this run, or
9
- * the seat is the deny posture string: the headless auto-deny (`settlement.kind:"no_approver"`);
8
+ * · `no_approver_wired` — nobody in this run can be asked: no approver (or, for a question, no question face) is
9
+ * wired, the seat is the deny posture string, or the run declared that nobody on it can be asked
10
+ * (`approvalReach: "none"`): the headless auto-deny (`settlement.kind:"no_approver"`);
10
11
  * · `mandate_unreconstructible` — an inherited durable-approval mandate binds the ask and THIS leg has no
11
12
  * park facility to honor it, so the delegated child refuses in-fold rather than let a synchronous layer
12
13
  * resolve what its ancestor bound to a park (an in-fold deny; no settlement rides the frame).
@@ -6,7 +6,7 @@ export function isAskUnresolvableCause(v) {
6
6
  }
7
7
  const CAUSE_CLAUSE = {
8
8
  approver_unavailable: "the approver reported that nobody is reachable for this ask",
9
- no_approver_wired: "no approver (or question face) is wired for this run — the headless posture",
9
+ no_approver_wired: "nobody in this run can be asked to answer it — the headless posture",
10
10
  mandate_unreconstructible: "an inherited durable-approval mandate binds this ask and this leg has no park facility to honor it",
11
11
  };
12
12
  const CAUSE_REMEDY_SEAT = {
@@ -166,11 +166,12 @@ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCaus
166
166
  */
167
167
  export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
168
168
  /**
169
- * The text an ask carries to a person when the classifier could not rule on it and the leg has an approval seat
170
- * (the gate's classifier station and the inherited station): the ask's own text first — the card still says what
171
- * is being asked — then why auto mode is asking at all. One writer for both stations.
169
+ * The text an ask carries to a person when the classifier did not rule on it ({@link rungDidNotRule}) and the leg has an
170
+ * approval seat (the gate's classifier station and the inherited station): the ask's own text first — the card still
171
+ * says what is being asked — then why auto mode is asking at all, read off the same cause the ask's `classifierNoRuling`
172
+ * member carries (a client that does not read the member reads this). One writer for both stations.
172
173
  */
173
- export declare function classifierUnavailableAskMessage(askText: string, cause: AutoModeUnavailableCause): string;
174
+ export declare function classifierNoRulingAskMessage(askText: string, cause: AutoModeFallbackCause): string;
174
175
  /**
175
176
  * The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate
176
177
  * this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict,
@@ -84,7 +84,9 @@ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
84
84
  "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
85
85
  "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
86
86
  }
87
- export function classifierUnavailableAskMessage(askText, cause) {
87
+ export function classifierNoRulingAskMessage(askText, cause) {
88
+ if (cause === "parse_error")
89
+ return `${askText}\n\nThe auto-mode classifier replied without a verdict, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
88
90
  const parenthetical = cause === "timeout" ? " (timed out)" : "";
89
91
  return `${askText}\n\nThe auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
90
92
  }