@fabricorg/platform-host 8.5.0 → 8.6.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 019ba30: Add the agent proposal submission envelope and Host HITL evaluator context with durable invocation identity. Snapshot proposals before awaiting authorization; validate strict approval evidence and support `redactAgentProposal` beside parameter redaction, after admission where declared. Persist and evaluate the redacted envelope, including on recovery and approval resume.
8
+
9
+ Idempotency compares redacted proposals and adds the `agent_proposal` conflict reason, including presence changes and legacy records. Consumers with exhaustive conflict-reason switches must handle the new value.
10
+
11
+ Run `ensureSchema()` to apply PostgreSQL migration 13 (`agent_proposal`) before submitting the first proposal. Inserts without proposals omit the new column. Staged and saga children do not inherit their parent's envelope.
12
+
3
13
  ## 8.5.0
4
14
 
5
15
  ### Minor Changes
package/README.md CHANGED
@@ -1712,3 +1712,40 @@ This capability lives on `OutboxRequeuePlatformHostStore`, which extends `Outbox
1712
1712
  `MemoryPlatformHostStore` and `PostgresPlatformHostStore` implement it, and a custom outbox store
1713
1713
  without it makes the operation throw. PostgreSQL adds migration 5, the
1714
1714
  `fabric_platform.event_outbox_requeues` table, which `ensureSchema()` creates.
1715
+
1716
+ ### Agent proposals and Host evaluator context
1717
+
1718
+ Agent callers can submit `SubmitActionInput.agentProposal` separately from action parameters:
1719
+ `{ confidence, riskTier, agentSessionId, agentRunId, reason, evidence }`. Confidence must be
1720
+ finite; risk tier is `low`, `medium`, or `high`. Session/run IDs must contain non-whitespace
1721
+ text and fit within 256 UTF-16 code units; reason may be empty and is limited to 4096 code
1722
+ units. Evidence uses the approval-decision strict JSON object validation and
1723
+ `MAX_APPROVAL_EVIDENCE_BYTES` (16 KiB). Invalid proposals and proposals from other actor
1724
+ types reject submission before invocation creation through the existing Error rejection path.
1725
+
1726
+ For the submission snapshot, Host reads `agentProposal` once and deeply copies it before
1727
+ its first await, including for actions without `admitSubmission`. The sibling
1728
+ `redactAgentProposal(actionId, proposal)` hook runs beside `redactActionParameters`: after
1729
+ admission for actions declaring `admitSubmission`, otherwise before composition resolution.
1730
+ Its result is validated and copied again. A thrown hook or invalid output rejects submission
1731
+ without creating an invocation, like a thrown parameter-redaction hook. Applications should
1732
+ remove secrets and unnecessary sensitive evidence through this hook before durable storage.
1733
+
1734
+ The redacted proposal is stored on `ActionInvocationRecord` and compared canonically on
1735
+ idempotent re-submission. Differences, including absence versus presence, add the
1736
+ `agent_proposal` conflict reason under the configured `enforce` or `audit-only` mode,
1737
+ including legacy records without parameter digests. Exhaustive conflict-reason switches
1738
+ must handle this new value.
1739
+
1740
+ `hitlEvaluator` receives `HostHitlEvaluatorContext`, extending the portable context with
1741
+ `actionInvocationId` and the durable `invocation` read before evaluation. Proposal fields
1742
+ supply confidence, risk tier, session ID, and run ID individually, falling back to parameters
1743
+ for absent fields. Reason and evidence are available through `invocation.agentProposal`.
1744
+ Approval authorization and resumed execution authorization receive that durable proposal too.
1745
+ Staged and saga children do not inherit a parent's proposal. Approval lifecycle event payloads
1746
+ and Host telemetry do not include the envelope.
1747
+
1748
+ Apply PostgreSQL migration 13 (`agent_proposal`) with `ensureSchema()` before the first
1749
+ proposal submission. Inserts without proposals omit the new column, following the
1750
+ `admitted_at` rollout pattern. Existing migration SQL is unchanged; readers accept legacy
1751
+ rows without a proposal. See `src/postgres-store.ts` (`ensureSchema`, `toActionRecord`).
package/dist/index.cjs CHANGED
@@ -297,6 +297,52 @@ async function appendLifecycleEvent(transaction, factory, invocation, kind) {
297
297
  }, metadata);
298
298
  }
299
299
  //#endregion
300
+ //#region src/agent-proposal.ts
301
+ /** Validate without invoking getters or retaining caller-owned evidence. */
302
+ function snapshotAgentProposal(value) {
303
+ const invalid = () => {
304
+ throw new Error("Invalid agent proposal.");
305
+ };
306
+ if (!value || typeof value !== "object" || Array.isArray(value) || util.types.isProxy(value)) return invalid();
307
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return invalid();
308
+ const allowed = [
309
+ "confidence",
310
+ "riskTier",
311
+ "agentSessionId",
312
+ "agentRunId",
313
+ "reason",
314
+ "evidence"
315
+ ];
316
+ for (const key of Reflect.ownKeys(value)) {
317
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
318
+ if (typeof key !== "string" || !allowed.includes(key) || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) return invalid();
319
+ }
320
+ if (value.confidence !== void 0 && (typeof value.confidence !== "number" || !Number.isFinite(value.confidence))) return invalid();
321
+ if (value.riskTier !== void 0 && ![
322
+ "low",
323
+ "medium",
324
+ "high"
325
+ ].includes(value.riskTier)) return invalid();
326
+ for (const key of [
327
+ "agentSessionId",
328
+ "agentRunId",
329
+ "reason"
330
+ ]) {
331
+ const field = value[key];
332
+ if (field !== void 0 && (typeof field !== "string" || field.length > (key === "reason" ? 4096 : 256) || key !== "reason" && field.trim().length === 0)) return invalid();
333
+ }
334
+ const evidence = snapshotApprovalDecision({
335
+ approved: true,
336
+ approverId: "proposal",
337
+ approverType: "agent",
338
+ ...value.evidence !== void 0 ? { evidence: value.evidence } : {}
339
+ }).evidence;
340
+ return {
341
+ ...value,
342
+ ...evidence !== void 0 ? { evidence } : {}
343
+ };
344
+ }
345
+ //#endregion
300
346
  //#region src/command-identity.ts
301
347
  const PARAMETER_DIGEST_ALGORITHM = "fabric-canonical-json-sha256-v1";
302
348
  function digestParameters(parameters) {
@@ -1002,7 +1048,11 @@ function snapshotSubmission(input) {
1002
1048
  const executionReason = scalar("executionReason");
1003
1049
  const idempotencyKey = scalar("idempotencyKey");
1004
1050
  const authorizationBindingId = scalar("authorizationBindingId");
1051
+ const proposal = input.agentProposal;
1052
+ if (proposal !== void 0 && actorType !== "agent") throw new Error("Agent proposal requires an agent actor.");
1053
+ const agentProposal = proposal === void 0 ? void 0 : snapshotAgentProposal(proposal);
1005
1054
  const live = {
1055
+ ...agentProposal !== void 0 ? { agentProposal } : {},
1006
1056
  actionId,
1007
1057
  tenantId,
1008
1058
  spaceId,
@@ -1071,6 +1121,7 @@ function privateSubmission(snapshot) {
1071
1121
  const provenance = provenanceRead ? copyProvenance(provenanceRead) : void 0;
1072
1122
  const authorizationBinding = bindingRead ? copyBinding(bindingRead) : void 0;
1073
1123
  return Object.freeze({
1124
+ ...snapshot.agentProposal !== void 0 ? { agentProposal: snapshot.agentProposal } : {},
1074
1125
  actionId: snapshot.actionId,
1075
1126
  tenantId: snapshot.tenantId,
1076
1127
  spaceId: snapshot.spaceId,
@@ -1167,6 +1218,7 @@ function parametersCopy(snapshot) {
1167
1218
  */
1168
1219
  function liveSubmissionView(snapshot, input) {
1169
1220
  const view = {
1221
+ ...snapshot.agentProposal !== void 0 ? { agentProposal: snapshotAgentProposal(snapshot.agentProposal) } : {},
1170
1222
  actionId: snapshot.actionId,
1171
1223
  tenantId: snapshot.tenantId,
1172
1224
  spaceId: snapshot.spaceId,
@@ -1208,6 +1260,7 @@ function submissionView(snapshot) {
1208
1260
  const provenance = fresh(snapshot.provenance);
1209
1261
  const authorizationBinding = fresh(snapshot.authorizationBinding);
1210
1262
  return {
1263
+ ...snapshot.agentProposal !== void 0 ? { agentProposal: snapshotAgentProposal(snapshot.agentProposal) } : {},
1211
1264
  actionId: snapshot.actionId,
1212
1265
  parameters: parametersCopy(snapshot),
1213
1266
  tenantId: snapshot.tenantId,
@@ -1544,7 +1597,8 @@ function createGovernedActionHost(options) {
1544
1597
  if (submission.authorizationBinding) validateAuthorizationBinding(submission, parameterDigest, action.execution?.authorityMoment, now());
1545
1598
  return {
1546
1599
  durableParameters,
1547
- parameterDigest
1600
+ parameterDigest,
1601
+ agentProposal: submission.agentProposal === void 0 ? void 0 : snapshotAgentProposal(options.redactAgentProposal ? options.redactAgentProposal(submission.actionId, submission.agentProposal) : submission.agentProposal)
1548
1602
  };
1549
1603
  };
1550
1604
  const early = declaresAdmission ? void 0 : prepare();
@@ -1559,9 +1613,10 @@ function createGovernedActionHost(options) {
1559
1613
  };
1560
1614
  (0, _fabricorg_platform.assertGovernanceRuntimeEvidence)(runtimeEvidence);
1561
1615
  const admission = declaresAdmission ? await admitSubmission(action, submission) : void 0;
1562
- const { durableParameters, parameterDigest } = early ?? prepare();
1616
+ const { durableParameters, parameterDigest, agentProposal } = early ?? prepare();
1563
1617
  const invocationInput = {
1564
1618
  id: actionInvocationId,
1619
+ ...agentProposal !== void 0 ? { agentProposal } : {},
1565
1620
  tenantId: submission.tenantId,
1566
1621
  spaceId: submission.spaceId,
1567
1622
  actionId: submission.actionId,
@@ -1615,6 +1670,7 @@ function createGovernedActionHost(options) {
1615
1670
  });
1616
1671
  if (durableInvocation.id !== actionInvocationId && submission.idempotencyKey) {
1617
1672
  const conflict = idempotencyConflict(durableInvocation, {
1673
+ agentProposal,
1618
1674
  actorId: submission.actorId,
1619
1675
  authorizationBindingId: submission.authorizationBindingId,
1620
1676
  actionVersion: action.version,
@@ -2336,21 +2392,24 @@ function createGovernedActionHost(options) {
2336
2392
  }
2337
2393
  if (invocation.actorType === "agent" && options.hitlEvaluator) {
2338
2394
  const hitlParameters = isRecord(parsed.data) ? parsed.data : invocation.parameters;
2395
+ const proposal = invocation.agentProposal;
2339
2396
  const approvalStore = asApprovalStore(options.store);
2340
2397
  if (!approvalStore) return hostOperation(() => failExecution(invocation, "failed", "The configured HITL evaluator requires a store with approval persistence support."));
2341
2398
  let evaluatedApprovalExpiresAt;
2342
2399
  if (!invocation.hitlRoute) {
2343
2400
  const decision = await options.hitlEvaluator({
2401
+ actionInvocationId: invocation.id,
2402
+ invocation,
2344
2403
  actionId: action.actionId,
2345
2404
  actorId: invocation.actorId,
2346
2405
  actorType: invocation.actorType,
2347
2406
  tenantId,
2348
2407
  spaceId,
2349
2408
  parameters: hitlParameters,
2350
- ...numberValue(hitlParameters.confidence) !== void 0 ? { confidence: numberValue(hitlParameters.confidence) } : {},
2351
- ...isRiskTier(hitlParameters.riskTier) ? { riskTier: hitlParameters.riskTier } : {},
2352
- ...stringValue(hitlParameters.agentSessionId) ? { agentSessionId: stringValue(hitlParameters.agentSessionId) } : {},
2353
- ...stringValue(hitlParameters.agentRunId) ? { agentRunId: stringValue(hitlParameters.agentRunId) } : {}
2409
+ ...(proposal?.confidence ?? numberValue(hitlParameters.confidence)) !== void 0 ? { confidence: proposal?.confidence ?? numberValue(hitlParameters.confidence) } : {},
2410
+ ...isRiskTier(proposal?.riskTier ?? hitlParameters.riskTier) ? { riskTier: proposal?.riskTier ?? hitlParameters.riskTier } : {},
2411
+ ...proposal?.agentSessionId ?? stringValue(hitlParameters.agentSessionId) ? { agentSessionId: proposal?.agentSessionId ?? stringValue(hitlParameters.agentSessionId) } : {},
2412
+ ...proposal?.agentRunId ?? stringValue(hitlParameters.agentRunId) ? { agentRunId: proposal?.agentRunId ?? stringValue(hitlParameters.agentRunId) } : {}
2354
2413
  });
2355
2414
  if (decision.approvalExpiresAt !== void 0 && !validApprovalDate(decision.approvalExpiresAt)) return hostOperation(() => failExecution(invocation, "failed", "approvalExpiresAt must be a valid Date."));
2356
2415
  evaluatedApprovalExpiresAt = decision.approvalExpiresAt === void 0 ? void 0 : new Date(decision.approvalExpiresAt);
@@ -4314,6 +4373,7 @@ function validateAuthorizationBinding(input, parameterDigest, authorityMoment, c
4314
4373
  }
4315
4374
  function idempotencyConflict(existing, incoming) {
4316
4375
  const reasons = [];
4376
+ if ((0, _fabricorg_platform.canonicalJson)(existing.agentProposal ?? null) !== (0, _fabricorg_platform.canonicalJson)(incoming.agentProposal ?? null)) reasons.push("agent_proposal");
4317
4377
  if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
4318
4378
  if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
4319
4379
  if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
@@ -5619,6 +5679,11 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5619
5679
  ON fabric_platform.action_invocations (approval_expires_at, id)
5620
5680
  WHERE status='waiting_for_approval' AND approval_expires_at IS NOT NULL;
5621
5681
  `
5682
+ },
5683
+ {
5684
+ version: 13,
5685
+ name: "agent_proposal",
5686
+ sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS agent_proposal jsonb;`
5622
5687
  }
5623
5688
  ]);
5624
5689
  }
@@ -5630,6 +5695,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5630
5695
  const admitted = input.admittedAt !== void 0;
5631
5696
  const admittedColumn = admitted ? "admitted_at," : "";
5632
5697
  const admittedValue = admitted ? "$25," : "";
5698
+ const proposalColumn = input.agentProposal !== void 0 ? "agent_proposal," : "";
5699
+ const proposalValue = input.agentProposal !== void 0 ? `$${admitted ? 26 : 25}::jsonb,` : "";
5633
5700
  let result;
5634
5701
  try {
5635
5702
  result = await this.sql.query(`INSERT INTO fabric_platform.action_invocations
@@ -5637,8 +5704,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5637
5704
  parameters,result,correlation_id,causation_id,idempotency_key,
5638
5705
  parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
5639
5706
  idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
5640
- authorization_binding_id,error,runtime_evidence,${admittedColumn}created_at,updated_at)
5641
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,${admittedValue}$24,$24)
5707
+ authorization_binding_id,error,runtime_evidence,${admittedColumn}${proposalColumn}created_at,updated_at)
5708
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,${admittedValue}${proposalValue}$24,$24)
5642
5709
  ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
5643
5710
  WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
5644
5711
  RETURNING *`, [
@@ -5666,9 +5733,11 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5666
5733
  input.error ?? null,
5667
5734
  input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
5668
5735
  now,
5669
- ...admitted ? [input.admittedAt] : []
5736
+ ...admitted ? [input.admittedAt] : [],
5737
+ ...input.agentProposal !== void 0 ? [JSON.stringify(input.agentProposal)] : []
5670
5738
  ]);
5671
5739
  } catch (error) {
5740
+ if (input.agentProposal !== void 0 && error?.code === "42703") throw new Error("Agent proposals require PostgreSQL migration 13 (agent_proposal); run ensureSchema().", { cause: error });
5672
5741
  if (admitted && error?.code === "42703") throw new Error("An admitted invocation needs the action_invocations.admitted_at column; run ensureSchema() to apply PostgreSQL migration 6 (invocation_admission) before submitting actions that declare admitSubmission.", { cause: error });
5673
5742
  throw error;
5674
5743
  }
@@ -5680,6 +5749,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5680
5749
  const admitted = invocation.admittedAt !== void 0;
5681
5750
  const admittedColumn = admitted ? ",admitted_at" : "";
5682
5751
  const admittedValue = admitted ? ",$30" : "";
5752
+ const proposalColumn = invocation.agentProposal !== void 0 ? ",agent_proposal" : "";
5753
+ const proposalValue = invocation.agentProposal !== void 0 ? `,$${admitted ? 31 : 30}::jsonb` : "";
5683
5754
  const values = [
5684
5755
  invocation.id,
5685
5756
  invocation.tenantId,
@@ -5710,7 +5781,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5710
5781
  input.workerId,
5711
5782
  input.leaseToken,
5712
5783
  current,
5713
- ...admitted ? [invocation.admittedAt] : []
5784
+ ...admitted ? [invocation.admittedAt] : [],
5785
+ ...invocation.agentProposal !== void 0 ? [JSON.stringify(invocation.agentProposal)] : []
5714
5786
  ];
5715
5787
  let result;
5716
5788
  try {
@@ -5728,15 +5800,16 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5728
5800
  parameters,result,correlation_id,causation_id,idempotency_key,parameter_digest,
5729
5801
  parameter_digest_algorithm,idempotency_actor_id,idempotency_authorization_binding_id,
5730
5802
  invocation_provenance,authorization_binding,execution_reason,authorization_binding_id,error,
5731
- runtime_evidence,staged_parent_invocation_id,staged_stage_key${admittedColumn},created_at,updated_at)
5803
+ runtime_evidence,staged_parent_invocation_id,staged_stage_key${admittedColumn}${proposalColumn},created_at,updated_at)
5732
5804
  SELECT $1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,
5733
- $18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$25${admittedValue},$29,$29
5805
+ $18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$25${admittedValue}${proposalValue},$29,$29
5734
5806
  FROM fence
5735
5807
  ON CONFLICT (tenant_id,space_id,staged_parent_invocation_id,staged_stage_key)
5736
5808
  WHERE staged_parent_invocation_id IS NOT NULL AND staged_stage_key IS NOT NULL
5737
5809
  DO UPDATE SET id=fabric_platform.action_invocations.id
5738
5810
  RETURNING *`, values);
5739
5811
  } catch (error) {
5812
+ if (invocation.agentProposal !== void 0 && error?.code === "42703") throw new Error("Agent proposals require PostgreSQL migration 13 (agent_proposal); run ensureSchema().", { cause: error });
5740
5813
  if (error?.code === "42703" || error?.code === "42P01") throw new Error("A staged child needs PostgreSQL migration 11 (staged_descendant_invocations); run ensureSchema() before dispatching staged invocations.", { cause: error });
5741
5814
  throw error;
5742
5815
  }
@@ -6856,6 +6929,7 @@ function toActionRecord(row) {
6856
6929
  ...row.mutation_footprint ? { mutationFootprint: row.mutation_footprint } : {},
6857
6930
  ...row.execution_principal ? { executionPrincipal: row.execution_principal } : {},
6858
6931
  ...row.runtime_evidence ? { runtimeEvidence: row.runtime_evidence } : {},
6932
+ ...row.agent_proposal != null ? { agentProposal: row.agent_proposal } : {},
6859
6933
  ...row.admitted_at ? { admittedAt: new Date(row.admitted_at) } : {},
6860
6934
  ...row.error ? { error: String(row.error) } : {},
6861
6935
  createdAt: new Date(row.created_at),