@fabricorg/platform-host 8.4.0 → 8.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +154 -4
- package/dist/index.cjs +665 -170
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -7
- package/dist/index.d.ts +150 -7
- package/dist/index.js +662 -171
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,102 @@
|
|
|
1
|
+
import { types } from "util";
|
|
1
2
|
import { AdapterCircuitBreakerRegistry, AdapterRegistry, FABRIC_GOVERNANCE_CONTRACT_VERSION, ReconciliationRequiredError, aggregatePolicyOutcomes, assertActionFinalization, assertAdapterCircuitBreakerConfig, assertAdapterOutcomeConsistency, assertAdapterStepDeadlines, assertGovernanceRuntimeEvidence, assertMutationGovernanceContext, canonicalJson, createFabricId, evaluatePolicyDefinitions, evaluateStateMachineTransition, executeWithAdapterRetry, isAdapterCircuitOpenError, isReconciliationRequiredError, isRetryableExecutionError } from "@fabricorg/platform";
|
|
2
3
|
import { performance } from "perf_hooks";
|
|
3
4
|
import { assertAssemblyRuntimeCompatible } from "@fabricorg/assembly";
|
|
4
5
|
import { createHash } from "crypto";
|
|
5
|
-
|
|
6
|
+
//#region src/action-finalization.ts
|
|
7
|
+
/** Remove private preparation and restore its compatible protocol on settlement. */
|
|
8
|
+
function finalizationSettlementPatch(record, patch) {
|
|
9
|
+
const checkpoint = Object.hasOwn(patch, "finalizationCheckpoint") ? patch.finalizationCheckpoint : record.finalizationCheckpoint;
|
|
10
|
+
if (!checkpoint || !patch.status || ![
|
|
11
|
+
"completed",
|
|
12
|
+
"failed",
|
|
13
|
+
"blocked_by_policy",
|
|
14
|
+
"validation_failed"
|
|
15
|
+
].includes(patch.status) || !Object.hasOwn(checkpoint, "adapterPreparation") && checkpoint.protocol !== "atomic-after-adapters-v2") return patch;
|
|
16
|
+
const { adapterPreparation: _preparation, ...evidence } = checkpoint;
|
|
17
|
+
return {
|
|
18
|
+
...patch,
|
|
19
|
+
finalizationCheckpoint: {
|
|
20
|
+
...evidence,
|
|
21
|
+
protocol: "atomic-after-adapters-v1"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Strict JSON evidence: reject lossy serialization rather than silently dropping values. */
|
|
26
|
+
function finalizationJson(value) {
|
|
27
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
28
|
+
function check(item) {
|
|
29
|
+
if (item === null || typeof item === "string" || typeof item === "boolean") return;
|
|
30
|
+
if (typeof item === "number" && Number.isFinite(item)) return;
|
|
31
|
+
if (typeof item !== "object" || !item || ancestors.has(item)) throw new Error("Finalization requires JSON-compatible data.");
|
|
32
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("Finalization requires plain JSON objects.");
|
|
33
|
+
if (Object.getOwnPropertySymbols(item).length > 0) throw new Error("Finalization requires JSON string keys.");
|
|
34
|
+
if (Array.isArray(item)) {
|
|
35
|
+
const keys = Object.keys(item);
|
|
36
|
+
if (keys.length !== item.length || keys.some((key, index) => key !== String(index))) throw new Error("Finalization requires dense JSON arrays.");
|
|
37
|
+
}
|
|
38
|
+
ancestors.add(item);
|
|
39
|
+
for (const child of Object.values(item)) check(child);
|
|
40
|
+
ancestors.delete(item);
|
|
41
|
+
}
|
|
42
|
+
check(value);
|
|
43
|
+
return JSON.parse(JSON.stringify(value));
|
|
44
|
+
}
|
|
45
|
+
/** Match the persisted JSON representation before validating finalization execution input. */
|
|
46
|
+
function normalizeFinalizationJson(value) {
|
|
47
|
+
return finalizationJson(JSON.parse(JSON.stringify(value)));
|
|
48
|
+
}
|
|
49
|
+
/** Omit only public optional fields; payloads and patch values retain strict JSON validation. */
|
|
50
|
+
function omitUndefinedOptionalFields(value, keys) {
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
|
|
52
|
+
const copy = { ...value };
|
|
53
|
+
for (const key of keys) if (Object.hasOwn(copy, key) && copy[key] === void 0) delete copy[key];
|
|
54
|
+
return copy;
|
|
55
|
+
}
|
|
56
|
+
function validateFinalization(value, action, privateFields, contracts) {
|
|
57
|
+
const normalized = omitUndefinedOptionalFields(value, [
|
|
58
|
+
"events",
|
|
59
|
+
"resultPatch",
|
|
60
|
+
"stagedInvocations"
|
|
61
|
+
]);
|
|
62
|
+
if (normalized !== value && normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
63
|
+
const record = normalized;
|
|
64
|
+
if (Array.isArray(record.events)) {
|
|
65
|
+
const events = Object.assign(record.events.slice(), record.events);
|
|
66
|
+
for (let index = 0; index < events.length; index++) if (Object.hasOwn(events, index)) events[index] = omitUndefinedOptionalFields(events[index], ["eventSchemaVersion"]);
|
|
67
|
+
record.events = events;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const result = finalizationJson(normalized);
|
|
71
|
+
if (!result || typeof result !== "object" || Array.isArray(result) || Object.keys(result).some((key) => key !== "events" && key !== "resultPatch" && key !== "stagedInvocations")) throw new Error("Finalization must return only events, resultPatch and stagedInvocations.");
|
|
72
|
+
if (result.resultPatch !== void 0) {
|
|
73
|
+
if (!result.resultPatch || typeof result.resultPatch !== "object" || Array.isArray(result.resultPatch)) throw new Error("Finalization resultPatch must be an object.");
|
|
74
|
+
if (Object.keys(result.resultPatch).some((key) => privateFields.includes(key))) throw new Error("Finalization resultPatch contains a private result key.");
|
|
75
|
+
}
|
|
76
|
+
if (result.events !== void 0) {
|
|
77
|
+
if (!Array.isArray(result.events)) throw new Error("Finalization events must be an array.");
|
|
78
|
+
for (const event of result.events) {
|
|
79
|
+
if (!event || typeof event !== "object" || Array.isArray(event) || Object.keys(event).some((key) => ![
|
|
80
|
+
"eventType",
|
|
81
|
+
"subjectType",
|
|
82
|
+
"subjectId",
|
|
83
|
+
"payload",
|
|
84
|
+
"eventSchemaVersion"
|
|
85
|
+
].includes(key))) throw new Error("Invalid finalization event fields.");
|
|
86
|
+
for (const key of [
|
|
87
|
+
"eventType",
|
|
88
|
+
"subjectType",
|
|
89
|
+
"subjectId"
|
|
90
|
+
]) if (typeof event[key] !== "string" || !event[key].trim()) throw new Error("Finalization event identifiers must be non-empty.");
|
|
91
|
+
if (!Object.hasOwn(event, "payload")) throw new Error("Finalization event requires a JSON payload.");
|
|
92
|
+
if (event.eventSchemaVersion !== void 0 && (!Number.isInteger(event.eventSchemaVersion) || event.eventSchemaVersion <= 0)) throw new Error("Finalization eventSchemaVersion must be a positive integer.");
|
|
93
|
+
if (!action.emitsEvents.includes(event.eventType)) throw new Error("Finalization emitted undeclared event type.");
|
|
94
|
+
if (contracts && (!contracts.hasEventType(event.eventType) || !contracts.hasSubjectType(event.subjectType))) throw new Error("Finalization emitted an unregistered event or subject type.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
6
100
|
//#region src/types.ts
|
|
7
101
|
/** Durable Host lifecycle generation, independent from the npm package version. */
|
|
8
102
|
const PLATFORM_HOST_CONTRACT_VERSION = 2;
|
|
@@ -68,6 +162,185 @@ var ExternalCompletionError = class extends Error {
|
|
|
68
162
|
return this.refusal === "still_executing";
|
|
69
163
|
}
|
|
70
164
|
};
|
|
165
|
+
/** Maximum serialized UTF-8 evidence size. */
|
|
166
|
+
const MAX_APPROVAL_EVIDENCE_BYTES = 16384;
|
|
167
|
+
const APPROVAL_REQUESTED_EVENT = "ApprovalRequested";
|
|
168
|
+
const APPROVAL_DECIDED_EVENT = "ApprovalDecided";
|
|
169
|
+
const APPROVAL_EXPIRED_EVENT = "ApprovalExpired";
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/approval-lifecycle.ts
|
|
172
|
+
function validApprovalDate(value) {
|
|
173
|
+
return value instanceof Date && Number.isFinite(value.getTime());
|
|
174
|
+
}
|
|
175
|
+
/** Snapshot before awaiting application authorization; reject lossy JSON conversions. */
|
|
176
|
+
function snapshotApprovalDecision(decision) {
|
|
177
|
+
const visit = (value, ancestors, depth) => {
|
|
178
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
179
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
180
|
+
if (typeof value !== "object" || types.isProxy(value)) throw new Error("Approval evidence must be plain JSON data.");
|
|
181
|
+
if (ancestors.includes(value)) throw new Error("Approval evidence must not contain cycles.");
|
|
182
|
+
if (depth > 64) throw new Error("Approval evidence exceeds maximum depth 64.");
|
|
183
|
+
const array = Array.isArray(value);
|
|
184
|
+
if (!array && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error("Approval evidence must contain only plain objects and JSON values.");
|
|
185
|
+
const keys = Reflect.ownKeys(value);
|
|
186
|
+
if (array && keys.length !== value.length + 1) throw new Error("Approval evidence arrays must be dense JSON arrays.");
|
|
187
|
+
for (const key of keys) {
|
|
188
|
+
if (array && key === "length") continue;
|
|
189
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
190
|
+
if (typeof key !== "string" || !descriptor.enumerable || !Object.hasOwn(descriptor, "value") || array && !/^(0|[1-9][0-9]*)$/.test(key)) throw new Error("Approval evidence must contain enumerable JSON data properties only.");
|
|
191
|
+
visit(descriptor.value, [...ancestors, value], depth + 1);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
if (types.isProxy(decision)) throw new Error("Approval decision must be plain data.");
|
|
195
|
+
for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(decision))) if (!Object.hasOwn(descriptor, "value")) throw new Error("Approval decision must be plain data.");
|
|
196
|
+
if (decision.evidence !== void 0) {
|
|
197
|
+
if (!decision.evidence || typeof decision.evidence !== "object" || Array.isArray(decision.evidence)) throw new Error("Approval evidence must be a JSON object.");
|
|
198
|
+
visit(decision.evidence, [], 0);
|
|
199
|
+
if (Buffer.byteLength(JSON.stringify(decision.evidence), "utf8") > 16384) throw new Error(`Approval evidence exceeds ${MAX_APPROVAL_EVIDENCE_BYTES} bytes.`);
|
|
200
|
+
}
|
|
201
|
+
if (decision.decidedAt !== void 0 && !validApprovalDate(decision.decidedAt)) throw new Error("decidedAt must be a valid Date.");
|
|
202
|
+
return structuredClone(decision);
|
|
203
|
+
}
|
|
204
|
+
function approvalSweepLimit(input) {
|
|
205
|
+
if (!validApprovalDate(input.now)) throw new Error("Approval sweep now must be a valid Date.");
|
|
206
|
+
const limit = input.limit ?? 100;
|
|
207
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) throw new Error("Approval sweep limit must be an integer from 1 to 1000.");
|
|
208
|
+
return limit;
|
|
209
|
+
}
|
|
210
|
+
function approvalExpiredPatch(record, now) {
|
|
211
|
+
return finalizationSettlementPatch(record, {
|
|
212
|
+
status: "failed",
|
|
213
|
+
approvalDecision: void 0,
|
|
214
|
+
approvalExpiry: {
|
|
215
|
+
expiresAt: new Date(record.approvalExpiresAt),
|
|
216
|
+
expiredAt: new Date(now)
|
|
217
|
+
},
|
|
218
|
+
error: "Approval expired before a decision was applied."
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/** Both stores serialize this read, event append, and update within their mutation transaction. */
|
|
222
|
+
async function applyApprovalTransition(transaction, input, kind) {
|
|
223
|
+
if (!transaction.getActionInvocationForUpdate) throw new Error("Approval transitions require locking invocation reads.");
|
|
224
|
+
const record = await transaction.getActionInvocationForUpdate(input.actionInvocationId, input.tenantId, input.spaceId);
|
|
225
|
+
if (!record) return { applied: false };
|
|
226
|
+
if (kind === "decided" && transaction.beginApprovalDecision) {
|
|
227
|
+
const { lifecycleEvent, ...decisionInput } = input;
|
|
228
|
+
const result = await transaction.beginApprovalDecision(decisionInput);
|
|
229
|
+
if (result.applied && result.invocation) await appendLifecycleEvent(transaction, lifecycleEvent, result.invocation, result.invocation.approvalExpiry ? "expired" : "decided");
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
let patch;
|
|
233
|
+
if (kind === "requested") {
|
|
234
|
+
if (!["pending", "running"].includes(record.status) || record.approvalDecision !== void 0 || record.leaseOwner !== void 0 && (record.leaseOwner !== input.leaseOwner || record.leaseToken !== input.leaseToken)) return {
|
|
235
|
+
applied: false,
|
|
236
|
+
invocation: record
|
|
237
|
+
};
|
|
238
|
+
patch = {
|
|
239
|
+
status: "waiting_for_approval",
|
|
240
|
+
...Object.hasOwn(input, "approvalExpiresAt") ? { approvalExpiresAt: input.approvalExpiresAt } : {}
|
|
241
|
+
};
|
|
242
|
+
} else {
|
|
243
|
+
if (record.status !== "waiting_for_approval") return {
|
|
244
|
+
applied: false,
|
|
245
|
+
invocation: record
|
|
246
|
+
};
|
|
247
|
+
const due = record.approvalExpiresAt !== void 0 && record.approvalExpiresAt.getTime() <= input.now.getTime();
|
|
248
|
+
if (kind === "expired" && !due) return {
|
|
249
|
+
applied: false,
|
|
250
|
+
invocation: record
|
|
251
|
+
};
|
|
252
|
+
if (due) {
|
|
253
|
+
kind = "expired";
|
|
254
|
+
patch = approvalExpiredPatch(record, input.now);
|
|
255
|
+
} else {
|
|
256
|
+
const decisionInput = input;
|
|
257
|
+
patch = decisionInput.decision.approved ? {
|
|
258
|
+
status: "running",
|
|
259
|
+
error: void 0,
|
|
260
|
+
approvalDecision: structuredClone(decisionInput.decision),
|
|
261
|
+
leaseOwner: decisionInput.leaseOwner,
|
|
262
|
+
leaseToken: (record.leaseToken ?? 0) + 1,
|
|
263
|
+
leaseExpiresAt: new Date(input.now.getTime() + decisionInput.leaseDurationMs),
|
|
264
|
+
attemptCount: Math.max(record.attemptCount, 1)
|
|
265
|
+
} : finalizationSettlementPatch(record, {
|
|
266
|
+
status: "failed",
|
|
267
|
+
approvalDecision: structuredClone(decisionInput.decision),
|
|
268
|
+
error: `Approval rejected: ${decisionInput.decision.reason ?? "no reason provided"}`
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const transitioned = {
|
|
273
|
+
...record,
|
|
274
|
+
...patch,
|
|
275
|
+
updatedAt: new Date(input.now)
|
|
276
|
+
};
|
|
277
|
+
if (Object.hasOwn(patch, "approvalDecision") && patch.approvalDecision === void 0) delete transitioned.approvalDecision;
|
|
278
|
+
if (transitioned.status !== "running") {
|
|
279
|
+
delete transitioned.leaseOwner;
|
|
280
|
+
delete transitioned.leaseExpiresAt;
|
|
281
|
+
}
|
|
282
|
+
await appendLifecycleEvent(transaction, input.lifecycleEvent, transitioned, kind);
|
|
283
|
+
await transaction.updateActionInvocation(record.id, record.tenantId, record.spaceId, patch);
|
|
284
|
+
return {
|
|
285
|
+
applied: true,
|
|
286
|
+
invocation: transitioned
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function appendLifecycleEvent(transaction, factory, invocation, kind) {
|
|
290
|
+
if (!factory) return;
|
|
291
|
+
if (!transaction.appendEventWithOutbox) throw new Error("Approval events require atomic outbox writes.");
|
|
292
|
+
const { event, metadata } = factory(invocation, kind);
|
|
293
|
+
await transaction.appendEventWithOutbox({
|
|
294
|
+
...event,
|
|
295
|
+
sequence: await transaction.nextEventSequence(invocation.tenantId, invocation.spaceId)
|
|
296
|
+
}, metadata);
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/agent-proposal.ts
|
|
300
|
+
/** Validate without invoking getters or retaining caller-owned evidence. */
|
|
301
|
+
function snapshotAgentProposal(value) {
|
|
302
|
+
const invalid = () => {
|
|
303
|
+
throw new Error("Invalid agent proposal.");
|
|
304
|
+
};
|
|
305
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || types.isProxy(value)) return invalid();
|
|
306
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return invalid();
|
|
307
|
+
const allowed = [
|
|
308
|
+
"confidence",
|
|
309
|
+
"riskTier",
|
|
310
|
+
"agentSessionId",
|
|
311
|
+
"agentRunId",
|
|
312
|
+
"reason",
|
|
313
|
+
"evidence"
|
|
314
|
+
];
|
|
315
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
316
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
317
|
+
if (typeof key !== "string" || !allowed.includes(key) || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) return invalid();
|
|
318
|
+
}
|
|
319
|
+
if (value.confidence !== void 0 && (typeof value.confidence !== "number" || !Number.isFinite(value.confidence))) return invalid();
|
|
320
|
+
if (value.riskTier !== void 0 && ![
|
|
321
|
+
"low",
|
|
322
|
+
"medium",
|
|
323
|
+
"high"
|
|
324
|
+
].includes(value.riskTier)) return invalid();
|
|
325
|
+
for (const key of [
|
|
326
|
+
"agentSessionId",
|
|
327
|
+
"agentRunId",
|
|
328
|
+
"reason"
|
|
329
|
+
]) {
|
|
330
|
+
const field = value[key];
|
|
331
|
+
if (field !== void 0 && (typeof field !== "string" || field.length > (key === "reason" ? 4096 : 256) || key !== "reason" && field.trim().length === 0)) return invalid();
|
|
332
|
+
}
|
|
333
|
+
const evidence = snapshotApprovalDecision({
|
|
334
|
+
approved: true,
|
|
335
|
+
approverId: "proposal",
|
|
336
|
+
approverType: "agent",
|
|
337
|
+
...value.evidence !== void 0 ? { evidence: value.evidence } : {}
|
|
338
|
+
}).evidence;
|
|
339
|
+
return {
|
|
340
|
+
...value,
|
|
341
|
+
...evidence !== void 0 ? { evidence } : {}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
71
344
|
//#endregion
|
|
72
345
|
//#region src/command-identity.ts
|
|
73
346
|
const PARAMETER_DIGEST_ALGORITHM = "fabric-canonical-json-sha256-v1";
|
|
@@ -107,82 +380,6 @@ function normalizeProvenance(input, options) {
|
|
|
107
380
|
};
|
|
108
381
|
}
|
|
109
382
|
//#endregion
|
|
110
|
-
//#region src/action-finalization.ts
|
|
111
|
-
/** Strict JSON evidence: reject lossy serialization rather than silently dropping values. */
|
|
112
|
-
function finalizationJson(value) {
|
|
113
|
-
const ancestors = /* @__PURE__ */ new Set();
|
|
114
|
-
function check(item) {
|
|
115
|
-
if (item === null || typeof item === "string" || typeof item === "boolean") return;
|
|
116
|
-
if (typeof item === "number" && Number.isFinite(item)) return;
|
|
117
|
-
if (typeof item !== "object" || !item || ancestors.has(item)) throw new Error("Finalization requires JSON-compatible data.");
|
|
118
|
-
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("Finalization requires plain JSON objects.");
|
|
119
|
-
if (Object.getOwnPropertySymbols(item).length > 0) throw new Error("Finalization requires JSON string keys.");
|
|
120
|
-
if (Array.isArray(item)) {
|
|
121
|
-
const keys = Object.keys(item);
|
|
122
|
-
if (keys.length !== item.length || keys.some((key, index) => key !== String(index))) throw new Error("Finalization requires dense JSON arrays.");
|
|
123
|
-
}
|
|
124
|
-
ancestors.add(item);
|
|
125
|
-
for (const child of Object.values(item)) check(child);
|
|
126
|
-
ancestors.delete(item);
|
|
127
|
-
}
|
|
128
|
-
check(value);
|
|
129
|
-
return JSON.parse(JSON.stringify(value));
|
|
130
|
-
}
|
|
131
|
-
/** Match the persisted JSON representation before validating finalization execution input. */
|
|
132
|
-
function normalizeFinalizationJson(value) {
|
|
133
|
-
return finalizationJson(JSON.parse(JSON.stringify(value)));
|
|
134
|
-
}
|
|
135
|
-
/** Omit only public optional fields; payloads and patch values retain strict JSON validation. */
|
|
136
|
-
function omitUndefinedOptionalFields(value, keys) {
|
|
137
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
|
|
138
|
-
const copy = { ...value };
|
|
139
|
-
for (const key of keys) if (Object.hasOwn(copy, key) && copy[key] === void 0) delete copy[key];
|
|
140
|
-
return copy;
|
|
141
|
-
}
|
|
142
|
-
function validateFinalization(value, action, privateFields, contracts) {
|
|
143
|
-
const normalized = omitUndefinedOptionalFields(value, [
|
|
144
|
-
"events",
|
|
145
|
-
"resultPatch",
|
|
146
|
-
"stagedInvocations"
|
|
147
|
-
]);
|
|
148
|
-
if (normalized !== value && normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
149
|
-
const record = normalized;
|
|
150
|
-
if (Array.isArray(record.events)) {
|
|
151
|
-
const events = Object.assign(record.events.slice(), record.events);
|
|
152
|
-
for (let index = 0; index < events.length; index++) if (Object.hasOwn(events, index)) events[index] = omitUndefinedOptionalFields(events[index], ["eventSchemaVersion"]);
|
|
153
|
-
record.events = events;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const result = finalizationJson(normalized);
|
|
157
|
-
if (!result || typeof result !== "object" || Array.isArray(result) || Object.keys(result).some((key) => key !== "events" && key !== "resultPatch" && key !== "stagedInvocations")) throw new Error("Finalization must return only events, resultPatch and stagedInvocations.");
|
|
158
|
-
if (result.resultPatch !== void 0) {
|
|
159
|
-
if (!result.resultPatch || typeof result.resultPatch !== "object" || Array.isArray(result.resultPatch)) throw new Error("Finalization resultPatch must be an object.");
|
|
160
|
-
if (Object.keys(result.resultPatch).some((key) => privateFields.includes(key))) throw new Error("Finalization resultPatch contains a private result key.");
|
|
161
|
-
}
|
|
162
|
-
if (result.events !== void 0) {
|
|
163
|
-
if (!Array.isArray(result.events)) throw new Error("Finalization events must be an array.");
|
|
164
|
-
for (const event of result.events) {
|
|
165
|
-
if (!event || typeof event !== "object" || Array.isArray(event) || Object.keys(event).some((key) => ![
|
|
166
|
-
"eventType",
|
|
167
|
-
"subjectType",
|
|
168
|
-
"subjectId",
|
|
169
|
-
"payload",
|
|
170
|
-
"eventSchemaVersion"
|
|
171
|
-
].includes(key))) throw new Error("Invalid finalization event fields.");
|
|
172
|
-
for (const key of [
|
|
173
|
-
"eventType",
|
|
174
|
-
"subjectType",
|
|
175
|
-
"subjectId"
|
|
176
|
-
]) if (typeof event[key] !== "string" || !event[key].trim()) throw new Error("Finalization event identifiers must be non-empty.");
|
|
177
|
-
if (!Object.hasOwn(event, "payload")) throw new Error("Finalization event requires a JSON payload.");
|
|
178
|
-
if (event.eventSchemaVersion !== void 0 && (!Number.isInteger(event.eventSchemaVersion) || event.eventSchemaVersion <= 0)) throw new Error("Finalization eventSchemaVersion must be a positive integer.");
|
|
179
|
-
if (!action.emitsEvents.includes(event.eventType)) throw new Error("Finalization emitted undeclared event type.");
|
|
180
|
-
if (contracts && (!contracts.hasEventType(event.eventType) || !contracts.hasSubjectType(event.subjectType))) throw new Error("Finalization emitted an unregistered event or subject type.");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
//#endregion
|
|
186
383
|
//#region src/observability.ts
|
|
187
384
|
/** Version of the vendor-neutral health/readiness contract. */
|
|
188
385
|
const PLATFORM_HOST_HEALTH_CONTRACT_VERSION = 1;
|
|
@@ -589,7 +786,7 @@ function createExternalCompletionLifecycle(deps) {
|
|
|
589
786
|
externalCompletion,
|
|
590
787
|
pendingCompletion: void 0
|
|
591
788
|
};
|
|
592
|
-
await writer.update(patch);
|
|
789
|
+
await writer.update(finalizationSettlementPatch(invocation, patch));
|
|
593
790
|
if (settles) emitInvocationStatusTelemetry(invocation, "completed", deps.telemetry, now(), invocation.status);
|
|
594
791
|
return withConsistency(actionResult({
|
|
595
792
|
...invocation,
|
|
@@ -626,7 +823,7 @@ function createExternalCompletionLifecycle(deps) {
|
|
|
626
823
|
externalCompletion,
|
|
627
824
|
pendingCompletion: void 0
|
|
628
825
|
};
|
|
629
|
-
await writer.update(patch);
|
|
826
|
+
await writer.update(finalizationSettlementPatch(invocation, patch));
|
|
630
827
|
if (settles) emitInvocationStatusTelemetry(invocation, "failed", deps.telemetry, now(), invocation.status);
|
|
631
828
|
return withConsistency(actionResult({
|
|
632
829
|
...invocation,
|
|
@@ -850,7 +1047,11 @@ function snapshotSubmission(input) {
|
|
|
850
1047
|
const executionReason = scalar("executionReason");
|
|
851
1048
|
const idempotencyKey = scalar("idempotencyKey");
|
|
852
1049
|
const authorizationBindingId = scalar("authorizationBindingId");
|
|
1050
|
+
const proposal = input.agentProposal;
|
|
1051
|
+
if (proposal !== void 0 && actorType !== "agent") throw new Error("Agent proposal requires an agent actor.");
|
|
1052
|
+
const agentProposal = proposal === void 0 ? void 0 : snapshotAgentProposal(proposal);
|
|
853
1053
|
const live = {
|
|
1054
|
+
...agentProposal !== void 0 ? { agentProposal } : {},
|
|
854
1055
|
actionId,
|
|
855
1056
|
tenantId,
|
|
856
1057
|
spaceId,
|
|
@@ -919,6 +1120,7 @@ function privateSubmission(snapshot) {
|
|
|
919
1120
|
const provenance = provenanceRead ? copyProvenance(provenanceRead) : void 0;
|
|
920
1121
|
const authorizationBinding = bindingRead ? copyBinding(bindingRead) : void 0;
|
|
921
1122
|
return Object.freeze({
|
|
1123
|
+
...snapshot.agentProposal !== void 0 ? { agentProposal: snapshot.agentProposal } : {},
|
|
922
1124
|
actionId: snapshot.actionId,
|
|
923
1125
|
tenantId: snapshot.tenantId,
|
|
924
1126
|
spaceId: snapshot.spaceId,
|
|
@@ -1015,6 +1217,7 @@ function parametersCopy(snapshot) {
|
|
|
1015
1217
|
*/
|
|
1016
1218
|
function liveSubmissionView(snapshot, input) {
|
|
1017
1219
|
const view = {
|
|
1220
|
+
...snapshot.agentProposal !== void 0 ? { agentProposal: snapshotAgentProposal(snapshot.agentProposal) } : {},
|
|
1018
1221
|
actionId: snapshot.actionId,
|
|
1019
1222
|
tenantId: snapshot.tenantId,
|
|
1020
1223
|
spaceId: snapshot.spaceId,
|
|
@@ -1056,6 +1259,7 @@ function submissionView(snapshot) {
|
|
|
1056
1259
|
const provenance = fresh(snapshot.provenance);
|
|
1057
1260
|
const authorizationBinding = fresh(snapshot.authorizationBinding);
|
|
1058
1261
|
return {
|
|
1262
|
+
...snapshot.agentProposal !== void 0 ? { agentProposal: snapshotAgentProposal(snapshot.agentProposal) } : {},
|
|
1059
1263
|
actionId: snapshot.actionId,
|
|
1060
1264
|
parameters: parametersCopy(snapshot),
|
|
1061
1265
|
tenantId: snapshot.tenantId,
|
|
@@ -1322,6 +1526,11 @@ const STAGED_ERROR = Object.freeze({
|
|
|
1322
1526
|
lookup: "Staged child lookup failed."
|
|
1323
1527
|
});
|
|
1324
1528
|
function createGovernedActionHost(options) {
|
|
1529
|
+
if (options.approvalDefaultTtlMs !== void 0 && (!Number.isSafeInteger(options.approvalDefaultTtlMs) || options.approvalDefaultTtlMs <= 0)) throw new Error("approvalDefaultTtlMs must be a positive safe integer in milliseconds.");
|
|
1530
|
+
if (options.approvalLifecycleEvents) {
|
|
1531
|
+
const approvalStore = asApprovalStore(options.store);
|
|
1532
|
+
if (!approvalStore || approvalStore.transactionalApprovalLifecycle !== true || typeof approvalStore.parkApproval !== "function" || typeof approvalStore.expireDueApprovals !== "function" || asOutboxStore(options.store)?.transactionalOutbox !== true || !asAtomicMutationStore(options.store)) throw new Error("Approval lifecycle events require transactionalOutbox, transactionWithEvents, and atomic approval lifecycle capabilities.");
|
|
1533
|
+
}
|
|
1325
1534
|
const retryableExecutionMaxAttempts = options.retryableExecutionMaxAttempts ?? 5;
|
|
1326
1535
|
if (!Number.isInteger(retryableExecutionMaxAttempts) || retryableExecutionMaxAttempts <= 0) throw new Error("retryableExecutionMaxAttempts must be a positive integer.");
|
|
1327
1536
|
const recoveryMaxAttempts = options.recoveryMaxAttempts ?? Math.max(5, retryableExecutionMaxAttempts);
|
|
@@ -1387,7 +1596,8 @@ function createGovernedActionHost(options) {
|
|
|
1387
1596
|
if (submission.authorizationBinding) validateAuthorizationBinding(submission, parameterDigest, action.execution?.authorityMoment, now());
|
|
1388
1597
|
return {
|
|
1389
1598
|
durableParameters,
|
|
1390
|
-
parameterDigest
|
|
1599
|
+
parameterDigest,
|
|
1600
|
+
agentProposal: submission.agentProposal === void 0 ? void 0 : snapshotAgentProposal(options.redactAgentProposal ? options.redactAgentProposal(submission.actionId, submission.agentProposal) : submission.agentProposal)
|
|
1391
1601
|
};
|
|
1392
1602
|
};
|
|
1393
1603
|
const early = declaresAdmission ? void 0 : prepare();
|
|
@@ -1402,9 +1612,10 @@ function createGovernedActionHost(options) {
|
|
|
1402
1612
|
};
|
|
1403
1613
|
assertGovernanceRuntimeEvidence(runtimeEvidence);
|
|
1404
1614
|
const admission = declaresAdmission ? await admitSubmission(action, submission) : void 0;
|
|
1405
|
-
const { durableParameters, parameterDigest } = early ?? prepare();
|
|
1615
|
+
const { durableParameters, parameterDigest, agentProposal } = early ?? prepare();
|
|
1406
1616
|
const invocationInput = {
|
|
1407
1617
|
id: actionInvocationId,
|
|
1618
|
+
...agentProposal !== void 0 ? { agentProposal } : {},
|
|
1408
1619
|
tenantId: submission.tenantId,
|
|
1409
1620
|
spaceId: submission.spaceId,
|
|
1410
1621
|
actionId: submission.actionId,
|
|
@@ -1458,6 +1669,7 @@ function createGovernedActionHost(options) {
|
|
|
1458
1669
|
});
|
|
1459
1670
|
if (durableInvocation.id !== actionInvocationId && submission.idempotencyKey) {
|
|
1460
1671
|
const conflict = idempotencyConflict(durableInvocation, {
|
|
1672
|
+
agentProposal,
|
|
1461
1673
|
actorId: submission.actorId,
|
|
1462
1674
|
authorizationBindingId: submission.authorizationBindingId,
|
|
1463
1675
|
actionVersion: action.version,
|
|
@@ -1697,6 +1909,7 @@ function createGovernedActionHost(options) {
|
|
|
1697
1909
|
};
|
|
1698
1910
|
}
|
|
1699
1911
|
async function persistExecutionInvocation(record, patch) {
|
|
1912
|
+
patch = finalizationSettlementPatch(record, patch);
|
|
1700
1913
|
const externalCompletionDigest = record.externalCompletion?.digest;
|
|
1701
1914
|
if (await persistInvocation(record, patch, onPreservedReconciliation, executionLease)) rememberVerdict(patch, externalCompletionDigest);
|
|
1702
1915
|
}
|
|
@@ -2109,7 +2322,7 @@ function createGovernedActionHost(options) {
|
|
|
2109
2322
|
}
|
|
2110
2323
|
const replayCheckpoint = action.finalize && (resumingRunningInvocation || invocation.attemptCount > 1) ? invocation.finalizationCheckpoint : void 0;
|
|
2111
2324
|
if (replayCheckpoint) {
|
|
2112
|
-
if (replayCheckpoint.protocol
|
|
2325
|
+
if (!["atomic-after-adapters-v1", "atomic-after-adapters-v2"].includes(replayCheckpoint.protocol) || replayCheckpoint.protocol === "atomic-after-adapters-v2" !== Object.hasOwn(replayCheckpoint, "adapterPreparation") || replayCheckpoint.actionVersion !== action.version || replayCheckpoint.steps.length !== (action.adapterSteps ?? []).length || replayCheckpoint.steps.some((step, index) => step.adapterType !== action.adapterSteps[index]?.adapterType || step.operation !== action.adapterSteps[index]?.operation) || invocation.pendingCompletion) return interruptedNonIdempotent("Finalization checkpoint protocol, action version, step identity or handoff is incompatible with replay.");
|
|
2113
2326
|
}
|
|
2114
2327
|
if (action.finalize && !replayCheckpoint && (resumingRunningInvocation || invocation.attemptCount > 1) && hasDurableProviderEffect) return interruptedNonIdempotent("Finalizing action recovery requires a preparation checkpoint; reconciliation is required.");
|
|
2115
2328
|
if (action.admitSubmission !== void 0 && !isAdmissionTime(invocation.admittedAt)) return hostOperation(() => failExecution(invocation, "failed", `Invocation of ${action.actionId} was not admitted at submission; an action that declares admitSubmission runs only on invocations its check admitted.`));
|
|
@@ -2178,21 +2391,27 @@ function createGovernedActionHost(options) {
|
|
|
2178
2391
|
}
|
|
2179
2392
|
if (invocation.actorType === "agent" && options.hitlEvaluator) {
|
|
2180
2393
|
const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
|
|
2394
|
+
const proposal = invocation.agentProposal;
|
|
2181
2395
|
const approvalStore = asApprovalStore(options.store);
|
|
2182
2396
|
if (!approvalStore) return hostOperation(() => failExecution(invocation, "failed", "The configured HITL evaluator requires a store with approval persistence support."));
|
|
2397
|
+
let evaluatedApprovalExpiresAt;
|
|
2183
2398
|
if (!invocation.hitlRoute) {
|
|
2184
2399
|
const decision = await options.hitlEvaluator({
|
|
2400
|
+
actionInvocationId: invocation.id,
|
|
2401
|
+
invocation,
|
|
2185
2402
|
actionId: action.actionId,
|
|
2186
2403
|
actorId: invocation.actorId,
|
|
2187
2404
|
actorType: invocation.actorType,
|
|
2188
2405
|
tenantId,
|
|
2189
2406
|
spaceId,
|
|
2190
2407
|
parameters: hitlParameters,
|
|
2191
|
-
...numberValue(hitlParameters.confidence) !== void 0 ? { confidence: numberValue(hitlParameters.confidence) } : {},
|
|
2192
|
-
...isRiskTier(hitlParameters.riskTier) ? { riskTier: hitlParameters.riskTier } : {},
|
|
2193
|
-
...stringValue(hitlParameters.agentSessionId) ? { agentSessionId: stringValue(hitlParameters.agentSessionId) } : {},
|
|
2194
|
-
...stringValue(hitlParameters.agentRunId) ? { agentRunId: stringValue(hitlParameters.agentRunId) } : {}
|
|
2408
|
+
...(proposal?.confidence ?? numberValue(hitlParameters.confidence)) !== void 0 ? { confidence: proposal?.confidence ?? numberValue(hitlParameters.confidence) } : {},
|
|
2409
|
+
...isRiskTier(proposal?.riskTier ?? hitlParameters.riskTier) ? { riskTier: proposal?.riskTier ?? hitlParameters.riskTier } : {},
|
|
2410
|
+
...proposal?.agentSessionId ?? stringValue(hitlParameters.agentSessionId) ? { agentSessionId: proposal?.agentSessionId ?? stringValue(hitlParameters.agentSessionId) } : {},
|
|
2411
|
+
...proposal?.agentRunId ?? stringValue(hitlParameters.agentRunId) ? { agentRunId: proposal?.agentRunId ?? stringValue(hitlParameters.agentRunId) } : {}
|
|
2195
2412
|
});
|
|
2413
|
+
if (decision.approvalExpiresAt !== void 0 && !validApprovalDate(decision.approvalExpiresAt)) return hostOperation(() => failExecution(invocation, "failed", "approvalExpiresAt must be a valid Date."));
|
|
2414
|
+
evaluatedApprovalExpiresAt = decision.approvalExpiresAt === void 0 ? void 0 : new Date(decision.approvalExpiresAt);
|
|
2196
2415
|
invocation = { ...await hostOperation(() => approvalStore.recordHitlDecision(actionInvocationId, tenantId, spaceId, {
|
|
2197
2416
|
...decision,
|
|
2198
2417
|
...options.hitlPolicyVersion ? { policyVersion: options.hitlPolicyVersion } : {},
|
|
@@ -2201,11 +2420,33 @@ function createGovernedActionHost(options) {
|
|
|
2201
2420
|
}
|
|
2202
2421
|
if (invocation.hitlRoute === "rejected") return hostOperation(() => failExecution(invocation, "failed", `HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`));
|
|
2203
2422
|
if ((invocation.hitlRoute === "needs-approval" || invocation.hitlRoute === "escalate") && !invocation.approvalDecision?.approved) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2423
|
+
const parkedAt = now();
|
|
2424
|
+
const approvalExpiresAt = invocation.approvalExpiresAt ?? evaluatedApprovalExpiresAt ?? (options.approvalDefaultTtlMs !== void 0 ? new Date(parkedAt.getTime() + options.approvalDefaultTtlMs) : void 0);
|
|
2425
|
+
if (evaluatedApprovalExpiresAt !== void 0 && invocation.approvalExpiresAt === void 0) return hostOperation(() => failExecution(invocation, "failed", "The configured HITL evaluator requires a store with approval deadline persistence support."));
|
|
2426
|
+
if (approvalExpiresAt && !validApprovalDate(approvalExpiresAt)) return hostOperation(() => failExecution(invocation, "failed", "Approval deadline exceeds the supported Date range."));
|
|
2427
|
+
if (approvalExpiresAt && (!approvalStore.expireDueApprovals || !approvalStore.parkApproval)) return hostOperation(() => failExecution(invocation, "failed", "Approval deadlines require a store with approval expiry support."));
|
|
2428
|
+
if (approvalStore.parkApproval) {
|
|
2429
|
+
let transition;
|
|
2430
|
+
try {
|
|
2431
|
+
transition = await approvalStore.parkApproval({
|
|
2432
|
+
actionInvocationId,
|
|
2433
|
+
tenantId,
|
|
2434
|
+
spaceId,
|
|
2435
|
+
now: parkedAt,
|
|
2436
|
+
...approvalExpiresAt ? { approvalExpiresAt } : {},
|
|
2437
|
+
...executionLease,
|
|
2438
|
+
...options.approvalLifecycleEvents ? { lifecycleEvent: approvalLifecycleEvent } : {}
|
|
2439
|
+
});
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
throw new RecoverableFinalizationError(caughtError({
|
|
2442
|
+
actionId: invocation.actionId,
|
|
2443
|
+
source: "execution"
|
|
2444
|
+
}, error));
|
|
2445
|
+
}
|
|
2446
|
+
invocation = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId) ?? invocation;
|
|
2447
|
+
if (transition.applied) emitInvocationStatusTelemetry(invocation, "waiting_for_approval", options.telemetry, parkedAt);
|
|
2448
|
+
} else await hostOperation(() => persistExecutionInvocation(invocation, { status: "waiting_for_approval" }));
|
|
2449
|
+
return actionResult(invocation);
|
|
2209
2450
|
}
|
|
2210
2451
|
}
|
|
2211
2452
|
const authorizationInput = toAuthorizationInput(action, invocation);
|
|
@@ -2438,13 +2679,23 @@ function createGovernedActionHost(options) {
|
|
|
2438
2679
|
...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
|
|
2439
2680
|
db,
|
|
2440
2681
|
services: options.services,
|
|
2441
|
-
...action.finalize ? {
|
|
2682
|
+
...action.finalize ? {
|
|
2683
|
+
finalizationProtocol: "atomic-after-adapters-v1",
|
|
2684
|
+
adapterPreparationProtocol: "v1"
|
|
2685
|
+
} : {}
|
|
2442
2686
|
}, parsed.data) : {
|
|
2443
2687
|
success: true,
|
|
2444
2688
|
data: {}
|
|
2445
2689
|
};
|
|
2446
2690
|
if (!handlerResult.success) throw handlerResult.error != null ? new Error(handlerResult.error) : new HostError("Action handler failed");
|
|
2447
2691
|
const handlerData = handlerResult.data ?? {};
|
|
2692
|
+
let adapterPreparation;
|
|
2693
|
+
if (handlerResult.adapterPreparation !== void 0) {
|
|
2694
|
+
if (!action.finalize) throw new HostError("adapterPreparation requires a finalizing action.");
|
|
2695
|
+
const value = handlerResult.adapterPreparation;
|
|
2696
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new HostError("adapterPreparation must be a plain JSON object.");
|
|
2697
|
+
adapterPreparation = finalizationJson(value);
|
|
2698
|
+
}
|
|
2448
2699
|
const rawStagedInvocations = handlerResult.stagedInvocations ?? [];
|
|
2449
2700
|
const stagedRecords = prepareStagedInvocationBatch({
|
|
2450
2701
|
parent: invocation,
|
|
@@ -2498,12 +2749,12 @@ function createGovernedActionHost(options) {
|
|
|
2498
2749
|
if (preparedFinalizationInput) {
|
|
2499
2750
|
const skipped = [];
|
|
2500
2751
|
for (const [index, step] of (action.adapterSteps ?? []).entries()) {
|
|
2501
|
-
const input = step.getInput(structuredClone(preparedFinalizationInput.params), structuredClone(preparedFinalizationInput.handlerData));
|
|
2752
|
+
const input = step.getInput(structuredClone(preparedFinalizationInput.params), structuredClone(preparedFinalizationInput.handlerData), structuredClone(adapterPreparation));
|
|
2502
2753
|
preparedInputs.set(index, input);
|
|
2503
2754
|
if (!input) skipped.push(index);
|
|
2504
2755
|
}
|
|
2505
2756
|
preparation = normalizeFinalizationJson({
|
|
2506
|
-
protocol: "atomic-after-adapters-v1",
|
|
2757
|
+
protocol: adapterPreparation !== void 0 ? "atomic-after-adapters-v2" : "atomic-after-adapters-v1",
|
|
2507
2758
|
actionVersion: action.version,
|
|
2508
2759
|
steps: (action.adapterSteps ?? []).map(({ adapterType, operation }) => ({
|
|
2509
2760
|
adapterType,
|
|
@@ -2512,6 +2763,7 @@ function createGovernedActionHost(options) {
|
|
|
2512
2763
|
...preparedFinalizationInput,
|
|
2513
2764
|
handlerEvents: events,
|
|
2514
2765
|
result: preparedFinalizationInput.handlerData,
|
|
2766
|
+
...adapterPreparation !== void 0 ? { adapterPreparation } : {},
|
|
2515
2767
|
...transitionEvent ? { stateTransition: transitionEvent } : {},
|
|
2516
2768
|
skipped,
|
|
2517
2769
|
...handlerStagedInvocations.length > 0 ? { stagedInvocations: handlerStagedInvocations } : {}
|
|
@@ -2661,7 +2913,7 @@ function createGovernedActionHost(options) {
|
|
|
2661
2913
|
}, error));
|
|
2662
2914
|
}
|
|
2663
2915
|
if (previousAdapterInvocation?.status === "ambiguous") return await settleUnknownOutcome(stepIndex, recordedAdapterReconciliation(previousAdapterInvocation), previousAdapterInvocation.reconciliation?.adapterEventSubject);
|
|
2664
|
-
const input = action.finalize ? preparation.skipped.includes(stepIndex) ? void 0 : replayCheckpoint ? step.getInput(structuredClone(replayCheckpoint.params), structuredClone(replayCheckpoint.handlerData)) : preparedInputs.get(stepIndex) : step.getInput(parsed.data, data);
|
|
2916
|
+
const input = action.finalize ? preparation.skipped.includes(stepIndex) ? void 0 : replayCheckpoint ? step.getInput(structuredClone(replayCheckpoint.params), structuredClone(replayCheckpoint.handlerData), structuredClone(replayCheckpoint.adapterPreparation)) : preparedInputs.get(stepIndex) : step.getInput(parsed.data, data);
|
|
2665
2917
|
if (!input) {
|
|
2666
2918
|
if (action.finalize) finalizationSteps.push({
|
|
2667
2919
|
stepIndex,
|
|
@@ -3253,14 +3505,14 @@ function createGovernedActionHost(options) {
|
|
|
3253
3505
|
...pendingHandoff,
|
|
3254
3506
|
parkedAt: now()
|
|
3255
3507
|
} : void 0;
|
|
3256
|
-
const finalPatch = parked ? {
|
|
3508
|
+
const finalPatch = finalizationSettlementPatch(invocation, parked ? {
|
|
3257
3509
|
status: "running",
|
|
3258
3510
|
result,
|
|
3259
3511
|
pendingCompletion: parked
|
|
3260
3512
|
} : {
|
|
3261
3513
|
status: "completed",
|
|
3262
3514
|
result
|
|
3263
|
-
};
|
|
3515
|
+
});
|
|
3264
3516
|
let verdictWritten = true;
|
|
3265
3517
|
let finalizationCallbackFailure;
|
|
3266
3518
|
try {
|
|
@@ -3421,6 +3673,7 @@ function createGovernedActionHost(options) {
|
|
|
3421
3673
|
}
|
|
3422
3674
|
}
|
|
3423
3675
|
async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
|
|
3676
|
+
decision = snapshotApprovalDecision(decision);
|
|
3424
3677
|
if (!decision.approverId.trim()) throw new Error("approverId is required");
|
|
3425
3678
|
const invocation = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
|
|
3426
3679
|
if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
@@ -3430,7 +3683,13 @@ function createGovernedActionHost(options) {
|
|
|
3430
3683
|
error: `Invocation is not waiting for approval (status: ${invocation.status})`
|
|
3431
3684
|
};
|
|
3432
3685
|
const action = actionResolver(invocation.actionId);
|
|
3433
|
-
if (!action)
|
|
3686
|
+
if (!action) {
|
|
3687
|
+
const error = `Unknown action: ${invocation.actionId}`;
|
|
3688
|
+
return invocation.approvalExpiresAt === void 0 ? fail(invocation, "failed", error) : {
|
|
3689
|
+
...actionResult(invocation),
|
|
3690
|
+
error
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3434
3693
|
const approvalAuthorizationInput = toAuthorizationInput(action, {
|
|
3435
3694
|
tenantId,
|
|
3436
3695
|
spaceId,
|
|
@@ -3441,7 +3700,7 @@ function createGovernedActionHost(options) {
|
|
|
3441
3700
|
const authorized = options.authorization.authorizeApproval ? await options.authorization.authorizeApproval({
|
|
3442
3701
|
...approvalAuthorizationInput,
|
|
3443
3702
|
actionInvocationId,
|
|
3444
|
-
decision,
|
|
3703
|
+
decision: structuredClone(decision),
|
|
3445
3704
|
invocation: structuredClone(invocation)
|
|
3446
3705
|
}) : await options.authorization.authorize(approvalAuthorizationInput);
|
|
3447
3706
|
if (!entitled || !authorized) return {
|
|
@@ -3466,14 +3725,15 @@ function createGovernedActionHost(options) {
|
|
|
3466
3725
|
},
|
|
3467
3726
|
leaseOwner,
|
|
3468
3727
|
leaseDurationMs: options.approvalResumeLeaseDurationMs ?? 3e5,
|
|
3469
|
-
now: transitionStartedAt
|
|
3728
|
+
now: transitionStartedAt,
|
|
3729
|
+
...options.approvalLifecycleEvents ? { lifecycleEvent: approvalLifecycleEvent } : {}
|
|
3470
3730
|
});
|
|
3471
3731
|
const transitioned = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
|
|
3472
3732
|
if (!transition.applied || !transitioned) {
|
|
3473
3733
|
if (!transitioned) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
3474
3734
|
return withConsistency(actionResult(transitioned), transitioned.actionId);
|
|
3475
3735
|
}
|
|
3476
|
-
if (!decision.approved) {
|
|
3736
|
+
if (transitioned.approvalExpiry || !decision.approved) {
|
|
3477
3737
|
emitInvocationStatusTelemetry(transitioned, "failed", options.telemetry, now(), "waiting_for_approval");
|
|
3478
3738
|
return withConsistency(actionResult(transitioned), transitioned.actionId);
|
|
3479
3739
|
}
|
|
@@ -3525,9 +3785,73 @@ function createGovernedActionHost(options) {
|
|
|
3525
3785
|
consistency
|
|
3526
3786
|
} : result;
|
|
3527
3787
|
}
|
|
3528
|
-
|
|
3529
|
-
const
|
|
3530
|
-
const
|
|
3788
|
+
function eventOutboxMetadata(invocation, envelope) {
|
|
3789
|
+
const traceContext = options.outbox?.traceContext?.(envelope);
|
|
3790
|
+
const payloadClassification = options.outbox?.classifyPayload(envelope) ?? "internal";
|
|
3791
|
+
return {
|
|
3792
|
+
producerModuleVersion: options.outbox?.producerModuleVersion(invocation.actionId, invocation.actionVersion) ?? String(invocation.actionVersion),
|
|
3793
|
+
payloadClassification,
|
|
3794
|
+
includeProvenance: options.outbox?.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
|
|
3795
|
+
includeActor: options.outbox?.includeActor?.(envelope, payloadClassification) === true,
|
|
3796
|
+
...traceContext ? { traceContext } : {}
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
const approvalLifecycleEvent = (invocation, kind) => {
|
|
3800
|
+
let eventType;
|
|
3801
|
+
let payload;
|
|
3802
|
+
if (kind === "requested") {
|
|
3803
|
+
eventType = APPROVAL_REQUESTED_EVENT;
|
|
3804
|
+
payload = {
|
|
3805
|
+
route: invocation.hitlRoute,
|
|
3806
|
+
riskTier: invocation.hitlRiskTier,
|
|
3807
|
+
reason: invocation.hitlReason ?? "",
|
|
3808
|
+
...invocation.approvalExpiresAt ? { expiresAt: invocation.approvalExpiresAt.toISOString() } : {}
|
|
3809
|
+
};
|
|
3810
|
+
} else if (kind === "decided") {
|
|
3811
|
+
eventType = APPROVAL_DECIDED_EVENT;
|
|
3812
|
+
const decision = invocation.approvalDecision;
|
|
3813
|
+
payload = {
|
|
3814
|
+
approved: decision.approved,
|
|
3815
|
+
approverId: decision.approverId,
|
|
3816
|
+
approverType: decision.approverType,
|
|
3817
|
+
...decision.reason !== void 0 ? { reason: decision.reason } : {},
|
|
3818
|
+
decidedAt: decision.decidedAt.toISOString(),
|
|
3819
|
+
evidencePresent: decision.evidence !== void 0
|
|
3820
|
+
};
|
|
3821
|
+
} else {
|
|
3822
|
+
eventType = APPROVAL_EXPIRED_EVENT;
|
|
3823
|
+
payload = {
|
|
3824
|
+
expiresAt: invocation.approvalExpiry.expiresAt.toISOString(),
|
|
3825
|
+
expiredAt: invocation.approvalExpiry.expiredAt.toISOString()
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
const event = buildEventEnvelope(invocation, {
|
|
3829
|
+
eventType,
|
|
3830
|
+
subjectType: "ActionInvocation",
|
|
3831
|
+
subjectId: invocation.id,
|
|
3832
|
+
payload
|
|
3833
|
+
}, `approval:${kind}`, 0, kind === "expired" ? invocation.approvalExpiry.expiredAt : invocation.updatedAt);
|
|
3834
|
+
return {
|
|
3835
|
+
event,
|
|
3836
|
+
metadata: eventOutboxMetadata(invocation, event)
|
|
3837
|
+
};
|
|
3838
|
+
};
|
|
3839
|
+
async function expireDueApprovals(input = {}) {
|
|
3840
|
+
const store = asApprovalStore(options.store);
|
|
3841
|
+
if (!store?.expireDueApprovals) {
|
|
3842
|
+
if (options.approvalDefaultTtlMs !== void 0 || options.approvalLifecycleEvents) throw new HostError("Approval expiry requires a store with approval expiry support.");
|
|
3843
|
+
return { expired: [] };
|
|
3844
|
+
}
|
|
3845
|
+
const expired = await store.expireDueApprovals({
|
|
3846
|
+
...input,
|
|
3847
|
+
now: input.now ?? now(),
|
|
3848
|
+
...options.approvalLifecycleEvents ? { lifecycleEvent: approvalLifecycleEvent } : {}
|
|
3849
|
+
});
|
|
3850
|
+
for (const invocation of expired) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, invocation.approvalExpiry.expiredAt, "waiting_for_approval");
|
|
3851
|
+
return { expired: expired.map((invocation) => invocation.id) };
|
|
3852
|
+
}
|
|
3853
|
+
function buildEventEnvelope(invocation, event, deduplicationKey, sequence, timestamp, defaultEventSchemaVersion = 1) {
|
|
3854
|
+
return {
|
|
3531
3855
|
id: lifecycleId("evt", invocation.id, deduplicationKey),
|
|
3532
3856
|
tenantId: invocation.tenantId,
|
|
3533
3857
|
spaceId: invocation.spaceId,
|
|
@@ -3539,7 +3863,7 @@ function createGovernedActionHost(options) {
|
|
|
3539
3863
|
actorType: invocation.actorType,
|
|
3540
3864
|
actionInvocationId: invocation.id,
|
|
3541
3865
|
payload: event.payload,
|
|
3542
|
-
sequence
|
|
3866
|
+
sequence,
|
|
3543
3867
|
occurredAt: timestamp,
|
|
3544
3868
|
recordedAt: timestamp,
|
|
3545
3869
|
correlationId: invocation.correlationId,
|
|
@@ -3550,6 +3874,10 @@ function createGovernedActionHost(options) {
|
|
|
3550
3874
|
} } : {},
|
|
3551
3875
|
...declaredConsistency(invocation.actionId) === "provisional-until-reconciled" ? { consistency: "provisional-until-reconciled" } : {}
|
|
3552
3876
|
};
|
|
3877
|
+
}
|
|
3878
|
+
async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1, transaction) {
|
|
3879
|
+
const timestamp = now();
|
|
3880
|
+
const envelope = buildEventEnvelope(invocation, event, deduplicationKey, await (transaction ?? options.store).nextEventSequence(invocation.tenantId, invocation.spaceId), timestamp, defaultEventSchemaVersion);
|
|
3553
3881
|
if (options.outbox) {
|
|
3554
3882
|
const hostLifecycleEvent = (/* @__PURE__ */ new Set([
|
|
3555
3883
|
"AdapterInvocationStarted",
|
|
@@ -3565,15 +3893,7 @@ function createGovernedActionHost(options) {
|
|
|
3565
3893
|
await (transaction ?? options.store).appendEvent(envelope);
|
|
3566
3894
|
return;
|
|
3567
3895
|
}
|
|
3568
|
-
const
|
|
3569
|
-
const payloadClassification = options.outbox.classifyPayload(envelope);
|
|
3570
|
-
const metadata = {
|
|
3571
|
-
producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
|
|
3572
|
-
payloadClassification,
|
|
3573
|
-
includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
|
|
3574
|
-
includeActor: options.outbox.includeActor?.(envelope, payloadClassification) === true,
|
|
3575
|
-
...traceContext ? { traceContext } : {}
|
|
3576
|
-
};
|
|
3896
|
+
const metadata = eventOutboxMetadata(invocation, envelope);
|
|
3577
3897
|
if (transaction) {
|
|
3578
3898
|
if (!transaction.appendEventWithOutbox) throw new HostError("Outbox egress requires an atomic transaction with appendEventWithOutbox.");
|
|
3579
3899
|
await transaction.appendEventWithOutbox(envelope, metadata);
|
|
@@ -3636,6 +3956,7 @@ function createGovernedActionHost(options) {
|
|
|
3636
3956
|
return !evidenceOnly;
|
|
3637
3957
|
}
|
|
3638
3958
|
async function persistInvocation(invocation, patch, onPreservedReconciliation, lease) {
|
|
3959
|
+
patch = finalizationSettlementPatch(invocation, patch);
|
|
3639
3960
|
const previousStatus = invocation.status;
|
|
3640
3961
|
const identity = lease ?? (invocation.leaseOwner && invocation.leaseToken !== void 0 ? {
|
|
3641
3962
|
leaseOwner: invocation.leaseOwner,
|
|
@@ -3951,6 +4272,7 @@ function createGovernedActionHost(options) {
|
|
|
3951
4272
|
...stagedStore ? { dispatchStagedInvocations } : {},
|
|
3952
4273
|
executeInvocation,
|
|
3953
4274
|
resumeApprovedInvocation,
|
|
4275
|
+
expireDueApprovals,
|
|
3954
4276
|
recordExecutionAttestation,
|
|
3955
4277
|
recordExternalReconciliation,
|
|
3956
4278
|
completeExternalInvocation: externalCompletion.completeExternalInvocation,
|
|
@@ -4050,6 +4372,7 @@ function validateAuthorizationBinding(input, parameterDigest, authorityMoment, c
|
|
|
4050
4372
|
}
|
|
4051
4373
|
function idempotencyConflict(existing, incoming) {
|
|
4052
4374
|
const reasons = [];
|
|
4375
|
+
if (canonicalJson(existing.agentProposal ?? null) !== canonicalJson(incoming.agentProposal ?? null)) reasons.push("agent_proposal");
|
|
4053
4376
|
if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
|
|
4054
4377
|
if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
|
|
4055
4378
|
if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
|
|
@@ -4278,6 +4601,8 @@ function emitRelayTelemetry(telemetry, name, occurredAt, workerId, attributes) {
|
|
|
4278
4601
|
var MemoryPlatformHostStore = class {
|
|
4279
4602
|
db;
|
|
4280
4603
|
transactionalOutbox;
|
|
4604
|
+
transactionalApprovalLifecycle;
|
|
4605
|
+
approvalTransactionProvider;
|
|
4281
4606
|
transactionalStagedInvocations;
|
|
4282
4607
|
adapterCommit;
|
|
4283
4608
|
transactionTail = Promise.resolve();
|
|
@@ -4300,6 +4625,11 @@ var MemoryPlatformHostStore = class {
|
|
|
4300
4625
|
constructor(db, transactionProvider) {
|
|
4301
4626
|
this.db = db;
|
|
4302
4627
|
this.transactionalOutbox = transactionProvider !== void 0;
|
|
4628
|
+
this.transactionalApprovalLifecycle = transactionProvider !== void 0;
|
|
4629
|
+
this.approvalTransactionProvider = transactionProvider ?? {
|
|
4630
|
+
snapshot: (db) => db,
|
|
4631
|
+
restore: () => void 0
|
|
4632
|
+
};
|
|
4303
4633
|
this.transactionalStagedInvocations = transactionProvider !== void 0;
|
|
4304
4634
|
if (transactionProvider) this.transactionWithEvents = (run) => this.runTransactionWithEvents(run, transactionProvider);
|
|
4305
4635
|
}
|
|
@@ -4546,6 +4876,15 @@ var MemoryPlatformHostStore = class {
|
|
|
4546
4876
|
const record = this.invocations.find((row) => row.id === id && row.tenantId === tenantId && row.spaceId === spaceId);
|
|
4547
4877
|
if (!record) throw new Error(`ActionInvocation not found: ${id}`);
|
|
4548
4878
|
Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
|
|
4879
|
+
for (const key of [
|
|
4880
|
+
"approvalExpiresAt",
|
|
4881
|
+
"approvalExpiry",
|
|
4882
|
+
"approvalDecision"
|
|
4883
|
+
]) {
|
|
4884
|
+
if (!Object.hasOwn(patch, key)) continue;
|
|
4885
|
+
if (patch[key] === void 0) delete record[key];
|
|
4886
|
+
else Object.assign(record, { [key]: structuredClone(patch[key]) });
|
|
4887
|
+
}
|
|
4549
4888
|
if (Object.hasOwn(patch, "finalizationCheckpoint")) {
|
|
4550
4889
|
if (patch.finalizationCheckpoint) record.finalizationCheckpoint = structuredClone(patch.finalizationCheckpoint);
|
|
4551
4890
|
else delete record.finalizationCheckpoint;
|
|
@@ -4570,6 +4909,8 @@ var MemoryPlatformHostStore = class {
|
|
|
4570
4909
|
async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
|
|
4571
4910
|
const record = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
|
|
4572
4911
|
if (!record) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
4912
|
+
if (record.hitlRoute || !["pending", "running"].includes(record.status)) return record;
|
|
4913
|
+
if (decision.approvalExpiresAt) record.approvalExpiresAt = new Date(decision.approvalExpiresAt);
|
|
4573
4914
|
record.hitlRoute = decision.route;
|
|
4574
4915
|
record.hitlRiskTier = decision.riskTier;
|
|
4575
4916
|
record.hitlReason = decision.reason;
|
|
@@ -4577,34 +4918,34 @@ var MemoryPlatformHostStore = class {
|
|
|
4577
4918
|
record.updatedAt = decision.evaluatedAt;
|
|
4578
4919
|
return record;
|
|
4579
4920
|
}
|
|
4921
|
+
runApprovalTransaction(run) {
|
|
4922
|
+
return this.transactionWithEvents ? this.transactionWithEvents(run) : this.runTransactionWithEvents(run, this.approvalTransactionProvider);
|
|
4923
|
+
}
|
|
4924
|
+
async parkApproval(input) {
|
|
4925
|
+
if (input.lifecycleEvent && !this.transactionalApprovalLifecycle) throw new Error("Approval events require transactional approval support.");
|
|
4926
|
+
return this.runApprovalTransaction((tx) => applyApprovalTransition(tx, input, "requested"));
|
|
4927
|
+
}
|
|
4580
4928
|
async beginApprovalDecision(input) {
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
record.leaseExpiresAt = new Date(input.now.getTime() + input.leaseDurationMs);
|
|
4602
|
-
record.leaseToken = (record.leaseToken ?? 0) + 1;
|
|
4603
|
-
record.attemptCount = Math.max(record.attemptCount, 1);
|
|
4604
|
-
return {
|
|
4605
|
-
applied: true,
|
|
4606
|
-
invocation: record
|
|
4607
|
-
};
|
|
4929
|
+
if (input.lifecycleEvent && !this.transactionalApprovalLifecycle) throw new Error("Approval events require transactional approval support.");
|
|
4930
|
+
return this.runApprovalTransaction((tx) => applyApprovalTransition(tx, input, "decided"));
|
|
4931
|
+
}
|
|
4932
|
+
async expireDueApprovals(input) {
|
|
4933
|
+
const limit = approvalSweepLimit(input);
|
|
4934
|
+
if (input.lifecycleEvent && !this.transactionalApprovalLifecycle) throw new Error("Approval events require transactional approval support.");
|
|
4935
|
+
const candidates = this.invocations.filter((row) => row.status === "waiting_for_approval" && row.approvalExpiresAt && row.approvalExpiresAt <= input.now && (input.tenantId === void 0 || row.tenantId === input.tenantId) && (input.spaceId === void 0 || row.spaceId === input.spaceId)).sort((a, b) => a.approvalExpiresAt.getTime() - b.approvalExpiresAt.getTime() || a.id.localeCompare(b.id)).slice(0, limit);
|
|
4936
|
+
return this.runApprovalTransaction(async (tx) => {
|
|
4937
|
+
const expired = [];
|
|
4938
|
+
for (const row of candidates) {
|
|
4939
|
+
const transition = await applyApprovalTransition(tx, {
|
|
4940
|
+
...input,
|
|
4941
|
+
actionInvocationId: row.id,
|
|
4942
|
+
tenantId: row.tenantId,
|
|
4943
|
+
spaceId: row.spaceId
|
|
4944
|
+
}, "expired");
|
|
4945
|
+
if (transition.applied) expired.push(transition.invocation);
|
|
4946
|
+
}
|
|
4947
|
+
return expired;
|
|
4948
|
+
});
|
|
4608
4949
|
}
|
|
4609
4950
|
async appendPolicyEvaluation(record) {
|
|
4610
4951
|
if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
|
|
@@ -5054,16 +5395,19 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5054
5395
|
sql;
|
|
5055
5396
|
transactionWithEvents;
|
|
5056
5397
|
transactionalOutbox;
|
|
5398
|
+
transactionalApprovalLifecycle;
|
|
5057
5399
|
transactionalStagedInvocations;
|
|
5058
5400
|
constructor(db, sql, transactionProvider) {
|
|
5059
5401
|
this.db = db;
|
|
5060
5402
|
this.sql = sql;
|
|
5061
5403
|
this.transactionalOutbox = transactionProvider !== void 0;
|
|
5404
|
+
this.transactionalApprovalLifecycle = transactionProvider !== void 0;
|
|
5062
5405
|
this.transactionalStagedInvocations = transactionProvider !== void 0;
|
|
5063
5406
|
if (transactionProvider) this.transactionWithEvents = async (run) => transactionProvider.run(async ({ db, sql }) => {
|
|
5064
5407
|
const scoped = new PostgresPlatformHostStore(db, sql);
|
|
5065
5408
|
return run({
|
|
5066
5409
|
db,
|
|
5410
|
+
beginApprovalDecision: (input) => scoped.beginApprovalDecision(input),
|
|
5067
5411
|
getActionInvocationForUpdate: async (id, tenantId, spaceId) => {
|
|
5068
5412
|
const result = await sql.query(`SELECT * FROM fabric_platform.action_invocations
|
|
5069
5413
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3
|
|
@@ -5322,6 +5666,23 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5322
5666
|
CREATE INDEX IF NOT EXISTS staged_invocations_parent_idx
|
|
5323
5667
|
ON fabric_platform.staged_invocations (tenant_id, space_id, parent_invocation_id, created_at);
|
|
5324
5668
|
`
|
|
5669
|
+
},
|
|
5670
|
+
{
|
|
5671
|
+
version: 12,
|
|
5672
|
+
name: "approval_lifecycle_expiry",
|
|
5673
|
+
sql: `
|
|
5674
|
+
ALTER TABLE fabric_platform.action_invocations
|
|
5675
|
+
ADD COLUMN IF NOT EXISTS approval_expires_at timestamptz,
|
|
5676
|
+
ADD COLUMN IF NOT EXISTS approval_expiry jsonb;
|
|
5677
|
+
CREATE INDEX IF NOT EXISTS action_invocations_approval_expiry_idx
|
|
5678
|
+
ON fabric_platform.action_invocations (approval_expires_at, id)
|
|
5679
|
+
WHERE status='waiting_for_approval' AND approval_expires_at IS NOT NULL;
|
|
5680
|
+
`
|
|
5681
|
+
},
|
|
5682
|
+
{
|
|
5683
|
+
version: 13,
|
|
5684
|
+
name: "agent_proposal",
|
|
5685
|
+
sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS agent_proposal jsonb;`
|
|
5325
5686
|
}
|
|
5326
5687
|
]);
|
|
5327
5688
|
}
|
|
@@ -5333,6 +5694,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5333
5694
|
const admitted = input.admittedAt !== void 0;
|
|
5334
5695
|
const admittedColumn = admitted ? "admitted_at," : "";
|
|
5335
5696
|
const admittedValue = admitted ? "$25," : "";
|
|
5697
|
+
const proposalColumn = input.agentProposal !== void 0 ? "agent_proposal," : "";
|
|
5698
|
+
const proposalValue = input.agentProposal !== void 0 ? `$${admitted ? 26 : 25}::jsonb,` : "";
|
|
5336
5699
|
let result;
|
|
5337
5700
|
try {
|
|
5338
5701
|
result = await this.sql.query(`INSERT INTO fabric_platform.action_invocations
|
|
@@ -5340,8 +5703,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5340
5703
|
parameters,result,correlation_id,causation_id,idempotency_key,
|
|
5341
5704
|
parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
|
|
5342
5705
|
idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
|
|
5343
|
-
authorization_binding_id,error,runtime_evidence,${admittedColumn}created_at,updated_at)
|
|
5344
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,${admittedValue}$24,$24)
|
|
5706
|
+
authorization_binding_id,error,runtime_evidence,${admittedColumn}${proposalColumn}created_at,updated_at)
|
|
5707
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,${admittedValue}${proposalValue}$24,$24)
|
|
5345
5708
|
ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
|
|
5346
5709
|
WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
|
|
5347
5710
|
RETURNING *`, [
|
|
@@ -5369,9 +5732,11 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5369
5732
|
input.error ?? null,
|
|
5370
5733
|
input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
|
|
5371
5734
|
now,
|
|
5372
|
-
...admitted ? [input.admittedAt] : []
|
|
5735
|
+
...admitted ? [input.admittedAt] : [],
|
|
5736
|
+
...input.agentProposal !== void 0 ? [JSON.stringify(input.agentProposal)] : []
|
|
5373
5737
|
]);
|
|
5374
5738
|
} catch (error) {
|
|
5739
|
+
if (input.agentProposal !== void 0 && error?.code === "42703") throw new Error("Agent proposals require PostgreSQL migration 13 (agent_proposal); run ensureSchema().", { cause: error });
|
|
5375
5740
|
if (admitted && error?.code === "42703") throw new Error("An admitted invocation needs the action_invocations.admitted_at column; run ensureSchema() to apply PostgreSQL migration 6 (invocation_admission) before submitting actions that declare admitSubmission.", { cause: error });
|
|
5376
5741
|
throw error;
|
|
5377
5742
|
}
|
|
@@ -5383,6 +5748,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5383
5748
|
const admitted = invocation.admittedAt !== void 0;
|
|
5384
5749
|
const admittedColumn = admitted ? ",admitted_at" : "";
|
|
5385
5750
|
const admittedValue = admitted ? ",$30" : "";
|
|
5751
|
+
const proposalColumn = invocation.agentProposal !== void 0 ? ",agent_proposal" : "";
|
|
5752
|
+
const proposalValue = invocation.agentProposal !== void 0 ? `,$${admitted ? 31 : 30}::jsonb` : "";
|
|
5386
5753
|
const values = [
|
|
5387
5754
|
invocation.id,
|
|
5388
5755
|
invocation.tenantId,
|
|
@@ -5413,7 +5780,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5413
5780
|
input.workerId,
|
|
5414
5781
|
input.leaseToken,
|
|
5415
5782
|
current,
|
|
5416
|
-
...admitted ? [invocation.admittedAt] : []
|
|
5783
|
+
...admitted ? [invocation.admittedAt] : [],
|
|
5784
|
+
...invocation.agentProposal !== void 0 ? [JSON.stringify(invocation.agentProposal)] : []
|
|
5417
5785
|
];
|
|
5418
5786
|
let result;
|
|
5419
5787
|
try {
|
|
@@ -5431,15 +5799,16 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5431
5799
|
parameters,result,correlation_id,causation_id,idempotency_key,parameter_digest,
|
|
5432
5800
|
parameter_digest_algorithm,idempotency_actor_id,idempotency_authorization_binding_id,
|
|
5433
5801
|
invocation_provenance,authorization_binding,execution_reason,authorization_binding_id,error,
|
|
5434
|
-
runtime_evidence,staged_parent_invocation_id,staged_stage_key${admittedColumn},created_at,updated_at)
|
|
5802
|
+
runtime_evidence,staged_parent_invocation_id,staged_stage_key${admittedColumn}${proposalColumn},created_at,updated_at)
|
|
5435
5803
|
SELECT $1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,
|
|
5436
|
-
$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$25${admittedValue},$29,$29
|
|
5804
|
+
$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$25${admittedValue}${proposalValue},$29,$29
|
|
5437
5805
|
FROM fence
|
|
5438
5806
|
ON CONFLICT (tenant_id,space_id,staged_parent_invocation_id,staged_stage_key)
|
|
5439
5807
|
WHERE staged_parent_invocation_id IS NOT NULL AND staged_stage_key IS NOT NULL
|
|
5440
5808
|
DO UPDATE SET id=fabric_platform.action_invocations.id
|
|
5441
5809
|
RETURNING *`, values);
|
|
5442
5810
|
} catch (error) {
|
|
5811
|
+
if (invocation.agentProposal !== void 0 && error?.code === "42703") throw new Error("Agent proposals require PostgreSQL migration 13 (agent_proposal); run ensureSchema().", { cause: error });
|
|
5443
5812
|
if (error?.code === "42703" || error?.code === "42P01") throw new Error("A staged child needs PostgreSQL migration 11 (staged_descendant_invocations); run ensureSchema() before dispatching staged invocations.", { cause: error });
|
|
5444
5813
|
throw error;
|
|
5445
5814
|
}
|
|
@@ -5532,7 +5901,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5532
5901
|
return result.rows[0] ? toActionRecord(result.rows[0]) : void 0;
|
|
5533
5902
|
}
|
|
5534
5903
|
async updateActionInvocation(id, tenantId, spaceId, patch) {
|
|
5904
|
+
const approvalFields = approvalPatchSql(patch, 18 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0) + (Object.hasOwn(patch, "finalizationCheckpoint") ? 1 : 0));
|
|
5535
5905
|
await this.sql.query(`UPDATE fabric_platform.action_invocations SET
|
|
5906
|
+
${approvalFields.sql}
|
|
5536
5907
|
status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
|
|
5537
5908
|
error=CASE WHEN $6::boolean THEN $7 ELSE error END,
|
|
5538
5909
|
authorization_reconciliation=CASE WHEN $8::boolean THEN $9::jsonb ELSE authorization_reconciliation END,
|
|
@@ -5543,10 +5914,10 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5543
5914
|
${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${18 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
|
|
5544
5915
|
${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $18::boolean THEN $19::integer ELSE deferred_before_effect_attempt END," : ""}
|
|
5545
5916
|
pending_state_transition=CASE WHEN $16::boolean THEN $17::jsonb ELSE pending_state_transition END,
|
|
5546
|
-
lease_owner=CASE WHEN ($13::jsonb IS NOT NULL AND $4='running') OR $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
5547
|
-
THEN NULL ELSE lease_owner END
|
|
5548
|
-
lease_expires_at=CASE WHEN ($13::jsonb IS NOT NULL AND $4='running') OR $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
5549
|
-
THEN NULL ELSE lease_expires_at END
|
|
5917
|
+
${Object.hasOwn(patch, "leaseOwner") ? "" : `lease_owner=CASE WHEN ($13::jsonb IS NOT NULL AND $4='running') OR $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
5918
|
+
THEN NULL ELSE lease_owner END,`}
|
|
5919
|
+
${Object.hasOwn(patch, "leaseExpiresAt") ? "" : `lease_expires_at=CASE WHEN ($13::jsonb IS NOT NULL AND $4='running') OR $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
5920
|
+
THEN NULL ELSE lease_expires_at END,`}
|
|
5550
5921
|
updated_at=now()
|
|
5551
5922
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`, [
|
|
5552
5923
|
id,
|
|
@@ -5567,14 +5938,15 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5567
5938
|
Object.hasOwn(patch, "pendingStateTransition"),
|
|
5568
5939
|
patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
|
|
5569
5940
|
...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
|
|
5570
|
-
...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
|
|
5941
|
+
...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : [],
|
|
5942
|
+
...approvalFields.values
|
|
5571
5943
|
]);
|
|
5572
5944
|
}
|
|
5573
5945
|
async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
|
|
5574
5946
|
const row = (await this.sql.query(`UPDATE fabric_platform.action_invocations SET
|
|
5575
5947
|
hitl_route=$4, hitl_risk_tier=$5, hitl_reason=$6,
|
|
5576
|
-
hitl_policy_version=$7, updated_at=$8
|
|
5577
|
-
WHERE id=$1 AND tenant_id=$2 AND space_id=$3
|
|
5948
|
+
hitl_policy_version=$7, updated_at=$8, approval_expires_at=$9
|
|
5949
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND hitl_route IS NULL AND status IN ('pending','running')
|
|
5578
5950
|
RETURNING *`, [
|
|
5579
5951
|
actionInvocationId,
|
|
5580
5952
|
tenantId,
|
|
@@ -5583,20 +5955,93 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5583
5955
|
decision.riskTier,
|
|
5584
5956
|
decision.reason,
|
|
5585
5957
|
decision.policyVersion ?? null,
|
|
5586
|
-
decision.evaluatedAt
|
|
5958
|
+
decision.evaluatedAt,
|
|
5959
|
+
decision.approvalExpiresAt ?? null
|
|
5587
5960
|
])).rows[0];
|
|
5588
|
-
if (
|
|
5589
|
-
|
|
5961
|
+
if (row) return toActionRecord(row);
|
|
5962
|
+
const existing = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
|
|
5963
|
+
if (!existing) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
5964
|
+
return existing;
|
|
5965
|
+
}
|
|
5966
|
+
async parkApproval(input) {
|
|
5967
|
+
if (this.transactionWithEvents) return this.transactionWithEvents((tx) => applyApprovalTransition(tx, input, "requested"));
|
|
5968
|
+
if (input.lifecycleEvent) throw new Error("Approval events require transactional approval support.");
|
|
5969
|
+
const result = await this.sql.query(`UPDATE fabric_platform.action_invocations SET status='waiting_for_approval',
|
|
5970
|
+
approval_expires_at=COALESCE($4::timestamptz,approval_expires_at), lease_owner=NULL, lease_expires_at=NULL, updated_at=$5
|
|
5971
|
+
WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status IN ('pending','running') AND approval_decision IS NULL
|
|
5972
|
+
AND (lease_owner IS NULL OR (lease_owner=$6 AND lease_token=$7)) RETURNING *`, [
|
|
5973
|
+
input.actionInvocationId,
|
|
5974
|
+
input.tenantId,
|
|
5975
|
+
input.spaceId,
|
|
5976
|
+
input.approvalExpiresAt ?? null,
|
|
5977
|
+
input.now,
|
|
5978
|
+
input.leaseOwner ?? null,
|
|
5979
|
+
input.leaseToken ?? 0
|
|
5980
|
+
]);
|
|
5981
|
+
return result.rows[0] ? {
|
|
5982
|
+
applied: true,
|
|
5983
|
+
invocation: toActionRecord(result.rows[0])
|
|
5984
|
+
} : { applied: false };
|
|
5985
|
+
}
|
|
5986
|
+
async expireDueApprovals(input) {
|
|
5987
|
+
const limit = approvalSweepLimit(input);
|
|
5988
|
+
if (input.lifecycleEvent && !this.transactionWithEvents) throw new Error("Approval events require transactional approval support.");
|
|
5989
|
+
if (!this.transactionWithEvents) return (await this.sql.query(`WITH due AS (
|
|
5990
|
+
SELECT id FROM fabric_platform.action_invocations WHERE status='waiting_for_approval' AND approval_expires_at <= $1
|
|
5991
|
+
AND ($2::text IS NULL OR tenant_id=$2) AND ($3::text IS NULL OR space_id=$3)
|
|
5992
|
+
ORDER BY approval_expires_at,id LIMIT $4 FOR UPDATE SKIP LOCKED
|
|
5993
|
+
) UPDATE fabric_platform.action_invocations AS invocation SET status='failed', approval_decision=NULL,
|
|
5994
|
+
approval_expiry=jsonb_build_object('expiresAt',approval_expires_at,'expiredAt',$1::timestamptz),
|
|
5995
|
+
error='Approval expired before a decision was applied.', lease_owner=NULL, lease_expires_at=NULL,
|
|
5996
|
+
finalization_checkpoint=CASE WHEN finalization_checkpoint ? 'adapterPreparation' OR finalization_checkpoint->>'protocol'='atomic-after-adapters-v2'
|
|
5997
|
+
THEN (finalization_checkpoint - 'adapterPreparation') || '{"protocol":"atomic-after-adapters-v1"}'::jsonb ELSE finalization_checkpoint END,
|
|
5998
|
+
updated_at=$1 FROM due WHERE invocation.id=due.id RETURNING invocation.*`, [
|
|
5999
|
+
input.now,
|
|
6000
|
+
input.tenantId ?? null,
|
|
6001
|
+
input.spaceId ?? null,
|
|
6002
|
+
limit
|
|
6003
|
+
])).rows.map(toActionRecord);
|
|
6004
|
+
const candidates = await this.sql.query(`SELECT * FROM fabric_platform.action_invocations WHERE status='waiting_for_approval' AND approval_expires_at <= $1
|
|
6005
|
+
AND ($2::text IS NULL OR tenant_id=$2) AND ($3::text IS NULL OR space_id=$3)
|
|
6006
|
+
ORDER BY approval_expires_at,id LIMIT $4`, [
|
|
6007
|
+
input.now,
|
|
6008
|
+
input.tenantId ?? null,
|
|
6009
|
+
input.spaceId ?? null,
|
|
6010
|
+
limit
|
|
6011
|
+
]);
|
|
6012
|
+
return this.transactionWithEvents(async (tx) => {
|
|
6013
|
+
if (!tx.getActionInvocationForUpdate) throw new Error("Approval expiry requires locking invocation reads.");
|
|
6014
|
+
for (const row of candidates.rows) await tx.getActionInvocationForUpdate(String(row.id), String(row.tenant_id), String(row.space_id));
|
|
6015
|
+
const expired = [];
|
|
6016
|
+
for (const row of candidates.rows) {
|
|
6017
|
+
const record = toActionRecord(row);
|
|
6018
|
+
const transition = await applyApprovalTransition(tx, {
|
|
6019
|
+
...input,
|
|
6020
|
+
actionInvocationId: record.id,
|
|
6021
|
+
tenantId: record.tenantId,
|
|
6022
|
+
spaceId: record.spaceId
|
|
6023
|
+
}, "expired");
|
|
6024
|
+
if (transition.applied) expired.push(transition.invocation);
|
|
6025
|
+
}
|
|
6026
|
+
return expired;
|
|
6027
|
+
});
|
|
5590
6028
|
}
|
|
5591
6029
|
async beginApprovalDecision(input) {
|
|
6030
|
+
if (this.transactionWithEvents) return this.transactionWithEvents((tx) => applyApprovalTransition(tx, input, "decided"));
|
|
6031
|
+
if (input.lifecycleEvent) throw new Error("Approval events require transactional approval support.");
|
|
5592
6032
|
const result = await this.sql.query(`UPDATE fabric_platform.action_invocations SET
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
6033
|
+
approval_expiry=CASE WHEN approval_expires_at <= $9 THEN jsonb_build_object('expiresAt',approval_expires_at,'expiredAt',$9::timestamptz) ELSE approval_expiry END,
|
|
6034
|
+
approval_decision=CASE WHEN approval_expires_at <= $9 THEN NULL ELSE $4::jsonb END,
|
|
6035
|
+
status=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN 'running' ELSE 'failed' END,
|
|
6036
|
+
finalization_checkpoint=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN finalization_checkpoint
|
|
6037
|
+
WHEN finalization_checkpoint ? 'adapterPreparation' OR finalization_checkpoint->>'protocol'='atomic-after-adapters-v2'
|
|
6038
|
+
THEN (finalization_checkpoint - 'adapterPreparation') || '{"protocol":"atomic-after-adapters-v1"}'::jsonb
|
|
6039
|
+
ELSE finalization_checkpoint END,
|
|
6040
|
+
error=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN NULL WHEN approval_expires_at <= $9 THEN 'Approval expired before a decision was applied.' ELSE $6 END,
|
|
6041
|
+
lease_owner=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN $7 ELSE NULL END,
|
|
6042
|
+
lease_expires_at=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN $8::timestamptz ELSE NULL END,
|
|
6043
|
+
lease_token=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN lease_token+1 ELSE lease_token END,
|
|
6044
|
+
attempt_count=CASE WHEN $5::boolean AND (approval_expires_at IS NULL OR approval_expires_at > $9) THEN GREATEST(attempt_count,1) ELSE attempt_count END,
|
|
5600
6045
|
updated_at=$9
|
|
5601
6046
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='waiting_for_approval'
|
|
5602
6047
|
RETURNING *`, [
|
|
@@ -6322,7 +6767,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
6322
6767
|
}
|
|
6323
6768
|
async updateLeasedActionInvocation(input) {
|
|
6324
6769
|
const patch = input.patch;
|
|
6770
|
+
const approvalFields = approvalPatchSql(patch, 20 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0) + (Object.hasOwn(patch, "finalizationCheckpoint") ? 1 : 0));
|
|
6325
6771
|
return (await this.sql.query(`UPDATE fabric_platform.action_invocations SET
|
|
6772
|
+
${approvalFields.sql}
|
|
6326
6773
|
status=COALESCE($6,status), result=COALESCE($7::jsonb,result),
|
|
6327
6774
|
error=CASE WHEN $8::boolean THEN $9 ELSE error END,
|
|
6328
6775
|
authorization_reconciliation=CASE WHEN $10::boolean THEN $11::jsonb ELSE authorization_reconciliation END,
|
|
@@ -6333,10 +6780,10 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
6333
6780
|
${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${20 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
|
|
6334
6781
|
${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $20::boolean THEN $21::integer ELSE deferred_before_effect_attempt END," : ""}
|
|
6335
6782
|
pending_state_transition=CASE WHEN $18::boolean THEN $19::jsonb ELSE pending_state_transition END,
|
|
6336
|
-
lease_owner=CASE WHEN ($15::jsonb IS NOT NULL AND $6='running') OR $6 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
6337
|
-
THEN NULL ELSE lease_owner END
|
|
6338
|
-
lease_expires_at=CASE WHEN ($15::jsonb IS NOT NULL AND $6='running') OR $6 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
6339
|
-
THEN NULL ELSE lease_expires_at END
|
|
6783
|
+
${Object.hasOwn(patch, "leaseOwner") ? "" : `lease_owner=CASE WHEN ($15::jsonb IS NOT NULL AND $6='running') OR $6 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
6784
|
+
THEN NULL ELSE lease_owner END,`}
|
|
6785
|
+
${Object.hasOwn(patch, "leaseExpiresAt") ? "" : `lease_expires_at=CASE WHEN ($15::jsonb IS NOT NULL AND $6='running') OR $6 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
6786
|
+
THEN NULL ELSE lease_expires_at END,`}
|
|
6340
6787
|
updated_at=now()
|
|
6341
6788
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3
|
|
6342
6789
|
AND status='running' AND lease_owner=$4 AND lease_token=$5
|
|
@@ -6361,7 +6808,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
6361
6808
|
Object.hasOwn(patch, "pendingStateTransition"),
|
|
6362
6809
|
patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
|
|
6363
6810
|
...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
|
|
6364
|
-
...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
|
|
6811
|
+
...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : [],
|
|
6812
|
+
...approvalFields.values
|
|
6365
6813
|
])).rows.length === 1;
|
|
6366
6814
|
}
|
|
6367
6815
|
async renewActionInvocationLease(input) {
|
|
@@ -6471,10 +6919,16 @@ function toActionRecord(row) {
|
|
|
6471
6919
|
...row.hitl_risk_tier ? { hitlRiskTier: String(row.hitl_risk_tier) } : {},
|
|
6472
6920
|
...row.hitl_reason ? { hitlReason: String(row.hitl_reason) } : {},
|
|
6473
6921
|
...row.hitl_policy_version ? { hitlPolicyVersion: String(row.hitl_policy_version) } : {},
|
|
6922
|
+
...row.approval_expires_at ? { approvalExpiresAt: new Date(row.approval_expires_at) } : {},
|
|
6923
|
+
...row.approval_expiry ? { approvalExpiry: {
|
|
6924
|
+
expiresAt: new Date(row.approval_expiry.expiresAt),
|
|
6925
|
+
expiredAt: new Date(row.approval_expiry.expiredAt)
|
|
6926
|
+
} } : {},
|
|
6474
6927
|
...row.approval_decision ? { approvalDecision: toApprovalDecision(row.approval_decision) } : {},
|
|
6475
6928
|
...row.mutation_footprint ? { mutationFootprint: row.mutation_footprint } : {},
|
|
6476
6929
|
...row.execution_principal ? { executionPrincipal: row.execution_principal } : {},
|
|
6477
6930
|
...row.runtime_evidence ? { runtimeEvidence: row.runtime_evidence } : {},
|
|
6931
|
+
...row.agent_proposal != null ? { agentProposal: row.agent_proposal } : {},
|
|
6478
6932
|
...row.admitted_at ? { admittedAt: new Date(row.admitted_at) } : {},
|
|
6479
6933
|
...row.error ? { error: String(row.error) } : {},
|
|
6480
6934
|
createdAt: new Date(row.created_at),
|
|
@@ -6536,6 +6990,7 @@ function toAttestationRecord(row) {
|
|
|
6536
6990
|
function toApprovalDecision(value) {
|
|
6537
6991
|
const decision = value;
|
|
6538
6992
|
return {
|
|
6993
|
+
...decision.evidence !== void 0 ? { evidence: structuredClone(decision.evidence) } : {},
|
|
6539
6994
|
approved: Boolean(decision.approved),
|
|
6540
6995
|
approverId: String(decision.approverId),
|
|
6541
6996
|
approverType: String(decision.approverType),
|
|
@@ -6629,6 +7084,30 @@ function toOutboxRequeueRecord(row, prefix) {
|
|
|
6629
7084
|
priorLeaseToken: Number(field("prior_lease_token"))
|
|
6630
7085
|
};
|
|
6631
7086
|
}
|
|
7087
|
+
/** Additional approval-only patch bindings; omitted fields do not touch older columns. */
|
|
7088
|
+
function approvalPatchSql(patch, start) {
|
|
7089
|
+
const columns = {
|
|
7090
|
+
approvalExpiresAt: "approval_expires_at",
|
|
7091
|
+
approvalExpiry: "approval_expiry",
|
|
7092
|
+
approvalDecision: "approval_decision",
|
|
7093
|
+
leaseOwner: "lease_owner",
|
|
7094
|
+
leaseExpiresAt: "lease_expires_at",
|
|
7095
|
+
leaseToken: "lease_token",
|
|
7096
|
+
attemptCount: "attempt_count"
|
|
7097
|
+
};
|
|
7098
|
+
const values = [];
|
|
7099
|
+
const assignments = [];
|
|
7100
|
+
for (const key of Object.keys(columns)) {
|
|
7101
|
+
if (!Object.hasOwn(patch, key)) continue;
|
|
7102
|
+
const json = key === "approvalExpiry" || key === "approvalDecision";
|
|
7103
|
+
values.push(patch[key] === void 0 ? null : json ? JSON.stringify(patch[key]) : patch[key]);
|
|
7104
|
+
assignments.push(`${columns[key]}=$${start + values.length - 1}${json ? "::jsonb" : ""},`);
|
|
7105
|
+
}
|
|
7106
|
+
return {
|
|
7107
|
+
sql: assignments.join("\n"),
|
|
7108
|
+
values
|
|
7109
|
+
};
|
|
7110
|
+
}
|
|
6632
7111
|
//#endregion
|
|
6633
7112
|
//#region src/worker.ts
|
|
6634
7113
|
const DEFAULT_BATCH_SIZE = 10;
|
|
@@ -6670,6 +7149,16 @@ async function runPlatformActionWorkerCycle(options) {
|
|
|
6670
7149
|
} catch (error) {
|
|
6671
7150
|
options.onError?.(error);
|
|
6672
7151
|
}
|
|
7152
|
+
let expiredApprovals = 0;
|
|
7153
|
+
if (typeof options.host.expireDueApprovals === "function") try {
|
|
7154
|
+
expiredApprovals = (await options.host.expireDueApprovals({
|
|
7155
|
+
...options.tenantId !== void 0 ? { tenantId: options.tenantId } : {},
|
|
7156
|
+
...options.spaceId !== void 0 ? { spaceId: options.spaceId } : {},
|
|
7157
|
+
limit: options.batchSize ?? DEFAULT_BATCH_SIZE
|
|
7158
|
+
})).expired.length;
|
|
7159
|
+
} catch (error) {
|
|
7160
|
+
options.onError?.(error);
|
|
7161
|
+
}
|
|
6673
7162
|
let claimed;
|
|
6674
7163
|
try {
|
|
6675
7164
|
claimed = await options.store.claimActionInvocations({
|
|
@@ -6718,6 +7207,7 @@ async function runPlatformActionWorkerCycle(options) {
|
|
|
6718
7207
|
claimed: claimed.length,
|
|
6719
7208
|
completed,
|
|
6720
7209
|
failed,
|
|
7210
|
+
...expiredApprovals > 0 ? { expiredApprovals } : {},
|
|
6721
7211
|
...waitingForApproval > 0 ? { waitingForApproval } : {},
|
|
6722
7212
|
...deferred > 0 ? { deferred } : {},
|
|
6723
7213
|
...staged && Object.values(staged).some((value) => value > 0) ? { staged } : {}
|
|
@@ -6726,6 +7216,7 @@ async function runPlatformActionWorkerCycle(options) {
|
|
|
6726
7216
|
claimed: result.claimed,
|
|
6727
7217
|
completed: result.completed,
|
|
6728
7218
|
failed: result.failed,
|
|
7219
|
+
...result.expiredApprovals !== void 0 ? { expiredApprovals: result.expiredApprovals } : {},
|
|
6729
7220
|
...result.waitingForApproval !== void 0 ? { waitingForApproval: result.waitingForApproval } : {},
|
|
6730
7221
|
...result.deferred !== void 0 ? { deferred: result.deferred } : {},
|
|
6731
7222
|
...result.staged ? {
|
|
@@ -6855,7 +7346,7 @@ function createDurableSagaParentLifecycle(options) {
|
|
|
6855
7346
|
correlationId: input.parentInvocationId,
|
|
6856
7347
|
causationId: lifecycleId
|
|
6857
7348
|
});
|
|
6858
|
-
if (patch) await transaction.updateActionInvocation(input.parentInvocationId, input.tenantId, input.spaceId, patch);
|
|
7349
|
+
if (patch) await transaction.updateActionInvocation(input.parentInvocationId, input.tenantId, input.spaceId, finalizationSettlementPatch(parent, patch));
|
|
6859
7350
|
});
|
|
6860
7351
|
}
|
|
6861
7352
|
return {
|
|
@@ -7934,6 +8425,6 @@ async function runEventAppendChecks(subject) {
|
|
|
7934
8425
|
};
|
|
7935
8426
|
}
|
|
7936
8427
|
//#endregion
|
|
7937
|
-
export { ActionAdmissionError, ExternalCompletionError, IdempotencyConflictError, MAX_STAGED_INVOCATIONS, MAX_STAGED_INVOCATION_BATCH_BYTES, MAX_STAGED_INVOCATION_BYTES, MAX_STAGED_INVOCATION_KEY_LENGTH, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, STAGED_INVOCATION_KEY_PATTERN, SubmissionRefusalError, applyPostgresMigrations, asStagedInvocationStore, canonicalJson, cloneOutboxRecord, cloneStagedInvocationRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, eventAppendChecks, getPlatformHostHealthSnapshot, outboxActorDisclosure, prepareStagedInvocationBatch, runEventAppendChecks, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, stagedInvocationRequestIdentity, tenantIsolationChecks, toEnterpriseEventEnvelope };
|
|
8428
|
+
export { APPROVAL_DECIDED_EVENT, APPROVAL_EXPIRED_EVENT, APPROVAL_REQUESTED_EVENT, ActionAdmissionError, ExternalCompletionError, IdempotencyConflictError, MAX_APPROVAL_EVIDENCE_BYTES, MAX_STAGED_INVOCATIONS, MAX_STAGED_INVOCATION_BATCH_BYTES, MAX_STAGED_INVOCATION_BYTES, MAX_STAGED_INVOCATION_KEY_LENGTH, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PLATFORM_HOST_HEALTH_CONTRACT_VERSION, PLATFORM_HOST_METRIC_NAMES, PostgresPlatformHostStore, STAGED_INVOCATION_KEY_PATTERN, SubmissionRefusalError, applyPostgresMigrations, asStagedInvocationStore, canonicalJson, cloneOutboxRecord, cloneStagedInvocationRecord, createDurableSagaParentLifecycle, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, emitPlatformHostTelemetry, eventAppendChecks, getPlatformHostHealthSnapshot, outboxActorDisclosure, prepareStagedInvocationBatch, runEventAppendChecks, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, runTenantIsolationChecks, stagedInvocationRequestIdentity, tenantIsolationChecks, toEnterpriseEventEnvelope };
|
|
7938
8429
|
|
|
7939
8430
|
//# sourceMappingURL=index.js.map
|