@fabricorg/platform-host 0.3.1 → 0.4.1

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
@@ -10,8 +10,10 @@ function createGovernedActionHost(options) {
10
10
  for (const adapter of options.adapters ?? []) adapters.register(adapter);
11
11
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
12
12
  const extractEvents = options.extractEvents ?? DEFAULT_EXTRACT_EVENTS;
13
+ const actionResolver = options.resolveAction ?? resolveAction;
14
+ const eventResultFields = options.eventResultFields ?? ["_events"];
13
15
  async function submitAction(input) {
14
- const action = resolveAction(input.actionId);
16
+ const action = actionResolver(input.actionId);
15
17
  if (!action) throw new Error(`Unknown action: ${input.actionId}`);
16
18
  const authorizationInput = toAuthorizationInput(action, input);
17
19
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
@@ -45,7 +47,9 @@ function createGovernedActionHost(options) {
45
47
  status: durableInvocation.status,
46
48
  workflowId: durableWorkflowId,
47
49
  result: durableInvocation.result,
48
- ...durableInvocation.error ? { error: durableInvocation.error } : {}
50
+ ...durableInvocation.error ? { error: durableInvocation.error } : {},
51
+ ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
52
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
49
53
  };
50
54
  }
51
55
  if (options.dispatcher) {
@@ -60,7 +64,9 @@ function createGovernedActionHost(options) {
60
64
  actionInvocationId: durableInvocation.id,
61
65
  status: durableInvocation.status,
62
66
  workflowId: dispatched.workflowId,
63
- ...dispatched.runId ? { runId: dispatched.runId } : {}
67
+ ...dispatched.runId ? { runId: dispatched.runId } : {},
68
+ ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
69
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
64
70
  };
65
71
  } catch (error) {
66
72
  const message = errorMessage(error);
@@ -80,22 +86,26 @@ function createGovernedActionHost(options) {
80
86
  );
81
87
  return { ...executed, workflowId: durableWorkflowId };
82
88
  }
83
- async function executeInvocation(actionInvocationId, tenantId, spaceId) {
84
- const invocation = await options.store.getActionInvocation(
89
+ async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}) {
90
+ const loadedInvocation = await options.store.getActionInvocation(
85
91
  actionInvocationId,
86
92
  tenantId,
87
93
  spaceId
88
94
  );
89
- if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
90
- 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) {
91
103
  return {
92
- actionInvocationId,
93
- status: invocation.status,
94
- result: invocation.result,
95
- ...invocation.error ? { error: invocation.error } : {}
104
+ ...actionResult(invocation),
105
+ error: `Invocation is leased by ${invocation.leaseOwner}`
96
106
  };
97
107
  }
98
- const action = resolveAction(invocation.actionId);
108
+ const action = actionResolver(invocation.actionId);
99
109
  if (!action) {
100
110
  return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
101
111
  }
@@ -106,15 +116,68 @@ function createGovernedActionHost(options) {
106
116
  `Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
107
117
  );
108
118
  }
109
- await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
110
- status: "running"
111
- });
119
+ if (invocation.status !== "running") {
120
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
121
+ status: "running"
122
+ });
123
+ }
112
124
  try {
113
125
  const parsed = action.schema.safeParse(invocation.parameters);
114
126
  if (!parsed.success) {
115
127
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
116
128
  return fail(invocation, "validation_failed", message);
117
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
+ }
118
181
  const authorizationInput = toAuthorizationInput(action, invocation);
119
182
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
120
183
  ...authorizationInput,
@@ -130,7 +193,8 @@ function createGovernedActionHost(options) {
130
193
  parameters: parsed.data,
131
194
  db: options.store.db,
132
195
  services: options.services,
133
- mode: "execute"
196
+ mode: "execute",
197
+ now: now()
134
198
  });
135
199
  for (const outcome of outcomes) {
136
200
  await options.store.appendPolicyEvaluation({
@@ -163,15 +227,17 @@ function createGovernedActionHost(options) {
163
227
  entityId
164
228
  ) ?? initialState(binding.entityType) : initialState(binding.entityType);
165
229
  const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
166
- const transition = validateTransition(
167
- binding.entityType,
168
- currentState,
169
- targetState,
170
- action.actionId
171
- );
172
- const replayingAppliedTransition = action.idempotent && currentState === targetState;
173
- if (!transition.valid && !replayingAppliedTransition) {
174
- return fail(invocation, "failed", transition.error ?? "Invalid state transition");
230
+ if (targetState !== "") {
231
+ const transition = validateTransition(
232
+ binding.entityType,
233
+ currentState,
234
+ targetState,
235
+ action.actionId
236
+ );
237
+ const replayingAppliedTransition = action.idempotent && currentState === targetState;
238
+ if (!transition.valid && !replayingAppliedTransition) {
239
+ return fail(invocation, "failed", transition.error ?? "Invalid state transition");
240
+ }
175
241
  }
176
242
  }
177
243
  let data;
@@ -200,7 +266,7 @@ function createGovernedActionHost(options) {
200
266
  domainEvents = extractEvents(data);
201
267
  if (action.eventPhase !== "after_adapters") {
202
268
  for (const [index, event] of domainEvents.entries()) {
203
- await appendEvent(invocation, event, `domain:${index}`);
269
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
204
270
  }
205
271
  }
206
272
  } catch (error) {
@@ -319,27 +385,96 @@ function createGovernedActionHost(options) {
319
385
  }
320
386
  if (action.eventPhase === "after_adapters") {
321
387
  for (const [index, event] of domainEvents.entries()) {
322
- await appendEvent(invocation, event, `domain:${index}`);
388
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
323
389
  }
324
390
  }
325
- const result = withoutPrivateHostFields(data);
391
+ const result = withoutPrivateHostFields(data, eventResultFields);
326
392
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
327
393
  status: "completed",
328
394
  result
329
395
  });
330
- return { actionInvocationId, status: "completed", result };
396
+ return {
397
+ actionInvocationId,
398
+ status: "completed",
399
+ result,
400
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
401
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
402
+ };
331
403
  } catch (error) {
332
404
  return fail(invocation, "failed", errorMessage(error));
333
405
  }
334
406
  }
335
- async function appendEvent(invocation, event, deduplicationKey) {
407
+ async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
408
+ if (!decision.approverId.trim()) throw new Error("approverId is required");
409
+ const invocation = await options.store.getActionInvocation(
410
+ actionInvocationId,
411
+ tenantId,
412
+ spaceId
413
+ );
414
+ if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
415
+ if (isTerminal(invocation.status)) return actionResult(invocation);
416
+ if (invocation.status !== "waiting_for_approval") {
417
+ return {
418
+ ...actionResult(invocation),
419
+ error: `Invocation is not waiting for approval (status: ${invocation.status})`
420
+ };
421
+ }
422
+ const action = actionResolver(invocation.actionId);
423
+ if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
424
+ const approvalAuthorizationInput = toAuthorizationInput(action, {
425
+ tenantId,
426
+ spaceId,
427
+ actorId: decision.approverId,
428
+ actorType: decision.approverType
429
+ });
430
+ const entitled = await options.authorization.checkEntitlement(
431
+ approvalAuthorizationInput
432
+ );
433
+ const authorized = options.authorization.authorizeApproval ? await options.authorization.authorizeApproval({
434
+ ...approvalAuthorizationInput,
435
+ decision
436
+ }) : await options.authorization.authorize(approvalAuthorizationInput);
437
+ if (!entitled || !authorized) {
438
+ return {
439
+ ...actionResult(invocation),
440
+ error: `Actor ${decision.approverId} is not authorized to decide this approval`
441
+ };
442
+ }
443
+ const approvalStore = asApprovalStore(options.store);
444
+ if (!approvalStore) {
445
+ return {
446
+ ...actionResult(invocation),
447
+ error: "Approval resume requires a store with approval persistence support."
448
+ };
449
+ }
450
+ const transitionStartedAt = now();
451
+ const decidedAt = decision.decidedAt ?? transitionStartedAt;
452
+ const leaseOwner = `approval-resume-${createFabricId("lease")}`;
453
+ const transition = await approvalStore.beginApprovalDecision({
454
+ actionInvocationId,
455
+ tenantId,
456
+ spaceId,
457
+ decision: { ...decision, decidedAt },
458
+ leaseOwner,
459
+ leaseDurationMs: options.approvalResumeLeaseDurationMs ?? 5 * 6e4,
460
+ now: transitionStartedAt
461
+ });
462
+ const transitioned = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
463
+ if (!transition.applied || !transitioned) {
464
+ if (!transitioned) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
465
+ return actionResult(transitioned);
466
+ }
467
+ if (!decision.approved) return actionResult(transitioned);
468
+ return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
469
+ }
470
+ async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1) {
336
471
  const timestamp = now();
337
472
  const envelope = {
338
473
  id: lifecycleId("evt", invocation.id, deduplicationKey),
339
474
  tenantId: invocation.tenantId,
340
475
  spaceId: invocation.spaceId,
341
476
  eventType: event.eventType,
342
- eventSchemaVersion: event.eventSchemaVersion ?? 1,
477
+ eventSchemaVersion: event.eventSchemaVersion ?? defaultEventSchemaVersion,
343
478
  subjectType: event.subjectType,
344
479
  subjectId: event.subjectId,
345
480
  actorId: invocation.actorId,
@@ -364,9 +499,38 @@ function createGovernedActionHost(options) {
364
499
  invocation.spaceId,
365
500
  { status, error }
366
501
  );
367
- return { actionInvocationId: invocation.id, status, error };
502
+ return {
503
+ actionInvocationId: invocation.id,
504
+ status,
505
+ error,
506
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
507
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
508
+ };
368
509
  }
369
- return { submitAction, executeInvocation };
510
+ return { submitAction, executeInvocation, resumeApprovedInvocation };
511
+ }
512
+ function actionResult(invocation) {
513
+ return {
514
+ actionInvocationId: invocation.id,
515
+ status: invocation.status,
516
+ result: invocation.result,
517
+ ...invocation.error ? { error: invocation.error } : {},
518
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
519
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
520
+ };
521
+ }
522
+ function asApprovalStore(store) {
523
+ const candidate = store;
524
+ return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
525
+ }
526
+ function numberValue(value) {
527
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
528
+ }
529
+ function stringValue(value) {
530
+ return typeof value === "string" && value.length > 0 ? value : void 0;
531
+ }
532
+ function isRiskTier(value) {
533
+ return value === "low" || value === "medium" || value === "high";
370
534
  }
371
535
  function toAuthorizationInput(action, input) {
372
536
  return {
@@ -395,9 +559,10 @@ function initialState(entityType) {
395
559
  function isTerminal(status) {
396
560
  return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
397
561
  }
398
- function withoutPrivateHostFields(data) {
399
- const { _events: _ignored, ...result } = data;
400
- return result;
562
+ function withoutPrivateHostFields(data, eventResultFields) {
563
+ return Object.fromEntries(
564
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
565
+ );
401
566
  }
402
567
  function errorMessage(error) {
403
568
  return error instanceof Error ? error.message : String(error);
@@ -444,6 +609,45 @@ var MemoryPlatformHostStore = class {
444
609
  const record = await this.getActionInvocation(id, tenantId, spaceId);
445
610
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
446
611
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
612
+ if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "validation_failed") {
613
+ delete record.leaseOwner;
614
+ delete record.leaseExpiresAt;
615
+ }
616
+ }
617
+ async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
618
+ const record = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
619
+ if (!record) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
620
+ record.hitlRoute = decision.route;
621
+ record.hitlRiskTier = decision.riskTier;
622
+ record.hitlReason = decision.reason;
623
+ if (decision.policyVersion) record.hitlPolicyVersion = decision.policyVersion;
624
+ record.updatedAt = decision.evaluatedAt;
625
+ return record;
626
+ }
627
+ async beginApprovalDecision(input) {
628
+ const record = await this.getActionInvocation(
629
+ input.actionInvocationId,
630
+ input.tenantId,
631
+ input.spaceId
632
+ );
633
+ if (!record || record.status !== "waiting_for_approval") {
634
+ return { applied: false, ...record ? { invocation: record } : {} };
635
+ }
636
+ record.approvalDecision = input.decision;
637
+ record.updatedAt = input.now;
638
+ if (!input.decision.approved) {
639
+ record.status = "failed";
640
+ record.error = `Approval rejected: ${input.decision.reason ?? "no reason provided"}`;
641
+ delete record.leaseOwner;
642
+ delete record.leaseExpiresAt;
643
+ return { applied: true, invocation: record };
644
+ }
645
+ record.status = "running";
646
+ record.error = void 0;
647
+ record.leaseOwner = input.leaseOwner;
648
+ record.leaseExpiresAt = new Date(input.now.getTime() + input.leaseDurationMs);
649
+ record.attemptCount = Math.max(record.attemptCount, 1);
650
+ return { applied: true, invocation: record };
447
651
  }
448
652
  async appendPolicyEvaluation(record) {
449
653
  if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
@@ -521,14 +725,20 @@ var PostgresPlatformHostStore = class {
521
725
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
522
726
  correlation_id text NOT NULL, causation_id text, idempotency_key text, error text,
523
727
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
524
- lease_expires_at timestamptz,
728
+ lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
729
+ hitl_reason text, hitl_policy_version text, approval_decision jsonb,
525
730
  created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
526
731
  );
527
732
  ALTER TABLE fabric_platform.action_invocations
528
733
  ADD COLUMN IF NOT EXISTS idempotency_key text,
529
734
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
530
735
  ADD COLUMN IF NOT EXISTS lease_owner text,
531
- ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz;
736
+ ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz,
737
+ ADD COLUMN IF NOT EXISTS hitl_route text,
738
+ ADD COLUMN IF NOT EXISTS hitl_risk_tier text,
739
+ ADD COLUMN IF NOT EXISTS hitl_reason text,
740
+ ADD COLUMN IF NOT EXISTS hitl_policy_version text,
741
+ ADD COLUMN IF NOT EXISTS approval_decision jsonb;
532
742
  CREATE UNIQUE INDEX IF NOT EXISTS action_invocations_idempotency_idx
533
743
  ON fabric_platform.action_invocations
534
744
  (tenant_id, space_id, action_id, idempotency_key)
@@ -612,9 +822,9 @@ var PostgresPlatformHostStore = class {
612
822
  `UPDATE fabric_platform.action_invocations SET
613
823
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
614
824
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
615
- lease_owner=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
825
+ lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
616
826
  THEN NULL ELSE lease_owner END,
617
- lease_expires_at=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
827
+ lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
618
828
  THEN NULL ELSE lease_expires_at END,
619
829
  updated_at=now()
620
830
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -629,6 +839,54 @@ var PostgresPlatformHostStore = class {
629
839
  ]
630
840
  );
631
841
  }
842
+ async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
843
+ const result = await this.sql.query(
844
+ `UPDATE fabric_platform.action_invocations SET
845
+ hitl_route=$4, hitl_risk_tier=$5, hitl_reason=$6,
846
+ hitl_policy_version=$7, updated_at=$8
847
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3
848
+ RETURNING *`,
849
+ [
850
+ actionInvocationId,
851
+ tenantId,
852
+ spaceId,
853
+ decision.route,
854
+ decision.riskTier,
855
+ decision.reason,
856
+ decision.policyVersion ?? null,
857
+ decision.evaluatedAt
858
+ ]
859
+ );
860
+ const row = result.rows[0];
861
+ if (!row) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
862
+ return toActionRecord(row);
863
+ }
864
+ async beginApprovalDecision(input) {
865
+ const result = await this.sql.query(
866
+ `UPDATE fabric_platform.action_invocations SET
867
+ approval_decision=$4::jsonb,
868
+ status=CASE WHEN $5::boolean THEN 'running' ELSE 'failed' END,
869
+ error=CASE WHEN $5::boolean THEN NULL ELSE $6 END,
870
+ lease_owner=CASE WHEN $5::boolean THEN $7 ELSE NULL END,
871
+ lease_expires_at=CASE WHEN $5::boolean THEN $8 ELSE NULL END,
872
+ attempt_count=CASE WHEN $5::boolean THEN GREATEST(attempt_count,1) ELSE attempt_count END,
873
+ updated_at=$9
874
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='waiting_for_approval'
875
+ RETURNING *`,
876
+ [
877
+ input.actionInvocationId,
878
+ input.tenantId,
879
+ input.spaceId,
880
+ JSON.stringify({ ...input.decision, decidedAt: input.decision.decidedAt.toISOString() }),
881
+ input.decision.approved,
882
+ `Approval rejected: ${input.decision.reason ?? "no reason provided"}`,
883
+ input.leaseOwner,
884
+ new Date(input.now.getTime() + input.leaseDurationMs),
885
+ input.now
886
+ ]
887
+ );
888
+ return result.rows[0] ? { applied: true, invocation: toActionRecord(result.rows[0]) } : { applied: false };
889
+ }
632
890
  async appendPolicyEvaluation(record) {
633
891
  await this.sql.query(
634
892
  `INSERT INTO fabric_platform.policy_evaluations
@@ -836,11 +1094,27 @@ function toActionRecord(row) {
836
1094
  attemptCount: Number(row.attempt_count ?? 0),
837
1095
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
838
1096
  ...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
1097
+ ...row.hitl_route ? { hitlRoute: String(row.hitl_route) } : {},
1098
+ ...row.hitl_risk_tier ? { hitlRiskTier: String(row.hitl_risk_tier) } : {},
1099
+ ...row.hitl_reason ? { hitlReason: String(row.hitl_reason) } : {},
1100
+ ...row.hitl_policy_version ? { hitlPolicyVersion: String(row.hitl_policy_version) } : {},
1101
+ ...row.approval_decision ? { approvalDecision: toApprovalDecision(row.approval_decision) } : {},
839
1102
  ...row.error ? { error: String(row.error) } : {},
840
1103
  createdAt: new Date(row.created_at),
841
1104
  updatedAt: new Date(row.updated_at)
842
1105
  };
843
1106
  }
1107
+ function toApprovalDecision(value) {
1108
+ const decision = value;
1109
+ return {
1110
+ approved: Boolean(decision.approved),
1111
+ approverId: String(decision.approverId),
1112
+ approverType: String(decision.approverType),
1113
+ ...decision.reason ? { reason: String(decision.reason) } : {},
1114
+ ...decision.editsReference ? { editsReference: String(decision.editsReference) } : {},
1115
+ decidedAt: new Date(decision.decidedAt)
1116
+ };
1117
+ }
844
1118
  function toEventRecord(row) {
845
1119
  return {
846
1120
  id: String(row.id),
@@ -881,21 +1155,29 @@ async function runPlatformActionWorkerCycle(options) {
881
1155
  });
882
1156
  let completed = 0;
883
1157
  let failed = 0;
1158
+ let waitingForApproval = 0;
884
1159
  for (const invocation of claimed) {
885
1160
  try {
886
1161
  const result = await options.host.executeInvocation(
887
1162
  invocation.id,
888
1163
  invocation.tenantId,
889
- invocation.spaceId
1164
+ invocation.spaceId,
1165
+ { leaseOwner: options.workerId }
890
1166
  );
891
1167
  if (result.status === "completed") completed += 1;
1168
+ else if (result.status === "waiting_for_approval") waitingForApproval += 1;
892
1169
  else failed += 1;
893
1170
  } catch (error) {
894
1171
  failed += 1;
895
1172
  options.onError?.(error, invocation);
896
1173
  }
897
1174
  }
898
- return { claimed: claimed.length, completed, failed };
1175
+ return {
1176
+ claimed: claimed.length,
1177
+ completed,
1178
+ failed,
1179
+ ...waitingForApproval > 0 ? { waitingForApproval } : {}
1180
+ };
899
1181
  }
900
1182
  async function runPlatformActionWorker(options) {
901
1183
  while (!options.signal?.aborted) {