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