@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.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,34 +84,98 @@ function createGovernedActionHost(options) {
|
|
|
80
84
|
);
|
|
81
85
|
return { ...executed, workflowId: durableWorkflowId };
|
|
82
86
|
}
|
|
83
|
-
async function executeInvocation(actionInvocationId, tenantId, spaceId) {
|
|
84
|
-
const
|
|
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 (!
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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);
|
|
99
107
|
if (!action) {
|
|
100
108
|
return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
|
|
101
109
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
if (invocation.attemptCount > 1 && !action.idempotent) {
|
|
111
|
+
return fail(
|
|
112
|
+
invocation,
|
|
113
|
+
"failed",
|
|
114
|
+
`Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (invocation.status !== "running") {
|
|
118
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
119
|
+
status: "running"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
105
122
|
try {
|
|
106
123
|
const parsed = action.schema.safeParse(invocation.parameters);
|
|
107
124
|
if (!parsed.success) {
|
|
108
125
|
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
109
126
|
return fail(invocation, "validation_failed", message);
|
|
110
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
|
+
}
|
|
111
179
|
const authorizationInput = toAuthorizationInput(action, invocation);
|
|
112
180
|
const definitions = options.resolvePolicies ? await options.resolvePolicies({
|
|
113
181
|
...authorizationInput,
|
|
@@ -127,7 +195,7 @@ function createGovernedActionHost(options) {
|
|
|
127
195
|
});
|
|
128
196
|
for (const outcome of outcomes) {
|
|
129
197
|
await options.store.appendPolicyEvaluation({
|
|
130
|
-
id:
|
|
198
|
+
id: lifecycleId("pol", actionInvocationId, outcome.policyId),
|
|
131
199
|
actionInvocationId,
|
|
132
200
|
tenantId,
|
|
133
201
|
spaceId,
|
|
@@ -143,7 +211,7 @@ function createGovernedActionHost(options) {
|
|
|
143
211
|
subjectType: "ActionInvocation",
|
|
144
212
|
subjectId: actionInvocationId,
|
|
145
213
|
payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
|
|
146
|
-
});
|
|
214
|
+
}, `compliance:${aggregate.policyId}`);
|
|
147
215
|
return fail(invocation, "blocked_by_policy", reason);
|
|
148
216
|
}
|
|
149
217
|
const binding = action.stateMachine;
|
|
@@ -162,7 +230,8 @@ function createGovernedActionHost(options) {
|
|
|
162
230
|
targetState,
|
|
163
231
|
action.actionId
|
|
164
232
|
);
|
|
165
|
-
|
|
233
|
+
const replayingAppliedTransition = action.idempotent && currentState === targetState;
|
|
234
|
+
if (!transition.valid && !replayingAppliedTransition) {
|
|
166
235
|
return fail(invocation, "failed", transition.error ?? "Invalid state transition");
|
|
167
236
|
}
|
|
168
237
|
}
|
|
@@ -191,16 +260,22 @@ function createGovernedActionHost(options) {
|
|
|
191
260
|
data = handlerResult.data ?? {};
|
|
192
261
|
domainEvents = extractEvents(data);
|
|
193
262
|
if (action.eventPhase !== "after_adapters") {
|
|
194
|
-
for (const event of domainEvents)
|
|
263
|
+
for (const [index, event] of domainEvents.entries()) {
|
|
264
|
+
await appendEvent(invocation, event, `domain:${index}`);
|
|
265
|
+
}
|
|
195
266
|
}
|
|
196
267
|
} catch (error) {
|
|
197
268
|
return fail(invocation, "failed", errorMessage(error));
|
|
198
269
|
}
|
|
199
|
-
for (const step of action.adapterSteps ?? []) {
|
|
270
|
+
for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
|
|
200
271
|
const input = step.getInput(parsed.data, data);
|
|
201
272
|
if (!input) continue;
|
|
202
273
|
const adapter = adapters.require(step.adapterType, step.operation);
|
|
203
|
-
const adapterInvocationId =
|
|
274
|
+
const adapterInvocationId = lifecycleId("adp", actionInvocationId, String(stepIndex));
|
|
275
|
+
const previousAdapterInvocation = await options.store.getAdapterInvocation(
|
|
276
|
+
adapterInvocationId
|
|
277
|
+
);
|
|
278
|
+
if (previousAdapterInvocation?.status === "succeeded") continue;
|
|
204
279
|
const adapterEventSubject = options.adapterEventSubject?.(
|
|
205
280
|
action.actionId,
|
|
206
281
|
parsed.data,
|
|
@@ -233,7 +308,7 @@ function createGovernedActionHost(options) {
|
|
|
233
308
|
subjectType: adapterEventSubject.subjectType,
|
|
234
309
|
subjectId: adapterEventSubject.subjectId,
|
|
235
310
|
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
236
|
-
});
|
|
311
|
+
}, `adapter:${stepIndex}:started`);
|
|
237
312
|
try {
|
|
238
313
|
const result2 = await executeWithAdapterRetry({
|
|
239
314
|
policy: step.retryPolicy ?? adapter.retryPolicy,
|
|
@@ -268,7 +343,7 @@ function createGovernedActionHost(options) {
|
|
|
268
343
|
subjectType: adapterEventSubject.subjectType,
|
|
269
344
|
subjectId: adapterEventSubject.subjectId,
|
|
270
345
|
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
271
|
-
});
|
|
346
|
+
}, `adapter:${stepIndex}:failed`);
|
|
272
347
|
return fail(invocation, "failed", result2.error ?? "Adapter failed");
|
|
273
348
|
}
|
|
274
349
|
const resultRecord = result2;
|
|
@@ -292,7 +367,7 @@ function createGovernedActionHost(options) {
|
|
|
292
367
|
input: recordedInput,
|
|
293
368
|
output
|
|
294
369
|
}
|
|
295
|
-
});
|
|
370
|
+
}, `adapter:${stepIndex}:succeeded`);
|
|
296
371
|
} catch (error) {
|
|
297
372
|
const message = errorMessage(error);
|
|
298
373
|
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
@@ -304,22 +379,93 @@ function createGovernedActionHost(options) {
|
|
|
304
379
|
}
|
|
305
380
|
}
|
|
306
381
|
if (action.eventPhase === "after_adapters") {
|
|
307
|
-
for (const event of domainEvents)
|
|
382
|
+
for (const [index, event] of domainEvents.entries()) {
|
|
383
|
+
await appendEvent(invocation, event, `domain:${index}`);
|
|
384
|
+
}
|
|
308
385
|
}
|
|
309
386
|
const result = withoutPrivateHostFields(data);
|
|
310
387
|
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
311
388
|
status: "completed",
|
|
312
389
|
result
|
|
313
390
|
});
|
|
314
|
-
return {
|
|
391
|
+
return {
|
|
392
|
+
actionInvocationId,
|
|
393
|
+
status: "completed",
|
|
394
|
+
result,
|
|
395
|
+
...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
|
|
396
|
+
...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
|
|
397
|
+
};
|
|
315
398
|
} catch (error) {
|
|
316
399
|
return fail(invocation, "failed", errorMessage(error));
|
|
317
400
|
}
|
|
318
401
|
}
|
|
319
|
-
async function
|
|
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
|
+
}
|
|
465
|
+
async function appendEvent(invocation, event, deduplicationKey) {
|
|
320
466
|
const timestamp = now();
|
|
321
467
|
const envelope = {
|
|
322
|
-
id:
|
|
468
|
+
id: lifecycleId("evt", invocation.id, deduplicationKey),
|
|
323
469
|
tenantId: invocation.tenantId,
|
|
324
470
|
spaceId: invocation.spaceId,
|
|
325
471
|
eventType: event.eventType,
|
|
@@ -348,9 +494,38 @@ function createGovernedActionHost(options) {
|
|
|
348
494
|
invocation.spaceId,
|
|
349
495
|
{ status, error }
|
|
350
496
|
);
|
|
351
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
actionInvocationId: invocation.id,
|
|
499
|
+
status,
|
|
500
|
+
error,
|
|
501
|
+
...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
|
|
502
|
+
...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
|
|
503
|
+
};
|
|
352
504
|
}
|
|
353
|
-
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";
|
|
354
529
|
}
|
|
355
530
|
function toAuthorizationInput(action, input) {
|
|
356
531
|
return {
|
|
@@ -389,6 +564,10 @@ function errorMessage(error) {
|
|
|
389
564
|
function isRecord(value) {
|
|
390
565
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
391
566
|
}
|
|
567
|
+
function lifecycleId(prefix, invocationId, key) {
|
|
568
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
|
|
569
|
+
return `${prefix}_${invocationId}_${safeKey}`;
|
|
570
|
+
}
|
|
392
571
|
|
|
393
572
|
// src/memory-store.ts
|
|
394
573
|
var MemoryPlatformHostStore = class {
|
|
@@ -424,19 +603,64 @@ var MemoryPlatformHostStore = class {
|
|
|
424
603
|
const record = await this.getActionInvocation(id, tenantId, spaceId);
|
|
425
604
|
if (!record) throw new Error(`ActionInvocation not found: ${id}`);
|
|
426
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 };
|
|
427
645
|
}
|
|
428
646
|
async appendPolicyEvaluation(record) {
|
|
647
|
+
if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
|
|
429
648
|
this.policyEvaluations.push(record);
|
|
430
649
|
}
|
|
431
650
|
async createAdapterInvocation(record) {
|
|
651
|
+
if (this.adapterInvocations.some((candidate) => candidate.id === record.id)) return;
|
|
432
652
|
this.adapterInvocations.push(record);
|
|
433
653
|
}
|
|
654
|
+
async getAdapterInvocation(id) {
|
|
655
|
+
return this.adapterInvocations.find((candidate) => candidate.id === id);
|
|
656
|
+
}
|
|
434
657
|
async updateAdapterInvocation(id, patch) {
|
|
435
658
|
const record = this.adapterInvocations.find((candidate) => candidate.id === id);
|
|
436
659
|
if (!record) throw new Error(`AdapterInvocation not found: ${id}`);
|
|
437
660
|
Object.assign(record, patch);
|
|
438
661
|
}
|
|
439
662
|
async appendEvent(event) {
|
|
663
|
+
if (this.events.some((candidate) => candidate.id === event.id)) return;
|
|
440
664
|
this.events.push(event);
|
|
441
665
|
}
|
|
442
666
|
async nextEventSequence(tenantId, spaceId) {
|
|
@@ -495,14 +719,20 @@ var PostgresPlatformHostStore = class {
|
|
|
495
719
|
parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
496
720
|
correlation_id text NOT NULL, causation_id text, idempotency_key text, error text,
|
|
497
721
|
attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
|
|
498
|
-
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,
|
|
499
724
|
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
|
|
500
725
|
);
|
|
501
726
|
ALTER TABLE fabric_platform.action_invocations
|
|
502
727
|
ADD COLUMN IF NOT EXISTS idempotency_key text,
|
|
503
728
|
ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
|
|
504
729
|
ADD COLUMN IF NOT EXISTS lease_owner text,
|
|
505
|
-
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;
|
|
506
736
|
CREATE UNIQUE INDEX IF NOT EXISTS action_invocations_idempotency_idx
|
|
507
737
|
ON fabric_platform.action_invocations
|
|
508
738
|
(tenant_id, space_id, action_id, idempotency_key)
|
|
@@ -586,9 +816,9 @@ var PostgresPlatformHostStore = class {
|
|
|
586
816
|
`UPDATE fabric_platform.action_invocations SET
|
|
587
817
|
status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
|
|
588
818
|
error=CASE WHEN $6::boolean THEN $7 ELSE error END,
|
|
589
|
-
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')
|
|
590
820
|
THEN NULL ELSE lease_owner END,
|
|
591
|
-
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')
|
|
592
822
|
THEN NULL ELSE lease_expires_at END,
|
|
593
823
|
updated_at=now()
|
|
594
824
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
@@ -603,11 +833,59 @@ var PostgresPlatformHostStore = class {
|
|
|
603
833
|
]
|
|
604
834
|
);
|
|
605
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
|
+
}
|
|
606
884
|
async appendPolicyEvaluation(record) {
|
|
607
885
|
await this.sql.query(
|
|
608
886
|
`INSERT INTO fabric_platform.policy_evaluations
|
|
609
887
|
(id,action_invocation_id,tenant_id,space_id,outcome,created_at)
|
|
610
|
-
VALUES ($1,$2,$3,$4,$5::jsonb,$6)`,
|
|
888
|
+
VALUES ($1,$2,$3,$4,$5::jsonb,$6) ON CONFLICT (id) DO NOTHING`,
|
|
611
889
|
[
|
|
612
890
|
record.id,
|
|
613
891
|
record.actionInvocationId,
|
|
@@ -623,7 +901,8 @@ var PostgresPlatformHostStore = class {
|
|
|
623
901
|
`INSERT INTO fabric_platform.adapter_invocations
|
|
624
902
|
(id,action_invocation_id,tenant_id,space_id,adapter_type,operation,vendor,status,
|
|
625
903
|
input,output,error,attempt,created_at,updated_at)
|
|
626
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14)
|
|
904
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14)
|
|
905
|
+
ON CONFLICT (id) DO NOTHING`,
|
|
627
906
|
[
|
|
628
907
|
record.id,
|
|
629
908
|
record.actionInvocationId,
|
|
@@ -642,6 +921,13 @@ var PostgresPlatformHostStore = class {
|
|
|
642
921
|
]
|
|
643
922
|
);
|
|
644
923
|
}
|
|
924
|
+
async getAdapterInvocation(id) {
|
|
925
|
+
const result = await this.sql.query(
|
|
926
|
+
`SELECT * FROM fabric_platform.adapter_invocations WHERE id=$1`,
|
|
927
|
+
[id]
|
|
928
|
+
);
|
|
929
|
+
return result.rows[0] ? toAdapterRecord(result.rows[0]) : void 0;
|
|
930
|
+
}
|
|
645
931
|
async updateAdapterInvocation(id, patch) {
|
|
646
932
|
await this.sql.query(
|
|
647
933
|
`UPDATE fabric_platform.adapter_invocations SET
|
|
@@ -665,7 +951,8 @@ var PostgresPlatformHostStore = class {
|
|
|
665
951
|
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
666
952
|
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
667
953
|
correlation_id,causation_id)
|
|
668
|
-
|
|
954
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
|
|
955
|
+
ON CONFLICT (id) DO NOTHING`,
|
|
669
956
|
[
|
|
670
957
|
event.id,
|
|
671
958
|
event.tenantId,
|
|
@@ -765,6 +1052,24 @@ var PostgresPlatformHostStore = class {
|
|
|
765
1052
|
return result.rows.map(toEventRecord);
|
|
766
1053
|
}
|
|
767
1054
|
};
|
|
1055
|
+
function toAdapterRecord(row) {
|
|
1056
|
+
return {
|
|
1057
|
+
id: String(row.id),
|
|
1058
|
+
actionInvocationId: String(row.action_invocation_id),
|
|
1059
|
+
tenantId: String(row.tenant_id),
|
|
1060
|
+
spaceId: String(row.space_id),
|
|
1061
|
+
adapterType: String(row.adapter_type),
|
|
1062
|
+
operation: String(row.operation),
|
|
1063
|
+
vendor: String(row.vendor),
|
|
1064
|
+
status: String(row.status),
|
|
1065
|
+
input: row.input,
|
|
1066
|
+
...row.output ? { output: row.output } : {},
|
|
1067
|
+
...row.error ? { error: String(row.error) } : {},
|
|
1068
|
+
attempt: Number(row.attempt),
|
|
1069
|
+
createdAt: new Date(row.created_at),
|
|
1070
|
+
updatedAt: new Date(row.updated_at)
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
768
1073
|
function toActionRecord(row) {
|
|
769
1074
|
return {
|
|
770
1075
|
id: String(row.id),
|
|
@@ -783,11 +1088,27 @@ function toActionRecord(row) {
|
|
|
783
1088
|
attemptCount: Number(row.attempt_count ?? 0),
|
|
784
1089
|
...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
|
|
785
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) } : {},
|
|
786
1096
|
...row.error ? { error: String(row.error) } : {},
|
|
787
1097
|
createdAt: new Date(row.created_at),
|
|
788
1098
|
updatedAt: new Date(row.updated_at)
|
|
789
1099
|
};
|
|
790
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
|
+
}
|
|
791
1112
|
function toEventRecord(row) {
|
|
792
1113
|
return {
|
|
793
1114
|
id: String(row.id),
|
|
@@ -828,21 +1149,29 @@ async function runPlatformActionWorkerCycle(options) {
|
|
|
828
1149
|
});
|
|
829
1150
|
let completed = 0;
|
|
830
1151
|
let failed = 0;
|
|
1152
|
+
let waitingForApproval = 0;
|
|
831
1153
|
for (const invocation of claimed) {
|
|
832
1154
|
try {
|
|
833
1155
|
const result = await options.host.executeInvocation(
|
|
834
1156
|
invocation.id,
|
|
835
1157
|
invocation.tenantId,
|
|
836
|
-
invocation.spaceId
|
|
1158
|
+
invocation.spaceId,
|
|
1159
|
+
{ leaseOwner: options.workerId }
|
|
837
1160
|
);
|
|
838
1161
|
if (result.status === "completed") completed += 1;
|
|
1162
|
+
else if (result.status === "waiting_for_approval") waitingForApproval += 1;
|
|
839
1163
|
else failed += 1;
|
|
840
1164
|
} catch (error) {
|
|
841
1165
|
failed += 1;
|
|
842
1166
|
options.onError?.(error, invocation);
|
|
843
1167
|
}
|
|
844
1168
|
}
|
|
845
|
-
return {
|
|
1169
|
+
return {
|
|
1170
|
+
claimed: claimed.length,
|
|
1171
|
+
completed,
|
|
1172
|
+
failed,
|
|
1173
|
+
...waitingForApproval > 0 ? { waitingForApproval } : {}
|
|
1174
|
+
};
|
|
846
1175
|
}
|
|
847
1176
|
async function runPlatformActionWorker(options) {
|
|
848
1177
|
while (!options.signal?.aborted) {
|