@decionis/agent-safe-pipeline 0.1.3 → 0.1.4
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 +318 -16
- package/dist/Index.d.ts +1 -0
- package/dist/Index.d.ts.map +1 -1
- package/dist/Index.js +1 -0
- package/dist/Index.js.map +1 -1
- package/dist/approval/PresenceApprovalCoordinator.d.ts +25 -0
- package/dist/approval/PresenceApprovalCoordinator.d.ts.map +1 -1
- package/dist/approval/PresenceApprovalCoordinator.js +54 -6
- package/dist/approval/PresenceApprovalCoordinator.js.map +1 -1
- package/dist/audit/AuditRecorder.d.ts +122 -0
- package/dist/audit/AuditRecorder.d.ts.map +1 -0
- package/dist/audit/AuditRecorder.js +312 -0
- package/dist/audit/AuditRecorder.js.map +1 -0
- package/dist/decision/DecionisGate.d.ts +91 -2
- package/dist/decision/DecionisGate.d.ts.map +1 -1
- package/dist/decision/DecionisGate.js +793 -44
- package/dist/decision/DecionisGate.js.map +1 -1
- package/dist/decision/DecisionAuthority.d.ts +50 -1
- package/dist/decision/DecisionAuthority.d.ts.map +1 -1
- package/dist/decision/DecisionAuthority.js.map +1 -1
- package/dist/decision/FixtureDecisionAuthority.d.ts +27 -1
- package/dist/decision/FixtureDecisionAuthority.d.ts.map +1 -1
- package/dist/decision/FixtureDecisionAuthority.js +47 -0
- package/dist/decision/FixtureDecisionAuthority.js.map +1 -1
- package/dist/decision/ImmutableGateDecision.d.ts.map +1 -1
- package/dist/decision/ImmutableGateDecision.js +18 -0
- package/dist/decision/ImmutableGateDecision.js.map +1 -1
- package/dist/execution/ActionRegistry.d.ts +37 -0
- package/dist/execution/ActionRegistry.d.ts.map +1 -1
- package/dist/execution/ActionRegistry.js +75 -1
- package/dist/execution/ActionRegistry.js.map +1 -1
- package/dist/execution/AuthorizationVerifier.d.ts +29 -0
- package/dist/execution/AuthorizationVerifier.d.ts.map +1 -1
- package/dist/execution/AuthorizationVerifier.js +126 -25
- package/dist/execution/AuthorizationVerifier.js.map +1 -1
- package/dist/execution/SafeExecutor.d.ts +72 -7
- package/dist/execution/SafeExecutor.d.ts.map +1 -1
- package/dist/execution/SafeExecutor.js +253 -17
- package/dist/execution/SafeExecutor.js.map +1 -1
- package/dist/intent/CanonicalIntentHasher.d.ts +1 -1
- package/dist/intent/CanonicalIntentHasher.d.ts.map +1 -1
- package/dist/intent/CanonicalIntentHasher.js +6 -2
- package/dist/intent/CanonicalIntentHasher.js.map +1 -1
- package/dist/intent/ExecutionIntent.d.ts +14 -2
- package/dist/intent/ExecutionIntent.d.ts.map +1 -1
- package/dist/intent/ExecutionIntent.js +7 -0
- package/dist/intent/ExecutionIntent.js.map +1 -1
- package/dist/intent/IntentCapture.d.ts +5 -0
- package/dist/intent/IntentCapture.d.ts.map +1 -1
- package/dist/intent/IntentCapture.js +12 -1
- package/dist/intent/IntentCapture.js.map +1 -1
- package/dist/shadow/ShadowPipeline.d.ts +75 -5
- package/dist/shadow/ShadowPipeline.d.ts.map +1 -1
- package/dist/shadow/ShadowPipeline.js +157 -7
- package/dist/shadow/ShadowPipeline.js.map +1 -1
- package/dist/testing/Index.d.ts +12 -0
- package/dist/testing/Index.d.ts.map +1 -0
- package/dist/testing/Index.js +12 -0
- package/dist/testing/Index.js.map +1 -0
- package/dist/testing/LocalAuthority.d.ts +203 -0
- package/dist/testing/LocalAuthority.d.ts.map +1 -0
- package/dist/testing/LocalAuthority.js +799 -0
- package/dist/testing/LocalAuthority.js.map +1 -0
- package/dist/testing/LocalPresence.d.ts +210 -0
- package/dist/testing/LocalPresence.d.ts.map +1 -0
- package/dist/testing/LocalPresence.js +473 -0
- package/dist/testing/LocalPresence.js.map +1 -0
- package/package.json +40 -4
|
@@ -2,26 +2,61 @@ import { z } from "zod";
|
|
|
2
2
|
import { AuthorityBaseUrl } from "../http/AuthorityBaseUrl.js";
|
|
3
3
|
import { BoundedResponseBody } from "../http/BoundedResponseBody.js";
|
|
4
4
|
import { CanonicalIntentHasher } from "../intent/CanonicalIntentHasher.js";
|
|
5
|
-
const
|
|
6
|
-
.
|
|
5
|
+
const boundedIdentifier = z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.max(200)
|
|
9
|
+
.refine(hasNoControlCharacters, { message: "IDENTIFIER_INVALID" });
|
|
10
|
+
const intentHash = z.string().regex(/^sha256:[0-9a-f]{64}$/);
|
|
11
|
+
const CLAIM_TOKEN_PATTERN = /^[\w-]{43,128}$/;
|
|
12
|
+
/**
|
|
13
|
+
* Mirrors `ExecutionTokenResponse` and `ExecutionTokenClaims` in the Decionis
|
|
14
|
+
* OpenAPI contract. Claims are declared `additionalProperties: true`, and the
|
|
15
|
+
* live response carries revalidation fields beyond the documented envelope, so
|
|
16
|
+
* unknown fields are tolerated; authority is established only by the positive
|
|
17
|
+
* assertions checked below, never by an extra field.
|
|
18
|
+
*/
|
|
19
|
+
const ClaimResponseSchema = z.looseObject({
|
|
7
20
|
valid: z.literal(true),
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
reason_codes: z.array(boundedIdentifier).max(50).optional(),
|
|
22
|
+
should_execute: z.boolean().optional(),
|
|
23
|
+
claims: z.looseObject({
|
|
24
|
+
iss: boundedIdentifier,
|
|
25
|
+
sub: boundedIdentifier.optional(),
|
|
26
|
+
aud: z.string().min(1).max(500).optional(),
|
|
27
|
+
org_id: z.string().uuid(),
|
|
28
|
+
dossier_id: boundedIdentifier,
|
|
29
|
+
decision_id: boundedIdentifier,
|
|
30
|
+
action: boundedIdentifier,
|
|
31
|
+
decision: z.literal("allow"),
|
|
32
|
+
scope: z.literal("execute"),
|
|
33
|
+
binding: z.looseObject({ intent_hash: intentHash }),
|
|
34
|
+
jti: boundedIdentifier,
|
|
35
|
+
iat: z.number().int(),
|
|
36
|
+
nbf: z.number().int(),
|
|
13
37
|
exp: z.number().int().positive(),
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
})
|
|
18
|
-
|
|
38
|
+
}),
|
|
39
|
+
claim_token: z.string().regex(CLAIM_TOKEN_PATTERN).nullable().optional(),
|
|
40
|
+
claim_lease_expires_at: z.string().datetime().nullable().optional(),
|
|
41
|
+
});
|
|
42
|
+
const FinalizeResponseSchema = z.looseObject({
|
|
43
|
+
finalized: z.boolean(),
|
|
44
|
+
reason_codes: z.array(boundedIdentifier).max(50).optional(),
|
|
45
|
+
});
|
|
19
46
|
const MAX_RESPONSE_BYTES = 100 * 1024;
|
|
47
|
+
/**
|
|
48
|
+
* Claims a Decionis execution grant through `/v1/execution/claim-token`
|
|
49
|
+
* immediately before dispatch and finalizes the attempt through
|
|
50
|
+
* `/v1/execution/finalize-token` afterwards, so commit evidence joins the
|
|
51
|
+
* Decision Dossier chain.
|
|
52
|
+
*/
|
|
20
53
|
export class DecionisGrantVerifier {
|
|
21
54
|
baseUrl;
|
|
22
55
|
apiKey;
|
|
23
56
|
timeoutMs;
|
|
24
57
|
fetchImpl;
|
|
58
|
+
/** Claim material is keyed by the frozen authorization it belongs to and never exposed. */
|
|
59
|
+
claims = new WeakMap();
|
|
25
60
|
constructor(options) {
|
|
26
61
|
this.baseUrl = AuthorityBaseUrl.normalize(options.baseUrl, options.allowInsecureLoopback === true);
|
|
27
62
|
this.apiKey = options.apiKey;
|
|
@@ -36,10 +71,11 @@ export class DecionisGrantVerifier {
|
|
|
36
71
|
decision.intentHash !== captured.intentHash) {
|
|
37
72
|
return null;
|
|
38
73
|
}
|
|
74
|
+
const commitCorrelationId = captured.intent.intentId;
|
|
39
75
|
const controller = new AbortController();
|
|
40
76
|
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
41
77
|
try {
|
|
42
|
-
const response = await this.fetchImpl(`${this.baseUrl}/v1/execution/
|
|
78
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/execution/claim-token`, {
|
|
43
79
|
method: "POST",
|
|
44
80
|
headers: {
|
|
45
81
|
authorization: `Bearer ${this.apiKey}`,
|
|
@@ -49,7 +85,11 @@ export class DecionisGrantVerifier {
|
|
|
49
85
|
execution_token: decision.authorization.token,
|
|
50
86
|
intent_hash: captured.intentHash,
|
|
51
87
|
intent: CanonicalIntentHasher.bindingOf(captured.intent),
|
|
88
|
+
// A Presence-bound decision is re-verified by the authority at claim
|
|
89
|
+
// time, so the same evidence must accompany the claim.
|
|
90
|
+
...(decision.evidence === undefined ? {} : { evidence: decision.evidence }),
|
|
52
91
|
consumed_by: captured.intent.actor.id,
|
|
92
|
+
commit_correlation_id: commitCorrelationId,
|
|
53
93
|
}),
|
|
54
94
|
signal: controller.signal,
|
|
55
95
|
});
|
|
@@ -58,24 +98,38 @@ export class DecionisGrantVerifier {
|
|
|
58
98
|
const text = await BoundedResponseBody.read(response, MAX_RESPONSE_BYTES);
|
|
59
99
|
if (text === null)
|
|
60
100
|
return null;
|
|
61
|
-
const parsed =
|
|
62
|
-
const
|
|
101
|
+
const parsed = ClaimResponseSchema.parse(JSON.parse(text));
|
|
102
|
+
const claims = parsed.claims;
|
|
103
|
+
const claimExpiryMs = claims.exp * 1_000;
|
|
63
104
|
const decisionExpirySeconds = Math.floor(Date.parse(decision.authorization.expiresAt) / 1_000);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
parsed.
|
|
67
|
-
|
|
105
|
+
const audience = `${captured.intent.downstreamTarget.system}:${captured.intent.downstreamTarget.operation}`;
|
|
106
|
+
if (parsed.should_execute === false ||
|
|
107
|
+
typeof parsed.claim_token !== "string" ||
|
|
108
|
+
claims.binding.intent_hash !== captured.intentHash ||
|
|
109
|
+
claims.decision_id !== decision.decisionId ||
|
|
110
|
+
claims.dossier_id !== decision.dossierId ||
|
|
111
|
+
claims.org_id !== captured.intent.tenantId ||
|
|
112
|
+
claims.sub !== captured.intent.actor.id ||
|
|
113
|
+
claims.action !== captured.intent.action ||
|
|
114
|
+
(claims.aud !== undefined && claims.aud !== audience) ||
|
|
115
|
+
claims.exp !== decisionExpirySeconds ||
|
|
68
116
|
claimExpiryMs <= Date.now() ||
|
|
69
117
|
claimExpiryMs > Date.parse(captured.intent.expiresAt)) {
|
|
70
118
|
return null;
|
|
71
119
|
}
|
|
72
|
-
|
|
73
|
-
decisionId:
|
|
74
|
-
dossierId:
|
|
75
|
-
grantId:
|
|
76
|
-
intentHash:
|
|
77
|
-
expiresAt: new Date(
|
|
120
|
+
const authorization = Object.freeze({
|
|
121
|
+
decisionId: claims.decision_id,
|
|
122
|
+
dossierId: claims.dossier_id,
|
|
123
|
+
grantId: claims.jti,
|
|
124
|
+
intentHash: claims.binding.intent_hash,
|
|
125
|
+
expiresAt: new Date(claims.exp * 1_000).toISOString(),
|
|
78
126
|
});
|
|
127
|
+
this.claims.set(authorization, {
|
|
128
|
+
executionToken: decision.authorization.token,
|
|
129
|
+
claimToken: parsed.claim_token,
|
|
130
|
+
commitCorrelationId,
|
|
131
|
+
});
|
|
132
|
+
return authorization;
|
|
79
133
|
}
|
|
80
134
|
catch {
|
|
81
135
|
return null;
|
|
@@ -84,5 +138,52 @@ export class DecionisGrantVerifier {
|
|
|
84
138
|
clearTimeout(timeout);
|
|
85
139
|
}
|
|
86
140
|
}
|
|
141
|
+
async finalize(input) {
|
|
142
|
+
const claim = this.claims.get(input.authorization);
|
|
143
|
+
if (claim === undefined)
|
|
144
|
+
return "PENDING";
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
147
|
+
try {
|
|
148
|
+
const response = await this.fetchImpl(`${this.baseUrl}/v1/execution/finalize-token`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
152
|
+
"content-type": "application/json",
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
execution_token: claim.executionToken,
|
|
156
|
+
claim_token: claim.claimToken,
|
|
157
|
+
outcome: input.outcome,
|
|
158
|
+
commit_correlation_id: claim.commitCorrelationId,
|
|
159
|
+
}),
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok)
|
|
163
|
+
return "PENDING";
|
|
164
|
+
const text = await BoundedResponseBody.read(response, MAX_RESPONSE_BYTES);
|
|
165
|
+
if (text === null)
|
|
166
|
+
return "PENDING";
|
|
167
|
+
const parsed = FinalizeResponseSchema.parse(JSON.parse(text));
|
|
168
|
+
if (!parsed.finalized)
|
|
169
|
+
return "PENDING";
|
|
170
|
+
this.claims.delete(input.authorization);
|
|
171
|
+
return "RECORDED";
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return "PENDING";
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
clearTimeout(timeout);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function hasNoControlCharacters(value) {
|
|
182
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
183
|
+
const codeUnit = value.charCodeAt(index);
|
|
184
|
+
if (codeUnit <= 0x1f || codeUnit === 0x7f)
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
87
188
|
}
|
|
88
189
|
//# sourceMappingURL=AuthorizationVerifier.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthorizationVerifier.js","sourceRoot":"","sources":["../../src/execution/AuthorizationVerifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthorizationVerifier.js","sourceRoot":"","sources":["../../src/execution/AuthorizationVerifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,25 +1,90 @@
|
|
|
1
1
|
import type { GateDecision } from "../decision/DecisionAuthority.js";
|
|
2
|
+
import type { AuditRecorder } from "../audit/AuditRecorder.js";
|
|
2
3
|
import type { CapturedIntent } from "../intent/ExecutionIntent.js";
|
|
3
4
|
import type { ActionRegistry } from "./ActionRegistry.js";
|
|
4
|
-
import type { AuthorizationVerifier, VerifiedAuthorization } from "./AuthorizationVerifier.js";
|
|
5
|
-
export type ExecutionBlockReason = "DECISION_NOT_ALLOW" | "INTENT_BINDING_MISMATCH" | "INTENT_CONFORMANCE_FAILED" | "AUTHORIZATION_MISSING" | "AUTHORIZATION_INVALID";
|
|
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" | "AUDIT_UNAVAILABLE";
|
|
7
|
+
export type ExecutionPreDispatchFailureReason = "HANDLER_FAILED_BEFORE_DISPATCH" | "AUDIT_UNAVAILABLE";
|
|
8
|
+
export interface ExecutionRecoveryReference extends VerifiedAuthorization {
|
|
9
|
+
readonly version: "agent-safe.recovery/1";
|
|
10
|
+
readonly idempotencyKey: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ExecutionBlockedResult {
|
|
13
|
+
readonly outcome: "BLOCKED";
|
|
14
|
+
readonly executed: false;
|
|
15
|
+
readonly reason: ExecutionBlockReason;
|
|
16
|
+
readonly result: null;
|
|
17
|
+
readonly authorization: null;
|
|
18
|
+
}
|
|
19
|
+
export interface ProviderOutcomeUnknownResult {
|
|
20
|
+
readonly outcome: "UNKNOWN_AFTER_DISPATCH";
|
|
21
|
+
readonly executed: null;
|
|
22
|
+
readonly reason: "PROVIDER_OUTCOME_UNKNOWN";
|
|
23
|
+
readonly result: null;
|
|
24
|
+
readonly authorization: VerifiedAuthorization;
|
|
25
|
+
readonly recovery: ExecutionRecoveryReference;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every outcome that consumed a grant reports whether the attempt's commit
|
|
29
|
+
* evidence reached the authority. Finalization is evidence, not authority;
|
|
30
|
+
* it never changes `outcome` or `executed`.
|
|
31
|
+
*/
|
|
6
32
|
export type SafeExecutionResult<TResult = unknown> = {
|
|
33
|
+
readonly outcome: "COMPLETED";
|
|
7
34
|
readonly executed: true;
|
|
35
|
+
readonly recovered: false;
|
|
36
|
+
readonly result: TResult;
|
|
37
|
+
readonly authorization: VerifiedAuthorization;
|
|
38
|
+
readonly finalization: ExecutionFinalization;
|
|
39
|
+
} | ExecutionBlockedResult | {
|
|
40
|
+
readonly outcome: "FAILED_BEFORE_DISPATCH";
|
|
41
|
+
readonly executed: false;
|
|
42
|
+
readonly reason: ExecutionPreDispatchFailureReason;
|
|
43
|
+
readonly result: null;
|
|
44
|
+
readonly authorization: VerifiedAuthorization;
|
|
45
|
+
readonly finalization: ExecutionFinalization;
|
|
46
|
+
} | (ProviderOutcomeUnknownResult & {
|
|
47
|
+
readonly finalization: ExecutionFinalization;
|
|
48
|
+
});
|
|
49
|
+
export type ExecutionReconciliationResult<TResult = unknown> = {
|
|
50
|
+
readonly outcome: "COMPLETED";
|
|
51
|
+
readonly executed: true;
|
|
52
|
+
readonly recovered: true;
|
|
8
53
|
readonly result: TResult;
|
|
9
54
|
readonly authorization: VerifiedAuthorization;
|
|
10
55
|
} | {
|
|
56
|
+
readonly outcome: "DEFINITELY_NOT_EXECUTED";
|
|
11
57
|
readonly executed: false;
|
|
12
|
-
readonly
|
|
58
|
+
readonly recovered: true;
|
|
59
|
+
readonly reason: "PROVIDER_CONFIRMED_NOT_EXECUTED";
|
|
13
60
|
readonly result: null;
|
|
14
|
-
readonly authorization:
|
|
15
|
-
};
|
|
61
|
+
readonly authorization: VerifiedAuthorization;
|
|
62
|
+
} | ExecutionBlockedResult | ProviderOutcomeUnknownResult;
|
|
16
63
|
export declare class SafeExecutor {
|
|
17
64
|
private readonly registry;
|
|
18
65
|
private readonly verifier;
|
|
66
|
+
private readonly audit?;
|
|
19
67
|
private readonly hasher;
|
|
20
|
-
constructor(registry: ActionRegistry, verifier: AuthorizationVerifier);
|
|
68
|
+
constructor(registry: ActionRegistry, verifier: AuthorizationVerifier, audit?: AuditRecorder | undefined);
|
|
21
69
|
run<TResult = unknown>(captured: CapturedIntent, decision: GateDecision): Promise<SafeExecutionResult<TResult>>;
|
|
70
|
+
/**
|
|
71
|
+
* Reports the attempt outcome to the verifier's authority. A missing,
|
|
72
|
+
* failing, or malformed finalization can never alter the execution result.
|
|
73
|
+
*/
|
|
74
|
+
private finalize;
|
|
75
|
+
private static finalizationCode;
|
|
76
|
+
reconcile<TResult = unknown>(captured: CapturedIntent, recovery: ExecutionRecoveryReference): Promise<ExecutionReconciliationResult<TResult>>;
|
|
77
|
+
/**
|
|
78
|
+
* Observational artifacts carry an explicit non-authoritative marker. They
|
|
79
|
+
* must never be interpreted as a `GateDecision`, even after an unsafe cast
|
|
80
|
+
* or a JSON round trip.
|
|
81
|
+
*/
|
|
82
|
+
private static isAuthoritativeDecision;
|
|
22
83
|
private intentConforms;
|
|
23
|
-
private
|
|
84
|
+
private block;
|
|
85
|
+
private failedBeforeDispatch;
|
|
86
|
+
private record;
|
|
87
|
+
private static recoveryReference;
|
|
88
|
+
private static authorizationFrom;
|
|
24
89
|
}
|
|
25
90
|
//# 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;
|
|
1
|
+
{"version":3,"file":"SafeExecutor.d.ts","sourceRoot":"","sources":["../../src/execution/SafeExecutor.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,23 +2,44 @@ import { CanonicalIntentHasher } from "../intent/CanonicalIntentHasher.js";
|
|
|
2
2
|
export class SafeExecutor {
|
|
3
3
|
registry;
|
|
4
4
|
verifier;
|
|
5
|
+
audit;
|
|
5
6
|
hasher = new CanonicalIntentHasher();
|
|
6
|
-
constructor(registry, verifier) {
|
|
7
|
+
constructor(registry, verifier, audit) {
|
|
7
8
|
this.registry = registry;
|
|
8
9
|
this.verifier = verifier;
|
|
10
|
+
this.audit = audit;
|
|
9
11
|
}
|
|
10
12
|
async run(captured, decision) {
|
|
13
|
+
const startedAt = Date.now();
|
|
14
|
+
// Stryker disable next-line all: Audit payload mapping is covered by lifecycle event assertions.
|
|
15
|
+
const intentRecorded = await this.record({ eventType: "INTENT_CAPTURED", captured });
|
|
16
|
+
// An observational artifact (shadow observation, serialized audit event) is
|
|
17
|
+
// rejected before it is recorded or inspected as if it were a decision.
|
|
18
|
+
if (!SafeExecutor.isAuthoritativeDecision(decision)) {
|
|
19
|
+
return await this.block(captured, undefined, "DECISION_NOT_AUTHORITATIVE", startedAt);
|
|
20
|
+
}
|
|
21
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
22
|
+
const decisionRecorded = await this.record({
|
|
23
|
+
eventType: decision.failClosed ? "AUTHORITY_FAILED_CLOSED" : "AUTHORITY_DECISION",
|
|
24
|
+
captured,
|
|
25
|
+
decision,
|
|
26
|
+
});
|
|
27
|
+
// Stryker restore all
|
|
28
|
+
if (this.audit?.requiresDeliveryBeforeExecution === true &&
|
|
29
|
+
(!intentRecorded || !decisionRecorded)) {
|
|
30
|
+
return await this.block(captured, decision, "AUDIT_UNAVAILABLE", startedAt);
|
|
31
|
+
}
|
|
11
32
|
if (decision.verdict !== "ALLOW" || decision.failClosed) {
|
|
12
|
-
return
|
|
33
|
+
return await this.block(captured, decision, "DECISION_NOT_ALLOW", startedAt);
|
|
13
34
|
}
|
|
14
35
|
if (decision.intentHash !== captured.intentHash) {
|
|
15
|
-
return
|
|
36
|
+
return await this.block(captured, decision, "INTENT_BINDING_MISMATCH", startedAt);
|
|
16
37
|
}
|
|
17
|
-
if (decision.authorization === null) {
|
|
18
|
-
return
|
|
38
|
+
if (decision.authorization === null || typeof decision.authorization !== "object") {
|
|
39
|
+
return await this.block(captured, decision, "AUTHORIZATION_MISSING", startedAt);
|
|
19
40
|
}
|
|
20
41
|
if (!this.intentConforms(captured)) {
|
|
21
|
-
return
|
|
42
|
+
return await this.block(captured, decision, "INTENT_CONFORMANCE_FAILED", startedAt);
|
|
22
43
|
}
|
|
23
44
|
this.registry.validate(captured);
|
|
24
45
|
let authorization;
|
|
@@ -26,10 +47,11 @@ export class SafeExecutor {
|
|
|
26
47
|
authorization = await this.verifier.verifyAndConsume(captured, decision);
|
|
27
48
|
}
|
|
28
49
|
catch {
|
|
29
|
-
return
|
|
50
|
+
return await this.block(captured, decision, "AUTHORIZATION_INVALID", startedAt);
|
|
51
|
+
}
|
|
52
|
+
if (authorization === null) {
|
|
53
|
+
return await this.block(captured, decision, "AUTHORIZATION_INVALID", startedAt);
|
|
30
54
|
}
|
|
31
|
-
if (authorization === null)
|
|
32
|
-
return SafeExecutor.blocked("AUTHORIZATION_INVALID");
|
|
33
55
|
const authorizationExpiry = Date.parse(authorization.expiresAt);
|
|
34
56
|
if (authorization.decisionId !== decision.decisionId ||
|
|
35
57
|
authorization.dossierId !== decision.dossierId ||
|
|
@@ -39,19 +61,182 @@ export class SafeExecutor {
|
|
|
39
61
|
Math.floor(Date.parse(decision.authorization.expiresAt) / 1_000) ||
|
|
40
62
|
authorizationExpiry <= Date.now() ||
|
|
41
63
|
authorizationExpiry > Date.parse(captured.intent.expiresAt)) {
|
|
42
|
-
return
|
|
64
|
+
return await this.block(captured, decision, "AUTHORIZATION_INVALID", startedAt);
|
|
43
65
|
}
|
|
44
66
|
if (!this.intentConforms(captured)) {
|
|
45
|
-
return
|
|
67
|
+
return await this.block(captured, decision, "INTENT_CONFORMANCE_FAILED", startedAt);
|
|
68
|
+
}
|
|
69
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
70
|
+
const grantRecorded = await this.record({
|
|
71
|
+
eventType: "GRANT_CONSUMED",
|
|
72
|
+
captured,
|
|
73
|
+
decision,
|
|
74
|
+
authorization,
|
|
75
|
+
durationMs: Date.now() - startedAt,
|
|
76
|
+
});
|
|
77
|
+
// Stryker restore all
|
|
78
|
+
if (this.audit?.requiresDeliveryBeforeExecution === true && !grantRecorded) {
|
|
79
|
+
return await this.failedBeforeDispatch(captured, decision, authorization, "AUDIT_UNAVAILABLE", startedAt);
|
|
80
|
+
}
|
|
81
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
82
|
+
const executionRecorded = await this.record({
|
|
83
|
+
eventType: "EXECUTION_STARTED",
|
|
84
|
+
captured,
|
|
85
|
+
decision,
|
|
86
|
+
authorization,
|
|
87
|
+
durationMs: Date.now() - startedAt,
|
|
88
|
+
});
|
|
89
|
+
// Stryker restore all
|
|
90
|
+
if (this.audit?.requiresDeliveryBeforeExecution === true && !executionRecorded) {
|
|
91
|
+
return await this.failedBeforeDispatch(captured, decision, authorization, "AUDIT_UNAVAILABLE", startedAt);
|
|
46
92
|
}
|
|
47
|
-
|
|
93
|
+
const attempt = await this.registry.executeTracked(captured, authorization);
|
|
94
|
+
if (attempt.status === "FAILED_BEFORE_DISPATCH") {
|
|
95
|
+
return await this.failedBeforeDispatch(captured, decision, authorization, "HANDLER_FAILED_BEFORE_DISPATCH", startedAt);
|
|
96
|
+
}
|
|
97
|
+
if (attempt.status === "UNKNOWN_AFTER_DISPATCH") {
|
|
98
|
+
const finalization = await this.finalize(captured, decision, authorization, "INDETERMINATE");
|
|
99
|
+
const result = {
|
|
100
|
+
outcome: "UNKNOWN_AFTER_DISPATCH",
|
|
101
|
+
executed: null,
|
|
102
|
+
reason: "PROVIDER_OUTCOME_UNKNOWN",
|
|
103
|
+
result: null,
|
|
104
|
+
authorization,
|
|
105
|
+
recovery: SafeExecutor.recoveryReference(captured, authorization),
|
|
106
|
+
finalization,
|
|
107
|
+
};
|
|
108
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
109
|
+
await this.record({
|
|
110
|
+
eventType: "EXECUTION_OUTCOME_UNKNOWN",
|
|
111
|
+
captured,
|
|
112
|
+
decision,
|
|
113
|
+
authorization,
|
|
114
|
+
reasonCodes: [result.reason, SafeExecutor.finalizationCode(finalization)],
|
|
115
|
+
durationMs: Date.now() - startedAt,
|
|
116
|
+
});
|
|
117
|
+
// Stryker restore all
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
const finalization = await this.finalize(captured, decision, authorization, "COMMITTED");
|
|
121
|
+
const result = {
|
|
122
|
+
outcome: "COMPLETED",
|
|
123
|
+
executed: true,
|
|
124
|
+
recovered: false,
|
|
125
|
+
result: attempt.result,
|
|
126
|
+
authorization,
|
|
127
|
+
finalization,
|
|
128
|
+
};
|
|
129
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
130
|
+
await this.record({
|
|
131
|
+
eventType: "EXECUTION_COMPLETED",
|
|
132
|
+
captured,
|
|
133
|
+
decision,
|
|
134
|
+
authorization,
|
|
135
|
+
reasonCodes: [SafeExecutor.finalizationCode(finalization)],
|
|
136
|
+
durationMs: Date.now() - startedAt,
|
|
137
|
+
});
|
|
138
|
+
// Stryker restore all
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reports the attempt outcome to the verifier's authority. A missing,
|
|
143
|
+
* failing, or malformed finalization can never alter the execution result.
|
|
144
|
+
*/
|
|
145
|
+
async finalize(captured, decision, authorization, outcome) {
|
|
146
|
+
const finalize = this.verifier.finalize;
|
|
147
|
+
if (finalize === undefined)
|
|
148
|
+
return "UNSUPPORTED";
|
|
48
149
|
try {
|
|
49
|
-
|
|
150
|
+
const status = await finalize.call(this.verifier, {
|
|
151
|
+
captured,
|
|
152
|
+
decision,
|
|
153
|
+
authorization,
|
|
154
|
+
outcome,
|
|
155
|
+
});
|
|
156
|
+
return status === "RECORDED" ? "RECORDED" : "PENDING";
|
|
50
157
|
}
|
|
51
158
|
catch {
|
|
52
|
-
|
|
159
|
+
return "PENDING";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
static finalizationCode(finalization) {
|
|
163
|
+
return `COMMIT_FINALIZATION_${finalization}`;
|
|
164
|
+
}
|
|
165
|
+
async reconcile(captured, recovery) {
|
|
166
|
+
const startedAt = Date.now();
|
|
167
|
+
if (recovery.version !== "agent-safe.recovery/1" ||
|
|
168
|
+
recovery.intentHash !== captured.intentHash ||
|
|
169
|
+
recovery.idempotencyKey !== captured.intent.idempotencyKey ||
|
|
170
|
+
!this.intentConforms(captured)) {
|
|
171
|
+
return await this.block(captured, undefined, "RECOVERY_BINDING_MISMATCH", startedAt);
|
|
172
|
+
}
|
|
173
|
+
const reconciliation = await this.registry.reconcile(captured, recovery.idempotencyKey);
|
|
174
|
+
const authorization = SafeExecutor.authorizationFrom(recovery);
|
|
175
|
+
if (reconciliation.status === "COMPLETED") {
|
|
176
|
+
const result = {
|
|
177
|
+
outcome: "COMPLETED",
|
|
178
|
+
executed: true,
|
|
179
|
+
recovered: true,
|
|
180
|
+
result: reconciliation.result,
|
|
181
|
+
authorization,
|
|
182
|
+
};
|
|
183
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
184
|
+
await this.record({
|
|
185
|
+
eventType: "RECONCILIATION_COMPLETED",
|
|
186
|
+
captured,
|
|
187
|
+
authorization,
|
|
188
|
+
durationMs: Date.now() - startedAt,
|
|
189
|
+
});
|
|
190
|
+
// Stryker restore all
|
|
191
|
+
return result;
|
|
53
192
|
}
|
|
54
|
-
|
|
193
|
+
if (reconciliation.status === "NOT_EXECUTED") {
|
|
194
|
+
const result = {
|
|
195
|
+
outcome: "DEFINITELY_NOT_EXECUTED",
|
|
196
|
+
executed: false,
|
|
197
|
+
recovered: true,
|
|
198
|
+
reason: "PROVIDER_CONFIRMED_NOT_EXECUTED",
|
|
199
|
+
result: null,
|
|
200
|
+
authorization,
|
|
201
|
+
};
|
|
202
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
203
|
+
await this.record({
|
|
204
|
+
eventType: "RECONCILIATION_NOT_EXECUTED",
|
|
205
|
+
captured,
|
|
206
|
+
authorization,
|
|
207
|
+
reasonCodes: [result.reason],
|
|
208
|
+
durationMs: Date.now() - startedAt,
|
|
209
|
+
});
|
|
210
|
+
// Stryker restore all
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
const result = {
|
|
214
|
+
outcome: "UNKNOWN_AFTER_DISPATCH",
|
|
215
|
+
executed: null,
|
|
216
|
+
reason: "PROVIDER_OUTCOME_UNKNOWN",
|
|
217
|
+
result: null,
|
|
218
|
+
authorization,
|
|
219
|
+
recovery,
|
|
220
|
+
};
|
|
221
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
222
|
+
await this.record({
|
|
223
|
+
eventType: "RECONCILIATION_UNKNOWN",
|
|
224
|
+
captured,
|
|
225
|
+
authorization,
|
|
226
|
+
reasonCodes: [result.reason],
|
|
227
|
+
durationMs: Date.now() - startedAt,
|
|
228
|
+
});
|
|
229
|
+
// Stryker restore all
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Observational artifacts carry an explicit non-authoritative marker. They
|
|
234
|
+
* must never be interpreted as a `GateDecision`, even after an unsafe cast
|
|
235
|
+
* or a JSON round trip.
|
|
236
|
+
*/
|
|
237
|
+
static isAuthoritativeDecision(decision) {
|
|
238
|
+
const candidate = decision;
|
|
239
|
+
return candidate.authority !== "OBSERVATIONAL" && candidate.mode !== "SHADOW";
|
|
55
240
|
}
|
|
56
241
|
intentConforms(captured) {
|
|
57
242
|
let recomputed = null;
|
|
@@ -66,8 +251,59 @@ export class SafeExecutor {
|
|
|
66
251
|
recomputed.canonicalIntent === captured.canonicalIntent &&
|
|
67
252
|
recomputed.byteLength === captured.byteLength);
|
|
68
253
|
}
|
|
69
|
-
|
|
70
|
-
|
|
254
|
+
async block(captured, decision, reason, startedAt) {
|
|
255
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
256
|
+
await this.record({
|
|
257
|
+
eventType: "EXECUTION_BLOCKED",
|
|
258
|
+
captured,
|
|
259
|
+
...(decision === undefined ? {} : { decision }),
|
|
260
|
+
reasonCodes: [reason],
|
|
261
|
+
durationMs: Date.now() - startedAt,
|
|
262
|
+
});
|
|
263
|
+
// Stryker restore all
|
|
264
|
+
return { outcome: "BLOCKED", executed: false, reason, result: null, authorization: null };
|
|
265
|
+
}
|
|
266
|
+
async failedBeforeDispatch(captured, decision, authorization, reason, startedAt) {
|
|
267
|
+
// The grant was consumed but no side effect was attempted.
|
|
268
|
+
const finalization = await this.finalize(captured, decision, authorization, "FAILED");
|
|
269
|
+
// Stryker disable all: Audit payload mapping is covered by lifecycle event assertions.
|
|
270
|
+
await this.record({
|
|
271
|
+
eventType: "EXECUTION_FAILED_BEFORE_DISPATCH",
|
|
272
|
+
captured,
|
|
273
|
+
decision,
|
|
274
|
+
authorization,
|
|
275
|
+
reasonCodes: [reason, SafeExecutor.finalizationCode(finalization)],
|
|
276
|
+
durationMs: Date.now() - startedAt,
|
|
277
|
+
});
|
|
278
|
+
// Stryker restore all
|
|
279
|
+
return {
|
|
280
|
+
outcome: "FAILED_BEFORE_DISPATCH",
|
|
281
|
+
executed: false,
|
|
282
|
+
reason,
|
|
283
|
+
result: null,
|
|
284
|
+
authorization,
|
|
285
|
+
finalization,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
async record(input) {
|
|
289
|
+
// Stryker disable next-line BooleanLiteral: The value is irrelevant when no audit policy exists.
|
|
290
|
+
return this.audit === undefined ? true : await this.audit.record(input);
|
|
291
|
+
}
|
|
292
|
+
static recoveryReference(captured, authorization) {
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
version: "agent-safe.recovery/1",
|
|
295
|
+
...authorization,
|
|
296
|
+
idempotencyKey: captured.intent.idempotencyKey,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
static authorizationFrom(recovery) {
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
decisionId: recovery.decisionId,
|
|
302
|
+
dossierId: recovery.dossierId,
|
|
303
|
+
grantId: recovery.grantId,
|
|
304
|
+
intentHash: recovery.intentHash,
|
|
305
|
+
expiresAt: recovery.expiresAt,
|
|
306
|
+
});
|
|
71
307
|
}
|
|
72
308
|
}
|
|
73
309
|
//# sourceMappingURL=SafeExecutor.js.map
|