@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.
- package/README.md +7 -6
- package/dist/Index.d.ts +6 -0
- package/dist/Index.d.ts.map +1 -1
- package/dist/Index.js +6 -0
- package/dist/Index.js.map +1 -1
- package/dist/approval/PresenceApprovalCoordinator.d.ts +2 -2
- package/dist/audit/AuditRecorder.d.ts +6 -6
- package/dist/audit/AuditRecorder.d.ts.map +1 -1
- package/dist/audit/AuditRecorder.js.map +1 -1
- package/dist/decision/DecionisGate.d.ts +16 -5
- package/dist/decision/DecionisGate.d.ts.map +1 -1
- package/dist/decision/DecionisGate.js +113 -11
- package/dist/decision/DecionisGate.js.map +1 -1
- package/dist/decision/DecisionAuthority.d.ts +42 -4
- package/dist/decision/DecisionAuthority.d.ts.map +1 -1
- package/dist/decision/DecisionAuthority.js +2 -2
- package/dist/decision/DecisionAuthority.js.map +1 -1
- package/dist/decision/FixtureDecisionAuthority.d.ts +7 -3
- package/dist/decision/FixtureDecisionAuthority.d.ts.map +1 -1
- package/dist/decision/FixtureDecisionAuthority.js +26 -3
- package/dist/decision/FixtureDecisionAuthority.js.map +1 -1
- package/dist/decision/ImmutableDecisionGate.d.ts +3 -0
- package/dist/decision/ImmutableDecisionGate.d.ts.map +1 -0
- package/dist/decision/{ImmutableGateDecision.js → ImmutableDecisionGate.js} +2 -2
- package/dist/decision/{ImmutableGateDecision.js.map → ImmutableDecisionGate.js.map} +1 -1
- package/dist/decision/ShadowGate.d.ts +2 -2
- package/dist/decision/ShadowGate.js +2 -2
- package/dist/decision/edge/BundleWindow.d.ts +43 -0
- package/dist/decision/edge/BundleWindow.d.ts.map +1 -0
- package/dist/decision/edge/BundleWindow.js +41 -0
- package/dist/decision/edge/BundleWindow.js.map +1 -0
- package/dist/decision/edge/EdgeBundleManager.d.ts +87 -0
- package/dist/decision/edge/EdgeBundleManager.d.ts.map +1 -0
- package/dist/decision/edge/EdgeBundleManager.js +175 -0
- package/dist/decision/edge/EdgeBundleManager.js.map +1 -0
- package/dist/decision/edge/EdgeBundleSource.d.ts +59 -0
- package/dist/decision/edge/EdgeBundleSource.d.ts.map +1 -0
- package/dist/decision/edge/EdgeBundleSource.js +99 -0
- package/dist/decision/edge/EdgeBundleSource.js.map +1 -0
- package/dist/decision/edge/EdgeDecisionAuthority.d.ts +104 -0
- package/dist/decision/edge/EdgeDecisionAuthority.d.ts.map +1 -0
- package/dist/decision/edge/EdgeDecisionAuthority.js +197 -0
- package/dist/decision/edge/EdgeDecisionAuthority.js.map +1 -0
- package/dist/decision/edge/EdgeModule.d.ts +63 -0
- package/dist/decision/edge/EdgeModule.d.ts.map +1 -0
- package/dist/decision/edge/EdgeModule.js +147 -0
- package/dist/decision/edge/EdgeModule.js.map +1 -0
- package/dist/execution/ActionRegistry.d.ts +23 -0
- package/dist/execution/ActionRegistry.d.ts.map +1 -1
- package/dist/execution/ActionRegistry.js +32 -2
- package/dist/execution/ActionRegistry.js.map +1 -1
- package/dist/execution/AuthorizationVerifier.d.ts +19 -4
- package/dist/execution/AuthorizationVerifier.d.ts.map +1 -1
- package/dist/execution/AuthorizationVerifier.js +41 -2
- package/dist/execution/AuthorizationVerifier.js.map +1 -1
- package/dist/execution/LocalAuthorizationVerifier.d.ts +74 -0
- package/dist/execution/LocalAuthorizationVerifier.d.ts.map +1 -0
- package/dist/execution/LocalAuthorizationVerifier.js +138 -0
- package/dist/execution/LocalAuthorizationVerifier.js.map +1 -0
- package/dist/execution/PostgresReplayStore.d.ts +57 -0
- package/dist/execution/PostgresReplayStore.d.ts.map +1 -0
- package/dist/execution/PostgresReplayStore.js +81 -0
- package/dist/execution/PostgresReplayStore.js.map +1 -0
- package/dist/execution/ReplayStore.d.ts +14 -0
- package/dist/execution/ReplayStore.d.ts.map +1 -1
- package/dist/execution/ReplayStore.js +8 -0
- package/dist/execution/ReplayStore.js.map +1 -1
- package/dist/execution/SafeExecutor.d.ts +117 -5
- package/dist/execution/SafeExecutor.d.ts.map +1 -1
- package/dist/execution/SafeExecutor.js +259 -67
- package/dist/execution/SafeExecutor.js.map +1 -1
- package/dist/report/DecisionReport.d.ts +2 -2
- package/dist/report/HostedOutcome.d.ts +2 -2
- package/dist/shadow/ShadowPipeline.d.ts +1 -1
- package/dist/testing/LocalAuthority.d.ts +35 -2
- package/dist/testing/LocalAuthority.d.ts.map +1 -1
- package/dist/testing/LocalAuthority.js +54 -1
- package/dist/testing/LocalAuthority.js.map +1 -1
- package/package.json +3 -3
- package/dist/decision/ImmutableGateDecision.d.ts +0 -3
- package/dist/decision/ImmutableGateDecision.d.ts.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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 `
|
|
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":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 (
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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: [
|
|
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,
|