@fabricorg/platform-host 0.6.0 → 0.7.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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ActorType, ActionId, ActionStatus, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope, ExecutionAttestation, ExternalReconciliation, PolicyObligation, PolicyObligationStatus, ActionDefinition, AdapterImplementation, FabricRuntimeServices, AgentActionPolicyEvaluator, MutationGovernanceResolver, RuntimePolicyDefinition } from '@fabricorg/platform';
2
2
 
3
3
  /** Durable Host lifecycle generation, independent from the npm package version. */
4
- declare const PLATFORM_HOST_CONTRACT_VERSION: 1;
4
+ declare const PLATFORM_HOST_CONTRACT_VERSION: 2;
5
5
  interface PendingAssetEvent {
6
6
  eventType: string;
7
7
  subjectType: string;
@@ -23,6 +23,8 @@ interface ActionInvocationRecord {
23
23
  correlationId: string;
24
24
  causationId?: string;
25
25
  idempotencyKey?: string;
26
+ /** Opaque, non-secret reference used to revalidate delegated execution authority. */
27
+ authorizationBindingId?: string;
26
28
  attemptCount: number;
27
29
  leaseOwner?: string;
28
30
  leaseExpiresAt?: Date;
@@ -118,6 +120,31 @@ interface PlatformHostStore<TDb> {
118
120
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
119
121
  getEntityState(tenantId: string, spaceId: string, entityType: string, entityId: string): Promise<string | undefined>;
120
122
  }
123
+ /**
124
+ * Transaction-scoped Host operations used to commit domain writes and their
125
+ * canonical events as one unit of work.
126
+ *
127
+ * The supplied `db` must be bound to the same database transaction as every
128
+ * lifecycle method on this object. Implementations must roll the whole unit
129
+ * back when `run` throws.
130
+ */
131
+ interface PlatformHostMutationTransaction<TDb> {
132
+ readonly db: TDb;
133
+ appendEvent(event: AssetEventEnvelope): Promise<void>;
134
+ nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
135
+ listEvents(tenantId: string, spaceId: string): Promise<AssetEventEnvelope[]>;
136
+ updateActionInvocation(id: string, tenantId: string, spaceId: string, patch: Partial<Pick<ActionInvocationRecord, "status" | "result" | "error">>): Promise<void>;
137
+ }
138
+ /**
139
+ * Optional production capability for atomic domain/event persistence.
140
+ *
141
+ * A mutating Host action uses this capability when available. Stores that do
142
+ * not implement it retain the legacy transaction boundary for compatibility,
143
+ * but cannot claim atomic domain-write/event persistence.
144
+ */
145
+ interface AtomicMutationPlatformHostStore<TDb> extends PlatformHostStore<TDb> {
146
+ transactionWithEvents<TResult>(run: (transaction: PlatformHostMutationTransaction<TDb>) => Promise<TResult>): Promise<TResult>;
147
+ }
121
148
  interface BeginApprovalDecisionInput {
122
149
  actionInvocationId: string;
123
150
  tenantId: string;
@@ -180,9 +207,27 @@ interface ActionAuthorizationInput {
180
207
  actorId: string;
181
208
  actorType: ActorType;
182
209
  }
210
+ type ActionExecutionReason = "initial" | "approval_resume" | "recovery";
211
+ /**
212
+ * Trusted execution-boundary authorization input.
213
+ *
214
+ * Parameters are the schema-parsed durable parameters, never caller-supplied
215
+ * transient values. `invocation` is the canonical record that will execute.
216
+ */
217
+ interface ActionExecutionAuthorizationInput extends ActionAuthorizationInput {
218
+ actionInvocationId: string;
219
+ parameters: unknown;
220
+ invocation: Readonly<ActionInvocationRecord>;
221
+ executionReason: ActionExecutionReason;
222
+ }
183
223
  interface PlatformHostAuthorization {
184
224
  checkEntitlement(input: ActionAuthorizationInput): Promise<boolean>;
185
225
  authorize(input: ActionAuthorizationInput): Promise<boolean>;
226
+ /**
227
+ * Optional final authorization immediately before policies and mutation
228
+ * execution. When absent, the Host reuses `authorize` at this boundary.
229
+ */
230
+ authorizeExecution?(input: ActionExecutionAuthorizationInput): Promise<boolean>;
186
231
  /** Optional distinct authorization boundary for human/system approval decisions. */
187
232
  authorizeApproval?(input: ActionAuthorizationInput & {
188
233
  decision: ActionApprovalDecision;
@@ -212,6 +257,8 @@ interface SubmitActionInput {
212
257
  causationId?: string;
213
258
  /** Stable logical command key. Reusing it returns/re-dispatches the original invocation. */
214
259
  idempotencyKey?: string;
260
+ /** Opaque, non-secret authorization/admission reference persisted with the invocation. */
261
+ authorizationBindingId?: string;
215
262
  }
216
263
  interface SubmitActionResult {
217
264
  actionInvocationId: string;
@@ -357,11 +404,26 @@ interface PlatformHostSqlResult<Row = Record<string, unknown>> {
357
404
  interface PlatformHostSqlClient {
358
405
  query<Row = Record<string, unknown>>(sql: string, values?: unknown[]): Promise<PlatformHostSqlResult<Row>>;
359
406
  }
407
+ interface PostgresPlatformHostTransactionContext<TDb> {
408
+ db: TDb;
409
+ sql: PlatformHostSqlClient;
410
+ }
411
+ /**
412
+ * Application-owned transaction binder.
413
+ *
414
+ * Platform Host cannot infer how an application's `TDb` is rebound to a
415
+ * `pg.PoolClient`. Production applications provide this adapter so domain
416
+ * methods and Host ledger writes share one PostgreSQL transaction.
417
+ */
418
+ interface PostgresPlatformHostTransactionProvider<TDb> {
419
+ run<TResult>(run: (context: PostgresPlatformHostTransactionContext<TDb>) => Promise<TResult>): Promise<TResult>;
420
+ }
360
421
  /** Durable mutation-pipeline ledger for Databricks Lakebase or standard Postgres. */
361
422
  declare class PostgresPlatformHostStore<TDb> implements RecoverablePlatformHostStore<TDb>, ApprovalPlatformHostStore<TDb>, GovernancePlatformHostStore<TDb> {
362
423
  readonly db: TDb;
363
424
  private readonly sql;
364
- constructor(db: TDb, sql: PlatformHostSqlClient);
425
+ readonly transactionWithEvents?: AtomicMutationPlatformHostStore<TDb>["transactionWithEvents"];
426
+ constructor(db: TDb, sql: PlatformHostSqlClient, transactionProvider?: PostgresPlatformHostTransactionProvider<TDb>);
365
427
  ensureSchema(): Promise<void>;
366
428
  transaction<TResult>(run: (db: TDb) => Promise<TResult>): Promise<TResult>;
367
429
  createActionInvocation(input: CreateActionInvocationInput): Promise<ActionInvocationRecord>;
@@ -401,4 +463,4 @@ declare function runPlatformActionWorkerCycle<TDb>(options: PlatformActionWorker
401
463
  /** Poll until aborted. Each iteration is bounded and waits without busy-spinning. */
402
464
  declare function runPlatformActionWorker<TDb>(options: PlatformActionWorkerOptions<TDb>): Promise<void>;
403
465
 
404
- export { type ActionApprovalDecision, type ActionAuthorizationInput, type ActionInvocationRecord, type AdapterInvocationRecord, type AdapterInvocationStatus, type ApprovalDecisionTransitionResult, type ApprovalPlatformHostStore, type BeginApprovalDecisionInput, type ClaimActionInvocationsInput, type CreateActionInvocationInput, type DispatchActionInput, type DispatchActionResult, type ExecuteActionResult, type ExecuteInvocationOptions, type ExecutionAttestationRecord, type ExternalReconciliationRecord, type GovernancePlatformHostStore, type GovernedActionHost, type HitlDecisionEvidence, type ListActionInvocationsInput, MemoryPlatformHostStore, PLATFORM_HOST_CONTRACT_VERSION, type PendingAssetEvent, type PersistedActionApprovalDecision, type PlatformActionDispatcher, type PlatformActionWorkerCycleResult, type PlatformActionWorkerOptions, type PlatformHostAuthorization, type PlatformHostOptions, type PlatformHostSqlClient, type PlatformHostSqlResult, type PlatformHostStore, type PolicyEvaluationRecord, type PolicyObligationRecord, PostgresPlatformHostStore, type RecoverablePlatformHostStore, type SubmitActionInput, type SubmitActionResult, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
466
+ export { type ActionApprovalDecision, type ActionAuthorizationInput, type ActionExecutionAuthorizationInput, type ActionExecutionReason, type ActionInvocationRecord, type AdapterInvocationRecord, type AdapterInvocationStatus, type ApprovalDecisionTransitionResult, type ApprovalPlatformHostStore, type AtomicMutationPlatformHostStore, type BeginApprovalDecisionInput, type ClaimActionInvocationsInput, type CreateActionInvocationInput, type DispatchActionInput, type DispatchActionResult, type ExecuteActionResult, type ExecuteInvocationOptions, type ExecutionAttestationRecord, type ExternalReconciliationRecord, type GovernancePlatformHostStore, type GovernedActionHost, type HitlDecisionEvidence, type ListActionInvocationsInput, MemoryPlatformHostStore, PLATFORM_HOST_CONTRACT_VERSION, type PendingAssetEvent, type PersistedActionApprovalDecision, type PlatformActionDispatcher, type PlatformActionWorkerCycleResult, type PlatformActionWorkerOptions, type PlatformHostAuthorization, type PlatformHostMutationTransaction, type PlatformHostOptions, type PlatformHostSqlClient, type PlatformHostSqlResult, type PlatformHostStore, type PolicyEvaluationRecord, type PolicyObligationRecord, PostgresPlatformHostStore, type PostgresPlatformHostTransactionContext, type PostgresPlatformHostTransactionProvider, type RecoverablePlatformHostStore, type SubmitActionInput, type SubmitActionResult, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ActorType, ActionId, ActionStatus, AgentActionRoute, AgentActionRiskTier, MutationFootprint, ExecutionPrincipal, GovernanceRuntimeEvidence, PolicyOutcome, AssetEventEnvelope, ExecutionAttestation, ExternalReconciliation, PolicyObligation, PolicyObligationStatus, ActionDefinition, AdapterImplementation, FabricRuntimeServices, AgentActionPolicyEvaluator, MutationGovernanceResolver, RuntimePolicyDefinition } from '@fabricorg/platform';
2
2
 
3
3
  /** Durable Host lifecycle generation, independent from the npm package version. */
4
- declare const PLATFORM_HOST_CONTRACT_VERSION: 1;
4
+ declare const PLATFORM_HOST_CONTRACT_VERSION: 2;
5
5
  interface PendingAssetEvent {
6
6
  eventType: string;
7
7
  subjectType: string;
@@ -23,6 +23,8 @@ interface ActionInvocationRecord {
23
23
  correlationId: string;
24
24
  causationId?: string;
25
25
  idempotencyKey?: string;
26
+ /** Opaque, non-secret reference used to revalidate delegated execution authority. */
27
+ authorizationBindingId?: string;
26
28
  attemptCount: number;
27
29
  leaseOwner?: string;
28
30
  leaseExpiresAt?: Date;
@@ -118,6 +120,31 @@ interface PlatformHostStore<TDb> {
118
120
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
119
121
  getEntityState(tenantId: string, spaceId: string, entityType: string, entityId: string): Promise<string | undefined>;
120
122
  }
123
+ /**
124
+ * Transaction-scoped Host operations used to commit domain writes and their
125
+ * canonical events as one unit of work.
126
+ *
127
+ * The supplied `db` must be bound to the same database transaction as every
128
+ * lifecycle method on this object. Implementations must roll the whole unit
129
+ * back when `run` throws.
130
+ */
131
+ interface PlatformHostMutationTransaction<TDb> {
132
+ readonly db: TDb;
133
+ appendEvent(event: AssetEventEnvelope): Promise<void>;
134
+ nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
135
+ listEvents(tenantId: string, spaceId: string): Promise<AssetEventEnvelope[]>;
136
+ updateActionInvocation(id: string, tenantId: string, spaceId: string, patch: Partial<Pick<ActionInvocationRecord, "status" | "result" | "error">>): Promise<void>;
137
+ }
138
+ /**
139
+ * Optional production capability for atomic domain/event persistence.
140
+ *
141
+ * A mutating Host action uses this capability when available. Stores that do
142
+ * not implement it retain the legacy transaction boundary for compatibility,
143
+ * but cannot claim atomic domain-write/event persistence.
144
+ */
145
+ interface AtomicMutationPlatformHostStore<TDb> extends PlatformHostStore<TDb> {
146
+ transactionWithEvents<TResult>(run: (transaction: PlatformHostMutationTransaction<TDb>) => Promise<TResult>): Promise<TResult>;
147
+ }
121
148
  interface BeginApprovalDecisionInput {
122
149
  actionInvocationId: string;
123
150
  tenantId: string;
@@ -180,9 +207,27 @@ interface ActionAuthorizationInput {
180
207
  actorId: string;
181
208
  actorType: ActorType;
182
209
  }
210
+ type ActionExecutionReason = "initial" | "approval_resume" | "recovery";
211
+ /**
212
+ * Trusted execution-boundary authorization input.
213
+ *
214
+ * Parameters are the schema-parsed durable parameters, never caller-supplied
215
+ * transient values. `invocation` is the canonical record that will execute.
216
+ */
217
+ interface ActionExecutionAuthorizationInput extends ActionAuthorizationInput {
218
+ actionInvocationId: string;
219
+ parameters: unknown;
220
+ invocation: Readonly<ActionInvocationRecord>;
221
+ executionReason: ActionExecutionReason;
222
+ }
183
223
  interface PlatformHostAuthorization {
184
224
  checkEntitlement(input: ActionAuthorizationInput): Promise<boolean>;
185
225
  authorize(input: ActionAuthorizationInput): Promise<boolean>;
226
+ /**
227
+ * Optional final authorization immediately before policies and mutation
228
+ * execution. When absent, the Host reuses `authorize` at this boundary.
229
+ */
230
+ authorizeExecution?(input: ActionExecutionAuthorizationInput): Promise<boolean>;
186
231
  /** Optional distinct authorization boundary for human/system approval decisions. */
187
232
  authorizeApproval?(input: ActionAuthorizationInput & {
188
233
  decision: ActionApprovalDecision;
@@ -212,6 +257,8 @@ interface SubmitActionInput {
212
257
  causationId?: string;
213
258
  /** Stable logical command key. Reusing it returns/re-dispatches the original invocation. */
214
259
  idempotencyKey?: string;
260
+ /** Opaque, non-secret authorization/admission reference persisted with the invocation. */
261
+ authorizationBindingId?: string;
215
262
  }
216
263
  interface SubmitActionResult {
217
264
  actionInvocationId: string;
@@ -357,11 +404,26 @@ interface PlatformHostSqlResult<Row = Record<string, unknown>> {
357
404
  interface PlatformHostSqlClient {
358
405
  query<Row = Record<string, unknown>>(sql: string, values?: unknown[]): Promise<PlatformHostSqlResult<Row>>;
359
406
  }
407
+ interface PostgresPlatformHostTransactionContext<TDb> {
408
+ db: TDb;
409
+ sql: PlatformHostSqlClient;
410
+ }
411
+ /**
412
+ * Application-owned transaction binder.
413
+ *
414
+ * Platform Host cannot infer how an application's `TDb` is rebound to a
415
+ * `pg.PoolClient`. Production applications provide this adapter so domain
416
+ * methods and Host ledger writes share one PostgreSQL transaction.
417
+ */
418
+ interface PostgresPlatformHostTransactionProvider<TDb> {
419
+ run<TResult>(run: (context: PostgresPlatformHostTransactionContext<TDb>) => Promise<TResult>): Promise<TResult>;
420
+ }
360
421
  /** Durable mutation-pipeline ledger for Databricks Lakebase or standard Postgres. */
361
422
  declare class PostgresPlatformHostStore<TDb> implements RecoverablePlatformHostStore<TDb>, ApprovalPlatformHostStore<TDb>, GovernancePlatformHostStore<TDb> {
362
423
  readonly db: TDb;
363
424
  private readonly sql;
364
- constructor(db: TDb, sql: PlatformHostSqlClient);
425
+ readonly transactionWithEvents?: AtomicMutationPlatformHostStore<TDb>["transactionWithEvents"];
426
+ constructor(db: TDb, sql: PlatformHostSqlClient, transactionProvider?: PostgresPlatformHostTransactionProvider<TDb>);
365
427
  ensureSchema(): Promise<void>;
366
428
  transaction<TResult>(run: (db: TDb) => Promise<TResult>): Promise<TResult>;
367
429
  createActionInvocation(input: CreateActionInvocationInput): Promise<ActionInvocationRecord>;
@@ -401,4 +463,4 @@ declare function runPlatformActionWorkerCycle<TDb>(options: PlatformActionWorker
401
463
  /** Poll until aborted. Each iteration is bounded and waits without busy-spinning. */
402
464
  declare function runPlatformActionWorker<TDb>(options: PlatformActionWorkerOptions<TDb>): Promise<void>;
403
465
 
404
- export { type ActionApprovalDecision, type ActionAuthorizationInput, type ActionInvocationRecord, type AdapterInvocationRecord, type AdapterInvocationStatus, type ApprovalDecisionTransitionResult, type ApprovalPlatformHostStore, type BeginApprovalDecisionInput, type ClaimActionInvocationsInput, type CreateActionInvocationInput, type DispatchActionInput, type DispatchActionResult, type ExecuteActionResult, type ExecuteInvocationOptions, type ExecutionAttestationRecord, type ExternalReconciliationRecord, type GovernancePlatformHostStore, type GovernedActionHost, type HitlDecisionEvidence, type ListActionInvocationsInput, MemoryPlatformHostStore, PLATFORM_HOST_CONTRACT_VERSION, type PendingAssetEvent, type PersistedActionApprovalDecision, type PlatformActionDispatcher, type PlatformActionWorkerCycleResult, type PlatformActionWorkerOptions, type PlatformHostAuthorization, type PlatformHostOptions, type PlatformHostSqlClient, type PlatformHostSqlResult, type PlatformHostStore, type PolicyEvaluationRecord, type PolicyObligationRecord, PostgresPlatformHostStore, type RecoverablePlatformHostStore, type SubmitActionInput, type SubmitActionResult, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
466
+ export { type ActionApprovalDecision, type ActionAuthorizationInput, type ActionExecutionAuthorizationInput, type ActionExecutionReason, type ActionInvocationRecord, type AdapterInvocationRecord, type AdapterInvocationStatus, type ApprovalDecisionTransitionResult, type ApprovalPlatformHostStore, type AtomicMutationPlatformHostStore, type BeginApprovalDecisionInput, type ClaimActionInvocationsInput, type CreateActionInvocationInput, type DispatchActionInput, type DispatchActionResult, type ExecuteActionResult, type ExecuteInvocationOptions, type ExecutionAttestationRecord, type ExternalReconciliationRecord, type GovernancePlatformHostStore, type GovernedActionHost, type HitlDecisionEvidence, type ListActionInvocationsInput, MemoryPlatformHostStore, PLATFORM_HOST_CONTRACT_VERSION, type PendingAssetEvent, type PersistedActionApprovalDecision, type PlatformActionDispatcher, type PlatformActionWorkerCycleResult, type PlatformActionWorkerOptions, type PlatformHostAuthorization, type PlatformHostMutationTransaction, type PlatformHostOptions, type PlatformHostSqlClient, type PlatformHostSqlResult, type PlatformHostStore, type PolicyEvaluationRecord, type PolicyObligationRecord, PostgresPlatformHostStore, type PostgresPlatformHostTransactionContext, type PostgresPlatformHostTransactionProvider, type RecoverablePlatformHostStore, type SubmitActionInput, type SubmitActionResult, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
package/dist/index.js CHANGED
@@ -3,13 +3,16 @@ import { AdapterRegistry, resolveAction, createFabricId, assertGovernanceRuntime
3
3
  // src/host.ts
4
4
 
5
5
  // src/types.ts
6
- var PLATFORM_HOST_CONTRACT_VERSION = 1;
6
+ var PLATFORM_HOST_CONTRACT_VERSION = 2;
7
7
 
8
8
  // src/host.ts
9
9
  var DEFAULT_EXTRACT_EVENTS = (data) => {
10
10
  const value = data._events;
11
11
  return Array.isArray(value) ? value : [];
12
12
  };
13
+ var RecoverableFinalizationError = class extends Error {
14
+ name = "RecoverableFinalizationError";
15
+ };
13
16
  function createGovernedActionHost(options) {
14
17
  const adapters = new AdapterRegistry();
15
18
  for (const adapter of options.adapters ?? []) adapters.register(adapter);
@@ -50,7 +53,8 @@ function createGovernedActionHost(options) {
50
53
  runtimeEvidence,
51
54
  correlationId,
52
55
  ...input.causationId ? { causationId: input.causationId } : {},
53
- ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}
56
+ ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
57
+ ...input.authorizationBindingId ? { authorizationBindingId: input.authorizationBindingId } : {}
54
58
  });
55
59
  const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
56
60
  if (durableInvocation.id !== actionInvocationId && isTerminal(durableInvocation.status)) {
@@ -98,7 +102,7 @@ function createGovernedActionHost(options) {
98
102
  );
99
103
  return { ...executed, workflowId: durableWorkflowId };
100
104
  }
101
- async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}) {
105
+ async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}, executionReasonOverride) {
102
106
  const loadedInvocation = await options.store.getActionInvocation(
103
107
  actionInvocationId,
104
108
  tenantId,
@@ -107,6 +111,7 @@ function createGovernedActionHost(options) {
107
111
  if (!loadedInvocation) {
108
112
  throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
109
113
  }
114
+ const resumingRunningInvocation = loadedInvocation.status === "running";
110
115
  let invocation = loadedInvocation;
111
116
  if (isTerminal(invocation.status) || invocation.status === "waiting_for_approval") {
112
117
  return actionResult(invocation);
@@ -208,6 +213,28 @@ function createGovernedActionHost(options) {
208
213
  }
209
214
  }
210
215
  const authorizationInput = toAuthorizationInput(action, invocation);
216
+ const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : "initial");
217
+ if (!await options.authorization.checkEntitlement(authorizationInput)) {
218
+ return fail(
219
+ invocation,
220
+ "failed",
221
+ `Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
222
+ );
223
+ }
224
+ const executionAuthorized = options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
225
+ ...authorizationInput,
226
+ actionInvocationId,
227
+ parameters: parsed.data,
228
+ invocation,
229
+ executionReason
230
+ }) : await options.authorization.authorize(authorizationInput);
231
+ if (!executionAuthorized) {
232
+ return fail(
233
+ invocation,
234
+ "failed",
235
+ `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
236
+ );
237
+ }
211
238
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
212
239
  ...authorizationInput,
213
240
  declaredPolicyIds: action.policies ?? []
@@ -292,8 +319,8 @@ function createGovernedActionHost(options) {
292
319
  let data;
293
320
  let domainEvents = [];
294
321
  try {
295
- const handlerResult = await options.store.transaction(
296
- (db) => action.handler ? action.handler(
322
+ const runHandler = async (db, transaction) => {
323
+ const handlerResult = action.handler ? await action.handler(
297
324
  {
298
325
  actionInvocationId,
299
326
  tenantId,
@@ -306,28 +333,39 @@ function createGovernedActionHost(options) {
306
333
  services: options.services
307
334
  },
308
335
  parsed.data
309
- ) : Promise.resolve({ success: true, data: {} })
310
- );
311
- if (!handlerResult.success) {
312
- return fail(invocation, "failed", handlerResult.error ?? "Action handler failed");
313
- }
314
- data = handlerResult.data ?? {};
315
- domainEvents = extractEvents(data);
316
- const undeclaredEvent = domainEvents.find(
317
- (event) => !action.emitsEvents.includes(event.eventType)
318
- );
319
- if (undeclaredEvent) {
320
- return fail(
321
- invocation,
322
- "failed",
323
- `${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`
336
+ ) : { success: true, data: {} };
337
+ if (!handlerResult.success) {
338
+ throw new Error(handlerResult.error ?? "Action handler failed");
339
+ }
340
+ const handlerData = handlerResult.data ?? {};
341
+ const events = extractEvents(handlerData);
342
+ const undeclaredEvent = events.find(
343
+ (event) => !action.emitsEvents.includes(event.eventType)
324
344
  );
325
- }
326
- if (action.eventPhase !== "after_adapters") {
327
- for (const [index, event] of domainEvents.entries()) {
328
- await appendEvent(invocation, event, `domain:${index}`, action.version);
345
+ if (undeclaredEvent) {
346
+ throw new Error(
347
+ `${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`
348
+ );
329
349
  }
330
- }
350
+ if (action.eventPhase !== "after_adapters") {
351
+ for (const [index, event] of events.entries()) {
352
+ await appendEvent(
353
+ invocation,
354
+ event,
355
+ `domain:${index}`,
356
+ action.version,
357
+ transaction
358
+ );
359
+ }
360
+ }
361
+ return { data: handlerData, domainEvents: events };
362
+ };
363
+ const atomicStore = asAtomicMutationStore(options.store);
364
+ const executed = atomicStore ? await atomicStore.transactionWithEvents(
365
+ (transaction) => runHandler(transaction.db, transaction)
366
+ ) : await options.store.transaction((db) => runHandler(db));
367
+ data = executed.data;
368
+ domainEvents = executed.domainEvents;
331
369
  } catch (error) {
332
370
  return fail(invocation, "failed", errorMessage(error));
333
371
  }
@@ -458,11 +496,6 @@ function createGovernedActionHost(options) {
458
496
  return fail(invocation, "failed", message);
459
497
  }
460
498
  }
461
- if (action.eventPhase === "after_adapters") {
462
- for (const [index, event] of domainEvents.entries()) {
463
- await appendEvent(invocation, event, `domain:${index}`, action.version);
464
- }
465
- }
466
499
  const governanceStore = asGovernanceStore(options.store);
467
500
  if (governanceStore && options.resolvePolicyObligations) {
468
501
  const obligations = await governanceStore.listPolicyObligations(actionInvocationId, tenantId, spaceId);
@@ -472,10 +505,43 @@ function createGovernedActionHost(options) {
472
505
  }
473
506
  }
474
507
  const result = withoutPrivateHostFields(data, eventResultFields);
475
- await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
476
- status: "completed",
477
- result
478
- });
508
+ try {
509
+ const atomicStore = asAtomicMutationStore(options.store);
510
+ if (action.eventPhase === "after_adapters" && atomicStore) {
511
+ await atomicStore.transactionWithEvents(async (transaction) => {
512
+ for (const [index, event] of domainEvents.entries()) {
513
+ await appendEvent(
514
+ invocation,
515
+ event,
516
+ `domain:${index}`,
517
+ action.version,
518
+ transaction
519
+ );
520
+ }
521
+ await transaction.updateActionInvocation(
522
+ actionInvocationId,
523
+ tenantId,
524
+ spaceId,
525
+ { status: "completed", result }
526
+ );
527
+ });
528
+ } else {
529
+ if (action.eventPhase === "after_adapters") {
530
+ for (const [index, event] of domainEvents.entries()) {
531
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
532
+ }
533
+ }
534
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
535
+ status: "completed",
536
+ result
537
+ });
538
+ }
539
+ } catch (error) {
540
+ if (action.eventPhase === "after_adapters") {
541
+ throw new RecoverableFinalizationError(errorMessage(error));
542
+ }
543
+ throw error;
544
+ }
479
545
  return {
480
546
  actionInvocationId,
481
547
  status: "completed",
@@ -484,6 +550,7 @@ function createGovernedActionHost(options) {
484
550
  ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
485
551
  };
486
552
  } catch (error) {
553
+ if (error instanceof RecoverableFinalizationError) throw error;
487
554
  return fail(invocation, "failed", errorMessage(error));
488
555
  }
489
556
  }
@@ -548,7 +615,13 @@ function createGovernedActionHost(options) {
548
615
  return actionResult(transitioned);
549
616
  }
550
617
  if (!decision.approved) return actionResult(transitioned);
551
- return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
618
+ return executeInvocation(
619
+ actionInvocationId,
620
+ tenantId,
621
+ spaceId,
622
+ { leaseOwner },
623
+ "approval_resume"
624
+ );
552
625
  }
553
626
  async function recordExecutionAttestation(actionInvocationId, tenantId, spaceId, attestation, adapterInvocationId) {
554
627
  const governanceStore = asGovernanceStore(options.store);
@@ -582,7 +655,7 @@ function createGovernedActionHost(options) {
582
655
  spaceId
583
656
  });
584
657
  }
585
- async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1) {
658
+ async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1, transaction) {
586
659
  const timestamp = now();
587
660
  const envelope = {
588
661
  id: lifecycleId("evt", invocation.id, deduplicationKey),
@@ -596,7 +669,7 @@ function createGovernedActionHost(options) {
596
669
  actorType: invocation.actorType,
597
670
  actionInvocationId: invocation.id,
598
671
  payload: event.payload,
599
- sequence: await options.store.nextEventSequence(
672
+ sequence: await (transaction ?? options.store).nextEventSequence(
600
673
  invocation.tenantId,
601
674
  invocation.spaceId
602
675
  ),
@@ -605,7 +678,7 @@ function createGovernedActionHost(options) {
605
678
  correlationId: invocation.correlationId,
606
679
  ...invocation.causationId ? { causationId: invocation.causationId } : {}
607
680
  };
608
- await options.store.appendEvent(envelope);
681
+ await (transaction ?? options.store).appendEvent(envelope);
609
682
  }
610
683
  async function fail(invocation, status, error) {
611
684
  await options.store.updateActionInvocation(
@@ -638,6 +711,10 @@ function asApprovalStore(store) {
638
711
  const candidate = store;
639
712
  return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
640
713
  }
714
+ function asAtomicMutationStore(store) {
715
+ const candidate = store;
716
+ return typeof candidate.transactionWithEvents === "function" ? store : void 0;
717
+ }
641
718
  function asGovernanceStore(store) {
642
719
  const candidate = store;
643
720
  return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
@@ -864,13 +941,26 @@ var MemoryPlatformHostStore = class {
864
941
  };
865
942
 
866
943
  // src/postgres-store.ts
867
- var PostgresPlatformHostStore = class {
868
- constructor(db, sql) {
944
+ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
945
+ constructor(db, sql, transactionProvider) {
869
946
  this.db = db;
870
947
  this.sql = sql;
948
+ if (transactionProvider) {
949
+ this.transactionWithEvents = async (run) => transactionProvider.run(async ({ db: db2, sql: sql2 }) => {
950
+ const scoped = new _PostgresPlatformHostStore(db2, sql2);
951
+ return run({
952
+ db: db2,
953
+ appendEvent: (event) => scoped.appendEvent(event),
954
+ nextEventSequence: (tenantId, spaceId) => scoped.nextEventSequence(tenantId, spaceId),
955
+ listEvents: (tenantId, spaceId) => scoped.listEvents(tenantId, spaceId),
956
+ updateActionInvocation: (id, tenantId, spaceId, patch) => scoped.updateActionInvocation(id, tenantId, spaceId, patch)
957
+ });
958
+ });
959
+ }
871
960
  }
872
961
  db;
873
962
  sql;
963
+ transactionWithEvents;
874
964
  async ensureSchema() {
875
965
  await this.sql.query(`
876
966
  CREATE SCHEMA IF NOT EXISTS fabric_platform;
@@ -879,7 +969,8 @@ var PostgresPlatformHostStore = class {
879
969
  action_id text NOT NULL, action_version integer NOT NULL,
880
970
  actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
881
971
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
882
- correlation_id text NOT NULL, causation_id text, idempotency_key text, error text,
972
+ correlation_id text NOT NULL, causation_id text, idempotency_key text,
973
+ authorization_binding_id text, error text,
883
974
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
884
975
  lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
885
976
  hitl_reason text, hitl_policy_version text, approval_decision jsonb,
@@ -888,6 +979,7 @@ var PostgresPlatformHostStore = class {
888
979
  );
889
980
  ALTER TABLE fabric_platform.action_invocations
890
981
  ADD COLUMN IF NOT EXISTS idempotency_key text,
982
+ ADD COLUMN IF NOT EXISTS authorization_binding_id text,
891
983
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
892
984
  ADD COLUMN IF NOT EXISTS lease_owner text,
893
985
  ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz,
@@ -962,8 +1054,9 @@ var PostgresPlatformHostStore = class {
962
1054
  const result = await this.sql.query(
963
1055
  `INSERT INTO fabric_platform.action_invocations
964
1056
  (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
965
- parameters,result,correlation_id,causation_id,idempotency_key,error,runtime_evidence,created_at,updated_at)
966
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15::jsonb,$16,$16)
1057
+ parameters,result,correlation_id,causation_id,idempotency_key,
1058
+ authorization_binding_id,error,runtime_evidence,created_at,updated_at)
1059
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16::jsonb,$17,$17)
967
1060
  ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
968
1061
  WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
969
1062
  RETURNING *`,
@@ -981,6 +1074,7 @@ var PostgresPlatformHostStore = class {
981
1074
  input.correlationId,
982
1075
  input.causationId ?? null,
983
1076
  input.idempotencyKey ?? null,
1077
+ input.authorizationBindingId ?? null,
984
1078
  input.error ?? null,
985
1079
  input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
986
1080
  now
@@ -1344,6 +1438,7 @@ function toActionRecord(row) {
1344
1438
  correlationId: String(row.correlation_id),
1345
1439
  ...row.causation_id ? { causationId: String(row.causation_id) } : {},
1346
1440
  ...row.idempotency_key ? { idempotencyKey: String(row.idempotency_key) } : {},
1441
+ ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
1347
1442
  attemptCount: Number(row.attempt_count ?? 0),
1348
1443
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
1349
1444
  ...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},