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