@sema-agent/core 7.42.1 → 7.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -3
  5. package/dist/agents/teacher.js +1 -1
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/core/allow-entry.d.ts +50 -31
  8. package/dist/core/allow-entry.js +100 -37
  9. package/dist/core/ask-origin.d.ts +16 -5
  10. package/dist/core/ask-origin.js +11 -3
  11. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  12. package/dist/core/ask-unresolvable-notice.js +1 -1
  13. package/dist/core/auto-mode.d.ts +5 -4
  14. package/dist/core/auto-mode.js +3 -1
  15. package/dist/core/checkpoint-store.d.ts +18 -7
  16. package/dist/core/checkpoint-store.js +6 -3
  17. package/dist/core/child-clamp-seats.d.ts +15 -1
  18. package/dist/core/child-clamp-seats.js +6 -2
  19. package/dist/core/classifier-unavailable-arm.d.ts +42 -23
  20. package/dist/core/classifier-unavailable-arm.js +14 -3
  21. package/dist/core/decision-receipt.js +2 -0
  22. package/dist/core/gate-lanes.js +37 -29
  23. package/dist/core/hooks.d.ts +6 -1
  24. package/dist/core/hooks.js +3 -0
  25. package/dist/core/mode-policy.d.ts +5 -4
  26. package/dist/core/mode-policy.js +4 -0
  27. package/dist/core/permission-rule-consent.d.ts +3 -2
  28. package/dist/core/permission-rule-consent.js +25 -20
  29. package/dist/core/permission-rule-model.d.ts +47 -7
  30. package/dist/core/permission-rule-model.js +80 -6
  31. package/dist/core/permission-rules.d.ts +8 -2
  32. package/dist/core/permission-rules.js +26 -9
  33. package/dist/core/runner/classifier-ladder.d.ts +10 -8
  34. package/dist/core/runner/classifier-ladder.js +3 -1
  35. package/dist/core/runner/denial-limit-arms.d.ts +25 -8
  36. package/dist/core/runner/denial-limit-arms.js +24 -10
  37. package/dist/core/runner/gate-exit.js +2 -1
  38. package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
  39. package/dist/core/runner/permission-rule-lanes.js +8 -2
  40. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  41. package/dist/core/runner/prepare-ask-lane.js +2 -1
  42. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
  43. package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
  44. package/dist/core/runner/prepare-config-doors.d.ts +3 -0
  45. package/dist/core/runner/prepare-config-doors.js +6 -5
  46. package/dist/core/runner/prepare-gate-stations.js +1 -1
  47. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  48. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  49. package/dist/core/runner/prepare-park-ask.d.ts +4 -4
  50. package/dist/core/runner/prepare-park-ask.js +2 -4
  51. package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
  52. package/dist/core/runner/prepare-policy-chain.js +59 -38
  53. package/dist/core/runner/prepare-project-context.js +6 -2
  54. package/dist/core/runner/prepare-task.js +6 -6
  55. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
  56. package/dist/core/runner/prepare-wiring-manifest.js +8 -9
  57. package/dist/core/runner/resume-checkpoint-screen.js +1 -1
  58. package/dist/core/runner/synthetic-tools.d.ts +3 -2
  59. package/dist/core/runner/synthetic-tools.js +1 -1
  60. package/dist/core/session-policy-store.d.ts +3 -3
  61. package/dist/core/session-policy-store.js +18 -4
  62. package/dist/core/skill-grant-binding.d.ts +44 -0
  63. package/dist/core/skill-grant-binding.js +22 -0
  64. package/dist/core/skill-grant.d.ts +59 -28
  65. package/dist/core/skill-grant.js +96 -23
  66. package/dist/core/task-spec.d.ts +14 -1
  67. package/dist/core/tighten-task-spec.d.ts +2 -0
  68. package/dist/core/tighten-task-spec.js +3 -0
  69. package/dist/core/tool-name-aliases.d.ts +35 -5
  70. package/dist/core/tool-name-aliases.js +29 -4
  71. package/dist/core/tool-policy.d.ts +10 -0
  72. package/dist/core/tool-policy.js +9 -2
  73. package/dist/core/tool-spec.d.ts +7 -0
  74. package/dist/core/wiring-manifest.d.ts +56 -14
  75. package/dist/core/wiring-manifest.js +29 -4
  76. package/dist/index.d.ts +3 -2
  77. package/dist/index.js +2 -2
  78. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  79. package/dist/orchestration/run-workflow-tool.js +1 -1
  80. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  81. package/dist/orchestration/workflow-primitives.js +6 -6
  82. package/package.json +1 -1
  83. package/test/export-surface.snapshot.json +11 -1
  84. package/wire-changes/core@7.43.0.json +985 -0
@@ -1,30 +1,31 @@
1
1
  /**
2
- * The ONE exit for an auto-mode classification that could not RULE (`unavailable`: the classifier leg failed, or its
3
- * per-level budget ran out) — shared by both classifier stations, the leg's gate and an ancestor's frozen classifier
4
- * in the policy fold, so the rule is written once.
2
+ * The ONE exit for an auto-mode classification that did not RULE ({@link rungDidNotRule}: the classifier leg failed or its
3
+ * budget ran out — `error` / `timeout` — or it replied with no verdict at all — `parse_error`) — shared by both classifier
4
+ * stations, the leg's gate and an ancestor's frozen classifier in the policy fold, so the rule is written once.
5
5
  *
6
- * ONE fork, on the APPROVAL SEAT (does an ask this station hands on reach a person — a live approver, or a durable
7
- * park): with one, the ask goes on, its text saying why auto mode is asking, and a person decides; without one, the
8
- * call is refused with CC's `x1t` sentence and the classification's FORM (`unavailable`) is attested on the deny. The seat is
9
- * the only input to the fork — not the cause, not the mode. The handed-on ask carries `requiresRealApproval`: no
10
- * automatic lane may answer it in the classifier's stead — not a blanket `onAsk:"allow"`, not the sandbox admission, not a
11
- * budget resolver over its park (the park mints the non-budgetable kind). CC refuses either way; asking where a person can be
12
- * asked is a declared departure (a classifier that is slow or down must not turn an attended session into a string
13
- * of refusals). Neither arm is a block: the denial limit counts classifier RULINGS, and a person's refusal of the
14
- * handed-on ask is a person's, not the classifier's.
6
+ * ONE fork, on the APPROVAL SEAT (does an ask this station hands on reach a person — a live approver, or a durable park):
7
+ * with one, the ask goes on carrying why (`classifierNoRuling`, its text saying it too), and a person decides — never
8
+ * counted (a person's refusal of it is a person's); without one, an unavailable classification is refused with CC's `x1t`
9
+ * sentence (its FORM, `unavailable`, attested on the deny) and is never counted, while a reply with no verdict goes back to
10
+ * the station's ruling path (`counted`): refused with its own sentence and counted on the denial limit, so the bound falls
11
+ * back to a person (CC: `Cr` excludes unavailable; failureMode is counted). The handed-on ask carries `requiresRealApproval`:
12
+ * no automatic lane may answer it in the classifier's stead — not a blanket `onAsk:"allow"`, not the sandbox admission, not a
13
+ * budget resolver over its park. Asking where a person can be asked is a declared departure (CC refuses).
15
14
  *
16
15
  * The observer hears every such classification with where the call went, except while the call is being aborted — a halt,
17
- * or the run's end (the classifier did not fail; the call ended). It is observe-only: a throw never changes the arm.
16
+ * or the run's end. It is observe-only: a throw never changes the arm.
18
17
  */
19
- import { type AutoModeClassifierRound, type AutoModeUnavailableCause } from "./auto-mode.js";
20
- import { type PermissionResult, type ToolCallRequest } from "./tool-policy.js";
21
- /** Where an `unavailable` classification sent the call: `asked` = on to the approval seat, `denied` = refused. */
18
+ import { type AutoModeClassifierRound, type AutoModeDenialTracker, type AutoModeFallbackCause, type UnarmedDenialLimitFallback } from "./auto-mode.js";
19
+ import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "./tool-policy.js";
20
+ /** Where a classification that did not rule sent the call: `asked` = on to the approval seat, `denied` = refused. */
22
21
  export type ClassifierUnavailableDisposition = "asked" | "denied";
23
- /** What the stations' observer receives — the facts the `classifier.unavailable` notice is composed from. */
22
+ /** What the stations' observer receives — the facts the `classifier.unavailable` notice is composed from. `cause` is why
23
+ * the classification did not rule ({@link AutoModeFallbackCause}: `error` / `timeout`, or `parse_error` for a reply with no
24
+ * verdict). */
24
25
  export interface ClassifierUnavailableInfo {
25
26
  toolName: string;
26
27
  toolCallId: string;
27
- cause: AutoModeUnavailableCause;
28
+ cause: AutoModeFallbackCause;
28
29
  disposition: ClassifierUnavailableDisposition;
29
30
  }
30
31
  export type ClassifierUnavailableObserver = (info: ClassifierUnavailableInfo) => void;
@@ -34,24 +35,42 @@ export interface ClassifierUnavailableSeat {
34
35
  approvalSeat?: boolean;
35
36
  onUnavailable?: ClassifierUnavailableObserver;
36
37
  }
37
- /** The arm the fork chose: the ask to hand on, or the refusal. */
38
- export type ClassifierUnavailableArm<A> = {
38
+ /** The arm the fork chose: the ask to hand on, the refusal, or — a reply with no verdict and no seat — the station's own
39
+ * counted ruling path. */
40
+ export type ClassifierNoRulingArm<A> = {
39
41
  kind: "asked";
40
42
  ask: A;
41
43
  } | {
42
44
  kind: "denied";
43
45
  deny: PermissionResult;
46
+ } | {
47
+ kind: "counted";
44
48
  };
45
- export declare function classifierUnavailableArm<A extends Extract<PermissionResult, {
49
+ export declare function classifierNoRulingArm<A extends Extract<PermissionResult, {
46
50
  action: "ask";
47
51
  }>>(args: {
48
52
  ask: A;
49
53
  req: ToolCallRequest;
50
- cause: AutoModeUnavailableCause;
54
+ cause: AutoModeFallbackCause;
51
55
  /** The ladder's attribution, for the refusal's sentence (it names both rungs when the ladder moved). */
52
56
  attribution?: AutoModeClassifierRound;
53
57
  seat: ClassifierUnavailableSeat | undefined;
54
58
  /** The call is being aborted (its per-call signal — a halt, or the run's end): the observer is not told (the arm is chosen
55
59
  * as ever). */
56
60
  aborted: boolean;
57
- }): ClassifierUnavailableArm<A>;
61
+ }): ClassifierNoRulingArm<A>;
62
+ /**
63
+ * The no-ruling card's settlement (both stations): a card THIS station's no-ruling arm handed on for this call (`mintedHere` —
64
+ * a policy's own `classifierNoRuling`, or a card another station minted, is not this station's classification and counts
65
+ * nothing here) that the seat's deployment POLICY refused (settlement `policy_refused` — a machine settled it, not a person)
66
+ * counts on the denial limit like a ruling refusal, because "handed to a person" means a person settled it. Returns the fallback the count minted when it reached the
67
+ * bound — the station then puts the same call to the same seat as the denial-limit fallback, and a deny nobody judged there
68
+ * stops the run through the existing headless arm. A person's answer (allow or refusal), a fallback card, and every other
69
+ * settlement count nothing.
70
+ */
71
+ export declare function settleNoRulingAsk(opts: {
72
+ ask: PermissionResult;
73
+ mintedHere: boolean;
74
+ resolved: Pick<ResolvedAsk, "action" | "settlement">;
75
+ tracker: AutoModeDenialTracker | undefined;
76
+ }): UnarmedDenialLimitFallback | undefined;
@@ -1,6 +1,6 @@
1
- import { classifierUnavailableAskMessage, classifierUnavailableDenyMessage } from "./auto-mode.js";
1
+ import { classifierNoRulingAskMessage, classifierUnavailableDenyMessage } from "./auto-mode.js";
2
2
  import { attestClassifierDenyCause, carryEngineAttestations } from "./tool-policy.js";
3
- export function classifierUnavailableArm(args) {
3
+ export function classifierNoRulingArm(args) {
4
4
  const { ask, req, cause } = args;
5
5
  const seated = args.seat?.approvalSeat === true;
6
6
  if (!args.aborted) {
@@ -11,7 +11,18 @@ export function classifierUnavailableArm(args) {
11
11
  }
12
12
  }
13
13
  if (seated)
14
- return { kind: "asked", ask: carryEngineAttestations(ask, { ...ask, message: classifierUnavailableAskMessage(ask.message ?? `approval required for "${req.toolName}"`, cause), requiresRealApproval: true }) };
14
+ return { kind: "asked", ask: carryEngineAttestations(ask, { ...ask, message: classifierNoRulingAskMessage(ask.message ?? `approval required for "${req.toolName}"`, cause), requiresRealApproval: true, classifierNoRuling: cause }) };
15
+ if (cause === "parse_error")
16
+ return { kind: "counted" };
15
17
  const deny = { action: "deny", message: classifierUnavailableDenyMessage(req.toolName, cause, args.attribution), decisionReason: "classifier" };
16
18
  return { kind: "denied", deny: attestClassifierDenyCause(deny, "unavailable", req) };
17
19
  }
20
+ export function settleNoRulingAsk(opts) {
21
+ const { ask, resolved } = opts;
22
+ if (!opts.mintedHere || ask.action !== "ask" || ask.classifierNoRuling === undefined || ask.denialLimitFallback !== undefined)
23
+ return undefined;
24
+ if (resolved.action !== "deny" || resolved.settlement?.kind !== "policy_refused")
25
+ return undefined;
26
+ const tracked = opts.tracker?.recordBlock();
27
+ return tracked?.limitReached === true ? tracked.fallback : undefined;
28
+ }
@@ -1,5 +1,6 @@
1
1
  import { types } from "node:util";
2
2
  import { ASK_CLASSES, ASK_ORIGINS, isAskClass, isAskOrigin, isPersistedRuleMandate, PERSISTED_RULE_MANDATES } from "./ask-origin.js";
3
+ import { AUTO_MODE_FALLBACK_CAUSES, isAutoModeFallbackCause } from "./auto-mode.js";
3
4
  import { thrownText } from "./thrown-value.js";
4
5
  const ENGINE_REFUSALS = new WeakSet();
5
6
  export const refusedOutOfContract = (message) => {
@@ -28,6 +29,7 @@ const CLOSED_MEMBERS = [
28
29
  { member: "guard", words: GUARDS, has: memberOf(GUARDS) },
29
30
  { member: "ruleUnreadable", words: RULE_UNREADABLE_WORDS, has: memberOf(RULE_UNREADABLE_WORDS) },
30
31
  { member: "authoredBy", words: AUTHORED_BY_WORDS, has: memberOf(AUTHORED_BY_WORDS) },
32
+ { member: "classifierNoRuling", words: AUTO_MODE_FALLBACK_CAUSES, has: isAutoModeFallbackCause },
31
33
  { member: "requiresRealApproval", words: [true, false], has: (v) => v === true || v === false },
32
34
  { member: "probeMandated", words: [true], has: (v) => v === true },
33
35
  { member: "mandated", words: [true], has: (v) => v === true },
@@ -1,7 +1,7 @@
1
1
  import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
2
2
  import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
3
- import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
- import { classifierUnavailableArm } from "./classifier-unavailable-arm.js";
3
+ import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, rungDidNotRule, unarmedWindow } from "./auto-mode.js";
4
+ import { classifierNoRulingArm, settleNoRulingAsk } from "./classifier-unavailable-arm.js";
5
5
  import { SETTLEMENT_NOBODY_JUDGED } from "./gate-outcome.js";
6
6
  import { inlineUntrusted } from "./untrusted-text.js";
7
7
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
@@ -228,6 +228,12 @@ export async function runGateLanes(pass) {
228
228
  pass.decision = shadows.length > 0 ? { ...standing, allowRuleShadowed: shadows } : standing;
229
229
  }
230
230
  const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
231
+ let noRulingMinted = false;
232
+ const fallbackAskOf = (standingAsk, fallback, latestBlockedAction) => {
233
+ const { classifierNoRuling: _why, ...standing } = standingAsk;
234
+ const fallbackAsk = { ...standing, message: denialLimitFallbackMessage(fallback, latestBlockedAction), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback };
235
+ return stampAskOrigin(fallbackAsk, originFacts, classifyStanding(fallbackAsk, originFacts));
236
+ };
231
237
  if (pass.decision.action === "ask") {
232
238
  const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
233
239
  pass.decision = stampAskOrigin(stamped, originFacts, classifyStanding(stamped, originFacts));
@@ -271,36 +277,31 @@ export async function runGateLanes(pass) {
271
277
  ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
272
278
  };
273
279
  }
274
- else if (verdict?.kind === "unavailable") {
275
- const arm = classifierUnavailableArm({ ask: pass.decision, req: pass.req, cause: verdict.cause, attribution: verdict.classifier, seat: input.autoMode.unavailable, aborted: classifySignal?.aborted === true });
276
- if (arm.kind === "asked") {
280
+ else if (verdict !== undefined) {
281
+ const noRuling = rungDidNotRule(verdict);
282
+ const arm = noRuling !== undefined ? classifierNoRulingArm({ ask: pass.decision, req: pass.req, cause: noRuling, attribution: verdict.classifier, seat: input.autoMode.unavailable, aborted: classifySignal?.aborted === true }) : undefined;
283
+ if (arm?.kind === "asked") {
284
+ noRulingMinted = true;
277
285
  pass.decision = stampAskOrigin(arm.ask, originFacts, classifyStanding(arm.ask, originFacts));
278
286
  pass.classifierRound = undefined;
279
287
  }
280
- else
288
+ else if (arm?.kind === "denied")
281
289
  pass.decision = arm.deny;
282
- }
283
- else if (verdict !== undefined) {
284
- const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
285
- const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
286
- const tracked = input.autoMode.denialTracking?.recordBlock();
287
- if (tracked?.limitReached === true) {
288
- const fallbackAsk = {
289
- ...pass.decision,
290
- message: denialLimitFallbackMessage(tracked.fallback, reason || category || toolName),
291
- decisionReason: "classifier",
292
- requiresRealApproval: true,
293
- denialLimitFallback: tracked.fallback,
294
- };
295
- pass.decision = stampAskOrigin(fallbackAsk, originFacts, classifyStanding(fallbackAsk, originFacts));
296
- }
297
290
  else {
298
- const denied = {
299
- action: "deny",
300
- message: verdict.kind === "block" ? `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : reason,
301
- decisionReason: "classifier",
302
- };
303
- pass.decision = verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", pass.req) : denied;
291
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
292
+ const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
293
+ const tracked = input.autoMode.denialTracking?.recordBlock();
294
+ if (tracked?.limitReached === true) {
295
+ pass.decision = fallbackAskOf(pass.decision, tracked.fallback, reason || category || toolName);
296
+ }
297
+ else {
298
+ const denied = {
299
+ action: "deny",
300
+ message: verdict.kind === "block" ? `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : reason,
301
+ decisionReason: "classifier",
302
+ };
303
+ pass.decision = verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", pass.req) : denied;
304
+ }
304
305
  }
305
306
  }
306
307
  if (pass.decision.action === "deny") {
@@ -389,6 +390,7 @@ export async function runGateLanes(pass) {
389
390
  const carryOf = (d) => ({
390
391
  ...(callSignal !== undefined ? { signal: callSignal } : {}),
391
392
  ...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
393
+ ...(d.action === "ask" && d.classifierNoRuling !== undefined ? { classifierNoRuling: d.classifierNoRuling } : {}),
392
394
  ...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
393
395
  ...(d.action === "ask" && d.askClass !== undefined && d.classes !== undefined ? { askClass: d.askClass, classes: d.classes } : {}),
394
396
  ...(d.action === "ask" && d.mandated === true ? { mandated: true, ...(d.mandate !== undefined ? { mandate: d.mandate } : {}) } : {}),
@@ -444,8 +446,14 @@ export async function runGateLanes(pass) {
444
446
  }
445
447
  }
446
448
  if (pass.decision.action === "ask") {
447
- const askBeforeResolve = pass.decision;
448
- const resolved = await (callSignal !== undefined ? resolveAsk(pass.decision, pass.req, callSignal) : resolveAsk(pass.decision, pass.req));
449
+ let askBeforeResolve = pass.decision;
450
+ const resolveOnce = (d) => (callSignal !== undefined ? resolveAsk(d, pass.req, callSignal) : resolveAsk(d, pass.req));
451
+ let resolved = await resolveOnce(askBeforeResolve);
452
+ const refallback = settleNoRulingAsk({ ask: askBeforeResolve, mintedHere: noRulingMinted, resolved, tracker: input.autoMode?.denialTracking });
453
+ if (refallback !== undefined && askBeforeResolve.action === "ask") {
454
+ askBeforeResolve = fallbackAskOf(askBeforeResolve, refallback, toolName);
455
+ resolved = await resolveOnce(askBeforeResolve);
456
+ }
449
457
  ledger.fromResolved(resolved, askBeforeResolve, toolName);
450
458
  if (resolved.action === "deny")
451
459
  pass.deniedBy = "ask_resolution";
@@ -7,7 +7,7 @@ export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-
7
7
  export { cloneObserverInput } from "./runner/gate-exit.js";
8
8
  import { type AskClass } from "./ask-class.js";
9
9
  import { type PermissionMode } from "./ask-origin.js";
10
- import type { AutoModeDenialTracker } from "./auto-mode.js";
10
+ import { type AutoModeDenialTracker } from "./auto-mode.js";
11
11
  export { formatHookFeedback } from "./reminder-mint.js";
12
12
  import type { WiringLegKind } from "./wiring-manifest.js";
13
13
  import { type SafeNotifier } from "./safe-notify.js";
@@ -1464,6 +1464,8 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1464
1464
  * `autoMode` presence alone as the arming fact.
1465
1465
  */
1466
1466
  permissionMode?: () => import("./ask-origin.js").PermissionMode;
1467
+ /** The leg's approval-seat form (`WiringManifest.ask.form`): the deny exit's way-out clause names a declaration, never a missing approver. */
1468
+ askSeatForm?: import("./wiring-manifest.js").AskSeamForm;
1467
1469
  /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
1468
1470
  * absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
1469
1471
  toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
@@ -1764,6 +1766,8 @@ export interface AskCarry {
1764
1766
  * the `0` a park row shows is written. Persisted on the row so a durable card shows the counts the
1765
1767
  * synchronous card shows. Absent then means: not a fallback ask. */
1766
1768
  readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1769
+ /** The surviving ask's no-ruling member ({@link import("./tool-policy.js").AskRequest.classifierNoRuling}), for the row's twin. */
1770
+ readonly classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
1767
1771
  /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
1768
1772
  * on the row so a durable card is rendered by the same word the synchronous card is. */
1769
1773
  readonly origin?: import("./ask-origin.js").AskOrigin;
@@ -1788,6 +1792,7 @@ export interface AskCarry {
1788
1792
  * the same screen the summary applies to the word). */
1789
1793
  export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1790
1794
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1795
+ classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
1791
1796
  origin?: import("./ask-origin.js").AskOrigin;
1792
1797
  askClass?: import("./ask-origin.js").AskClass;
1793
1798
  classes?: readonly import("./ask-origin.js").AskClass[];
@@ -8,6 +8,7 @@ export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-
8
8
  export { cloneObserverInput } from "./runner/gate-exit.js";
9
9
  import { brandPolicyAskClass } from "./ask-class.js";
10
10
  import { isAskOrigin, screenAskClassMembers } from "./ask-origin.js";
11
+ import { isAutoModeFallbackCause } from "./auto-mode.js";
11
12
  import { inlineUntrusted } from "./untrusted-text.js";
12
13
  export { formatHookFeedback } from "./reminder-mint.js";
13
14
  import { createSafeNotifier } from "./safe-notify.js";
@@ -242,9 +243,11 @@ export function askCarryRowMembers(carry) {
242
243
  if (carry === undefined)
243
244
  return {};
244
245
  const origin = carry.origin;
246
+ const why = carry.classifierNoRuling;
245
247
  const classed = screenAskClassMembers(carry.askClass, carry.classes);
246
248
  return {
247
249
  ...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
250
+ ...(isAutoModeFallbackCause(why) ? { classifierNoRuling: why } : {}),
248
251
  ...(isAskOrigin(origin) ? { origin } : {}),
249
252
  ...(classed !== undefined ? classed : {}),
250
253
  };
@@ -163,17 +163,18 @@ export declare function modeRequestAfterResume(seat: PermissionMode | undefined,
163
163
  export declare function refusePermissionModeDoors(spec: Pick<TaskSpec, "permissionMode" | "shellGate">): PermissionMode | undefined;
164
164
  /**
165
165
  * The doors a RESUMED leg's prepare refuses at, read as one screen for the resume ladder to run BEFORE the claim: the mode
166
- * doors on the re-supplied config ({@link refusePermissionModeDoors}), its output-style seat (`resolveOutputStyleFrame`) and
167
- * the mode words the checkpoint row recorded (`recordedModeSeed`) — prepare's own functions, so the refusal carries the very
166
+ * doors on the re-supplied config ({@link refusePermissionModeDoors}), its output-style seat (`resolveOutputStyleFrame`), its
167
+ * approval-reach declaration (`screenApprovalReach`), the skills it invokes by name (`screenUserInvokedSkills` — the part no
168
+ * roster changes; a name the leg's roster reads as nothing is prepare's) and the mode words the checkpoint row recorded (`recordedModeSeed`) — prepare's own functions, so the refusal carries the very
168
169
  * sentence and code prepare would throw. It leaves by the resume's ONE refusal channel, `CheckpointError`
169
170
  * `checkpoint.config_refused` (`message` = prepare's sentence, `detail.code` = prepare's code, `detail.field` = the input that
170
171
  * carries it: `"taskConfig"` or `"inheritedGate"`), where prepare itself would end the run `failed` after the claim — a
171
172
  * refusal there consumes the row and reopens it as a system retry that replays the recorded decision, which a CONFIG
172
173
  * correction cannot reach. Before the claim the row stays `pending` and byte-identical, and the same token is redeemable by a
173
- * corrected resume. A throw that is not one of prepare's four codes (a caller's accessor failing) is not a refusal and leaves
174
+ * corrected resume. A throw that is not one of prepare's five codes (a caller's accessor failing) is not a refusal and leaves
174
175
  * as it was thrown.
175
176
  */
176
- export declare function refuseResumeDoors(config: Pick<TaskSpec, "permissionMode" | "shellGate" | "outputStyle">, recordedGate: Parameters<typeof recordedModeSeed>[0]): void;
177
+ export declare function refuseResumeDoors(config: Pick<TaskSpec, "permissionMode" | "shellGate" | "outputStyle" | "approvalReach" | "skills" | "userInvokedSkills">, recordedGate: Parameters<typeof recordedModeSeed>[0]): void;
177
178
  /** The three-member answer every mode question gets — the manifest member, the steer verb's return value and the
178
179
  * fold notice's detail share it. `requested` is absent when the caller declared nothing (a root inherits `default`,
179
180
  * a child inherits its parent). */
@@ -9,6 +9,8 @@ import { createSafeNotifier } from "./safe-notify.js";
9
9
  import { assertNoRetiredKeys, RETIRED_TASK_SPEC_KEYS } from "./retired-keys.js";
10
10
  import { resolveOutputStyleFrame } from "./output-style.js";
11
11
  import { CheckpointError, isCheckpointConfigRefusalCode, recordedModeSeed } from "./checkpoint-store.js";
12
+ import { screenApprovalReach } from "./wiring-manifest.js";
13
+ import { screenUserInvokedSkills } from "./skill-grant.js";
12
14
  export const PERMISSION_MODE_FOLD_REASONS = Object.freeze(["operator_deny", "face_absent", "inherited_ceiling"]);
13
15
  export const ACCEPT_EDITS_FILE_COMMANDS = Object.freeze(["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"]);
14
16
  const MODE_POLICY_TABLE = {
@@ -112,6 +114,8 @@ export function refuseResumeDoors(config, recordedGate) {
112
114
  screenAs("taskConfig", () => {
113
115
  refusePermissionModeDoors(config);
114
116
  resolveOutputStyleFrame(config.outputStyle);
117
+ screenApprovalReach(config.approvalReach);
118
+ screenUserInvokedSkills(config);
115
119
  });
116
120
  screenAs("inheritedGate", () => void recordedModeSeed(recordedGate));
117
121
  }
@@ -282,9 +282,10 @@ export declare function prepareCardApproval(opts: {
282
282
  principal?: string;
283
283
  /** design/182 §4.5 (additive): the structural owner — only the local-owner path needs it. */
284
284
  owner?: RuleOwner;
285
- /** The tool whose ask this card belongs to. Only the shell tool the rule lane speaks for is accepted. */
285
+ /** The tool whose ask this card belongs to. Only the shell tool the rule lane speaks for and the name tools (the skill-activation
286
+ * card) are accepted. */
286
287
  toolName: string;
287
- /** The command as adjudicated — the same bytes the ask carried. */
288
+ /** The command as adjudicated — the same bytes the ask carried; for a name tool (`Skill`), the name its call carried (`args.skill`). */
288
289
  command: string;
289
290
  /** The ask's call id and argument digest, recorded for reconciliation. */
290
291
  toolCallId?: string;
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
2
+ import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
3
3
  import { effectiveOrThrow } from "./permission-rule-provider.js";
4
4
  import { errText, isValidConsentScope, landRuleAdd, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
5
5
  export class InMemoryRuleApprovalRecordStore {
@@ -279,7 +279,8 @@ export async function prepareCardApproval(opts) {
279
279
  e.code = "config.invalid_argument";
280
280
  throw e;
281
281
  }
282
- if (opts.toolName !== CARD_RULE_TOOL)
282
+ const nameCard = ruleToolGrammarOf(opts.toolName) === "name";
283
+ if (opts.toolName !== CARD_RULE_TOOL && !nameCard)
283
284
  return undefined;
284
285
  const scope = (() => {
285
286
  if (opts.scope !== undefined) {
@@ -303,22 +304,26 @@ export async function prepareCardApproval(opts) {
303
304
  throw e;
304
305
  })();
305
306
  let coverage;
306
- try {
307
- const view = await effectiveOrThrow(storeForOwner(opts.deps.provider, owner), opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
308
- const table = view.rules;
309
- coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
310
- }
311
- catch {
312
- coverage = undefined;
313
- }
314
- const suggested = suggestRulesForCommand(opts.command, {
315
- ...(coverage !== undefined ? { coverage } : {}),
316
- scope,
317
- ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
318
- ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}),
319
- ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
320
- ...(opts.deps.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: opts.deps.deniesDirectoryRead } : {}),
321
- });
307
+ if (!nameCard) {
308
+ try {
309
+ const view = await effectiveOrThrow(storeForOwner(opts.deps.provider, owner), opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
310
+ const table = view.rules;
311
+ coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
312
+ }
313
+ catch {
314
+ coverage = undefined;
315
+ }
316
+ }
317
+ const suggested = nameCard
318
+ ? [suggestRuleForName(opts.toolName, opts.command)].filter((o) => o !== undefined)
319
+ : suggestRulesForCommand(opts.command, {
320
+ ...(coverage !== undefined ? { coverage } : {}),
321
+ scope,
322
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
323
+ ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}),
324
+ ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
325
+ ...(opts.deps.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: opts.deps.deniesDirectoryRead } : {}),
326
+ });
322
327
  const candidates = [];
323
328
  const claimedBy = new Map();
324
329
  const claim = (text) => {
@@ -349,7 +354,7 @@ export async function prepareCardApproval(opts) {
349
354
  rev: 0,
350
355
  candidates,
351
356
  offers,
352
- command: opts.command,
357
+ ...(nameCard ? {} : { command: opts.command }),
353
358
  createdAt: nowIso(opts.deps),
354
359
  ...(opts.toolCallId !== undefined ? { toolCallId: opts.toolCallId } : {}),
355
360
  ...(opts.boundInputHash !== undefined ? { boundInputHash: opts.boundInputHash } : {}),
@@ -510,7 +515,7 @@ async function confirmEditedCandidate(rec, edit, deps) {
510
515
  return no("edit_rejected", { message: "the record's candidates carry no single common scope — an edited candidate inherits the card's scope, and a record without one is malformed" });
511
516
  }
512
517
  if (rec.command === undefined) {
513
- return no("edit_rejected", { message: "the record does not carry the adjudicated command (minted before card edits existed) — coverage cannot be verified, so the edit is refused" });
518
+ return no("edit_rejected", { message: "the record does not carry an adjudicated command (a name card, or one minted before card edits existed) — coverage cannot be verified, so the edit is refused" });
514
519
  }
515
520
  const checked = checkEditedRuleText(edit.text, rec.command);
516
521
  if (!checked.ok) {
@@ -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