@fabricorg/platform-host 0.3.1 → 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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActionId, ActorType, ActionStatus, PolicyOutcome, AssetEventEnvelope, AdapterImplementation, FabricRuntimeServices, RuntimePolicyDefinition } from '@fabricorg/platform';
1
+ import { ActorType, ActionId, ActionStatus, AgentActionRoute, AgentActionRiskTier, PolicyOutcome, AssetEventEnvelope, AdapterImplementation, FabricRuntimeServices, AgentActionPolicyEvaluator, RuntimePolicyDefinition } from '@fabricorg/platform';
2
2
 
3
3
  interface PendingAssetEvent {
4
4
  eventType: string;
@@ -24,10 +24,33 @@ interface ActionInvocationRecord {
24
24
  attemptCount: number;
25
25
  leaseOwner?: string;
26
26
  leaseExpiresAt?: Date;
27
+ hitlRoute?: AgentActionRoute;
28
+ hitlRiskTier?: AgentActionRiskTier;
29
+ hitlReason?: string;
30
+ hitlPolicyVersion?: string;
31
+ approvalDecision?: PersistedActionApprovalDecision;
27
32
  error?: string;
28
33
  createdAt: Date;
29
34
  updatedAt: Date;
30
35
  }
36
+ interface HitlDecisionEvidence {
37
+ route: AgentActionRoute;
38
+ riskTier: AgentActionRiskTier;
39
+ reason: string;
40
+ policyVersion?: string;
41
+ evaluatedAt: Date;
42
+ }
43
+ interface ActionApprovalDecision {
44
+ approved: boolean;
45
+ approverId: string;
46
+ approverType: ActorType;
47
+ reason?: string;
48
+ editsReference?: string;
49
+ decidedAt?: Date;
50
+ }
51
+ interface PersistedActionApprovalDecision extends Omit<ActionApprovalDecision, "decidedAt"> {
52
+ decidedAt: Date;
53
+ }
31
54
  interface PolicyEvaluationRecord {
32
55
  id: string;
33
56
  actionInvocationId: string;
@@ -69,6 +92,24 @@ interface PlatformHostStore<TDb> {
69
92
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
70
93
  getEntityState(tenantId: string, spaceId: string, entityType: string, entityId: string): Promise<string | undefined>;
71
94
  }
95
+ interface BeginApprovalDecisionInput {
96
+ actionInvocationId: string;
97
+ tenantId: string;
98
+ spaceId: string;
99
+ decision: PersistedActionApprovalDecision;
100
+ leaseOwner: string;
101
+ leaseDurationMs: number;
102
+ now: Date;
103
+ }
104
+ interface ApprovalDecisionTransitionResult {
105
+ applied: boolean;
106
+ invocation?: ActionInvocationRecord;
107
+ }
108
+ /** Optional store capability required only when agent HITL or approval resume is used. */
109
+ interface ApprovalPlatformHostStore<TDb> extends PlatformHostStore<TDb> {
110
+ recordHitlDecision(actionInvocationId: string, tenantId: string, spaceId: string, decision: HitlDecisionEvidence): Promise<ActionInvocationRecord>;
111
+ beginApprovalDecision(input: BeginApprovalDecisionInput): Promise<ApprovalDecisionTransitionResult>;
112
+ }
72
113
  interface ListActionInvocationsInput {
73
114
  tenantId?: string;
74
115
  spaceId?: string;
@@ -103,6 +144,10 @@ interface ActionAuthorizationInput {
103
144
  interface PlatformHostAuthorization {
104
145
  checkEntitlement(input: ActionAuthorizationInput): Promise<boolean>;
105
146
  authorize(input: ActionAuthorizationInput): Promise<boolean>;
147
+ /** Optional distinct authorization boundary for human/system approval decisions. */
148
+ authorizeApproval?(input: ActionAuthorizationInput & {
149
+ decision: ActionApprovalDecision;
150
+ }): Promise<boolean>;
106
151
  }
107
152
  interface DispatchActionInput {
108
153
  actionInvocationId: string;
@@ -136,12 +181,20 @@ interface SubmitActionResult {
136
181
  runId?: string;
137
182
  result?: Record<string, unknown>;
138
183
  error?: string;
184
+ hitlRoute?: AgentActionRoute;
185
+ hitlRiskTier?: AgentActionRiskTier;
139
186
  }
140
187
  interface ExecuteActionResult {
141
188
  actionInvocationId: string;
142
189
  status: ActionStatus;
143
190
  result?: Record<string, unknown>;
144
191
  error?: string;
192
+ hitlRoute?: AgentActionRoute;
193
+ hitlRiskTier?: AgentActionRiskTier;
194
+ }
195
+ interface ExecuteInvocationOptions {
196
+ /** Required when executing a row already protected by a worker or approval-resume lease. */
197
+ leaseOwner?: string;
145
198
  }
146
199
  interface PlatformActionWorkerOptions<TDb> {
147
200
  host: GovernedActionHost;
@@ -159,6 +212,8 @@ interface PlatformActionWorkerCycleResult {
159
212
  claimed: number;
160
213
  completed: number;
161
214
  failed: number;
215
+ /** Present when claimed agent work was durably parked instead of failing. */
216
+ waitingForApproval?: number;
162
217
  }
163
218
  interface PlatformHostOptions<TDb> {
164
219
  store: PlatformHostStore<TDb>;
@@ -166,6 +221,12 @@ interface PlatformHostOptions<TDb> {
166
221
  adapters?: readonly AdapterImplementation[];
167
222
  dispatcher?: PlatformActionDispatcher;
168
223
  services?: FabricRuntimeServices;
224
+ /** Optional vertical-owned evaluator. When absent, agent behavior is unchanged. */
225
+ hitlEvaluator?: AgentActionPolicyEvaluator;
226
+ /** Durable evaluator/ruleset version recorded with every HITL decision. */
227
+ hitlPolicyVersion?: string;
228
+ /** Lease held while an approved invocation resumes; defaults to five minutes. */
229
+ approvalResumeLeaseDurationMs?: number;
169
230
  /** Remove forbidden/sensitive fields before durable invocation persistence. */
170
231
  redactActionParameters?: (actionId: ActionId, parameters: Record<string, unknown>) => Record<string, unknown>;
171
232
  resolvePolicies?: (input: ActionAuthorizationInput & {
@@ -181,12 +242,13 @@ interface PlatformHostOptions<TDb> {
181
242
  }
182
243
  interface GovernedActionHost {
183
244
  submitAction(input: SubmitActionInput): Promise<SubmitActionResult>;
184
- executeInvocation(actionInvocationId: string, tenantId: string, spaceId: string): Promise<ExecuteActionResult>;
245
+ executeInvocation(actionInvocationId: string, tenantId: string, spaceId: string, options?: ExecuteInvocationOptions): Promise<ExecuteActionResult>;
246
+ resumeApprovedInvocation(actionInvocationId: string, tenantId: string, spaceId: string, decision: ActionApprovalDecision): Promise<ExecuteActionResult>;
185
247
  }
186
248
 
187
249
  declare function createGovernedActionHost<TDb>(options: PlatformHostOptions<TDb>): GovernedActionHost;
188
250
 
189
- declare class MemoryPlatformHostStore<TDb> implements PlatformHostStore<TDb> {
251
+ declare class MemoryPlatformHostStore<TDb> implements RecoverablePlatformHostStore<TDb>, ApprovalPlatformHostStore<TDb> {
190
252
  readonly db: TDb;
191
253
  readonly invocations: ActionInvocationRecord[];
192
254
  readonly policyEvaluations: PolicyEvaluationRecord[];
@@ -197,6 +259,8 @@ declare class MemoryPlatformHostStore<TDb> implements PlatformHostStore<TDb> {
197
259
  createActionInvocation(input: CreateActionInvocationInput): Promise<ActionInvocationRecord>;
198
260
  getActionInvocation(id: string, tenantId: string, spaceId: string): Promise<ActionInvocationRecord | undefined>;
199
261
  updateActionInvocation(id: string, tenantId: string, spaceId: string, patch: Partial<Pick<ActionInvocationRecord, "status" | "result" | "error">>): Promise<void>;
262
+ recordHitlDecision(actionInvocationId: string, tenantId: string, spaceId: string, decision: HitlDecisionEvidence): Promise<ActionInvocationRecord>;
263
+ beginApprovalDecision(input: BeginApprovalDecisionInput): Promise<ApprovalDecisionTransitionResult>;
200
264
  appendPolicyEvaluation(record: PolicyEvaluationRecord): Promise<void>;
201
265
  createAdapterInvocation(record: AdapterInvocationRecord): Promise<void>;
202
266
  getAdapterInvocation(id: string): Promise<AdapterInvocationRecord | undefined>;
@@ -217,7 +281,7 @@ interface PlatformHostSqlClient {
217
281
  query<Row = Record<string, unknown>>(sql: string, values?: unknown[]): Promise<PlatformHostSqlResult<Row>>;
218
282
  }
219
283
  /** Durable mutation-pipeline ledger for Databricks Lakebase or standard Postgres. */
220
- declare class PostgresPlatformHostStore<TDb> implements PlatformHostStore<TDb> {
284
+ declare class PostgresPlatformHostStore<TDb> implements RecoverablePlatformHostStore<TDb>, ApprovalPlatformHostStore<TDb> {
221
285
  readonly db: TDb;
222
286
  private readonly sql;
223
287
  constructor(db: TDb, sql: PlatformHostSqlClient);
@@ -226,6 +290,8 @@ declare class PostgresPlatformHostStore<TDb> implements PlatformHostStore<TDb> {
226
290
  createActionInvocation(input: CreateActionInvocationInput): Promise<ActionInvocationRecord>;
227
291
  getActionInvocation(id: string, tenantId: string, spaceId: string): Promise<ActionInvocationRecord | undefined>;
228
292
  updateActionInvocation(id: string, tenantId: string, spaceId: string, patch: Partial<Pick<ActionInvocationRecord, "status" | "result" | "error">>): Promise<void>;
293
+ recordHitlDecision(actionInvocationId: string, tenantId: string, spaceId: string, decision: HitlDecisionEvidence): Promise<ActionInvocationRecord>;
294
+ beginApprovalDecision(input: BeginApprovalDecisionInput): Promise<ApprovalDecisionTransitionResult>;
229
295
  appendPolicyEvaluation(record: PolicyEvaluationRecord): Promise<void>;
230
296
  createAdapterInvocation(record: AdapterInvocationRecord): Promise<void>;
231
297
  getAdapterInvocation(id: string): Promise<AdapterInvocationRecord | undefined>;
@@ -248,4 +314,4 @@ declare function runPlatformActionWorkerCycle<TDb>(options: PlatformActionWorker
248
314
  /** Poll until aborted. Each iteration is bounded and waits without busy-spinning. */
249
315
  declare function runPlatformActionWorker<TDb>(options: PlatformActionWorkerOptions<TDb>): Promise<void>;
250
316
 
251
- export { type ActionAuthorizationInput, type ActionInvocationRecord, type AdapterInvocationRecord, type AdapterInvocationStatus, type ClaimActionInvocationsInput, type CreateActionInvocationInput, type DispatchActionInput, type DispatchActionResult, type ExecuteActionResult, type GovernedActionHost, type ListActionInvocationsInput, MemoryPlatformHostStore, type PendingAssetEvent, type PlatformActionDispatcher, type PlatformActionWorkerCycleResult, type PlatformActionWorkerOptions, type PlatformHostAuthorization, type PlatformHostOptions, type PlatformHostSqlClient, type PlatformHostSqlResult, type PlatformHostStore, type PolicyEvaluationRecord, PostgresPlatformHostStore, type RecoverablePlatformHostStore, type SubmitActionInput, type SubmitActionResult, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
317
+ 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 GovernedActionHost, type HitlDecisionEvidence, type ListActionInvocationsInput, MemoryPlatformHostStore, type PendingAssetEvent, type PersistedActionApprovalDecision, type PlatformActionDispatcher, type PlatformActionWorkerCycleResult, type PlatformActionWorkerOptions, type PlatformHostAuthorization, type PlatformHostOptions, type PlatformHostSqlClient, type PlatformHostSqlResult, type PlatformHostStore, type PolicyEvaluationRecord, PostgresPlatformHostStore, type RecoverablePlatformHostStore, type SubmitActionInput, type SubmitActionResult, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
package/dist/index.js CHANGED
@@ -45,7 +45,9 @@ function createGovernedActionHost(options) {
45
45
  status: durableInvocation.status,
46
46
  workflowId: durableWorkflowId,
47
47
  result: durableInvocation.result,
48
- ...durableInvocation.error ? { error: durableInvocation.error } : {}
48
+ ...durableInvocation.error ? { error: durableInvocation.error } : {},
49
+ ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
50
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
49
51
  };
50
52
  }
51
53
  if (options.dispatcher) {
@@ -60,7 +62,9 @@ function createGovernedActionHost(options) {
60
62
  actionInvocationId: durableInvocation.id,
61
63
  status: durableInvocation.status,
62
64
  workflowId: dispatched.workflowId,
63
- ...dispatched.runId ? { runId: dispatched.runId } : {}
65
+ ...dispatched.runId ? { runId: dispatched.runId } : {},
66
+ ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
67
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
64
68
  };
65
69
  } catch (error) {
66
70
  const message = errorMessage(error);
@@ -80,19 +84,23 @@ function createGovernedActionHost(options) {
80
84
  );
81
85
  return { ...executed, workflowId: durableWorkflowId };
82
86
  }
83
- async function executeInvocation(actionInvocationId, tenantId, spaceId) {
84
- const invocation = await options.store.getActionInvocation(
87
+ async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}) {
88
+ const loadedInvocation = await options.store.getActionInvocation(
85
89
  actionInvocationId,
86
90
  tenantId,
87
91
  spaceId
88
92
  );
89
- if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
90
- if (isTerminal(invocation.status)) {
93
+ if (!loadedInvocation) {
94
+ throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
95
+ }
96
+ let invocation = loadedInvocation;
97
+ if (isTerminal(invocation.status) || invocation.status === "waiting_for_approval") {
98
+ return actionResult(invocation);
99
+ }
100
+ if (invocation.status === "running" && invocation.leaseOwner && executionOptions.leaseOwner !== invocation.leaseOwner) {
91
101
  return {
92
- actionInvocationId,
93
- status: invocation.status,
94
- result: invocation.result,
95
- ...invocation.error ? { error: invocation.error } : {}
102
+ ...actionResult(invocation),
103
+ error: `Invocation is leased by ${invocation.leaseOwner}`
96
104
  };
97
105
  }
98
106
  const action = resolveAction(invocation.actionId);
@@ -106,15 +114,68 @@ function createGovernedActionHost(options) {
106
114
  `Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
107
115
  );
108
116
  }
109
- await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
110
- status: "running"
111
- });
117
+ if (invocation.status !== "running") {
118
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
119
+ status: "running"
120
+ });
121
+ }
112
122
  try {
113
123
  const parsed = action.schema.safeParse(invocation.parameters);
114
124
  if (!parsed.success) {
115
125
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
116
126
  return fail(invocation, "validation_failed", message);
117
127
  }
128
+ if (invocation.actorType === "agent" && options.hitlEvaluator) {
129
+ const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
130
+ const approvalStore = asApprovalStore(options.store);
131
+ if (!approvalStore) {
132
+ return fail(
133
+ invocation,
134
+ "failed",
135
+ "The configured HITL evaluator requires a store with approval persistence support."
136
+ );
137
+ }
138
+ if (!invocation.hitlRoute) {
139
+ const decision = await options.hitlEvaluator({
140
+ actionId: action.actionId,
141
+ actorId: invocation.actorId,
142
+ actorType: invocation.actorType,
143
+ tenantId,
144
+ spaceId,
145
+ parameters: hitlParameters,
146
+ ...numberValue(hitlParameters.confidence) !== void 0 ? { confidence: numberValue(hitlParameters.confidence) } : {},
147
+ ...isRiskTier(hitlParameters.riskTier) ? { riskTier: hitlParameters.riskTier } : {},
148
+ ...stringValue(hitlParameters.agentSessionId) ? { agentSessionId: stringValue(hitlParameters.agentSessionId) } : {},
149
+ ...stringValue(hitlParameters.agentRunId) ? { agentRunId: stringValue(hitlParameters.agentRunId) } : {}
150
+ });
151
+ invocation = await approvalStore.recordHitlDecision(
152
+ actionInvocationId,
153
+ tenantId,
154
+ spaceId,
155
+ {
156
+ ...decision,
157
+ ...options.hitlPolicyVersion ? { policyVersion: options.hitlPolicyVersion } : {},
158
+ evaluatedAt: now()
159
+ }
160
+ );
161
+ }
162
+ if (invocation.hitlRoute === "rejected") {
163
+ return fail(
164
+ invocation,
165
+ "failed",
166
+ `HITL rejected: ${invocation.hitlReason ?? "no reason provided"}`
167
+ );
168
+ }
169
+ if ((invocation.hitlRoute === "needs-approval" || invocation.hitlRoute === "escalate") && !invocation.approvalDecision?.approved) {
170
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
171
+ status: "waiting_for_approval"
172
+ });
173
+ return {
174
+ ...actionResult(invocation),
175
+ status: "waiting_for_approval"
176
+ };
177
+ }
178
+ }
118
179
  const authorizationInput = toAuthorizationInput(action, invocation);
119
180
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
120
181
  ...authorizationInput,
@@ -327,11 +388,80 @@ function createGovernedActionHost(options) {
327
388
  status: "completed",
328
389
  result
329
390
  });
330
- return { actionInvocationId, status: "completed", result };
391
+ return {
392
+ actionInvocationId,
393
+ status: "completed",
394
+ result,
395
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
396
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
397
+ };
331
398
  } catch (error) {
332
399
  return fail(invocation, "failed", errorMessage(error));
333
400
  }
334
401
  }
402
+ async function resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, decision) {
403
+ if (!decision.approverId.trim()) throw new Error("approverId is required");
404
+ const invocation = await options.store.getActionInvocation(
405
+ actionInvocationId,
406
+ tenantId,
407
+ spaceId
408
+ );
409
+ if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
410
+ if (isTerminal(invocation.status)) return actionResult(invocation);
411
+ if (invocation.status !== "waiting_for_approval") {
412
+ return {
413
+ ...actionResult(invocation),
414
+ error: `Invocation is not waiting for approval (status: ${invocation.status})`
415
+ };
416
+ }
417
+ const action = resolveAction(invocation.actionId);
418
+ if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
419
+ const approvalAuthorizationInput = toAuthorizationInput(action, {
420
+ tenantId,
421
+ spaceId,
422
+ actorId: decision.approverId,
423
+ actorType: decision.approverType
424
+ });
425
+ const entitled = await options.authorization.checkEntitlement(
426
+ approvalAuthorizationInput
427
+ );
428
+ const authorized = options.authorization.authorizeApproval ? await options.authorization.authorizeApproval({
429
+ ...approvalAuthorizationInput,
430
+ decision
431
+ }) : await options.authorization.authorize(approvalAuthorizationInput);
432
+ if (!entitled || !authorized) {
433
+ return {
434
+ ...actionResult(invocation),
435
+ error: `Actor ${decision.approverId} is not authorized to decide this approval`
436
+ };
437
+ }
438
+ const approvalStore = asApprovalStore(options.store);
439
+ if (!approvalStore) {
440
+ return {
441
+ ...actionResult(invocation),
442
+ error: "Approval resume requires a store with approval persistence support."
443
+ };
444
+ }
445
+ const transitionStartedAt = now();
446
+ const decidedAt = decision.decidedAt ?? transitionStartedAt;
447
+ const leaseOwner = `approval-resume-${createFabricId("lease")}`;
448
+ const transition = await approvalStore.beginApprovalDecision({
449
+ actionInvocationId,
450
+ tenantId,
451
+ spaceId,
452
+ decision: { ...decision, decidedAt },
453
+ leaseOwner,
454
+ leaseDurationMs: options.approvalResumeLeaseDurationMs ?? 5 * 6e4,
455
+ now: transitionStartedAt
456
+ });
457
+ const transitioned = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
458
+ if (!transition.applied || !transitioned) {
459
+ if (!transitioned) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
460
+ return actionResult(transitioned);
461
+ }
462
+ if (!decision.approved) return actionResult(transitioned);
463
+ return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
464
+ }
335
465
  async function appendEvent(invocation, event, deduplicationKey) {
336
466
  const timestamp = now();
337
467
  const envelope = {
@@ -364,9 +494,38 @@ function createGovernedActionHost(options) {
364
494
  invocation.spaceId,
365
495
  { status, error }
366
496
  );
367
- return { actionInvocationId: invocation.id, status, error };
497
+ return {
498
+ actionInvocationId: invocation.id,
499
+ status,
500
+ error,
501
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
502
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
503
+ };
368
504
  }
369
- return { submitAction, executeInvocation };
505
+ return { submitAction, executeInvocation, resumeApprovedInvocation };
506
+ }
507
+ function actionResult(invocation) {
508
+ return {
509
+ actionInvocationId: invocation.id,
510
+ status: invocation.status,
511
+ result: invocation.result,
512
+ ...invocation.error ? { error: invocation.error } : {},
513
+ ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
514
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
515
+ };
516
+ }
517
+ function asApprovalStore(store) {
518
+ const candidate = store;
519
+ return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
520
+ }
521
+ function numberValue(value) {
522
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
523
+ }
524
+ function stringValue(value) {
525
+ return typeof value === "string" && value.length > 0 ? value : void 0;
526
+ }
527
+ function isRiskTier(value) {
528
+ return value === "low" || value === "medium" || value === "high";
370
529
  }
371
530
  function toAuthorizationInput(action, input) {
372
531
  return {
@@ -444,6 +603,45 @@ var MemoryPlatformHostStore = class {
444
603
  const record = await this.getActionInvocation(id, tenantId, spaceId);
445
604
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
446
605
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
606
+ if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "validation_failed") {
607
+ delete record.leaseOwner;
608
+ delete record.leaseExpiresAt;
609
+ }
610
+ }
611
+ async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
612
+ const record = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
613
+ if (!record) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
614
+ record.hitlRoute = decision.route;
615
+ record.hitlRiskTier = decision.riskTier;
616
+ record.hitlReason = decision.reason;
617
+ if (decision.policyVersion) record.hitlPolicyVersion = decision.policyVersion;
618
+ record.updatedAt = decision.evaluatedAt;
619
+ return record;
620
+ }
621
+ async beginApprovalDecision(input) {
622
+ const record = await this.getActionInvocation(
623
+ input.actionInvocationId,
624
+ input.tenantId,
625
+ input.spaceId
626
+ );
627
+ if (!record || record.status !== "waiting_for_approval") {
628
+ return { applied: false, ...record ? { invocation: record } : {} };
629
+ }
630
+ record.approvalDecision = input.decision;
631
+ record.updatedAt = input.now;
632
+ if (!input.decision.approved) {
633
+ record.status = "failed";
634
+ record.error = `Approval rejected: ${input.decision.reason ?? "no reason provided"}`;
635
+ delete record.leaseOwner;
636
+ delete record.leaseExpiresAt;
637
+ return { applied: true, invocation: record };
638
+ }
639
+ record.status = "running";
640
+ record.error = void 0;
641
+ record.leaseOwner = input.leaseOwner;
642
+ record.leaseExpiresAt = new Date(input.now.getTime() + input.leaseDurationMs);
643
+ record.attemptCount = Math.max(record.attemptCount, 1);
644
+ return { applied: true, invocation: record };
447
645
  }
448
646
  async appendPolicyEvaluation(record) {
449
647
  if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
@@ -521,14 +719,20 @@ var PostgresPlatformHostStore = class {
521
719
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
522
720
  correlation_id text NOT NULL, causation_id text, idempotency_key text, error text,
523
721
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
524
- lease_expires_at timestamptz,
722
+ lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
723
+ hitl_reason text, hitl_policy_version text, approval_decision jsonb,
525
724
  created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
526
725
  );
527
726
  ALTER TABLE fabric_platform.action_invocations
528
727
  ADD COLUMN IF NOT EXISTS idempotency_key text,
529
728
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
530
729
  ADD COLUMN IF NOT EXISTS lease_owner text,
531
- ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz;
730
+ ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz,
731
+ ADD COLUMN IF NOT EXISTS hitl_route text,
732
+ ADD COLUMN IF NOT EXISTS hitl_risk_tier text,
733
+ ADD COLUMN IF NOT EXISTS hitl_reason text,
734
+ ADD COLUMN IF NOT EXISTS hitl_policy_version text,
735
+ ADD COLUMN IF NOT EXISTS approval_decision jsonb;
532
736
  CREATE UNIQUE INDEX IF NOT EXISTS action_invocations_idempotency_idx
533
737
  ON fabric_platform.action_invocations
534
738
  (tenant_id, space_id, action_id, idempotency_key)
@@ -612,9 +816,9 @@ var PostgresPlatformHostStore = class {
612
816
  `UPDATE fabric_platform.action_invocations SET
613
817
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
614
818
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
615
- lease_owner=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
819
+ lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
616
820
  THEN NULL ELSE lease_owner END,
617
- lease_expires_at=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
821
+ lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
618
822
  THEN NULL ELSE lease_expires_at END,
619
823
  updated_at=now()
620
824
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -629,6 +833,54 @@ var PostgresPlatformHostStore = class {
629
833
  ]
630
834
  );
631
835
  }
836
+ async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
837
+ const result = await this.sql.query(
838
+ `UPDATE fabric_platform.action_invocations SET
839
+ hitl_route=$4, hitl_risk_tier=$5, hitl_reason=$6,
840
+ hitl_policy_version=$7, updated_at=$8
841
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3
842
+ RETURNING *`,
843
+ [
844
+ actionInvocationId,
845
+ tenantId,
846
+ spaceId,
847
+ decision.route,
848
+ decision.riskTier,
849
+ decision.reason,
850
+ decision.policyVersion ?? null,
851
+ decision.evaluatedAt
852
+ ]
853
+ );
854
+ const row = result.rows[0];
855
+ if (!row) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
856
+ return toActionRecord(row);
857
+ }
858
+ async beginApprovalDecision(input) {
859
+ const result = await this.sql.query(
860
+ `UPDATE fabric_platform.action_invocations SET
861
+ approval_decision=$4::jsonb,
862
+ status=CASE WHEN $5::boolean THEN 'running' ELSE 'failed' END,
863
+ error=CASE WHEN $5::boolean THEN NULL ELSE $6 END,
864
+ lease_owner=CASE WHEN $5::boolean THEN $7 ELSE NULL END,
865
+ lease_expires_at=CASE WHEN $5::boolean THEN $8 ELSE NULL END,
866
+ attempt_count=CASE WHEN $5::boolean THEN GREATEST(attempt_count,1) ELSE attempt_count END,
867
+ updated_at=$9
868
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 AND status='waiting_for_approval'
869
+ RETURNING *`,
870
+ [
871
+ input.actionInvocationId,
872
+ input.tenantId,
873
+ input.spaceId,
874
+ JSON.stringify({ ...input.decision, decidedAt: input.decision.decidedAt.toISOString() }),
875
+ input.decision.approved,
876
+ `Approval rejected: ${input.decision.reason ?? "no reason provided"}`,
877
+ input.leaseOwner,
878
+ new Date(input.now.getTime() + input.leaseDurationMs),
879
+ input.now
880
+ ]
881
+ );
882
+ return result.rows[0] ? { applied: true, invocation: toActionRecord(result.rows[0]) } : { applied: false };
883
+ }
632
884
  async appendPolicyEvaluation(record) {
633
885
  await this.sql.query(
634
886
  `INSERT INTO fabric_platform.policy_evaluations
@@ -836,11 +1088,27 @@ function toActionRecord(row) {
836
1088
  attemptCount: Number(row.attempt_count ?? 0),
837
1089
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
838
1090
  ...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
1091
+ ...row.hitl_route ? { hitlRoute: String(row.hitl_route) } : {},
1092
+ ...row.hitl_risk_tier ? { hitlRiskTier: String(row.hitl_risk_tier) } : {},
1093
+ ...row.hitl_reason ? { hitlReason: String(row.hitl_reason) } : {},
1094
+ ...row.hitl_policy_version ? { hitlPolicyVersion: String(row.hitl_policy_version) } : {},
1095
+ ...row.approval_decision ? { approvalDecision: toApprovalDecision(row.approval_decision) } : {},
839
1096
  ...row.error ? { error: String(row.error) } : {},
840
1097
  createdAt: new Date(row.created_at),
841
1098
  updatedAt: new Date(row.updated_at)
842
1099
  };
843
1100
  }
1101
+ function toApprovalDecision(value) {
1102
+ const decision = value;
1103
+ return {
1104
+ approved: Boolean(decision.approved),
1105
+ approverId: String(decision.approverId),
1106
+ approverType: String(decision.approverType),
1107
+ ...decision.reason ? { reason: String(decision.reason) } : {},
1108
+ ...decision.editsReference ? { editsReference: String(decision.editsReference) } : {},
1109
+ decidedAt: new Date(decision.decidedAt)
1110
+ };
1111
+ }
844
1112
  function toEventRecord(row) {
845
1113
  return {
846
1114
  id: String(row.id),
@@ -881,21 +1149,29 @@ async function runPlatformActionWorkerCycle(options) {
881
1149
  });
882
1150
  let completed = 0;
883
1151
  let failed = 0;
1152
+ let waitingForApproval = 0;
884
1153
  for (const invocation of claimed) {
885
1154
  try {
886
1155
  const result = await options.host.executeInvocation(
887
1156
  invocation.id,
888
1157
  invocation.tenantId,
889
- invocation.spaceId
1158
+ invocation.spaceId,
1159
+ { leaseOwner: options.workerId }
890
1160
  );
891
1161
  if (result.status === "completed") completed += 1;
1162
+ else if (result.status === "waiting_for_approval") waitingForApproval += 1;
892
1163
  else failed += 1;
893
1164
  } catch (error) {
894
1165
  failed += 1;
895
1166
  options.onError?.(error, invocation);
896
1167
  }
897
1168
  }
898
- return { claimed: claimed.length, completed, failed };
1169
+ return {
1170
+ claimed: claimed.length,
1171
+ completed,
1172
+ failed,
1173
+ ...waitingForApproval > 0 ? { waitingForApproval } : {}
1174
+ };
899
1175
  }
900
1176
  async function runPlatformActionWorker(options) {
901
1177
  while (!options.signal?.aborted) {