@fabricorg/platform-host 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -47,7 +47,9 @@ function createGovernedActionHost(options) {
47
47
  status: durableInvocation.status,
48
48
  workflowId: durableWorkflowId,
49
49
  result: durableInvocation.result,
50
- ...durableInvocation.error ? { error: durableInvocation.error } : {}
50
+ ...durableInvocation.error ? { error: durableInvocation.error } : {},
51
+ ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
52
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
51
53
  };
52
54
  }
53
55
  if (options.dispatcher) {
@@ -62,7 +64,9 @@ function createGovernedActionHost(options) {
62
64
  actionInvocationId: durableInvocation.id,
63
65
  status: durableInvocation.status,
64
66
  workflowId: dispatched.workflowId,
65
- ...dispatched.runId ? { runId: dispatched.runId } : {}
67
+ ...dispatched.runId ? { runId: dispatched.runId } : {},
68
+ ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
69
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
66
70
  };
67
71
  } catch (error) {
68
72
  const message = errorMessage(error);
@@ -82,34 +86,98 @@ function createGovernedActionHost(options) {
82
86
  );
83
87
  return { ...executed, workflowId: durableWorkflowId };
84
88
  }
85
- async function executeInvocation(actionInvocationId, tenantId, spaceId) {
86
- const invocation = await options.store.getActionInvocation(
89
+ async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}) {
90
+ const loadedInvocation = await options.store.getActionInvocation(
87
91
  actionInvocationId,
88
92
  tenantId,
89
93
  spaceId
90
94
  );
91
- if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
92
- if (isTerminal(invocation.status)) {
95
+ if (!loadedInvocation) {
96
+ throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
97
+ }
98
+ let invocation = loadedInvocation;
99
+ if (isTerminal(invocation.status) || invocation.status === "waiting_for_approval") {
100
+ return actionResult(invocation);
101
+ }
102
+ if (invocation.status === "running" && invocation.leaseOwner && executionOptions.leaseOwner !== invocation.leaseOwner) {
93
103
  return {
94
- actionInvocationId,
95
- status: invocation.status,
96
- result: invocation.result,
97
- ...invocation.error ? { error: invocation.error } : {}
104
+ ...actionResult(invocation),
105
+ error: `Invocation is leased by ${invocation.leaseOwner}`
98
106
  };
99
107
  }
100
108
  const action = platform.resolveAction(invocation.actionId);
101
109
  if (!action) {
102
110
  return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
103
111
  }
104
- await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
105
- status: "running"
106
- });
112
+ if (invocation.attemptCount > 1 && !action.idempotent) {
113
+ return fail(
114
+ invocation,
115
+ "failed",
116
+ `Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
117
+ );
118
+ }
119
+ if (invocation.status !== "running") {
120
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
121
+ status: "running"
122
+ });
123
+ }
107
124
  try {
108
125
  const parsed = action.schema.safeParse(invocation.parameters);
109
126
  if (!parsed.success) {
110
127
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
111
128
  return fail(invocation, "validation_failed", message);
112
129
  }
130
+ if (invocation.actorType === "agent" && options.hitlEvaluator) {
131
+ const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
132
+ const approvalStore = asApprovalStore(options.store);
133
+ if (!approvalStore) {
134
+ return fail(
135
+ invocation,
136
+ "failed",
137
+ "The configured HITL evaluator requires a store with approval persistence support."
138
+ );
139
+ }
140
+ if (!invocation.hitlRoute) {
141
+ const decision = await options.hitlEvaluator({
142
+ actionId: action.actionId,
143
+ actorId: invocation.actorId,
144
+ actorType: invocation.actorType,
145
+ tenantId,
146
+ spaceId,
147
+ parameters: hitlParameters,
148
+ ...numberValue(hitlParameters.confidence) !== void 0 ? { confidence: numberValue(hitlParameters.confidence) } : {},
149
+ ...isRiskTier(hitlParameters.riskTier) ? { riskTier: hitlParameters.riskTier } : {},
150
+ ...stringValue(hitlParameters.agentSessionId) ? { agentSessionId: stringValue(hitlParameters.agentSessionId) } : {},
151
+ ...stringValue(hitlParameters.agentRunId) ? { agentRunId: stringValue(hitlParameters.agentRunId) } : {}
152
+ });
153
+ invocation = await approvalStore.recordHitlDecision(
154
+ actionInvocationId,
155
+ tenantId,
156
+ spaceId,
157
+ {
158
+ ...decision,
159
+ ...options.hitlPolicyVersion ? { policyVersion: options.hitlPolicyVersion } : {},
160
+ evaluatedAt: now()
161
+ }
162
+ );
163
+ }
164
+ if (invocation.hitlRoute === "rejected") {
165
+ return fail(
166
+ invocation,
167
+ "failed",
168
+ `HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`
169
+ );
170
+ }
171
+ if ((invocation.hitlRoute === "needs-approval" || invocation.hitlRoute === "escalate") && !invocation.approvalDecision?.approved) {
172
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
173
+ status: "waiting_for_approval"
174
+ });
175
+ return {
176
+ ...actionResult(invocation),
177
+ status: "waiting_for_approval"
178
+ };
179
+ }
180
+ }
113
181
  const authorizationInput = toAuthorizationInput(action, invocation);
114
182
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
115
183
  ...authorizationInput,
@@ -129,7 +197,7 @@ function createGovernedActionHost(options) {
129
197
  });
130
198
  for (const outcome of outcomes) {
131
199
  await options.store.appendPolicyEvaluation({
132
- id: platform.createFabricId("pol"),
200
+ id: lifecycleId("pol", actionInvocationId, outcome.policyId),
133
201
  actionInvocationId,
134
202
  tenantId,
135
203
  spaceId,
@@ -145,7 +213,7 @@ function createGovernedActionHost(options) {
145
213
  subjectType: "ActionInvocation",
146
214
  subjectId: actionInvocationId,
147
215
  payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
148
- });
216
+ }, `compliance:${aggregate.policyId}`);
149
217
  return fail(invocation, "blocked_by_policy", reason);
150
218
  }
151
219
  const binding = action.stateMachine;
@@ -164,7 +232,8 @@ function createGovernedActionHost(options) {
164
232
  targetState,
165
233
  action.actionId
166
234
  );
167
- if (!transition.valid) {
235
+ const replayingAppliedTransition = action.idempotent && currentState === targetState;
236
+ if (!transition.valid && !replayingAppliedTransition) {
168
237
  return fail(invocation, "failed", transition.error ?? "Invalid state transition");
169
238
  }
170
239
  }
@@ -193,16 +262,22 @@ function createGovernedActionHost(options) {
193
262
  data = handlerResult.data ?? {};
194
263
  domainEvents = extractEvents(data);
195
264
  if (action.eventPhase !== "after_adapters") {
196
- for (const event of domainEvents) await appendEvent(invocation, event);
265
+ for (const [index, event] of domainEvents.entries()) {
266
+ await appendEvent(invocation, event, `domain:${index}`);
267
+ }
197
268
  }
198
269
  } catch (error) {
199
270
  return fail(invocation, "failed", errorMessage(error));
200
271
  }
201
- for (const step of action.adapterSteps ?? []) {
272
+ for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
202
273
  const input = step.getInput(parsed.data, data);
203
274
  if (!input) continue;
204
275
  const adapter = adapters.require(step.adapterType, step.operation);
205
- const adapterInvocationId = platform.createFabricId("adp");
276
+ const adapterInvocationId = lifecycleId("adp", actionInvocationId, String(stepIndex));
277
+ const previousAdapterInvocation = await options.store.getAdapterInvocation(
278
+ adapterInvocationId
279
+ );
280
+ if (previousAdapterInvocation?.status === "succeeded") continue;
206
281
  const adapterEventSubject = options.adapterEventSubject?.(
207
282
  action.actionId,
208
283
  parsed.data,
@@ -235,7 +310,7 @@ function createGovernedActionHost(options) {
235
310
  subjectType: adapterEventSubject.subjectType,
236
311
  subjectId: adapterEventSubject.subjectId,
237
312
  payload: { adapterType: step.adapterType, operation: step.operation }
238
- });
313
+ }, `adapter:${stepIndex}:started`);
239
314
  try {
240
315
  const result2 = await platform.executeWithAdapterRetry({
241
316
  policy: step.retryPolicy ?? adapter.retryPolicy,
@@ -270,7 +345,7 @@ function createGovernedActionHost(options) {
270
345
  subjectType: adapterEventSubject.subjectType,
271
346
  subjectId: adapterEventSubject.subjectId,
272
347
  payload: { adapterType: step.adapterType, operation: step.operation }
273
- });
348
+ }, `adapter:${stepIndex}:failed`);
274
349
  return fail(invocation, "failed", result2.error ?? "Adapter failed");
275
350
  }
276
351
  const resultRecord = result2;
@@ -294,7 +369,7 @@ function createGovernedActionHost(options) {
294
369
  input: recordedInput,
295
370
  output
296
371
  }
297
- });
372
+ }, `adapter:${stepIndex}:succeeded`);
298
373
  } catch (error) {
299
374
  const message = errorMessage(error);
300
375
  await options.store.updateAdapterInvocation(adapterInvocationId, {
@@ -306,22 +381,93 @@ function createGovernedActionHost(options) {
306
381
  }
307
382
  }
308
383
  if (action.eventPhase === "after_adapters") {
309
- for (const event of domainEvents) await appendEvent(invocation, event);
384
+ for (const [index, event] of domainEvents.entries()) {
385
+ await appendEvent(invocation, event, `domain:${index}`);
386
+ }
310
387
  }
311
388
  const result = withoutPrivateHostFields(data);
312
389
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
313
390
  status: "completed",
314
391
  result
315
392
  });
316
- return { actionInvocationId, status: "completed", result };
393
+ return {
394
+ actionInvocationId,
395
+ status: "completed",
396
+ result,
397
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
398
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
399
+ };
317
400
  } catch (error) {
318
401
  return fail(invocation, "failed", errorMessage(error));
319
402
  }
320
403
  }
321
- async function appendEvent(invocation, event) {
404
+ async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
405
+ if (!decision.approverId.trim()) throw new Error("approverId is required");
406
+ const invocation = await options.store.getActionInvocation(
407
+ actionInvocationId,
408
+ tenantId,
409
+ spaceId
410
+ );
411
+ if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
412
+ if (isTerminal(invocation.status)) return actionResult(invocation);
413
+ if (invocation.status !== "waiting_for_approval") {
414
+ return {
415
+ ...actionResult(invocation),
416
+ error: `Invocation is not waiting for approval (status: ${invocation.status})`
417
+ };
418
+ }
419
+ const action = platform.resolveAction(invocation.actionId);
420
+ if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
421
+ const approvalAuthorizationInput = toAuthorizationInput(action, {
422
+ tenantId,
423
+ spaceId,
424
+ actorId: decision.approverId,
425
+ actorType: decision.approverType
426
+ });
427
+ const entitled = await options.authorization.checkEntitlement(
428
+ approvalAuthorizationInput
429
+ );
430
+ const authorized = options.authorization.authorizeApproval ? await options.authorization.authorizeApproval({
431
+ ...approvalAuthorizationInput,
432
+ decision
433
+ }) : await options.authorization.authorize(approvalAuthorizationInput);
434
+ if (!entitled || !authorized) {
435
+ return {
436
+ ...actionResult(invocation),
437
+ error: `Actor ${decision.approverId} is not authorized to decide this approval`
438
+ };
439
+ }
440
+ const approvalStore = asApprovalStore(options.store);
441
+ if (!approvalStore) {
442
+ return {
443
+ ...actionResult(invocation),
444
+ error: "Approval resume requires a store with approval persistence support."
445
+ };
446
+ }
447
+ const transitionStartedAt = now();
448
+ const decidedAt = decision.decidedAt ?? transitionStartedAt;
449
+ const leaseOwner = `approval-resume-${platform.createFabricId("lease")}`;
450
+ const transition = await approvalStore.beginApprovalDecision({
451
+ actionInvocationId,
452
+ tenantId,
453
+ spaceId,
454
+ decision: { ...decision, decidedAt },
455
+ leaseOwner,
456
+ leaseDurationMs: options.approvalResumeLeaseDurationMs ?? 5 * 6e4,
457
+ now: transitionStartedAt
458
+ });
459
+ const transitioned = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
460
+ if (!transition.applied || !transitioned) {
461
+ if (!transitioned) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
462
+ return actionResult(transitioned);
463
+ }
464
+ if (!decision.approved) return actionResult(transitioned);
465
+ return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
466
+ }
467
+ async function appendEvent(invocation, event, deduplicationKey) {
322
468
  const timestamp = now();
323
469
  const envelope = {
324
- id: platform.createFabricId("evt"),
470
+ id: lifecycleId("evt", invocation.id, deduplicationKey),
325
471
  tenantId: invocation.tenantId,
326
472
  spaceId: invocation.spaceId,
327
473
  eventType: event.eventType,
@@ -350,9 +496,38 @@ function createGovernedActionHost(options) {
350
496
  invocation.spaceId,
351
497
  { status, error }
352
498
  );
353
- return { actionInvocationId: invocation.id, status, error };
499
+ return {
500
+ actionInvocationId: invocation.id,
501
+ status,
502
+ error,
503
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
504
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
505
+ };
354
506
  }
355
- return { submitAction, executeInvocation };
507
+ return { submitAction, executeInvocation, resumeApprovedInvocation };
508
+ }
509
+ function actionResult(invocation) {
510
+ return {
511
+ actionInvocationId: invocation.id,
512
+ status: invocation.status,
513
+ result: invocation.result,
514
+ ...invocation.error ? { error: invocation.error } : {},
515
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
516
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
517
+ };
518
+ }
519
+ function asApprovalStore(store) {
520
+ const candidate = store;
521
+ return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
522
+ }
523
+ function numberValue(value) {
524
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
525
+ }
526
+ function stringValue(value) {
527
+ return typeof value === "string" && value.length > 0 ? value : void 0;
528
+ }
529
+ function isRiskTier(value) {
530
+ return value === "low" || value === "medium" || value === "high";
356
531
  }
357
532
  function toAuthorizationInput(action, input) {
358
533
  return {
@@ -391,6 +566,10 @@ function errorMessage(error) {
391
566
  function isRecord(value) {
392
567
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
393
568
  }
569
+ function lifecycleId(prefix, invocationId, key) {
570
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
571
+ return `${prefix}_${invocationId}_${safeKey}`;
572
+ }
394
573
 
395
574
  // src/memory-store.ts
396
575
  var MemoryPlatformHostStore = class {
@@ -426,19 +605,64 @@ var MemoryPlatformHostStore = class {
426
605
  const record = await this.getActionInvocation(id, tenantId, spaceId);
427
606
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
428
607
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
608
+ if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "validation_failed") {
609
+ delete record.leaseOwner;
610
+ delete record.leaseExpiresAt;
611
+ }
612
+ }
613
+ async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
614
+ const record = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
615
+ if (!record) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
616
+ record.hitlRoute = decision.route;
617
+ record.hitlRiskTier = decision.riskTier;
618
+ record.hitlReason = decision.reason;
619
+ if (decision.policyVersion) record.hitlPolicyVersion = decision.policyVersion;
620
+ record.updatedAt = decision.evaluatedAt;
621
+ return record;
622
+ }
623
+ async beginApprovalDecision(input) {
624
+ const record = await this.getActionInvocation(
625
+ input.actionInvocationId,
626
+ input.tenantId,
627
+ input.spaceId
628
+ );
629
+ if (!record || record.status !== "waiting_for_approval") {
630
+ return { applied: false, ...record ? { invocation: record } : {} };
631
+ }
632
+ record.approvalDecision = input.decision;
633
+ record.updatedAt = input.now;
634
+ if (!input.decision.approved) {
635
+ record.status = "failed";
636
+ record.error = `Approval rejected: ${input.decision.reason ?? "no reason provided"}`;
637
+ delete record.leaseOwner;
638
+ delete record.leaseExpiresAt;
639
+ return { applied: true, invocation: record };
640
+ }
641
+ record.status = "running";
642
+ record.error = void 0;
643
+ record.leaseOwner = input.leaseOwner;
644
+ record.leaseExpiresAt = new Date(input.now.getTime() + input.leaseDurationMs);
645
+ record.attemptCount = Math.max(record.attemptCount, 1);
646
+ return { applied: true, invocation: record };
429
647
  }
430
648
  async appendPolicyEvaluation(record) {
649
+ if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
431
650
  this.policyEvaluations.push(record);
432
651
  }
433
652
  async createAdapterInvocation(record) {
653
+ if (this.adapterInvocations.some((candidate) => candidate.id === record.id)) return;
434
654
  this.adapterInvocations.push(record);
435
655
  }
656
+ async getAdapterInvocation(id) {
657
+ return this.adapterInvocations.find((candidate) => candidate.id === id);
658
+ }
436
659
  async updateAdapterInvocation(id, patch) {
437
660
  const record = this.adapterInvocations.find((candidate) => candidate.id === id);
438
661
  if (!record) throw new Error(`AdapterInvocation not found: ${id}`);
439
662
  Object.assign(record, patch);
440
663
  }
441
664
  async appendEvent(event) {
665
+ if (this.events.some((candidate) => candidate.id === event.id)) return;
442
666
  this.events.push(event);
443
667
  }
444
668
  async nextEventSequence(tenantId, spaceId) {
@@ -497,14 +721,20 @@ var PostgresPlatformHostStore = class {
497
721
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
498
722
  correlation_id text NOT NULL, causation_id text, idempotency_key text, error text,
499
723
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
500
- lease_expires_at timestamptz,
724
+ lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
725
+ hitl_reason text, hitl_policy_version text, approval_decision jsonb,
501
726
  created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
502
727
  );
503
728
  ALTER TABLE fabric_platform.action_invocations
504
729
  ADD COLUMN IF NOT EXISTS idempotency_key text,
505
730
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
506
731
  ADD COLUMN IF NOT EXISTS lease_owner text,
507
- ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz;
732
+ ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz,
733
+ ADD COLUMN IF NOT EXISTS hitl_route text,
734
+ ADD COLUMN IF NOT EXISTS hitl_risk_tier text,
735
+ ADD COLUMN IF NOT EXISTS hitl_reason text,
736
+ ADD COLUMN IF NOT EXISTS hitl_policy_version text,
737
+ ADD COLUMN IF NOT EXISTS approval_decision jsonb;
508
738
  CREATE UNIQUE INDEX IF NOT EXISTS action_invocations_idempotency_idx
509
739
  ON fabric_platform.action_invocations
510
740
  (tenant_id, space_id, action_id, idempotency_key)
@@ -588,9 +818,9 @@ var PostgresPlatformHostStore = class {
588
818
  `UPDATE fabric_platform.action_invocations SET
589
819
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
590
820
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
591
- lease_owner=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
821
+ lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
592
822
  THEN NULL ELSE lease_owner END,
593
- lease_expires_at=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
823
+ lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
594
824
  THEN NULL ELSE lease_expires_at END,
595
825
  updated_at=now()
596
826
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -605,11 +835,59 @@ var PostgresPlatformHostStore = class {
605
835
  ]
606
836
  );
607
837
  }
838
+ async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
839
+ const result = await this.sql.query(
840
+ `UPDATE fabric_platform.action_invocations SET
841
+ hitl_route=$4, hitl_risk_tier=$5, hitl_reason=$6,
842
+ hitl_policy_version=$7, updated_at=$8
843
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3
844
+ RETURNING *`,
845
+ [
846
+ actionInvocationId,
847
+ tenantId,
848
+ spaceId,
849
+ decision.route,
850
+ decision.riskTier,
851
+ decision.reason,
852
+ decision.policyVersion ?? null,
853
+ decision.evaluatedAt
854
+ ]
855
+ );
856
+ const row = result.rows[0];
857
+ if (!row) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
858
+ return toActionRecord(row);
859
+ }
860
+ async beginApprovalDecision(input) {
861
+ const result = await this.sql.query(
862
+ `UPDATE fabric_platform.action_invocations SET
863
+ approval_decision=$4::jsonb,
864
+ status=CASE WHEN $5::boolean THEN 'running' ELSE 'failed' END,
865
+ error=CASE WHEN $5::boolean THEN NULL ELSE $6 END,
866
+ lease_owner=CASE WHEN $5::boolean THEN $7 ELSE NULL END,
867
+ lease_expires_at=CASE WHEN $5::boolean THEN $8 ELSE NULL END,
868
+ attempt_count=CASE WHEN $5::boolean THEN GREATEST(attempt_count,1) ELSE attempt_count END,
869
+ updated_at=$9
870
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='waiting_for_approval'
871
+ RETURNING *`,
872
+ [
873
+ input.actionInvocationId,
874
+ input.tenantId,
875
+ input.spaceId,
876
+ JSON.stringify({ ...input.decision, decidedAt: input.decision.decidedAt.toISOString() }),
877
+ input.decision.approved,
878
+ `Approval rejected: ${input.decision.reason ?? "no reason provided"}`,
879
+ input.leaseOwner,
880
+ new Date(input.now.getTime() + input.leaseDurationMs),
881
+ input.now
882
+ ]
883
+ );
884
+ return result.rows[0] ? { applied: true, invocation: toActionRecord(result.rows[0]) } : { applied: false };
885
+ }
608
886
  async appendPolicyEvaluation(record) {
609
887
  await this.sql.query(
610
888
  `INSERT INTO fabric_platform.policy_evaluations
611
889
  (id,action_invocation_id,tenant_id,space_id,outcome,created_at)
612
- VALUES ($1,$2,$3,$4,$5::jsonb,$6)`,
890
+ VALUES ($1,$2,$3,$4,$5::jsonb,$6) ON CONFLICT (id) DO NOTHING`,
613
891
  [
614
892
  record.id,
615
893
  record.actionInvocationId,
@@ -625,7 +903,8 @@ var PostgresPlatformHostStore = class {
625
903
  `INSERT INTO fabric_platform.adapter_invocations
626
904
  (id,action_invocation_id,tenant_id,space_id,adapter_type,operation,vendor,status,
627
905
  input,output,error,attempt,created_at,updated_at)
628
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14)`,
906
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14)
907
+ ON CONFLICT (id) DO NOTHING`,
629
908
  [
630
909
  record.id,
631
910
  record.actionInvocationId,
@@ -644,6 +923,13 @@ var PostgresPlatformHostStore = class {
644
923
  ]
645
924
  );
646
925
  }
926
+ async getAdapterInvocation(id) {
927
+ const result = await this.sql.query(
928
+ `SELECT * FROM fabric_platform.adapter_invocations WHERE id=$1`,
929
+ [id]
930
+ );
931
+ return result.rows[0] ? toAdapterRecord(result.rows[0]) : void 0;
932
+ }
647
933
  async updateAdapterInvocation(id, patch) {
648
934
  await this.sql.query(
649
935
  `UPDATE fabric_platform.adapter_invocations SET
@@ -667,7 +953,8 @@ var PostgresPlatformHostStore = class {
667
953
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
668
954
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
669
955
  correlation_id,causation_id)
670
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)`,
956
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
957
+ ON CONFLICT (id) DO NOTHING`,
671
958
  [
672
959
  event.id,
673
960
  event.tenantId,
@@ -767,6 +1054,24 @@ var PostgresPlatformHostStore = class {
767
1054
  return result.rows.map(toEventRecord);
768
1055
  }
769
1056
  };
1057
+ function toAdapterRecord(row) {
1058
+ return {
1059
+ id: String(row.id),
1060
+ actionInvocationId: String(row.action_invocation_id),
1061
+ tenantId: String(row.tenant_id),
1062
+ spaceId: String(row.space_id),
1063
+ adapterType: String(row.adapter_type),
1064
+ operation: String(row.operation),
1065
+ vendor: String(row.vendor),
1066
+ status: String(row.status),
1067
+ input: row.input,
1068
+ ...row.output ? { output: row.output } : {},
1069
+ ...row.error ? { error: String(row.error) } : {},
1070
+ attempt: Number(row.attempt),
1071
+ createdAt: new Date(row.created_at),
1072
+ updatedAt: new Date(row.updated_at)
1073
+ };
1074
+ }
770
1075
  function toActionRecord(row) {
771
1076
  return {
772
1077
  id: String(row.id),
@@ -785,11 +1090,27 @@ function toActionRecord(row) {
785
1090
  attemptCount: Number(row.attempt_count ?? 0),
786
1091
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
787
1092
  ...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
1093
+ ...row.hitl_route ? { hitlRoute: String(row.hitl_route) } : {},
1094
+ ...row.hitl_risk_tier ? { hitlRiskTier: String(row.hitl_risk_tier) } : {},
1095
+ ...row.hitl_reason ? { hitlReason: String(row.hitl_reason) } : {},
1096
+ ...row.hitl_policy_version ? { hitlPolicyVersion: String(row.hitl_policy_version) } : {},
1097
+ ...row.approval_decision ? { approvalDecision: toApprovalDecision(row.approval_decision) } : {},
788
1098
  ...row.error ? { error: String(row.error) } : {},
789
1099
  createdAt: new Date(row.created_at),
790
1100
  updatedAt: new Date(row.updated_at)
791
1101
  };
792
1102
  }
1103
+ function toApprovalDecision(value) {
1104
+ const decision = value;
1105
+ return {
1106
+ approved: Boolean(decision.approved),
1107
+ approverId: String(decision.approverId),
1108
+ approverType: String(decision.approverType),
1109
+ ...decision.reason ? { reason: String(decision.reason) } : {},
1110
+ ...decision.editsReference ? { editsReference: String(decision.editsReference) } : {},
1111
+ decidedAt: new Date(decision.decidedAt)
1112
+ };
1113
+ }
793
1114
  function toEventRecord(row) {
794
1115
  return {
795
1116
  id: String(row.id),
@@ -830,21 +1151,29 @@ async function runPlatformActionWorkerCycle(options) {
830
1151
  });
831
1152
  let completed = 0;
832
1153
  let failed = 0;
1154
+ let waitingForApproval = 0;
833
1155
  for (const invocation of claimed) {
834
1156
  try {
835
1157
  const result = await options.host.executeInvocation(
836
1158
  invocation.id,
837
1159
  invocation.tenantId,
838
- invocation.spaceId
1160
+ invocation.spaceId,
1161
+ { leaseOwner: options.workerId }
839
1162
  );
840
1163
  if (result.status === "completed") completed += 1;
1164
+ else if (result.status === "waiting_for_approval") waitingForApproval += 1;
841
1165
  else failed += 1;
842
1166
  } catch (error) {
843
1167
  failed += 1;
844
1168
  options.onError?.(error, invocation);
845
1169
  }
846
1170
  }
847
- return { claimed: claimed.length, completed, failed };
1171
+ return {
1172
+ claimed: claimed.length,
1173
+ completed,
1174
+ failed,
1175
+ ...waitingForApproval > 0 ? { waitingForApproval } : {}
1176
+ };
848
1177
  }
849
1178
  async function runPlatformActionWorker(options) {
850
1179
  while (!options.signal?.aborted) {