@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/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
- let util = require("util");
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 !== "atomic-after-adapters-v1" || 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.");
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
- await hostOperation(() => persistExecutionInvocation(invocation, { status: "waiting_for_approval" }));
2206
- return {
2207
- ...actionResult(invocation),
2208
- status: "waiting_for_approval"
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 ? { finalizationProtocol: "atomic-after-adapters-v1" } : {}
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) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
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
- async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1, transaction) {
3530
- const timestamp = now();
3531
- const envelope = {
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: await (transaction ?? options.store).nextEventSequence(invocation.tenantId, invocation.spaceId),
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 traceContext = options.outbox.traceContext?.(envelope);
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
- const record = await this.getActionInvocation(input.actionInvocationId, input.tenantId, input.spaceId);
4583
- if (!record || record.status !== "waiting_for_approval") return {
4584
- applied: false,
4585
- ...record ? { invocation: record } : {}
4586
- };
4587
- record.approvalDecision = input.decision;
4588
- record.updatedAt = input.now;
4589
- if (!input.decision.approved) {
4590
- record.status = "failed";
4591
- record.error = `Approval rejected: ${input.decision.reason ?? "no reason provided"}`;
4592
- delete record.leaseOwner;
4593
- delete record.leaseExpiresAt;
4594
- return {
4595
- applied: true,
4596
- invocation: record
4597
- };
4598
- }
4599
- record.status = "running";
4600
- record.error = void 0;
4601
- record.leaseOwner = input.leaseOwner;
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 (!row) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
5590
- return toActionRecord(row);
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
- approval_decision=$4::jsonb,
5595
- status=CASE WHEN $5::boolean THEN 'running' ELSE 'failed' END,
5596
- error=CASE WHEN $5::boolean THEN NULL ELSE $6 END,
5597
- lease_owner=CASE WHEN $5::boolean THEN $7 ELSE NULL END,
5598
- lease_expires_at=CASE WHEN $5::boolean THEN $8::timestamptz ELSE NULL END,
5599
- lease_token=CASE WHEN $5::boolean THEN lease_token+1 ELSE lease_token END,
5600
- attempt_count=CASE WHEN $5::boolean THEN GREATEST(attempt_count,1) ELSE attempt_count END,
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;