@sema-agent/core 7.5.1 → 7.6.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 (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
2
2
  import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
3
3
  import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
4
+ import type { GateOutcome } from "./gate-outcome.js";
4
5
  import { type AskClass } from "./ask-class.js";
5
6
  import { type AutoModeDenialTracker } from "./auto-mode.js";
6
7
  import type { WiringLegKind } from "./wiring-manifest.js";
@@ -385,30 +386,7 @@ export declare function runHookSeat<T>(seat: HookSeatName, bound: HookSeatBound,
385
386
  * expiry, including the ones where the bound was never reached.)
386
387
  */
387
388
  export declare function hookSeatExpiredError(seat: HookSeatName, timeoutMs: number, cause: "timeout" | "aborted", consequence: string): Error;
388
- /**
389
- * Where a {@link Hooks.permissionDenied} deny came from — our gate-source enum standing in for CC's
390
- * `decision_reason_type` ('classifier'|'asyncAgent'|'mode'|'rule'), named after OUR adjudicate-chain
391
- * sources (design/134 R5):
392
- * - `"policy"` — the ToolPolicy denied directly, or a policy-raised `ask` resolved to deny
393
- * (headless auto-deny / approver said no / approval aborted).
394
- * - `"hook"` — a PreToolUse hook `ask` (folded allow→ask) resolved to deny, OR the engine's fail-closed
395
- * interception of a hook that THREW (RB-442). (A hook's own `deny` short-circuit is EXCLUDED from the
396
- * event — a decision the hook made; the crash interception is a decision the ENGINE made. The
397
- * exclusion is ours on that merit; see {@link Hooks.permissionDenied} for why it must NOT be read
398
- * as a parity claim about this seat's trigger surface.)
399
- * - `"safety"` — the gate's deterministic egress/irreversibility tighten (design/70 / design/77 §4)
400
- * raised the `ask` that resolved to deny, from an explicit per-tool mark.
401
- * - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE
402
- * `TaskSpec.shellGate` doctrine (design/80 D-E), not an explicit per-tool mark.
403
- * - `"planMode"` — the design/108 plan-mode write-deny short-circuit (a read-only fidelity gate).
404
- * - `"org"` (design/182 §7) — the ORG layer: an organization deny rule, or the fail-closed tighten an
405
- * org-governed deployment applies while it cannot adjudicate against a snapshot (that tighten's ask
406
- * then resolving to deny). Its own word rather than `"policy"` because the authority is the ORG's
407
- * published snapshot, not this deployment's `ToolPolicy` — an operator reading "policy" on a call
408
- * their own policy allowed would be looking in the wrong place.
409
- */
410
- export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier" | "org";
411
- /** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */
389
+ /** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + the gate outcome). */
412
390
  export interface PermissionDeniedPayload {
413
391
  /** #281 件A — the run/leg identity envelope. Present on every ENGINE emission (the gate's single
414
392
  * deny exit, the hook-crash intercept, the plan-mode and compliance denies); absent only when a
@@ -421,19 +399,15 @@ export interface PermissionDeniedPayload {
421
399
  toolCallId: string;
422
400
  /** Human/model-readable deny reason (CC `reason`) — the RAW decision text, NOT `<system-reminder>`-wrapped. */
423
401
  reason: string;
424
- /** Which gate source produced the deny (our `decision_reason_type` analog). */
425
- source: PermissionDeniedSource;
426
- /** The ask resolver's own deny-arm classification, carried BESIDE `source` (two different
427
- * questions: `source` names which LAYER raised the gate; this names HOW the ask resolution
428
- * refused a person's no vs a timeout vs headless vs an approver contract violation …). Present
429
- * only on a deny that came through an ask resolution AND whose word passed the closed-vocabulary
430
- * screen; a policy's direct deny, a hook deny, and the crash/plan-mode/compliance emissions carry
431
- * none. See {@link import("./tool-policy.js").AskDenyResolution}. */
432
- resolution?: import("./tool-policy.js").AskDenyResolution;
433
- /** #548 — the deny is the classifier denial-limit fallback's AUTO-DENY (core's own window elapsed;
434
- * see {@link import("./tool-policy.js").ResolvedAsk.autoDenied}). Carried verbatim from the resolver;
435
- * absent on every other deny. */
436
- autoDenied?: true;
402
+ /**
403
+ * The gate outcome this deny is — the SAME object the call's `tool_end` frame carries as `gate` and
404
+ * the engine's per-call sideband records (one mint, three faces). `disposition.deniedBy` says which
405
+ * LAYER refused (our `decision_reason_type` analog for the refusing half); when the refusal came through
406
+ * an ask resolution, `settlement` says how the wait ended and `origin` who asked. Nothing is read out of
407
+ * an absence: a direct policy / hook / plan-mode / compliance deny carries no settlement because it
408
+ * settled none.
409
+ */
410
+ gate: GateOutcome;
437
411
  /** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
438
412
  * already happened and nothing this callback does can change it, so the signal says exactly one
439
413
  * thing: stop reading, nobody is waiting for your answer any more. */
@@ -943,46 +917,14 @@ export interface ToolGateResult {
943
917
  /** Model-readable block reason (already `<system-reminder>`-wrapped). */
944
918
  reason?: string;
945
919
  /**
946
- * What ended the approval this call was waiting on, when this gate pass settled one (the ask
947
- * resolution's own {@link import("./tool-policy.js").ApprovalSettledBy}, carried out verbatimthis
948
- * layer classifies nothing). Present on BOTH terminal exits, the block and the allow: an approved
949
- * call is a settled approval too, and a caller that saw the source only on refusals would read its
950
- * absence on an execution as "nobody approved this". Absent on everything that settled no approval
951
- * a straight policy deny, a hook deny, a posture verdict where nobody was asked, every ordinary
952
- * allow. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
953
- * frame: the machine-readable half of the disclosure `reason` makes in words, on a channel no tool
954
- * and no post-tool hook can write.
955
- */
956
- settledBy?: import("./tool-policy.js").ApprovalSettledBy;
957
- /**
958
- * The ask resolver's deny-arm classification (see {@link import("./tool-policy.js").AskDenyResolution}),
959
- * present only on a BLOCK whose deny came through an ask resolution and passed the closed-vocabulary
960
- * screen at the deny exit (a self-declared word on a policy's own deny is dropped there, never
961
- * forwarded). Rides beside {@link settledBy} to the caller's per-call sideband and the call's
962
- * `tool_end` frame — the machine-readable "why was this refused" a consumer classifies on.
963
- */
964
- resolution?: import("./tool-policy.js").AskDenyResolution;
965
- /**
966
- * `true` ⇔ this BLOCK is the classifier denial-limit fallback's own auto-deny: the ask's window
967
- * (`AskRequest.denialLimitFallback.autoDenyAfterMs`) elapsed with nobody answering and the resolver
968
- * refused it itself — the same fact the `permissionDenied` observer payload's `autoDenied` carries,
969
- * read from the same two places (this gate's own resolution, or the module-private attestation an
970
- * inherited arm bound to THIS call). Rides to the call's `tool_end` frame so a consumer can tell
971
- * "the classifier fallback's own 120 s window auto-denied" from "the approval factory's own window
972
- * (`approvalTimeoutMs`) elapsed" — both settle as `settledBy:"timeout"` / `resolution:"window_expired"`,
973
- * and the factory's deadline NEVER carries this. A durable park's expiry settles on the decide lane
974
- * with no `resolution` word, so it is told apart by that absence, not by this bit.
975
- * Absent everywhere else (a person's no, a policy deny, an executing call).
976
- */
977
- autoDenied?: true;
978
- /**
979
- * design/252 G-7 — WHOSE settlement that was: the identifier the approval channel reported, carried
980
- * out verbatim beside {@link settledBy}. This layer authenticates nothing and compares nothing; the
981
- * value was screened for shape at the seam that received it (`screenApproverAttribution`) and is a
982
- * transcription from there on. Present only where the caller settled something AND reported an
983
- * identity — absent everywhere else, and absence never means "nobody": it means nobody said.
920
+ * The record of this pass ({@link GateOutcome}): its final disposition, the ask it settled (if any) and
921
+ * that ask's origin. Present on BOTH terminal exits, the block and the allow an approved call is a
922
+ * settled approval too, and a caller that saw the record only on refusals would read its absence on an
923
+ * execution as "nobody approved this". Absent only on the durable-suspend exit, where the pass decided
924
+ * nothing yet. The caller records it on its own per-call sideband so it reaches that call's `tool_end`
925
+ * frame, and the deny observer receives the same object: one mint, three faces.
984
926
  */
985
- approver?: string;
927
+ gate?: GateOutcome;
986
928
  /** Rewritten args to execute with (re-validated by the loop); omitted when nothing rewrote. */
987
929
  updatedInput?: unknown;
988
930
  /**
@@ -1767,20 +1709,4 @@ export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1767
1709
  denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1768
1710
  origin?: import("./ask-origin.js").AskOrigin;
1769
1711
  };
1770
- /**
1771
- * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
1772
- * structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
1773
- *
1774
- * 1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput`
1775
- * and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook
1776
- * `ask` is remembered (it does not short-circuit — a later policy `deny` outranks it).
1777
- * 2. **adjudicate** — run the tool policy on the FINAL `currentInput` (never the model's stale args),
1778
- * fold it with any remembered hook-ask via `deny > ask > allow`, and resolve a surviving `ask`
1779
- * through `onAsk`. The policy ALWAYS runs regardless of the hook's verdict. A policy `allow` may
1780
- * itself carry an `updatedInput` rewrite (redact/clamp), applied last over any hook rewrite.
1781
- *
1782
- * Because both phases run in this one linear function, the ordering — and the "policy is final"
1783
- * invariant — is enforced by the call stack, not by registration convention. Returns the gate result
1784
- * for the harness `tool_call` hook plus the PreToolUse context to attach to the tool result.
1785
- */
1786
1712
  export declare function runToolGate(input: ToolGateInput): Promise<ToolGateResult>;
@@ -1,4 +1,5 @@
1
- import { coreMintedAutoDeniedOf, coreMintedResolutionOf, decisionText, describeThrown, isAskDenyResolution, refuseOutOfContractDecision } from "./tool-policy.js";
1
+ import { decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
2
+ import { createSettlementLedger, directDeny, mintGateOutcome } from "./runner/gate-exit.js";
2
3
  import { brandPolicyAskClass } from "./ask-class.js";
3
4
  import { askOriginOf, classifierMayAnswer, isAskOrigin } from "./ask-origin.js";
4
5
  import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
@@ -455,8 +456,8 @@ export async function runToolGate(input) {
455
456
  const preToolContext = [];
456
457
  let hookAsk;
457
458
  let parkFailed;
458
- let askDenyResolution;
459
- let askAutoDenied = false;
459
+ let deniedBy;
460
+ const ledger = createSettlementLedger({ toolCallId, toolName });
460
461
  const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
461
462
  const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
462
463
  const seatBound = (abortEnds) => ({
@@ -493,13 +494,15 @@ export async function runToolGate(input) {
493
494
  catch (err) {
494
495
  const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
495
496
  traceHookCrash(input, err, notifier);
496
- await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) });
497
- return { block: true, reason: formatHookFeedback(reason, input.reminderMark), preToolContext };
497
+ const gate = directDeny("hook");
498
+ await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
499
+ return { block: true, reason: formatHookFeedback(reason, input.reminderMark), gate, preToolContext };
498
500
  }
499
501
  if (unanswered !== undefined) {
500
502
  const reason = await preToolUseUnansweredBlock(`this call to "${toolName}"`, unanswered);
501
- await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) });
502
- return { block: true, reason: formatHookFeedback(reason, input.reminderMark), preToolContext };
503
+ const gate = directDeny("hook");
504
+ await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
505
+ return { block: true, reason: formatHookFeedback(reason, input.reminderMark), gate, preToolContext };
503
506
  }
504
507
  if (r) {
505
508
  if (r.additionalContext) {
@@ -509,6 +512,7 @@ export async function runToolGate(input) {
509
512
  return {
510
513
  block: true,
511
514
  reason: formatHookFeedback(decisionText(r) ?? `tool "${toolName}" blocked by a PreToolUse hook`, input.reminderMark),
515
+ gate: directDeny("hook"),
512
516
  preToolContext,
513
517
  };
514
518
  }
@@ -522,7 +526,12 @@ export async function runToolGate(input) {
522
526
  }
523
527
  const req = { toolName, args: currentInput, toolCallId };
524
528
  let decision = adjudicate ? await (callSignal !== undefined ? adjudicate(req, callSignal) : adjudicate(req)) : { action: "allow" };
525
- let denySource = "policy";
529
+ {
530
+ const refusedBy = ledger.fromDecision(decision);
531
+ if (refusedBy !== undefined)
532
+ deniedBy = refusedBy;
533
+ }
534
+ let tightenedBy;
526
535
  let policyRewrite;
527
536
  if ((decision.action === "allow" || decision.action === "ask") && decision.updatedInput !== undefined) {
528
537
  policyRewrite = decision.updatedInput;
@@ -538,7 +547,6 @@ export async function runToolGate(input) {
538
547
  ...(hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
539
548
  ...(hookAsk.updatedInput !== undefined ? { updatedInput: hookAsk.updatedInput } : {}),
540
549
  };
541
- denySource = "hook";
542
550
  }
543
551
  else if (decision.action === "ask" && hookAsk) {
544
552
  const hookMessage = hookAsk.message;
@@ -558,7 +566,7 @@ export async function runToolGate(input) {
558
566
  message: `tool "${toolName}" performs an external write (egress) — explicit approval required`,
559
567
  decisionReason: "safety",
560
568
  };
561
- denySource = "safety";
569
+ tightenedBy = "safety";
562
570
  }
563
571
  const alreadyAsking = decision.action === "ask";
564
572
  if ((decision.action === "allow" || alreadyAsking) && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
@@ -609,7 +617,7 @@ export async function runToolGate(input) {
609
617
  ...(probeCause !== undefined ? { probeCause } : {}),
610
618
  ...(probeMandated === true ? { probeMandated: true } : {}),
611
619
  };
612
- denySource = input.shellGated === true ? "shellGate" : "safety";
620
+ tightenedBy = input.shellGated === true ? "shell_gate" : "safety";
613
621
  }
614
622
  else if (alreadyAsking && probeMandated === true && decision.action === "ask") {
615
623
  decision = { ...decision, probeMandated: true };
@@ -629,7 +637,7 @@ export async function runToolGate(input) {
629
637
  message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
630
638
  decisionReason: "safety",
631
639
  };
632
- denySource = "safety";
640
+ tightenedBy = "safety";
633
641
  }
634
642
  if (decision.action === "allow" && input.writeProtectionCheck !== undefined) {
635
643
  const writeProtectedHit = input.writeProtectionCheck(toolName, policyRewrite !== undefined ? policyRewrite : currentInput);
@@ -639,18 +647,17 @@ export async function runToolGate(input) {
639
647
  message: `tool "${toolName}" writes to a write-protected path (table entry "${writeProtectedHit.name}") — explicit approval required`,
640
648
  decisionReason: "safety",
641
649
  };
642
- denySource = "safety";
650
+ tightenedBy = "safety";
643
651
  }
644
652
  }
645
653
  let orgRealApprovalRequired = false;
646
- let orgAskOrigin;
647
- const askOriginFacts = () => ({
654
+ const askOriginFacts = (org) => ({
648
655
  contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
649
656
  markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
650
- org: orgAskOrigin,
657
+ org,
658
+ tightened: tightenedBy,
651
659
  });
652
660
  let orgTightenCount = 0;
653
- let resolvedApprover;
654
661
  let orgEvidence = (() => {
655
662
  const w = input.orgRules === undefined ? "not_wired" : "not_adjudicated";
656
663
  return { revisionAbsent: w, ruleAbsent: w };
@@ -662,9 +669,9 @@ export async function runToolGate(input) {
662
669
  });
663
670
  const applyOrgLayer = async (current, args) => {
664
671
  if (input.orgRules === undefined)
665
- return current;
672
+ return { decision: current, org: undefined };
666
673
  if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
667
- return current;
674
+ return { decision: current, org: undefined };
668
675
  let decided = current;
669
676
  let pending;
670
677
  try {
@@ -684,7 +691,6 @@ export async function runToolGate(input) {
684
691
  if (answer.status === "unavailable") {
685
692
  orgEvidence = { revisionAbsent: "unavailable", ruleAbsent: "unavailable" };
686
693
  orgRealApprovalRequired = true;
687
- orgAskOrigin = "unavailable";
688
694
  await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
689
695
  if (decided.action === "allow") {
690
696
  decided = {
@@ -695,42 +701,43 @@ export async function runToolGate(input) {
695
701
  requiresRealApproval: true,
696
702
  };
697
703
  orgTightenCount += 1;
698
- denySource = "org";
699
704
  }
700
705
  else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
701
706
  decided = { ...decided, requiresRealApproval: true };
702
707
  orgTightenCount += 1;
703
708
  }
704
- return decided;
709
+ return { decision: decided, org: "unavailable" };
705
710
  }
706
711
  const revisionCell = answer.revision !== undefined ? { revision: answer.revision } : { revisionAbsent: "not_reported" };
707
712
  if (answer.verdict === undefined) {
708
713
  orgEvidence = { ...revisionCell, ruleAbsent: "no_match" };
709
- return decided;
714
+ return { decision: decided, org: undefined };
710
715
  }
711
716
  const { behavior, rule } = answer.verdict;
712
717
  orgEvidence = { ...revisionCell, rule };
713
718
  if (behavior === "deny") {
714
719
  orgTightenCount += 1;
715
- denySource = "org";
716
- return { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON };
720
+ deniedBy = "org";
721
+ return { decision: { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON }, org: undefined };
717
722
  }
718
723
  if (decided.action === "deny")
719
- return decided;
724
+ return { decision: decided, org: undefined };
720
725
  orgTightenCount += 1;
721
- denySource = "org";
722
726
  orgRealApprovalRequired = true;
723
- orgAskOrigin = "rule";
724
- return decided.action === "ask"
725
- ? { ...decided, requiresRealApproval: true }
726
- : {
727
- action: "ask",
728
- message: `an organization policy rule (${rule}) requires approval for this call`,
729
- decisionReason: ORG_RULE_DECISION_REASON,
730
- requiresRealApproval: true,
731
- };
727
+ return {
728
+ decision: decided.action === "ask"
729
+ ? { ...decided, requiresRealApproval: true }
730
+ : {
731
+ action: "ask",
732
+ message: `an organization policy rule (${rule}) requires approval for this call`,
733
+ decisionReason: ORG_RULE_DECISION_REASON,
734
+ requiresRealApproval: true,
735
+ },
736
+ org: "rule",
737
+ };
732
738
  };
733
- decision = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
739
+ const orgAnswer = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
740
+ decision = orgAnswer.decision;
734
741
  if (decision.action === "ask") {
735
742
  if (policyRewrite !== undefined) {
736
743
  currentInput = policyRewrite;
@@ -815,11 +822,13 @@ export async function runToolGate(input) {
815
822
  };
816
823
  }
817
824
  }
818
- const originFacts = askOriginFacts();
825
+ const originFacts = askOriginFacts(orgAnswer.org);
819
826
  if (decision.action === "ask") {
820
827
  const stamped = { ...decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
821
828
  decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
822
829
  }
830
+ if (decision.action === "ask")
831
+ ledger.supersede();
823
832
  if (input.autoMode && decision.action === "ask" && decision.origin !== undefined && classifierMayAnswer(decision.origin)) {
824
833
  const verdict = await input.autoMode.decider
825
834
  .decide({ req, askMessage: decisionText(decision) }, input.abortSignal)
@@ -846,7 +855,6 @@ export async function runToolGate(input) {
846
855
  denialLimitFallback: tracked.fallback,
847
856
  };
848
857
  decision = { ...fallbackAsk, origin: askOriginOf(fallbackAsk, originFacts) };
849
- denySource = "classifier";
850
858
  }
851
859
  else {
852
860
  decision = {
@@ -854,7 +862,7 @@ export async function runToolGate(input) {
854
862
  message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
855
863
  decisionReason: "classifier",
856
864
  };
857
- denySource = "classifier";
865
+ deniedBy = "classifier";
858
866
  }
859
867
  }
860
868
  }
@@ -885,9 +893,13 @@ export async function runToolGate(input) {
885
893
  const egressTool = input.egress === true;
886
894
  const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
887
895
  const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
888
- const realApprovalOf = (d) => d.action === "ask" && d.requiresRealApproval === true
889
- ? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
890
- : undefined;
896
+ const realApprovalOf = (d) => {
897
+ if (d.action !== "ask" || d.requiresRealApproval !== true)
898
+ return undefined;
899
+ if (d.origin === undefined)
900
+ throw new Error(`a real-approval ask for "${toolName}" reached a park site with no origin word — the origin stamp precedes every park`);
901
+ return { origin: d.origin, ...(originFacts.org !== undefined ? { org: originFacts.org } : {}) };
902
+ };
891
903
  const carryOf = (d) => ({
892
904
  ...(callSignal !== undefined ? { signal: callSignal } : {}),
893
905
  ...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
@@ -923,6 +935,10 @@ export async function runToolGate(input) {
923
935
  return { suspend: suspended, preToolContext };
924
936
  }
925
937
  }
938
+ if (decision.origin === undefined)
939
+ throw new Error(`the content ask for "${toolName}" reached its refusal without an origin word`);
940
+ ledger.set({ settlement: { kind: input.resolveContentAsk !== undefined ? "approver_unavailable" : "no_approver", who: { party: "none" }, when: Date.now() }, origin: decision.origin });
941
+ deniedBy = "ask_resolution";
926
942
  decision = {
927
943
  action: "deny",
928
944
  message: "AskUserQuestion could not be parked for a durable human answer on this path (no checkpoint " +
@@ -941,12 +957,9 @@ export async function runToolGate(input) {
941
957
  if (decision.action === "ask") {
942
958
  const askBeforeResolve = decision;
943
959
  const resolved = await (callSignal !== undefined ? resolveAsk(decision, req, callSignal) : resolveAsk(decision, req));
944
- if (resolved.action !== "ask" && resolved.approver !== undefined)
945
- resolvedApprover = resolved.approver;
946
- if (resolved.action === "deny" && isAskDenyResolution(resolved.resolution))
947
- askDenyResolution = resolved.resolution;
948
- if (resolved.action === "deny" && resolved.autoDenied === true)
949
- askAutoDenied = true;
960
+ ledger.fromResolved(resolved, askBeforeResolve, toolName);
961
+ if (resolved.action === "deny")
962
+ deniedBy = "ask_resolution";
950
963
  decision = resolved;
951
964
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
952
965
  const parkArgs = [req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
@@ -962,9 +975,8 @@ export async function runToolGate(input) {
962
975
  if (decision.action === "allow") {
963
976
  input.autoMode?.denialTracking?.recordAllow();
964
977
  }
965
- else if (askDenyResolution === "no_approver" ||
966
- askDenyResolution === "blanket_allow_refused" ||
967
- askDenyResolution === "approver_unavailable" ||
978
+ else if ((resolved.action === "deny" &&
979
+ (resolved.settlement.kind === "no_approver" || resolved.settlement.kind === "blanket_allow_refused" || resolved.settlement.kind === "approver_unavailable")) ||
968
980
  resolved.approverUnavailable === true) {
969
981
  try {
970
982
  input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: unarmedWindow(askBeforeResolve.denialLimitFallback) });
@@ -983,7 +995,7 @@ export async function runToolGate(input) {
983
995
  action: "deny",
984
996
  message: `the approval-edit chain for "${toolName}" exceeded the re-approval cap — denied fail-closed; re-submit the edited action directly`,
985
997
  };
986
- denySource = "policy";
998
+ deniedBy = "policy";
987
999
  break;
988
1000
  }
989
1001
  if (preToolUse) {
@@ -999,12 +1011,12 @@ export async function runToolGate(input) {
999
1011
  catch (err) {
1000
1012
  traceHookCrash(input, err, notifier);
1001
1013
  editDenied = { action: "deny", message: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
1002
- denySource = "hook";
1014
+ deniedBy = "hook";
1003
1015
  break;
1004
1016
  }
1005
1017
  if (hrUnanswered !== undefined) {
1006
1018
  editDenied = { action: "deny", message: await preToolUseUnansweredBlock(`the approved edit for "${toolName}"`, hrUnanswered) };
1007
- denySource = "hook";
1019
+ deniedBy = "hook";
1008
1020
  break;
1009
1021
  }
1010
1022
  if (hr) {
@@ -1012,7 +1024,7 @@ export async function runToolGate(input) {
1012
1024
  preToolContext.push(hr.additionalContext);
1013
1025
  if (hr.action === "deny") {
1014
1026
  editDenied = { action: "deny", message: decisionText(hr) ?? `the approved edit for "${toolName}" was blocked by a PreToolUse hook` };
1015
- denySource = "hook";
1027
+ deniedBy = "hook";
1016
1028
  break;
1017
1029
  }
1018
1030
  if (hr.action === "ask") {
@@ -1020,7 +1032,7 @@ export async function runToolGate(input) {
1020
1032
  action: "deny",
1021
1033
  message: `the approved edit for "${toolName}" raises a new PreToolUse approval requirement the original approval does not cover — denied fail-closed; re-submit the edited action directly`,
1022
1034
  };
1023
- denySource = "hook";
1035
+ deniedBy = "hook";
1024
1036
  break;
1025
1037
  }
1026
1038
  if (hr.updatedInput !== undefined) {
@@ -1043,19 +1055,20 @@ export async function runToolGate(input) {
1043
1055
  };
1044
1056
  }
1045
1057
  }
1046
- if (recheck.action === "deny") {
1047
- editDenied = recheck;
1048
- denySource = "policy";
1049
- break;
1058
+ {
1059
+ const refusedBy = ledger.fromDecision(recheck);
1060
+ if (recheck.action === "deny" && refusedBy !== undefined) {
1061
+ editDenied = recheck;
1062
+ deniedBy = refusedBy;
1063
+ break;
1064
+ }
1050
1065
  }
1051
1066
  if (recheck.updatedInput !== undefined) {
1052
1067
  editArgs = recheck.updatedInput;
1053
1068
  editRewrittenSinceHuman = true;
1054
1069
  }
1055
- const orgTightensBefore = orgTightenCount;
1056
- orgAskOrigin = undefined;
1057
- recheck = await applyOrgLayer(recheck, editArgs);
1058
- const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
1070
+ const editOrg = await applyOrgLayer(recheck, editArgs);
1071
+ recheck = editOrg.decision;
1059
1072
  if (recheck.action === "deny") {
1060
1073
  editDenied = recheck;
1061
1074
  break;
@@ -1072,16 +1085,13 @@ export async function runToolGate(input) {
1072
1085
  editRewrittenSinceHuman = true;
1073
1086
  }
1074
1087
  const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
1075
- const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts()) };
1088
+ const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org)) };
1076
1089
  const editAskReq = { toolName, args: editArgs, toolCallId };
1077
1090
  const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
1078
- resolvedApprover = rr.action !== "ask" ? rr.approver : undefined;
1091
+ ledger.fromResolved(rr, editAskDecision, toolName);
1079
1092
  if (rr.action !== "allow") {
1080
- if (rr.action === "deny" && isAskDenyResolution(rr.resolution))
1081
- askDenyResolution = rr.resolution;
1082
1093
  editDenied = rr;
1083
- if (!orgRaisedThisRound)
1084
- denySource = "policy";
1094
+ deniedBy = "ask_resolution";
1085
1095
  break;
1086
1096
  }
1087
1097
  if (rr.updatedInput === undefined) {
@@ -1100,7 +1110,7 @@ export async function runToolGate(input) {
1100
1110
  action: "deny",
1101
1111
  message: `the approved edit for "${toolName}" was rewritten by the restriction chain onto a write-protected path (table entry "${editHit.name}") that no approval covers — denied fail-closed; re-submit the edited action directly`,
1102
1112
  };
1103
- denySource = "safety";
1113
+ deniedBy = "write_protection";
1104
1114
  }
1105
1115
  }
1106
1116
  decision = editDenied ?? { ...decision, updatedInput: editArgs };
@@ -1115,18 +1125,14 @@ export async function runToolGate(input) {
1115
1125
  if (decision.updatedInput !== undefined) {
1116
1126
  currentInput = decision.updatedInput;
1117
1127
  }
1118
- const denyResolution = askDenyResolution ?? coreMintedResolutionOf(decision, { toolCallId, toolName });
1119
- const denyAutoDenied = askAutoDenied || coreMintedAutoDeniedOf(decision, { toolCallId, toolName });
1120
- await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(denyResolution !== undefined ? { resolution: denyResolution } : {}), ...(denyAutoDenied ? { autoDenied: true } : {}), ...(input.identity !== undefined ? { identity: input.identity } : {}) });
1121
- const denySettledBy = decision.settledBy;
1122
- const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
1128
+ if (deniedBy === undefined)
1129
+ throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
1130
+ const gate = mintGateOutcome({ deniedBy, ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
1131
+ await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, gate, ...(input.identity !== undefined ? { identity: input.identity } : {}) });
1123
1132
  return {
1124
1133
  block: true,
1125
1134
  reason: formatHookFeedback(denyReason, input.reminderMark),
1126
- ...(denySettledBy !== undefined ? { settledBy: denySettledBy } : {}),
1127
- ...(denyResolution !== undefined ? { resolution: denyResolution } : {}),
1128
- ...(denyAutoDenied ? { autoDenied: true } : {}),
1129
- ...(denyApprover !== undefined ? { approver: denyApprover } : {}),
1135
+ gate,
1130
1136
  preToolContext,
1131
1137
  };
1132
1138
  }
@@ -1135,12 +1141,9 @@ export async function runToolGate(input) {
1135
1141
  if (rw !== undefined)
1136
1142
  currentInput = rw;
1137
1143
  }
1138
- const allowSettledBy = decision.action === "allow" ? decision.settledBy : undefined;
1139
- const allowApprover = decision.action === "allow" && allowSettledBy !== undefined ? resolvedApprover : undefined;
1140
1144
  return {
1141
1145
  updatedInput: currentInput === event.input ? undefined : currentInput,
1142
- ...(allowSettledBy !== undefined ? { settledBy: allowSettledBy } : {}),
1143
- ...(allowApprover !== undefined ? { approver: allowApprover } : {}),
1146
+ gate: mintGateOutcome(ledger.settled !== undefined ? { settled: ledger.settled } : {}),
1144
1147
  preToolContext,
1145
1148
  };
1146
1149
  }