@fabricorg/platform-host 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AdapterRegistry, resolveAction, createFabricId, evaluatePolicyDefinitions, aggregatePolicyOutcomes, validateTransition, executeWithAdapterRetry, resolveStateMachine } from '@fabricorg/platform';
1
+ import { AdapterRegistry, resolveAction, createFabricId, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, validateTransition, executeWithAdapterRetry, resolveStateMachine } from '@fabricorg/platform';
2
2
 
3
3
  // src/host.ts
4
4
  var DEFAULT_EXTRACT_EVENTS = (data) => {
@@ -127,6 +127,23 @@ function createGovernedActionHost(options) {
127
127
  const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
128
128
  return fail(invocation, "validation_failed", message);
129
129
  }
130
+ if (options.resolveMutationGovernance && !invocation.mutationFootprint) {
131
+ const governanceStore2 = asGovernanceStore(options.store);
132
+ if (!governanceStore2) {
133
+ return fail(invocation, "failed", "The configured mutation-governance resolver requires a governance-capable store.");
134
+ }
135
+ const context = await options.resolveMutationGovernance({
136
+ actionInvocationId,
137
+ actionId: action.actionId,
138
+ tenantId,
139
+ spaceId,
140
+ actorId: invocation.actorId,
141
+ actorType: invocation.actorType,
142
+ parameters: parsed.data
143
+ });
144
+ assertMutationGovernanceContext(context);
145
+ invocation = await governanceStore2.recordMutationGovernance(actionInvocationId, tenantId, spaceId, context);
146
+ }
130
147
  if (invocation.actorType === "agent" && options.hitlEvaluator) {
131
148
  const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
132
149
  const approvalStore = asApprovalStore(options.store);
@@ -217,6 +234,26 @@ function createGovernedActionHost(options) {
217
234
  }, `compliance:${aggregate.policyId}`);
218
235
  return fail(invocation, "blocked_by_policy", reason);
219
236
  }
237
+ if (options.resolvePolicyObligations) {
238
+ const governanceStore2 = asGovernanceStore(options.store);
239
+ if (!governanceStore2) {
240
+ return fail(invocation, "failed", "Policy obligations require a governance-capable store.");
241
+ }
242
+ const existing = await governanceStore2.listPolicyObligations(actionInvocationId, tenantId, spaceId);
243
+ if (existing.length === 0) {
244
+ const createdAt = now();
245
+ const obligations = await options.resolvePolicyObligations(outcomes);
246
+ await governanceStore2.appendPolicyObligations(obligations.map((obligation) => ({
247
+ ...obligation,
248
+ status: obligation.status ?? "pending",
249
+ actionInvocationId,
250
+ tenantId,
251
+ spaceId,
252
+ createdAt,
253
+ updatedAt: createdAt
254
+ })));
255
+ }
256
+ }
220
257
  const binding = action.stateMachine;
221
258
  if (binding) {
222
259
  const entityId = binding.getEntityId(parsed.data);
@@ -372,6 +409,22 @@ function createGovernedActionHost(options) {
372
409
  output,
373
410
  updatedAt: now()
374
411
  });
412
+ if (options.extractExecutionAttestation) {
413
+ const attestation = await options.extractExecutionAttestation({
414
+ actionInvocationId,
415
+ adapterInvocationId,
416
+ adapterType: step.adapterType,
417
+ operation: step.operation,
418
+ vendor: adapter.vendor,
419
+ input: recordedInput,
420
+ startedAt,
421
+ completedAt: now(),
422
+ output
423
+ });
424
+ if (attestation) {
425
+ await recordExecutionAttestation(actionInvocationId, tenantId, spaceId, attestation, adapterInvocationId);
426
+ }
427
+ }
375
428
  await appendEvent(invocation, {
376
429
  eventType: "AdapterInvocationSucceeded",
377
430
  subjectType: adapterEventSubject.subjectType,
@@ -398,6 +451,14 @@ function createGovernedActionHost(options) {
398
451
  await appendEvent(invocation, event, `domain:${index}`, action.version);
399
452
  }
400
453
  }
454
+ const governanceStore = asGovernanceStore(options.store);
455
+ if (governanceStore && options.resolvePolicyObligations) {
456
+ const obligations = await governanceStore.listPolicyObligations(actionInvocationId, tenantId, spaceId);
457
+ const unsatisfied = obligations.filter((obligation) => (obligation.required ?? true) && obligation.status !== "satisfied" && obligation.status !== "waived");
458
+ if (unsatisfied.length > 0) {
459
+ return fail(invocation, "failed", `Unsatisfied policy obligations: ${unsatisfied.map((item) => item.id).join(", ")}`);
460
+ }
461
+ }
401
462
  const result = withoutPrivateHostFields(data, eventResultFields);
402
463
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
403
464
  status: "completed",
@@ -477,6 +538,38 @@ function createGovernedActionHost(options) {
477
538
  if (!decision.approved) return actionResult(transitioned);
478
539
  return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
479
540
  }
541
+ async function recordExecutionAttestation(actionInvocationId, tenantId, spaceId, attestation, adapterInvocationId) {
542
+ const governanceStore = asGovernanceStore(options.store);
543
+ if (!governanceStore) throw new Error("Execution attestations require a governance-capable store.");
544
+ const invocation = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
545
+ if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
546
+ const id = lifecycleId("att", actionInvocationId, adapterInvocationId ?? `${attestation.provider}:${attestation.operation}:${attestation.externalOperationId ?? "external"}`);
547
+ await governanceStore.appendExecutionAttestation({
548
+ ...attestation,
549
+ id,
550
+ actionInvocationId,
551
+ tenantId,
552
+ spaceId,
553
+ ...adapterInvocationId ? { adapterInvocationId } : {},
554
+ recordedAt: now()
555
+ });
556
+ for (const obligationId of attestation.satisfiesObligations ?? []) {
557
+ await governanceStore.updatePolicyObligation(actionInvocationId, obligationId, "satisfied", attestation.evidenceReferences);
558
+ }
559
+ }
560
+ async function recordExternalReconciliation(actionInvocationId, tenantId, spaceId, reconciliation) {
561
+ const governanceStore = asGovernanceStore(options.store);
562
+ if (!governanceStore) throw new Error("External reconciliation requires a governance-capable store.");
563
+ const invocation = await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
564
+ if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
565
+ await governanceStore.appendExternalReconciliation({
566
+ ...reconciliation,
567
+ id: lifecycleId("rec", actionInvocationId, `${reconciliation.provider}:${reconciliation.attempt}`),
568
+ actionInvocationId,
569
+ tenantId,
570
+ spaceId
571
+ });
572
+ }
480
573
  async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1) {
481
574
  const timestamp = now();
482
575
  const envelope = {
@@ -517,7 +610,7 @@ function createGovernedActionHost(options) {
517
610
  ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
518
611
  };
519
612
  }
520
- return { submitAction, executeInvocation, resumeApprovedInvocation };
613
+ return { submitAction, executeInvocation, resumeApprovedInvocation, recordExecutionAttestation, recordExternalReconciliation };
521
614
  }
522
615
  function actionResult(invocation) {
523
616
  return {
@@ -533,6 +626,10 @@ function asApprovalStore(store) {
533
626
  const candidate = store;
534
627
  return typeof candidate.recordHitlDecision === "function" && typeof candidate.beginApprovalDecision === "function" ? store : void 0;
535
628
  }
629
+ function asGovernanceStore(store) {
630
+ const candidate = store;
631
+ return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
632
+ }
536
633
  function numberValue(value) {
537
634
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
538
635
  }
@@ -595,6 +692,9 @@ var MemoryPlatformHostStore = class {
595
692
  policyEvaluations = [];
596
693
  adapterInvocations = [];
597
694
  events = [];
695
+ policyObligations = [];
696
+ executionAttestations = [];
697
+ externalReconciliations = [];
598
698
  async transaction(run) {
599
699
  return run(this.db);
600
700
  }
@@ -663,6 +763,40 @@ var MemoryPlatformHostStore = class {
663
763
  if (this.policyEvaluations.some((candidate) => candidate.id === record.id)) return;
664
764
  this.policyEvaluations.push(record);
665
765
  }
766
+ async recordMutationGovernance(actionInvocationId, tenantId, spaceId, context) {
767
+ const record = await this.getActionInvocation(actionInvocationId, tenantId, spaceId);
768
+ if (!record) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
769
+ record.mutationFootprint = context.footprint;
770
+ record.executionPrincipal = context.executionPrincipal;
771
+ record.updatedAt = /* @__PURE__ */ new Date();
772
+ return record;
773
+ }
774
+ async appendPolicyObligations(records) {
775
+ for (const record of records) {
776
+ if (!this.policyObligations.some((candidate) => candidate.actionInvocationId === record.actionInvocationId && candidate.id === record.id)) {
777
+ this.policyObligations.push(record);
778
+ }
779
+ }
780
+ }
781
+ async listPolicyObligations(actionInvocationId, tenantId, spaceId) {
782
+ return this.policyObligations.filter((record) => record.actionInvocationId === actionInvocationId && record.tenantId === tenantId && record.spaceId === spaceId);
783
+ }
784
+ async updatePolicyObligation(actionInvocationId, obligationId, status, evidenceReferences) {
785
+ const record = this.policyObligations.find((candidate) => candidate.actionInvocationId === actionInvocationId && candidate.id === obligationId);
786
+ if (!record) throw new Error(`Policy obligation not found: ${obligationId}`);
787
+ record.status = status;
788
+ if (evidenceReferences) record.evidenceReferences = evidenceReferences;
789
+ record.updatedAt = /* @__PURE__ */ new Date();
790
+ }
791
+ async appendExecutionAttestation(record) {
792
+ if (!this.executionAttestations.some((candidate) => candidate.id === record.id)) this.executionAttestations.push(record);
793
+ }
794
+ async listExecutionAttestations(actionInvocationId, tenantId, spaceId) {
795
+ return this.executionAttestations.filter((record) => record.actionInvocationId === actionInvocationId && record.tenantId === tenantId && record.spaceId === spaceId);
796
+ }
797
+ async appendExternalReconciliation(record) {
798
+ if (!this.externalReconciliations.some((candidate) => candidate.id === record.id)) this.externalReconciliations.push(record);
799
+ }
666
800
  async createAdapterInvocation(record) {
667
801
  if (this.adapterInvocations.some((candidate) => candidate.id === record.id)) return;
668
802
  this.adapterInvocations.push(record);
@@ -737,6 +871,7 @@ var PostgresPlatformHostStore = class {
737
871
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
738
872
  lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
739
873
  hitl_reason text, hitl_policy_version text, approval_decision jsonb,
874
+ mutation_footprint jsonb, execution_principal jsonb,
740
875
  created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
741
876
  );
742
877
  ALTER TABLE fabric_platform.action_invocations
@@ -748,7 +883,9 @@ var PostgresPlatformHostStore = class {
748
883
  ADD COLUMN IF NOT EXISTS hitl_risk_tier text,
749
884
  ADD COLUMN IF NOT EXISTS hitl_reason text,
750
885
  ADD COLUMN IF NOT EXISTS hitl_policy_version text,
751
- ADD COLUMN IF NOT EXISTS approval_decision jsonb;
886
+ ADD COLUMN IF NOT EXISTS approval_decision jsonb,
887
+ ADD COLUMN IF NOT EXISTS mutation_footprint jsonb,
888
+ ADD COLUMN IF NOT EXISTS execution_principal jsonb;
752
889
  CREATE UNIQUE INDEX IF NOT EXISTS action_invocations_idempotency_idx
753
890
  ON fabric_platform.action_invocations
754
891
  (tenant_id, space_id, action_id, idempotency_key)
@@ -767,6 +904,24 @@ var PostgresPlatformHostStore = class {
767
904
  input jsonb NOT NULL, output jsonb, error text, attempt integer NOT NULL,
768
905
  created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
769
906
  );
907
+ CREATE TABLE IF NOT EXISTS fabric_platform.policy_obligations (
908
+ action_invocation_id text NOT NULL, id text NOT NULL,
909
+ tenant_id text NOT NULL, space_id text NOT NULL, type text NOT NULL,
910
+ status text NOT NULL, required boolean NOT NULL DEFAULT true,
911
+ description text, evidence_references jsonb, metadata jsonb,
912
+ created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
913
+ PRIMARY KEY (action_invocation_id,id)
914
+ );
915
+ CREATE TABLE IF NOT EXISTS fabric_platform.execution_attestations (
916
+ id text PRIMARY KEY, action_invocation_id text NOT NULL,
917
+ tenant_id text NOT NULL, space_id text NOT NULL, adapter_invocation_id text,
918
+ attestation jsonb NOT NULL, recorded_at timestamptz NOT NULL
919
+ );
920
+ CREATE TABLE IF NOT EXISTS fabric_platform.external_reconciliations (
921
+ id text PRIMARY KEY, action_invocation_id text NOT NULL,
922
+ tenant_id text NOT NULL, space_id text NOT NULL,
923
+ reconciliation jsonb NOT NULL
924
+ );
770
925
  CREATE TABLE IF NOT EXISTS fabric_platform.event_sequences (
771
926
  tenant_id text NOT NULL, space_id text NOT NULL, next_sequence bigint NOT NULL,
772
927
  PRIMARY KEY (tenant_id, space_id)
@@ -912,6 +1067,80 @@ var PostgresPlatformHostStore = class {
912
1067
  ]
913
1068
  );
914
1069
  }
1070
+ async recordMutationGovernance(actionInvocationId, tenantId, spaceId, context) {
1071
+ const result = await this.sql.query(
1072
+ `UPDATE fabric_platform.action_invocations SET mutation_footprint=$4::jsonb,
1073
+ execution_principal=$5::jsonb, updated_at=now()
1074
+ WHERE id=$1 AND tenant_id=$2 AND space_id=$3 RETURNING *`,
1075
+ [actionInvocationId, tenantId, spaceId, JSON.stringify(context.footprint), JSON.stringify(context.executionPrincipal)]
1076
+ );
1077
+ if (!result.rows[0]) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
1078
+ return toActionRecord(result.rows[0]);
1079
+ }
1080
+ async appendPolicyObligations(records) {
1081
+ for (const record of records) {
1082
+ await this.sql.query(
1083
+ `INSERT INTO fabric_platform.policy_obligations
1084
+ (action_invocation_id,id,tenant_id,space_id,type,status,required,description,evidence_references,metadata,created_at,updated_at)
1085
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12)
1086
+ ON CONFLICT (action_invocation_id,id) DO NOTHING`,
1087
+ [
1088
+ record.actionInvocationId,
1089
+ record.id,
1090
+ record.tenantId,
1091
+ record.spaceId,
1092
+ record.type,
1093
+ record.status,
1094
+ record.required ?? true,
1095
+ record.description ?? null,
1096
+ record.evidenceReferences ? JSON.stringify(record.evidenceReferences) : null,
1097
+ record.metadata ? JSON.stringify(record.metadata) : null,
1098
+ record.createdAt,
1099
+ record.updatedAt
1100
+ ]
1101
+ );
1102
+ }
1103
+ }
1104
+ async listPolicyObligations(actionInvocationId, tenantId, spaceId) {
1105
+ const result = await this.sql.query(
1106
+ `SELECT * FROM fabric_platform.policy_obligations WHERE action_invocation_id=$1 AND tenant_id=$2 AND space_id=$3 ORDER BY created_at,id`,
1107
+ [actionInvocationId, tenantId, spaceId]
1108
+ );
1109
+ return result.rows.map(toObligationRecord);
1110
+ }
1111
+ async updatePolicyObligation(actionInvocationId, obligationId, status, evidenceReferences) {
1112
+ await this.sql.query(
1113
+ `UPDATE fabric_platform.policy_obligations SET status=$3,
1114
+ evidence_references=COALESCE($4::jsonb,evidence_references), updated_at=now()
1115
+ WHERE action_invocation_id=$1 AND id=$2`,
1116
+ [actionInvocationId, obligationId, status, evidenceReferences ? JSON.stringify(evidenceReferences) : null]
1117
+ );
1118
+ }
1119
+ async appendExecutionAttestation(record) {
1120
+ const { id, actionInvocationId, tenantId, spaceId, adapterInvocationId, recordedAt, ...attestation } = record;
1121
+ await this.sql.query(
1122
+ `INSERT INTO fabric_platform.execution_attestations
1123
+ (id,action_invocation_id,tenant_id,space_id,adapter_invocation_id,attestation,recorded_at)
1124
+ VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7) ON CONFLICT (id) DO NOTHING`,
1125
+ [id, actionInvocationId, tenantId, spaceId, adapterInvocationId ?? null, JSON.stringify(attestation), recordedAt]
1126
+ );
1127
+ }
1128
+ async listExecutionAttestations(actionInvocationId, tenantId, spaceId) {
1129
+ const result = await this.sql.query(
1130
+ `SELECT * FROM fabric_platform.execution_attestations WHERE action_invocation_id=$1 AND tenant_id=$2 AND space_id=$3 ORDER BY recorded_at,id`,
1131
+ [actionInvocationId, tenantId, spaceId]
1132
+ );
1133
+ return result.rows.map(toAttestationRecord);
1134
+ }
1135
+ async appendExternalReconciliation(record) {
1136
+ const { id, actionInvocationId, tenantId, spaceId, ...reconciliation } = record;
1137
+ await this.sql.query(
1138
+ `INSERT INTO fabric_platform.external_reconciliations
1139
+ (id,action_invocation_id,tenant_id,space_id,reconciliation)
1140
+ VALUES ($1,$2,$3,$4,$5::jsonb) ON CONFLICT (id) DO NOTHING`,
1141
+ [id, actionInvocationId, tenantId, spaceId, JSON.stringify(reconciliation)]
1142
+ );
1143
+ }
915
1144
  async createAdapterInvocation(record) {
916
1145
  await this.sql.query(
917
1146
  `INSERT INTO fabric_platform.adapter_invocations
@@ -1109,11 +1338,41 @@ function toActionRecord(row) {
1109
1338
  ...row.hitl_reason ? { hitlReason: String(row.hitl_reason) } : {},
1110
1339
  ...row.hitl_policy_version ? { hitlPolicyVersion: String(row.hitl_policy_version) } : {},
1111
1340
  ...row.approval_decision ? { approvalDecision: toApprovalDecision(row.approval_decision) } : {},
1341
+ ...row.mutation_footprint ? { mutationFootprint: row.mutation_footprint } : {},
1342
+ ...row.execution_principal ? { executionPrincipal: row.execution_principal } : {},
1112
1343
  ...row.error ? { error: String(row.error) } : {},
1113
1344
  createdAt: new Date(row.created_at),
1114
1345
  updatedAt: new Date(row.updated_at)
1115
1346
  };
1116
1347
  }
1348
+ function toObligationRecord(row) {
1349
+ return {
1350
+ id: String(row.id),
1351
+ actionInvocationId: String(row.action_invocation_id),
1352
+ tenantId: String(row.tenant_id),
1353
+ spaceId: String(row.space_id),
1354
+ type: String(row.type),
1355
+ status: String(row.status),
1356
+ required: Boolean(row.required),
1357
+ ...row.description ? { description: String(row.description) } : {},
1358
+ ...row.evidence_references ? { evidenceReferences: row.evidence_references } : {},
1359
+ ...row.metadata ? { metadata: row.metadata } : {},
1360
+ createdAt: new Date(row.created_at),
1361
+ updatedAt: new Date(row.updated_at)
1362
+ };
1363
+ }
1364
+ function toAttestationRecord(row) {
1365
+ const attestation = row.attestation;
1366
+ return {
1367
+ ...attestation,
1368
+ id: String(row.id),
1369
+ actionInvocationId: String(row.action_invocation_id),
1370
+ tenantId: String(row.tenant_id),
1371
+ spaceId: String(row.space_id),
1372
+ ...row.adapter_invocation_id ? { adapterInvocationId: String(row.adapter_invocation_id) } : {},
1373
+ recordedAt: new Date(row.recorded_at)
1374
+ };
1375
+ }
1117
1376
  function toApprovalDecision(value) {
1118
1377
  const decision = value;
1119
1378
  return {