@decionis/agent-safe-pipeline 0.3.5 → 0.3.7

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 (81) hide show
  1. package/README.md +7 -6
  2. package/dist/Index.d.ts +6 -0
  3. package/dist/Index.d.ts.map +1 -1
  4. package/dist/Index.js +6 -0
  5. package/dist/Index.js.map +1 -1
  6. package/dist/approval/PresenceApprovalCoordinator.d.ts +2 -2
  7. package/dist/audit/AuditRecorder.d.ts +6 -6
  8. package/dist/audit/AuditRecorder.d.ts.map +1 -1
  9. package/dist/audit/AuditRecorder.js.map +1 -1
  10. package/dist/decision/DecionisGate.d.ts +16 -5
  11. package/dist/decision/DecionisGate.d.ts.map +1 -1
  12. package/dist/decision/DecionisGate.js +113 -11
  13. package/dist/decision/DecionisGate.js.map +1 -1
  14. package/dist/decision/DecisionAuthority.d.ts +42 -4
  15. package/dist/decision/DecisionAuthority.d.ts.map +1 -1
  16. package/dist/decision/DecisionAuthority.js +2 -2
  17. package/dist/decision/DecisionAuthority.js.map +1 -1
  18. package/dist/decision/FixtureDecisionAuthority.d.ts +7 -3
  19. package/dist/decision/FixtureDecisionAuthority.d.ts.map +1 -1
  20. package/dist/decision/FixtureDecisionAuthority.js +26 -3
  21. package/dist/decision/FixtureDecisionAuthority.js.map +1 -1
  22. package/dist/decision/ImmutableDecisionGate.d.ts +3 -0
  23. package/dist/decision/ImmutableDecisionGate.d.ts.map +1 -0
  24. package/dist/decision/{ImmutableGateDecision.js → ImmutableDecisionGate.js} +2 -2
  25. package/dist/decision/{ImmutableGateDecision.js.map → ImmutableDecisionGate.js.map} +1 -1
  26. package/dist/decision/ShadowGate.d.ts +2 -2
  27. package/dist/decision/ShadowGate.js +2 -2
  28. package/dist/decision/edge/BundleWindow.d.ts +43 -0
  29. package/dist/decision/edge/BundleWindow.d.ts.map +1 -0
  30. package/dist/decision/edge/BundleWindow.js +41 -0
  31. package/dist/decision/edge/BundleWindow.js.map +1 -0
  32. package/dist/decision/edge/EdgeBundleManager.d.ts +87 -0
  33. package/dist/decision/edge/EdgeBundleManager.d.ts.map +1 -0
  34. package/dist/decision/edge/EdgeBundleManager.js +175 -0
  35. package/dist/decision/edge/EdgeBundleManager.js.map +1 -0
  36. package/dist/decision/edge/EdgeBundleSource.d.ts +59 -0
  37. package/dist/decision/edge/EdgeBundleSource.d.ts.map +1 -0
  38. package/dist/decision/edge/EdgeBundleSource.js +99 -0
  39. package/dist/decision/edge/EdgeBundleSource.js.map +1 -0
  40. package/dist/decision/edge/EdgeDecisionAuthority.d.ts +104 -0
  41. package/dist/decision/edge/EdgeDecisionAuthority.d.ts.map +1 -0
  42. package/dist/decision/edge/EdgeDecisionAuthority.js +197 -0
  43. package/dist/decision/edge/EdgeDecisionAuthority.js.map +1 -0
  44. package/dist/decision/edge/EdgeModule.d.ts +63 -0
  45. package/dist/decision/edge/EdgeModule.d.ts.map +1 -0
  46. package/dist/decision/edge/EdgeModule.js +147 -0
  47. package/dist/decision/edge/EdgeModule.js.map +1 -0
  48. package/dist/execution/ActionRegistry.d.ts +23 -0
  49. package/dist/execution/ActionRegistry.d.ts.map +1 -1
  50. package/dist/execution/ActionRegistry.js +32 -2
  51. package/dist/execution/ActionRegistry.js.map +1 -1
  52. package/dist/execution/AuthorizationVerifier.d.ts +19 -4
  53. package/dist/execution/AuthorizationVerifier.d.ts.map +1 -1
  54. package/dist/execution/AuthorizationVerifier.js +41 -2
  55. package/dist/execution/AuthorizationVerifier.js.map +1 -1
  56. package/dist/execution/LocalAuthorizationVerifier.d.ts +74 -0
  57. package/dist/execution/LocalAuthorizationVerifier.d.ts.map +1 -0
  58. package/dist/execution/LocalAuthorizationVerifier.js +138 -0
  59. package/dist/execution/LocalAuthorizationVerifier.js.map +1 -0
  60. package/dist/execution/PostgresReplayStore.d.ts +57 -0
  61. package/dist/execution/PostgresReplayStore.d.ts.map +1 -0
  62. package/dist/execution/PostgresReplayStore.js +81 -0
  63. package/dist/execution/PostgresReplayStore.js.map +1 -0
  64. package/dist/execution/ReplayStore.d.ts +14 -0
  65. package/dist/execution/ReplayStore.d.ts.map +1 -1
  66. package/dist/execution/ReplayStore.js +8 -0
  67. package/dist/execution/ReplayStore.js.map +1 -1
  68. package/dist/execution/SafeExecutor.d.ts +117 -5
  69. package/dist/execution/SafeExecutor.d.ts.map +1 -1
  70. package/dist/execution/SafeExecutor.js +259 -67
  71. package/dist/execution/SafeExecutor.js.map +1 -1
  72. package/dist/report/DecisionReport.d.ts +2 -2
  73. package/dist/report/HostedOutcome.d.ts +2 -2
  74. package/dist/shadow/ShadowPipeline.d.ts +1 -1
  75. package/dist/testing/LocalAuthority.d.ts +35 -2
  76. package/dist/testing/LocalAuthority.d.ts.map +1 -1
  77. package/dist/testing/LocalAuthority.js +54 -1
  78. package/dist/testing/LocalAuthority.js.map +1 -1
  79. package/package.json +3 -3
  80. package/dist/decision/ImmutableGateDecision.d.ts +0 -3
  81. package/dist/decision/ImmutableGateDecision.d.ts.map +0 -1
@@ -1,9 +1,10 @@
1
- import type { GateDecision } from "../decision/DecisionAuthority.js";
1
+ import type { DecisionGate } from "../decision/DecisionAuthority.js";
2
2
  import type { AuditRecorder } from "../audit/AuditRecorder.js";
3
3
  import type { CapturedIntent } from "../intent/ExecutionIntent.js";
4
+ import type { JsonObject } from "../intent/JsonValue.js";
4
5
  import type { ActionRegistry } from "./ActionRegistry.js";
5
- import type { AuthorizationVerifier, ExecutionFinalization, VerifiedAuthorization } from "./AuthorizationVerifier.js";
6
- export type ExecutionBlockReason = "DECISION_NOT_AUTHORITATIVE" | "DECISION_NOT_ALLOW" | "INTENT_BINDING_MISMATCH" | "INTENT_CONFORMANCE_FAILED" | "AUTHORIZATION_MISSING" | "AUTHORIZATION_INVALID" | "RECOVERY_BINDING_MISMATCH" | "BOUNDARY_MISMATCH" | "WORKLOAD_MISMATCH" | "AUDIT_UNAVAILABLE";
6
+ import type { AuthorizationVerifier, ExecutionCommitOutcome, ExecutionFinalization, VerifiedAuthorization } from "./AuthorizationVerifier.js";
7
+ export type ExecutionBlockReason = "DECISION_NOT_AUTHORITATIVE" | "DECISION_NOT_ALLOW" | "INTENT_BINDING_MISMATCH" | "INTENT_CONFORMANCE_FAILED" | "AUTHORIZATION_MISSING" | "AUTHORIZATION_INVALID" | "RECOVERY_BINDING_MISMATCH" | "RECOVERY_ATTEMPT_UNKNOWN" | "BOUNDARY_MISMATCH" | "WORKLOAD_MISMATCH" | "AUDIT_UNAVAILABLE";
7
8
  /**
8
9
  * What this executor is, beyond the registry and the verifier it was given.
9
10
  *
@@ -24,6 +25,20 @@ export interface SafeExecutorOptions {
24
25
  * a policy that requires provenance refuses at the authority instead.
25
26
  */
26
27
  readonly workloadDigest?: string;
28
+ /**
29
+ * This executor's own record of the attempts it dispatched. A recovery
30
+ * reference is something a caller presents, so its decision and grant are
31
+ * claims; with a record to hold them against, reconciliation proceeds only
32
+ * for a binding this executor journaled before dispatch. Absent, the
33
+ * lookup still runs, and its audit events say `NON_AUTHORITATIVE`, because
34
+ * nothing here vouches for a binding it was only told about.
35
+ */
36
+ readonly attempts?: DispatchedAttempts;
37
+ }
38
+ /** Where an executor reads back the authorizations it dispatched an intent under. */
39
+ export interface DispatchedAttempts {
40
+ /** Every authorization journaled for this intent, oldest first; none when it was never dispatched here. */
41
+ authorizationsOf(captured: CapturedIntent): Promise<readonly VerifiedAuthorization[]>;
27
42
  }
28
43
  export type ExecutionPreDispatchFailureReason = "HANDLER_FAILED_BEFORE_DISPATCH" | "AUDIT_UNAVAILABLE";
29
44
  /**
@@ -89,6 +104,8 @@ export type SafeExecutionResult<TResult = unknown> = {
89
104
  readonly outcome: "FAILED_BEFORE_DISPATCH";
90
105
  readonly executed: false;
91
106
  readonly reason: ExecutionPreDispatchFailureReason;
107
+ /** Why the handler refused, when it threw a `PreDispatchRefusal`. */
108
+ readonly code?: string;
92
109
  readonly result: null;
93
110
  readonly authorization: VerifiedAuthorization;
94
111
  readonly finalization: ExecutionFinalization;
@@ -111,6 +128,36 @@ export type ExecutionReconciliationResult<TResult = unknown> = {
111
128
  readonly result: null;
112
129
  readonly authorization: VerifiedAuthorization;
113
130
  } | ExecutionBlockedResult | ProviderOutcomeUnknownResult;
131
+ /**
132
+ * A grant admitted for a later claim. Every check `run` makes before it
133
+ * consumes a grant has passed, and nothing has been consumed: the grant is
134
+ * still the authority's to hand out once. Only `hold` issues one, only the
135
+ * executor that issued it can claim it, and it is claimed at most once.
136
+ *
137
+ * This is for an effect that another system performs at a moment this
138
+ * process does not choose, such as a card authorization the issuer asks
139
+ * about in milliseconds: the decision is made before, the grant waits here,
140
+ * and the claim happens when that system asks for it.
141
+ */
142
+ export interface HeldExecution {
143
+ readonly captured: CapturedIntent;
144
+ readonly decision: DecisionGate;
145
+ /** The grant's own expiry: after it there is nothing left to claim. */
146
+ readonly expiresAt: string;
147
+ }
148
+ export interface ExecutionHeldResult {
149
+ readonly outcome: "HELD";
150
+ readonly executed: false;
151
+ readonly result: null;
152
+ readonly authorization: null;
153
+ readonly held: HeldExecution;
154
+ }
155
+ export type HeldClaimResult = {
156
+ readonly outcome: "CLAIMED";
157
+ readonly authorization: VerifiedAuthorization;
158
+ } | ExecutionBlockedResult | Extract<SafeExecutionResult<never>, {
159
+ outcome: "FAILED_BEFORE_DISPATCH";
160
+ }>;
114
161
  export declare class SafeExecutor {
115
162
  private readonly registry;
116
163
  private readonly verifier;
@@ -118,8 +165,72 @@ export declare class SafeExecutor {
118
165
  private readonly hasher;
119
166
  private readonly boundaryId;
120
167
  private readonly workloadDigest;
168
+ /** The holds this executor issued and nobody has claimed, keyed by the frozen handle. */
169
+ private readonly holds;
170
+ /** The holds claimed and not yet settled, with the authorization each one consumed. */
171
+ private readonly claims;
172
+ private readonly attempts;
121
173
  constructor(registry: ActionRegistry, verifier: AuthorizationVerifier, audit?: AuditRecorder | undefined, options?: SafeExecutorOptions);
122
- run<TResult = unknown>(captured: CapturedIntent, decision: GateDecision): Promise<SafeExecutionResult<TResult>>;
174
+ run<TResult = unknown>(captured: CapturedIntent, decision: DecisionGate): Promise<SafeExecutionResult<TResult>>;
175
+ /**
176
+ * Every check made before a grant may be consumed, with the evidence that
177
+ * the intent was captured and decided. A refusal is recorded and returned;
178
+ * null means the decision passed, and so carries a grant.
179
+ */
180
+ private admit;
181
+ /**
182
+ * Consumes the grant through the verifier, checks what came back against
183
+ * the decision and the intent, and records the consumption and the start
184
+ * of execution. Nothing here dispatches.
185
+ */
186
+ private consume;
187
+ /**
188
+ * Admits a decision exactly as `run` would and holds its grant instead of
189
+ * consuming it. The intent, the decision and the hold are recorded; the
190
+ * grant stays unclaimed until `claimHeld` is called with the handle, and an
191
+ * unclaimed hold simply expires with its grant.
192
+ */
193
+ hold(captured: CapturedIntent, decision: DecisionGate): Promise<ExecutionHeldResult | ExecutionBlockedResult>;
194
+ /**
195
+ * Admits a hold this executor did not make: one another replica made, or
196
+ * this process made before a restart, read back from a store the replicas
197
+ * share. Every check `hold` makes is made again on the way in, and the
198
+ * record says the hold was adopted. Adoption adds no authority: the grant
199
+ * is still the authority's to hand out once, so a hold claimed elsewhere
200
+ * is refused at the claim by the authority or the shared replay store,
201
+ * never admitted twice by this executor's own memory.
202
+ */
203
+ adoptHold(captured: CapturedIntent, decision: DecisionGate): Promise<ExecutionHeldResult | ExecutionBlockedResult>;
204
+ /**
205
+ * Registers a claim this executor did not make, so that `settleHeld` can
206
+ * finalize it here: the hold's intent and decision, and the authorization
207
+ * the claim returned, as the store that shared them kept them. Nothing is
208
+ * consumed and nothing is checked against the authority; the finalization
209
+ * is, as always, the authority's to accept or refuse.
210
+ */
211
+ adoptClaim(captured: CapturedIntent, decision: DecisionGate, authorization: VerifiedAuthorization, material?: JsonObject | null): HeldExecution;
212
+ /**
213
+ * What another process needs to finalize a claim this executor's verifier
214
+ * made, for a store the replicas share to keep sealed; null when the
215
+ * verifier keeps its claims to itself or did not make this one.
216
+ */
217
+ claimMaterial(authorization: VerifiedAuthorization): JsonObject | null;
218
+ private holdWith;
219
+ /**
220
+ * Claims a held grant, once. The handle is forgotten before the verifier
221
+ * is asked, so a second claim of the same hold, concurrent or later, is
222
+ * refused here without reaching the authority; a handle this executor did
223
+ * not issue is refused the same way. What the claim returns is checked
224
+ * exactly as `run` checks it.
225
+ */
226
+ claimHeld(held: HeldExecution): Promise<HeldClaimResult>;
227
+ /**
228
+ * Reports how a claimed hold ended, once: `COMMITTED` when the effect
229
+ * happened, `FAILED` when it definitively did not, `INDETERMINATE` when
230
+ * nobody can say. The terminal record names the outcome, as `run`'s do.
231
+ */
232
+ settleHeld(held: HeldExecution, outcome: ExecutionCommitOutcome, receipt?: string | null): Promise<ExecutionFinalization>;
233
+ private static settledEvent;
123
234
  /**
124
235
  * Reports the attempt outcome to the verifier's authority, with the
125
236
  * provider's effect receipt when the attempt brought one back. A missing,
@@ -130,7 +241,7 @@ export declare class SafeExecutor {
130
241
  reconcile<TResult = unknown>(captured: CapturedIntent, recovery: ExecutionRecoveryReference): Promise<ExecutionReconciliationResult<TResult>>;
131
242
  /**
132
243
  * Observational artifacts carry an explicit non-authoritative marker. They
133
- * must never be interpreted as a `GateDecision`, even after an unsafe cast
244
+ * must never be interpreted as a `DecisionGate`, even after an unsafe cast
134
245
  * or a JSON round trip.
135
246
  */
136
247
  private static isAuthoritativeDecision;
@@ -152,6 +263,7 @@ export declare class SafeExecutor {
152
263
  private failedBeforeDispatch;
153
264
  private record;
154
265
  private static recoveryReference;
266
+ private static sameBinding;
155
267
  private static authorizationFrom;
156
268
  }
157
269
  //# sourceMappingURL=SafeExecutor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SafeExecutor.d.ts","sourceRoot":"","sources":["../../src/execution/SafeExecutor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SafeExecutor.d.ts","sourceRoot":"","sources":["../../src/execution/SafeExecutor.ts"],"names":[],"mappings":"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"}
@@ -7,15 +7,108 @@ export class SafeExecutor {
7
7
  hasher = new CanonicalIntentHasher();
8
8
  boundaryId;
9
9
  workloadDigest;
10
+ /** The holds this executor issued and nobody has claimed, keyed by the frozen handle. */
11
+ holds = new WeakMap();
12
+ /** The holds claimed and not yet settled, with the authorization each one consumed. */
13
+ claims = new WeakMap();
14
+ attempts;
10
15
  constructor(registry, verifier, audit, options) {
11
16
  this.registry = registry;
12
17
  this.verifier = verifier;
13
18
  this.audit = audit;
14
19
  this.boundaryId = options?.boundaryId ?? null;
15
20
  this.workloadDigest = options?.workloadDigest ?? null;
21
+ this.attempts = options?.attempts ?? null;
16
22
  }
17
23
  async run(captured, decision) {
18
24
  const startedAt = Date.now();
25
+ const refused = await this.admit(captured, decision, startedAt);
26
+ if (refused !== null)
27
+ return refused;
28
+ const claim = await this.consume(captured, decision, startedAt);
29
+ if (claim.outcome !== "CLAIMED")
30
+ return claim;
31
+ const authorization = claim.authorization;
32
+ const attempt = await this.registry.executeTracked(captured, authorization);
33
+ if (attempt.status === "FAILED_BEFORE_DISPATCH") {
34
+ return await this.failedBeforeDispatch(captured, decision, authorization, "HANDLER_FAILED_BEFORE_DISPATCH", startedAt, attempt.code);
35
+ }
36
+ if (attempt.status === "REFUSED_AFTER_DISPATCH") {
37
+ // The provider said no. That is an outcome, so it is finalized `FAILED`
38
+ // rather than left indeterminate, and `executed` is false rather than
39
+ // null: nothing about it is unknown.
40
+ const finalization = await this.finalize(captured, decision, authorization, "FAILED", attempt.receipt);
41
+ const result = {
42
+ outcome: "DEFINITELY_NOT_EXECUTED",
43
+ executed: false,
44
+ recovered: false,
45
+ reason: attempt.reason,
46
+ result: null,
47
+ authorization,
48
+ finalization,
49
+ };
50
+ // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
51
+ await this.record({
52
+ eventType: "EXECUTION_REFUSED_AFTER_DISPATCH",
53
+ captured,
54
+ decision,
55
+ authorization,
56
+ reasonCodes: [attempt.reason, SafeExecutor.finalizationCode(finalization)],
57
+ durationMs: Date.now() - startedAt,
58
+ });
59
+ // Stryker restore all
60
+ return result;
61
+ }
62
+ if (attempt.status === "UNKNOWN_AFTER_DISPATCH") {
63
+ const finalization = await this.finalize(captured, decision, authorization, "INDETERMINATE", attempt.receipt);
64
+ const result = {
65
+ outcome: "UNKNOWN_AFTER_DISPATCH",
66
+ executed: null,
67
+ reason: "PROVIDER_OUTCOME_UNKNOWN",
68
+ result: null,
69
+ authorization,
70
+ recovery: SafeExecutor.recoveryReference(captured, authorization),
71
+ finalization,
72
+ };
73
+ // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
74
+ await this.record({
75
+ eventType: "EXECUTION_OUTCOME_UNKNOWN",
76
+ captured,
77
+ decision,
78
+ authorization,
79
+ reasonCodes: [result.reason, SafeExecutor.finalizationCode(finalization)],
80
+ durationMs: Date.now() - startedAt,
81
+ });
82
+ // Stryker restore all
83
+ return result;
84
+ }
85
+ const finalization = await this.finalize(captured, decision, authorization, "COMMITTED", attempt.receipt);
86
+ const result = {
87
+ outcome: "COMPLETED",
88
+ executed: true,
89
+ recovered: false,
90
+ result: attempt.result,
91
+ authorization,
92
+ finalization,
93
+ };
94
+ // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
95
+ await this.record({
96
+ eventType: "EXECUTION_COMPLETED",
97
+ captured,
98
+ decision,
99
+ authorization,
100
+ reasonCodes: [SafeExecutor.finalizationCode(finalization)],
101
+ durationMs: Date.now() - startedAt,
102
+ });
103
+ // Stryker restore all
104
+ return result;
105
+ }
106
+ /**
107
+ * Every check made before a grant may be consumed, with the evidence that
108
+ * the intent was captured and decided. A refusal is recorded and returned;
109
+ * null means the decision passed, and so carries a grant.
110
+ */
111
+ async admit(captured, decision, startedAt) {
19
112
  // Stryker disable next-line all: Audit payload mapping is covered by lifecycle event assertions.
20
113
  const intentRecorded = await this.record({ eventType: "INTENT_CAPTURED", captured });
21
114
  // An observational artifact (shadow observation, serialized audit event) is
@@ -55,6 +148,14 @@ export class SafeExecutor {
55
148
  return await this.block(captured, decision, "WORKLOAD_MISMATCH", startedAt);
56
149
  }
57
150
  this.registry.validate(captured);
151
+ return null;
152
+ }
153
+ /**
154
+ * Consumes the grant through the verifier, checks what came back against
155
+ * the decision and the intent, and records the consumption and the start
156
+ * of execution. Nothing here dispatches.
157
+ */
158
+ async consume(captured, decision, startedAt) {
58
159
  let authorization;
59
160
  try {
60
161
  authorization = await this.verifier.verifyAndConsume(captured, decision);
@@ -103,79 +204,141 @@ export class SafeExecutor {
103
204
  if (this.audit?.requiresDeliveryBeforeExecution === true && !executionRecorded) {
104
205
  return await this.failedBeforeDispatch(captured, decision, authorization, "AUDIT_UNAVAILABLE", startedAt);
105
206
  }
106
- const attempt = await this.registry.executeTracked(captured, authorization);
107
- if (attempt.status === "FAILED_BEFORE_DISPATCH") {
108
- return await this.failedBeforeDispatch(captured, decision, authorization, "HANDLER_FAILED_BEFORE_DISPATCH", startedAt);
109
- }
110
- if (attempt.status === "REFUSED_AFTER_DISPATCH") {
111
- // The provider said no. That is an outcome, so it is finalized `FAILED`
112
- // rather than left indeterminate, and `executed` is false rather than
113
- // null: nothing about it is unknown.
114
- const finalization = await this.finalize(captured, decision, authorization, "FAILED", attempt.receipt);
115
- const result = {
116
- outcome: "DEFINITELY_NOT_EXECUTED",
117
- executed: false,
118
- recovered: false,
119
- reason: attempt.reason,
120
- result: null,
121
- authorization,
122
- finalization,
123
- };
124
- // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
125
- await this.record({
126
- eventType: "EXECUTION_REFUSED_AFTER_DISPATCH",
127
- captured,
128
- decision,
129
- authorization,
130
- reasonCodes: [attempt.reason, SafeExecutor.finalizationCode(finalization)],
131
- durationMs: Date.now() - startedAt,
132
- });
133
- // Stryker restore all
134
- return result;
207
+ return { outcome: "CLAIMED", authorization };
208
+ }
209
+ /**
210
+ * Admits a decision exactly as `run` would and holds its grant instead of
211
+ * consuming it. The intent, the decision and the hold are recorded; the
212
+ * grant stays unclaimed until `claimHeld` is called with the handle, and an
213
+ * unclaimed hold simply expires with its grant.
214
+ */
215
+ async hold(captured, decision) {
216
+ return await this.holdWith(captured, decision, []);
217
+ }
218
+ /**
219
+ * Admits a hold this executor did not make: one another replica made, or
220
+ * this process made before a restart, read back from a store the replicas
221
+ * share. Every check `hold` makes is made again on the way in, and the
222
+ * record says the hold was adopted. Adoption adds no authority: the grant
223
+ * is still the authority's to hand out once, so a hold claimed elsewhere
224
+ * is refused at the claim by the authority or the shared replay store,
225
+ * never admitted twice by this executor's own memory.
226
+ */
227
+ async adoptHold(captured, decision) {
228
+ return await this.holdWith(captured, decision, ["HOLD_ADOPTED"]);
229
+ }
230
+ /**
231
+ * Registers a claim this executor did not make, so that `settleHeld` can
232
+ * finalize it here: the hold's intent and decision, and the authorization
233
+ * the claim returned, as the store that shared them kept them. Nothing is
234
+ * consumed and nothing is checked against the authority; the finalization
235
+ * is, as always, the authority's to accept or refuse.
236
+ */
237
+ adoptClaim(captured, decision, authorization, material = null) {
238
+ if (!SafeExecutor.isAuthoritativeDecision(decision) ||
239
+ decision.intentHash !== captured.intentHash ||
240
+ authorization.intentHash !== captured.intentHash) {
241
+ throw new Error("CLAIM_ADOPTION_MISMATCH");
135
242
  }
136
- if (attempt.status === "UNKNOWN_AFTER_DISPATCH") {
137
- const finalization = await this.finalize(captured, decision, authorization, "INDETERMINATE", attempt.receipt);
138
- const result = {
139
- outcome: "UNKNOWN_AFTER_DISPATCH",
140
- executed: null,
141
- reason: "PROVIDER_OUTCOME_UNKNOWN",
142
- result: null,
143
- authorization,
144
- recovery: SafeExecutor.recoveryReference(captured, authorization),
145
- finalization,
146
- };
147
- // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
148
- await this.record({
149
- eventType: "EXECUTION_OUTCOME_UNKNOWN",
150
- captured,
151
- decision,
152
- authorization,
153
- reasonCodes: [result.reason, SafeExecutor.finalizationCode(finalization)],
154
- durationMs: Date.now() - startedAt,
155
- });
156
- // Stryker restore all
157
- return result;
243
+ if (material !== null) {
244
+ const adopt = this.verifier.adoptClaim;
245
+ if (adopt === undefined || !adopt.call(this.verifier, authorization, material)) {
246
+ throw new Error("CLAIM_MATERIAL_REFUSED");
247
+ }
158
248
  }
159
- const finalization = await this.finalize(captured, decision, authorization, "COMMITTED", attempt.receipt);
160
- const result = {
161
- outcome: "COMPLETED",
162
- executed: true,
163
- recovered: false,
164
- result: attempt.result,
165
- authorization,
166
- finalization,
167
- };
249
+ const held = Object.freeze({
250
+ captured,
251
+ decision,
252
+ expiresAt: authorization.expiresAt,
253
+ });
254
+ this.claims.set(held, authorization);
255
+ return held;
256
+ }
257
+ /**
258
+ * What another process needs to finalize a claim this executor's verifier
259
+ * made, for a store the replicas share to keep sealed; null when the
260
+ * verifier keeps its claims to itself or did not make this one.
261
+ */
262
+ claimMaterial(authorization) {
263
+ const material = this.verifier.claimMaterial;
264
+ return material === undefined ? null : material.call(this.verifier, authorization);
265
+ }
266
+ async holdWith(captured, decision, reasonCodes) {
267
+ const startedAt = Date.now();
268
+ const refused = await this.admit(captured, decision, startedAt);
269
+ if (refused !== null)
270
+ return refused;
271
+ const admitted = decision;
168
272
  // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
169
- await this.record({
170
- eventType: "EXECUTION_COMPLETED",
273
+ const heldRecorded = await this.record({
274
+ eventType: "GRANT_HELD",
171
275
  captured,
172
276
  decision,
277
+ ...(reasonCodes.length === 0 ? {} : { reasonCodes: [...reasonCodes] }),
278
+ durationMs: Date.now() - startedAt,
279
+ });
280
+ // Stryker restore all
281
+ if (this.audit?.requiresDeliveryBeforeExecution === true && !heldRecorded) {
282
+ return await this.block(captured, decision, "AUDIT_UNAVAILABLE", startedAt);
283
+ }
284
+ const held = Object.freeze({
285
+ captured,
286
+ decision,
287
+ expiresAt: admitted.authorization.expiresAt,
288
+ });
289
+ this.holds.set(held, admitted);
290
+ return { outcome: "HELD", executed: false, result: null, authorization: null, held };
291
+ }
292
+ /**
293
+ * Claims a held grant, once. The handle is forgotten before the verifier
294
+ * is asked, so a second claim of the same hold, concurrent or later, is
295
+ * refused here without reaching the authority; a handle this executor did
296
+ * not issue is refused the same way. What the claim returns is checked
297
+ * exactly as `run` checks it.
298
+ */
299
+ async claimHeld(held) {
300
+ const startedAt = Date.now();
301
+ const decision = this.holds.get(held);
302
+ if (decision === undefined) {
303
+ return await this.block(held.captured, held.decision, "AUTHORIZATION_INVALID", startedAt);
304
+ }
305
+ this.holds.delete(held);
306
+ const claim = await this.consume(held.captured, decision, startedAt);
307
+ if (claim.outcome === "CLAIMED")
308
+ this.claims.set(held, claim.authorization);
309
+ return claim;
310
+ }
311
+ /**
312
+ * Reports how a claimed hold ended, once: `COMMITTED` when the effect
313
+ * happened, `FAILED` when it definitively did not, `INDETERMINATE` when
314
+ * nobody can say. The terminal record names the outcome, as `run`'s do.
315
+ */
316
+ async settleHeld(held, outcome, receipt = null) {
317
+ const startedAt = Date.now();
318
+ const authorization = this.claims.get(held);
319
+ if (authorization === undefined)
320
+ throw new Error("HELD_EXECUTION_NOT_CLAIMED");
321
+ this.claims.delete(held);
322
+ const finalization = await this.finalize(held.captured, held.decision, authorization, outcome, receipt);
323
+ const eventType = SafeExecutor.settledEvent(outcome);
324
+ // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
325
+ await this.record({
326
+ eventType,
327
+ captured: held.captured,
328
+ decision: held.decision,
173
329
  authorization,
174
330
  reasonCodes: [SafeExecutor.finalizationCode(finalization)],
175
331
  durationMs: Date.now() - startedAt,
176
332
  });
177
333
  // Stryker restore all
178
- return result;
334
+ return finalization;
335
+ }
336
+ static settledEvent(outcome) {
337
+ if (outcome === "COMMITTED")
338
+ return "EXECUTION_COMPLETED";
339
+ if (outcome === "FAILED")
340
+ return "EXECUTION_REFUSED_AFTER_DISPATCH";
341
+ return "EXECUTION_OUTCOME_UNKNOWN";
179
342
  }
180
343
  /**
181
344
  * Reports the attempt outcome to the verifier's authority, with the
@@ -211,8 +374,22 @@ export class SafeExecutor {
211
374
  !this.intentConforms(captured)) {
212
375
  return await this.block(captured, undefined, "RECOVERY_BINDING_MISMATCH", startedAt);
213
376
  }
214
- const reconciliation = await this.registry.reconcile(captured, recovery.idempotencyKey);
377
+ // The decision and grant are the caller's claim until the executor's own
378
+ // record confirms them; the provider is not asked about a binding that
379
+ // was never dispatched here.
380
+ let authority = "NON_AUTHORITATIVE";
381
+ if (this.attempts !== null) {
382
+ const recorded = await this.attempts.authorizationsOf(captured);
383
+ if (recorded.length === 0) {
384
+ return await this.block(captured, undefined, "RECOVERY_ATTEMPT_UNKNOWN", startedAt);
385
+ }
386
+ if (!recorded.some((candidate) => SafeExecutor.sameBinding(candidate, recovery))) {
387
+ return await this.block(captured, undefined, "RECOVERY_BINDING_MISMATCH", startedAt);
388
+ }
389
+ authority = "AUTHORITATIVE";
390
+ }
215
391
  const authorization = SafeExecutor.authorizationFrom(recovery);
392
+ const reconciliation = await this.registry.reconcile(captured, recovery.idempotencyKey);
216
393
  if (reconciliation.status === "COMPLETED") {
217
394
  const result = {
218
395
  outcome: "COMPLETED",
@@ -226,6 +403,7 @@ export class SafeExecutor {
226
403
  eventType: "RECONCILIATION_COMPLETED",
227
404
  captured,
228
405
  authorization,
406
+ authority,
229
407
  durationMs: Date.now() - startedAt,
230
408
  });
231
409
  // Stryker restore all
@@ -245,6 +423,7 @@ export class SafeExecutor {
245
423
  eventType: "RECONCILIATION_NOT_EXECUTED",
246
424
  captured,
247
425
  authorization,
426
+ authority,
248
427
  reasonCodes: [result.reason],
249
428
  durationMs: Date.now() - startedAt,
250
429
  });
@@ -264,6 +443,7 @@ export class SafeExecutor {
264
443
  eventType: "RECONCILIATION_UNKNOWN",
265
444
  captured,
266
445
  authorization,
446
+ authority,
267
447
  reasonCodes: [result.reason],
268
448
  durationMs: Date.now() - startedAt,
269
449
  });
@@ -272,7 +452,7 @@ export class SafeExecutor {
272
452
  }
273
453
  /**
274
454
  * Observational artifacts carry an explicit non-authoritative marker. They
275
- * must never be interpreted as a `GateDecision`, even after an unsafe cast
455
+ * must never be interpreted as a `DecisionGate`, even after an unsafe cast
276
456
  * or a JSON round trip.
277
457
  */
278
458
  static isAuthoritativeDecision(decision) {
@@ -325,7 +505,7 @@ export class SafeExecutor {
325
505
  // Stryker restore all
326
506
  return { outcome: "BLOCKED", executed: false, reason, result: null, authorization: null };
327
507
  }
328
- async failedBeforeDispatch(captured, decision, authorization, reason, startedAt) {
508
+ async failedBeforeDispatch(captured, decision, authorization, reason, startedAt, code) {
329
509
  // The grant was consumed but no side effect was attempted.
330
510
  const finalization = await this.finalize(captured, decision, authorization, "FAILED");
331
511
  // Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
@@ -334,7 +514,11 @@ export class SafeExecutor {
334
514
  captured,
335
515
  decision,
336
516
  authorization,
337
- reasonCodes: [reason, SafeExecutor.finalizationCode(finalization)],
517
+ reasonCodes: [
518
+ reason,
519
+ ...(code === undefined ? [] : [code]),
520
+ SafeExecutor.finalizationCode(finalization),
521
+ ],
338
522
  durationMs: Date.now() - startedAt,
339
523
  });
340
524
  // Stryker restore all
@@ -342,6 +526,7 @@ export class SafeExecutor {
342
526
  outcome: "FAILED_BEFORE_DISPATCH",
343
527
  executed: false,
344
528
  reason,
529
+ ...(code === undefined ? {} : { code }),
345
530
  result: null,
346
531
  authorization,
347
532
  finalization,
@@ -367,6 +552,13 @@ export class SafeExecutor {
367
552
  idempotencyKey: captured.intent.idempotencyKey,
368
553
  });
369
554
  }
555
+ static sameBinding(recorded, recovery) {
556
+ return (recorded.decisionId === recovery.decisionId &&
557
+ recorded.dossierId === recovery.dossierId &&
558
+ recorded.grantId === recovery.grantId &&
559
+ recorded.intentHash === recovery.intentHash &&
560
+ recorded.expiresAt === recovery.expiresAt);
561
+ }
370
562
  static authorizationFrom(recovery) {
371
563
  return Object.freeze({
372
564
  decisionId: recovery.decisionId,