@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/CHANGELOG.md +14 -0
- package/README.md +52 -2
- package/dist/index.cjs +366 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -5
- package/dist/index.d.ts +74 -5
- package/dist/index.js +366 -37
- package/dist/index.js.map +1 -1
- package/package.json +66 -62
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
|
|
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 (!
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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)
|
|
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 {
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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) {
|