@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,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";
@@ -259,9 +259,10 @@ export declare function noticeAudienceOf(code: string): NoticeAudience;
259
259
  * {@link NOTICE_AUDIENCE} like every other row, and NEVER minted by this package (the catalog gate reds a mint site for
260
260
  * one in src/). One code, one minter: a code two processes mint would be two facts under one spelling.
261
261
  *
262
- * Member-level exception, stated where a consumer reads the minter: `task.user_steer_undrained` is an engine row, and
263
- * its `detail.cause: "crashed"` arm is minted by the server — the engine's process is gone when that loss is counted
264
- * (see `USER_STEER_UNDRAINED_CAUSES`). The code keeps one row here and one spelling for both arms.
262
+ * Member-level exception, stated where a consumer reads the minter: `task.user_steer_undrained` and
263
+ * `task.user_followup_undrained` are engine rows, and their `detail.cause: "crashed"` arm is minted by the server — the
264
+ * engine's process is gone when that loss is counted (see `USER_INPUT_UNDRAINED_CAUSES`); the server picks the code by the
265
+ * lane the input was queued on, as this engine does. Each code keeps one row here and one spelling for both arms.
265
266
  */
266
267
  export type NoticeMinter = "engine" | "server";
267
268
  declare const NOTICE_MINTER_TABLE: {
@@ -84,6 +84,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
84
84
  "memory.capture_optout_unavailable",
85
85
  "memory.capture_optout_sweep_failed",
86
86
  "memory.target_outside_store",
87
+ "memory.scope_home_not_directory",
87
88
  ]);
88
89
  export function governanceRetryClass(code) {
89
90
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
@@ -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
  };
@@ -1789,7 +1789,7 @@ export declare class MemoryEngine {
1789
1789
  */
1790
1790
  private gateDerivedIndex;
1791
1791
  /** The file backend's verdict at an act the engine itself performs on a store path ({@link
1792
- * storeTargetEscape}): throws `memory.target_outside_store` when `path` would land outside `root`;
1792
+ * landsOutsideStore}): throws `memory.target_outside_store` when `path` would land outside `root`;
1793
1793
  * each caller's existing failure lane reports it. `root` is the mount's canonical root where a
1794
1794
  * handle exists, so a root re-pointed after the mount is judged against the one it mounted. */
1795
1795
  private refuseOutsideStore;
@@ -1798,7 +1798,7 @@ export declare class MemoryEngine {
1798
1798
  * is never a supported layout (the harvest scan refuses symlinked entry files outright), and this
1799
1799
  * site already reports write failures by throwing, so ELOOP joins the failures it already had. A
1800
1800
  * target whose directory resolves outside `root` (a directory on the path is a link) is refused
1801
- * the same way, before anything is read or written ({@link storeTargetEscape}). */
1801
+ * the same way, before anything is read or written ({@link landsOutsideStore}). */
1802
1802
  private writeIfChanged;
1803
1803
  /** Persist the canonical projection back to the session file (id minting / frontmatter completion).
1804
1804
  * C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
@@ -18,7 +18,7 @@ import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCand
18
18
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
19
19
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
20
20
  import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
21
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
21
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
22
22
  import { createSessionIncarnationSeat, recalledForAnyIdentity, recalledForIdentity, rememberForIdentity } from "./session-incarnation.js";
23
23
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
24
24
  import { writableRootOf } from "./types.js";
@@ -1416,11 +1416,8 @@ export class MemoryEngine {
1416
1416
  ensureDirExists(this.memoryDir);
1417
1417
  const physicalRoot = landingPathOf(this.memoryDir);
1418
1418
  this.mountedRoot ??= physicalRoot;
1419
- if (physicalRoot !== this.mountedRoot) {
1420
- const e = new Error(`${TARGET_OUTSIDE_STORE}: the memory dir ${this.memoryDir} now resolves to ${physicalRoot}, not ${this.mountedRoot} where this engine first mounted it — refused; open a new engine on the directory you mean`);
1421
- e.code = TARGET_OUTSIDE_STORE;
1422
- throw e;
1423
- }
1419
+ if (physicalRoot !== this.mountedRoot)
1420
+ throw storeRootMovedRefusal("engine", this.memoryDir);
1424
1421
  ensureDirExists(this.controlDir);
1425
1422
  await this.adoptLayoutBeforeMount(writeScope);
1426
1423
  const restricted = opts?.adoptionRestricted === true;
@@ -2216,7 +2213,7 @@ export class MemoryEngine {
2216
2213
  if (content === undefined)
2217
2214
  continue;
2218
2215
  try {
2219
- this.refuseOutsideStore(handle.memoryDir, m.path, "restoring");
2216
+ this.refuseOutsideStore(handle.memoryDir, m.path, "restoring a memory file");
2220
2217
  mkdirSync(dirname(m.path), { recursive: true });
2221
2218
  writeFileNoFollow(m.path, content);
2222
2219
  report.restored.push(m.relPath);
@@ -4947,15 +4944,11 @@ export class MemoryEngine {
4947
4944
  };
4948
4945
  }
4949
4946
  refuseOutsideStore(root, path, act) {
4950
- const escape = storeTargetEscape(root, path);
4951
- if (escape === undefined)
4952
- return;
4953
- const e = new Error(`${TARGET_OUTSIDE_STORE}: ${act} ${path} was refused — ${escape}; not performed`);
4954
- e.code = TARGET_OUTSIDE_STORE;
4955
- throw e;
4947
+ if (landsOutsideStore(root, path))
4948
+ throw storeActRefusal(act, path);
4956
4949
  }
4957
4950
  writeIfChanged(target, text, root) {
4958
- this.refuseOutsideStore(root, target, "the projection write of");
4951
+ this.refuseOutsideStore(root, target, "the projection write");
4959
4952
  const current = readNoFollowSafe(target);
4960
4953
  if (current === text)
4961
4954
  return;
@@ -682,9 +682,9 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
682
682
  * let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
683
683
  private txnInFlight;
684
684
  /**
685
- * The act gate for store paths ({@link storeTargetEscape}) over `ops`' writes and deletes — shadow
685
+ * The act gate for store paths ({@link landsOutsideStore}) over `ops`' writes and deletes — shadow
686
686
  * ops name control-plane files, not store paths. Refuses with `memory.target_outside_store` at the
687
- * first op that would land outside the store, naming the op and where it resolves. Runs over a
687
+ * first op that would land outside the store, naming the op (its path on `detail.path`). Runs over a
688
688
  * whole batch before its commit marker (no journal is written; a caller that staged files removes
689
689
  * them) and per op at the act itself; past the marker a refusal keeps the journal, which replays
690
690
  * once the path resolves inside the store again.
@@ -8,7 +8,7 @@ import { ambiguousOriginRepresentation, committedDistilledOf, committedOriginOf,
8
8
  import { convertV1SidecarKeys } from "./delegation-settlement.js";
9
9
  import { layerObservationLookup, observedDivergenceNotice } from "./layer-observations.js";
10
10
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
11
- import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, writeAllSync, } from "./layout.js";
11
+ import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, writeAllSync, } from "./layout.js";
12
12
  import { screenInboundEntries } from "./data-plane.js";
13
13
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
14
14
  import { thrownText } from "../thrown-value.js";
@@ -1404,7 +1404,7 @@ export class FileMemoryEngineBackend {
1404
1404
  }
1405
1405
  }
1406
1406
  this.assertTxnLockOwnership(fenceToken);
1407
- const replayAt = `the journal replay (journal kept at ${jp})`;
1407
+ const replayAt = "the journal replay (journal kept)";
1408
1408
  this.refuseEscapingTargets(journal.ops, replayAt);
1409
1409
  for (const op of journal.ops) {
1410
1410
  this.refuseEscapingTargets([op], replayAt);
@@ -1470,10 +1470,8 @@ export class FileMemoryEngineBackend {
1470
1470
  if (op.kind !== "write" && op.kind !== "delete")
1471
1471
  continue;
1472
1472
  for (const path of op.staged === undefined ? [op.target] : [op.target, op.staged]) {
1473
- const escape = storeTargetEscape(this.physicalRoot, path);
1474
- if (escape === undefined)
1475
- continue;
1476
- throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: ${at} refused a ${op.kind} of ${op.target}${path === op.target ? "" : ` (its staged file ${path})`} — ${escape}; not performed`, TARGET_OUTSIDE_STORE);
1473
+ if (landsOutsideStore(this.physicalRoot, path))
1474
+ throw storeActRefusal(`${path === op.target ? "a" : "the staged file of a"} ${op.kind} at ${at}`, path);
1477
1475
  }
1478
1476
  }
1479
1477
  }
@@ -1481,7 +1479,7 @@ export class FileMemoryEngineBackend {
1481
1479
  const now = landingPathOf(this.directoryRoot);
1482
1480
  if (now === this.physicalRoot)
1483
1481
  return;
1484
- throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: the store root ${this.directoryRoot} now resolves to ${now}, not ${this.physicalRoot} where this backend opened it — refused; open a new backend on the directory you mean`, TARGET_OUTSIDE_STORE);
1482
+ throw storeRootMovedRefusal("backend", this.directoryRoot);
1485
1483
  }
1486
1484
  sweepStagedFiles() {
1487
1485
  const walk = (d, depth) => {
@@ -259,8 +259,41 @@ export declare function adoptRootScopeLayout(rootPath: string, controlDir: strin
259
259
  export declare const canonicalize: typeof physicalPathOfExisting;
260
260
  /** True iff `child` (canonicalized) is `root` or inside it. */
261
261
  export declare function isContainedIn(root: string, child: string): boolean;
262
- /** The code an act on a store path is refused with when it would land outside the store ({@link storeTargetEscape}). */
262
+ /** The code an act on a store path is refused with when it would land outside the store ({@link landsOutsideStore}). */
263
263
  export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
264
+ /** The code a scope home that is not a real directory is refused with ({@link assertHomePhysicalForm}). */
265
+ export declare const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
266
+ /** What a scope home can be besides a real directory (an `lstat` verdict). */
267
+ export declare const SCOPE_HOME_FORMS: readonly ["symlink", "file", "other"];
268
+ export type ScopeHomeForm = (typeof SCOPE_HOME_FORMS)[number];
269
+ /** How a refusal's sentence names each form. */
270
+ declare const SCOPE_HOME_FORM_PHRASE: {
271
+ readonly symlink: "a symbolic link";
272
+ readonly file: "a regular file";
273
+ readonly other: "a special file";
274
+ };
275
+ export type HomeFormPhraseCoversEveryForm = AssertAllKeysHandled<Exclude<ScopeHomeForm, keyof typeof SCOPE_HOME_FORM_PHRASE>>;
276
+ /** What a store-form refusal carries beside its sentence. The ONE host path lives here and never in the
277
+ * sentence; `form` is present on {@link SCOPE_HOME_NOT_DIRECTORY} only. */
278
+ export type StoreFormRefusalDetail = {
279
+ readonly path: string;
280
+ readonly form?: ScopeHomeForm;
281
+ };
282
+ /** A refusal the store's physical form earns: a plain `Error` (never the control-plane-damage class —
283
+ * the engine's own account is intact, the layout it was handed is not the layout it needs) carrying a
284
+ * `code` and a `detail`. */
285
+ export type StoreFormRefusal = Error & {
286
+ code: typeof TARGET_OUTSIDE_STORE | typeof SCOPE_HOME_NOT_DIRECTORY;
287
+ detail: StoreFormRefusalDetail;
288
+ };
289
+ /** Seat 1 — an act the store's physical form refuses: `act` names what was refused (no path), `path` is the
290
+ * target whose directory resolves outside the store ({@link landsOutsideStore}). */
291
+ export declare function storeActRefusal(act: string, path: string): StoreFormRefusal;
292
+ /** Seat 2 — the store root now resolves to another directory than the one the `holder` first opened; `path` is
293
+ * the root's spelling. */
294
+ export declare function storeRootMovedRefusal(holder: "backend" | "engine", path: string): StoreFormRefusal;
295
+ /** Seat 3 — a scope home that is not a real directory. */
296
+ export declare function scopeHomeRefusal(form: ScopeHomeForm, path: string): StoreFormRefusal;
264
297
  /** Where an act at `p` lands: the physical path of its normalized spelling (the spelling a child path
265
298
  * joined under it carries), a MISSING tail rejoined under the deepest existing directory (what the act
266
299
  * creates). Any other resolution fault (EACCES, ELOOP, ENOTDIR) is THROWN as itself: an act that
@@ -268,8 +301,8 @@ export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
268
301
  * (a link whose own mode refuses `realpath` is still followed by `unlink` and `rename`). */
269
302
  export declare function landingPathOf(p: string): string;
270
303
  /**
271
- * Where a write, rename or delete at `target` would physically LAND, judged against the store's
272
- * `physicalRoot`: `undefined` = inside, otherwise the reason. The target's directory, resolved through
304
+ * Whether a write, rename or delete at `target` would physically LAND outside the store, judged against the
305
+ * store's `physicalRoot` (`true` = outside; the refusal is {@link storeActRefusal}). The target's directory, resolved through
273
306
  * every link on its existing prefix (a missing tail is what the act creates, under the deepest existing
274
307
  * directory), must be the root or inside it. `physicalRoot` is the root's physical form as the store
275
308
  * was opened and is not re-resolved here: a store mounted through a link is judged by what the link
@@ -280,7 +313,7 @@ export declare function landingPathOf(p: string): string;
280
313
  * keeps the spelling while the act follows the link out. A directory that cannot be resolved throws its
281
314
  * fault ({@link landingPathOf}).
282
315
  */
283
- export declare function storeTargetEscape(physicalRoot: string, target: string): string | undefined;
316
+ export declare function landsOutsideStore(physicalRoot: string, target: string): boolean;
284
317
  /** mkdir -p convenience used by the engine/backend (0o700 like the file stores). */
285
318
  export declare function ensureDirExists(dir: string): void;
286
319
  /** The one remedy every surfaced permission refusal carries — `undefined` unless `err` is a write
@@ -472,11 +472,18 @@ function assertHomePhysicalForm(home) {
472
472
  return "absent";
473
473
  if (st.isDirectory())
474
474
  return "dir";
475
- throw new ControlPlaneCorruptError(`scope home ${home} is ${st.isSymbolicLink() ? "a symbolic link" : "not a directory"} — a scope home must be a real directory (fail-closed)`);
475
+ throw scopeHomeRefusal(st.isSymbolicLink() ? "symlink" : st.isFile() ? "file" : "other", home);
476
476
  }
477
477
  function ensureScopeHome(home) {
478
- if (assertHomePhysicalForm(home) === "absent")
479
- mkdirSync(home, { recursive: true });
478
+ if (assertHomePhysicalForm(home) === "absent") {
479
+ try {
480
+ mkdirSync(home, { recursive: true });
481
+ }
482
+ catch (err) {
483
+ assertHomePhysicalForm(home);
484
+ throw err;
485
+ }
486
+ }
480
487
  assertHomePhysicalForm(home);
481
488
  }
482
489
  export function registerScope(memoryDir, controlDir, scope, opts) {
@@ -725,6 +732,28 @@ export function isContainedIn(root, child) {
725
732
  return c === r || c.startsWith(`${r}${sep}`);
726
733
  }
727
734
  export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
735
+ export const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
736
+ export const SCOPE_HOME_FORMS = Object.freeze(["symlink", "file", "other"]);
737
+ const SCOPE_HOME_FORM_PHRASE = {
738
+ symlink: "a symbolic link",
739
+ file: "a regular file",
740
+ other: "a special file",
741
+ };
742
+ function storeFormRefusal(code, sentence, detail) {
743
+ const e = new Error(`${code}: ${sentence}`);
744
+ e.code = code;
745
+ e.detail = detail;
746
+ return e;
747
+ }
748
+ export function storeActRefusal(act, path) {
749
+ return storeFormRefusal(TARGET_OUTSIDE_STORE, `${act} was refused — its directory resolves outside the store root; not performed`, { path });
750
+ }
751
+ export function storeRootMovedRefusal(holder, path) {
752
+ return storeFormRefusal(TARGET_OUTSIDE_STORE, `the ${holder === "backend" ? "store root" : "memory dir"} now resolves to a different directory than the one this ${holder} ${holder === "backend" ? "opened" : "first mounted"} — refused; open a new ${holder} on the directory you mean`, { path });
753
+ }
754
+ export function scopeHomeRefusal(form, path) {
755
+ return storeFormRefusal(SCOPE_HOME_NOT_DIRECTORY, `a scope home must be a real directory, but this one is ${SCOPE_HOME_FORM_PHRASE[form]} — refused (fail-closed); put the real directory back`, { path, form });
756
+ }
728
757
  export function landingPathOf(p) {
729
758
  const normalized = resolve(p);
730
759
  try {
@@ -736,9 +765,9 @@ export function landingPathOf(p) {
736
765
  return canonicalize(normalized);
737
766
  }
738
767
  }
739
- export function storeTargetEscape(physicalRoot, target) {
768
+ export function landsOutsideStore(physicalRoot, target) {
740
769
  const dir = landingPathOf(dirname(target));
741
- return dir === physicalRoot || dir.startsWith(`${physicalRoot}${sep}`) ? undefined : `its directory resolves to ${dir}, outside the store root ${physicalRoot}`;
770
+ return dir !== physicalRoot && !dir.startsWith(`${physicalRoot}${sep}`);
742
771
  }
743
772
  export function ensureDirExists(dir) {
744
773
  if (!existsSync(dir))
@@ -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) {