@fabricorg/platform-host 8.6.0 → 8.7.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,11 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a28154f: Register Host-owned staged invocation principals, persist their names through recovery, and dispatch children through entitlement, admission, and authorization as the registered actor. PostgreSQL migration 14 adds nullable principal evidence columns and is required before deploying 8.7.0 anywhere staged invocations are used, including principal-less stages. Run `ensureSchema()`, deploy 8.7.0 or later to every process that runs the Host (executes staging parent actions, dispatches stages, or runs the Platform worker) without principal declarations, and only then ship modules that declare principals. An older worker can claim a principal-bearing stage and dispatch it as the parent's actor because it does not read or fence the new principal column.
8
+
3
9
  ## 8.6.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -1478,6 +1478,17 @@ child schema before the parent transaction commits. `redactActionParameters` run
1478
1478
  the redacted, schema-valid parameters are the only parameters written to the stage record and later
1479
1479
  used to create the child invocation.
1480
1480
 
1481
+ A parent action may select a Host-owned dispatch identity by declaring
1482
+ `stagedInvocationPrincipals`, which maps an allowlisted child action ID to a principal name. Host
1483
+ composition registers those names through `PlatformHostOptions.stagedInvocationPrincipals` with an
1484
+ `actorId` and Platform `ActorType`. A registry-backed Host refuses construction when a referenced
1485
+ name is absent; a resolve-only Host refuses the stage when it encounters an absent registration.
1486
+ The request allowlist remains exactly `key`, `actionId`, and `parameters`, so a handler or finalizer
1487
+ cannot supply `principal`, `actorId`, `actorType`, or an authorization binding. Host snapshots its
1488
+ registration at construction, records the principal name in the staged row, and resolves that
1489
+ durable name at dispatch. If a later Host no longer registers it, dispatch definitively rejects the
1490
+ stage instead of using the parent actor.
1491
+
1481
1492
  Staging requires `transactionWithEvents`, a transaction-scoped `stageInvocations`, and a store whose
1482
1493
  `transactionalStagedInvocations` flag is true. Host construction checks the advertised staged-store
1483
1494
  operations and atomic-store capability: scoped origin lookup, lease-fenced child creation, claim,
@@ -1487,8 +1498,9 @@ execution preflight and interrupts that invocation. For `before_adapters`, the h
1487
1498
  commits with the handler mutation. For `after_adapters`, handler and finalizer requests commit with
1488
1499
  events and the completed parent verdict. A finalizing action retains its handler requests in the
1489
1500
  durable finalization checkpoint. Replaying an identical parent batch is a no-op; any changed key,
1490
- action, version evidence, parameter digest, parameter value, batch size, or batch digest moves the
1491
- parent to reconciliation without changing stored requests.
1501
+ action, version evidence, parameter digest, parameter value, principal name, batch size, or batch
1502
+ digest moves the parent to reconciliation without changing stored requests. Principal-less batches
1503
+ retain their earlier canonical identity and digest.
1492
1504
 
1493
1505
  A pending stage is claimable only while its immutable parent scope matches and the parent is
1494
1506
  `completed`. Parents ending `failed`, `blocked_by_policy`, or `validation_failed` have pending stages
@@ -1500,29 +1512,56 @@ contains an action with a non-empty `stagesInvocations` allowlist. `dispatchStag
1500
1512
  forces a sweep and `dispatchStagedInvocations: false` disables it. A deployment that has no staging
1501
1513
  declarations therefore does not query the migration-11 tables by default.
1502
1514
 
1503
- The child has a Host-only `stagedOrigin` of parent invocation ID plus stage key, backed by a separate
1504
- unique index. It carries no idempotency key, so a public caller cannot pre-claim its identity with a
1505
- staged-looking string. A crash after parent commit is recovered by a later sweep. A crash after child
1506
- creation but before the stage checkpoint resolves the same child by `stagedOrigin`. PostgreSQL
1507
- migration 11 creates `staged_invocations`, adds the nullable origin columns and partial unique index,
1508
- and adds the claim and parent indexes.
1515
+ The child has a Host-only `stagedOrigin` of parent invocation ID plus stage key and, when selected,
1516
+ the durable principal name. `authorizeExecution` can inspect `invocation.stagedOrigin.principal`
1517
+ alongside the child's durable parameters and actor. Public `SubmitActionInput` has no
1518
+ `stagedOrigin`; Host adds it through the internal staged-dispatch path.
1519
+ The lookup and unique index intentionally remain keyed by tenant, space, parent invocation ID, and
1520
+ stage key: one parent stage key identifies one child, while the principal is evidence for that
1521
+ durable intent rather than a second child identity. The child carries no idempotency key, so a public
1522
+ caller cannot pre-claim its identity with a staged-looking string. A crash after parent commit is
1523
+ recovered by a later sweep. A crash after child creation but before stage settlement resolves the
1524
+ same child, including its principal evidence, by `stagedOrigin`. PostgreSQL migration 11 creates the
1525
+ staged tables and origin index; migration 14 adds nullable `principal` and
1526
+ `staged_origin_principal` columns for the staged record and child invocation.
1527
+
1528
+ Migration 14 is a deployment prerequisite for this Host version wherever staged invocations are
1529
+ used, including stages that do not select a principal: the staging and fenced child-insert SQL name
1530
+ the new nullable columns in both cases. Use this rollout order for a shared PostgreSQL database:
1531
+
1532
+ 1. Run `ensureSchema()` and confirm migration 14 (`staged_invocation_principals`) is recorded.
1533
+ 2. Deploy `@fabricorg/platform-host` 8.7.0 or later to every process that runs this Host: processes
1534
+ that execute staging parent actions (including inline execution inside `submitAction`), run
1535
+ `dispatchStagedInvocations`, or run the Platform worker. Keep module principal declarations
1536
+ disabled during this step. An older Host that executes a parent ignores the declaration and
1537
+ stages the child without a principal, so every later dispatch uses the parent's actor.
1538
+ 3. After those processes are upgraded, ship modules that declare `stagedInvocationPrincipals`.
1539
+
1540
+ An older worker can claim a principal-bearing row because its claim query does not filter on the
1541
+ new column. It then reads no principal evidence and can submit the child as the parent's actor; its
1542
+ older fenced insert also does not compare the principal. Depending on which version handles the stage, it can be
1543
+ rejected after its intent is lost or the child can complete under the wrong actor. Running migration
1544
+ 14 before step 2 also matters for principal-less stages because this Host version names the columns
1545
+ when staging and dispatching them.
1509
1546
 
1510
1547
  Each claimed row goes through one settlement sequence (`dispatchStagedRow` in `src/host.ts`):
1511
1548
 
1512
1549
  1. Host renews the claim's lease, skipping the row if the lease is gone, and looks the child up by
1513
- `stagedOrigin` in the parent's tenant and space. Every claim replaces the lease token, so a
1550
+ `stagedOrigin` in the parent's tenant and space. Each claim replaces the lease token, so a
1514
1551
  creation still in flight from an earlier attempt holds an older token and fails the creation
1515
1552
  fence; this lookup therefore settles what earlier attempts did. A child found here goes to step 4
1516
1553
  without the pre-submission checks or any entitlement, authorization, or admission call.
1517
1554
  2. With no child, Host checks that the parent is still `completed` and that the child action is
1518
1555
  still registered, does not require capture-time authority, and accepts the stored parameters. It
1519
- then submits through the shared submission pipeline under the claim's lease token, bounded by
1520
- `submissionTimeoutMs`.
1556
+ resolves any recorded principal against the current Host registration, then submits through the
1557
+ shared submission pipeline under the claim's lease token, bounded by `submissionTimeoutMs`.
1558
+ Entitlement, submission authorization, admission, and execution authorization all evaluate the
1559
+ registered principal just as they do for any other submission.
1521
1560
  3. If that submission's own fenced create returned the child, Host settles with its ID: `dispatched`
1522
1561
  when the handoff succeeded or no dispatcher is configured, otherwise a retry that records the ID
1523
1562
  (`dead_lettered` once the attempt budget is spent). In every other case (a failed pre-submission
1524
1563
  check, a refusal, an error, or a timeout) Host first rotates the lease token with
1525
- `rotateStagedInvocationLease`. The only other possible creator is this attempt's own timed-out
1564
+ `rotateStagedInvocationLease`. The remaining possible creator is this attempt's own timed-out
1526
1565
  submission, which still holds the old token, so after the rotation it can no longer create the
1527
1566
  child and the lookup that follows is authoritative. A child found there goes to step 4. With none,
1528
1567
  a definitive refusal settles `rejected`, and anything else settles for retry or, at the attempt
package/dist/index.cjs CHANGED
@@ -1375,6 +1375,7 @@ const MAX_STAGED_INVOCATION_KEY_LENGTH = 128;
1375
1375
  const MAX_STAGED_INVOCATION_BYTES = 65536;
1376
1376
  const MAX_STAGED_INVOCATION_BATCH_BYTES = 262144;
1377
1377
  const STAGED_INVOCATION_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
1378
+ const STAGED_INVOCATION_PRINCIPAL_PATTERN = /^[a-z][a-z0-9._:-]{0,127}$/;
1378
1379
  /** Validate and normalize one immutable batch before its parent transaction commits. */
1379
1380
  function prepareStagedInvocationBatch(input) {
1380
1381
  if (!Array.isArray(input.requests)) throw new Error("stagedInvocations must be an array.");
@@ -1394,6 +1395,8 @@ function prepareStagedInvocationBatch(input) {
1394
1395
  if (!(input.parentAction.stagesInvocations ?? []).includes(request.actionId)) throw new Error(`Action "${input.parentAction.actionId}" may not stage "${String(request.actionId)}".`);
1395
1396
  const child = input.resolveAction(request.actionId);
1396
1397
  if (!child) throw new Error(`Staged action "${String(request.actionId)}" is not registered.`);
1398
+ const principal = input.parentAction.stagedInvocationPrincipals?.[child.actionId];
1399
+ if (principal !== void 0 && !Object.hasOwn(input.stagedInvocationPrincipals ?? {}, principal)) throw new Error(`Staged invocation principal "${principal}" is not registered with this Platform Host.`);
1397
1400
  const parameters = finalizationJson(request.parameters);
1398
1401
  if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) throw new Error(`stagedInvocations[${index}] parameters must be a JSON object.`);
1399
1402
  if (!child.schema.safeParse(parameters).success) throw new Error(`stagedInvocations[${index}] parameters do not match action "${child.actionId}".`);
@@ -1408,24 +1411,27 @@ function prepareStagedInvocationBatch(input) {
1408
1411
  if (byteLength((0, _fabricorg_platform.canonicalJson)({
1409
1412
  key: request.key,
1410
1413
  actionId: child.actionId,
1411
- parameters: durableParameters
1414
+ parameters: durableParameters,
1415
+ ...principal !== void 0 ? { principal } : {}
1412
1416
  })) > 65536) throw new Error(`stagedInvocations[${index}] exceeds ${MAX_STAGED_INVOCATION_BYTES} bytes.`);
1413
1417
  return {
1414
1418
  key: request.key,
1415
1419
  actionId: child.actionId,
1416
1420
  expectedActionVersion: child.version,
1417
1421
  parameters: durableParameters,
1418
- parameterDigest: digestParameters(durableParameters)
1422
+ parameterDigest: digestParameters(durableParameters),
1423
+ ...principal !== void 0 ? { principal } : {}
1419
1424
  };
1420
1425
  });
1421
1426
  for (const [index, request] of normalized.entries()) if (normalized.findIndex((candidate) => candidate.key === request.key) !== index) throw new Error(`stagedInvocations contains duplicate key "${request.key}".`);
1422
1427
  normalized.sort((left, right) => compareText(left.key, right.key) || compareText(left.actionId, right.actionId));
1423
- const batchIdentity = normalized.map(({ key, actionId, expectedActionVersion, parameters, parameterDigest }) => ({
1428
+ const batchIdentity = normalized.map(({ key, actionId, expectedActionVersion, parameters, parameterDigest, principal }) => ({
1424
1429
  key,
1425
1430
  actionId,
1426
1431
  expectedActionVersion,
1427
1432
  parameters,
1428
- parameterDigest
1433
+ parameterDigest,
1434
+ ...principal !== void 0 ? { principal } : {}
1429
1435
  }));
1430
1436
  if (byteLength((0, _fabricorg_platform.canonicalJson)(batchIdentity)) > 262144) throw new Error(`stagedInvocations batch exceeds ${MAX_STAGED_INVOCATION_BATCH_BYTES} bytes.`);
1431
1437
  const batchDigest = digestParameters({
@@ -1460,7 +1466,8 @@ function stagedInvocationRequestIdentity(record) {
1460
1466
  expectedActionVersion: record.expectedActionVersion,
1461
1467
  parameters: record.parameters,
1462
1468
  parameterDigest: record.parameterDigest,
1463
- batchDigest: record.batchDigest
1469
+ batchDigest: record.batchDigest,
1470
+ ...record.principal !== void 0 ? { principal: record.principal } : {}
1464
1471
  });
1465
1472
  }
1466
1473
  function cloneStagedInvocationRecord(record) {
@@ -1527,6 +1534,20 @@ const STAGED_ERROR = Object.freeze({
1527
1534
  lookup: "Staged child lookup failed."
1528
1535
  });
1529
1536
  function createGovernedActionHost(options) {
1537
+ const configuredStagedPrincipals = options.stagedInvocationPrincipals;
1538
+ if (configuredStagedPrincipals !== void 0 && (!configuredStagedPrincipals || typeof configuredStagedPrincipals !== "object" || Array.isArray(configuredStagedPrincipals))) throw new Error("stagedInvocationPrincipals must be an object.");
1539
+ const stagedInvocationPrincipals = Object.freeze(Object.fromEntries(Object.entries(configuredStagedPrincipals ?? {}).map(([name, candidate]) => {
1540
+ if (!STAGED_INVOCATION_PRINCIPAL_PATTERN.test(name)) throw new Error(`Invalid staged invocation principal name: ${name}`);
1541
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error(`Staged invocation principal "${name}" must be an object.`);
1542
+ const actorId = candidate.actorId;
1543
+ const actorType = candidate.actorType;
1544
+ if (typeof actorId !== "string" || actorId.length === 0) throw new Error(`Staged invocation principal "${name}" actorId must be a non-empty string.`);
1545
+ if (typeof actorType !== "string" || !(0, _fabricorg_platform.isActorType)(actorType)) throw new Error(`Staged invocation principal "${name}" has invalid actorType.`);
1546
+ return [name, Object.freeze({
1547
+ actorId,
1548
+ actorType
1549
+ })];
1550
+ })));
1530
1551
  if (options.approvalDefaultTtlMs !== void 0 && (!Number.isSafeInteger(options.approvalDefaultTtlMs) || options.approvalDefaultTtlMs <= 0)) throw new Error("approvalDefaultTtlMs must be a positive safe integer in milliseconds.");
1531
1552
  if (options.approvalLifecycleEvents) {
1532
1553
  const approvalStore = asApprovalStore(options.store);
@@ -1560,6 +1581,9 @@ function createGovernedActionHost(options) {
1560
1581
  const configuredActionResolver = options.resolveAction ?? options.registry?.resolveAction;
1561
1582
  if (!configuredActionResolver) throw new Error("Platform Host requires an explicit module registry or resolveAction function.");
1562
1583
  const actionResolver = configuredActionResolver;
1584
+ if (options.registry?.orderedModules) {
1585
+ for (const module of options.registry.orderedModules) for (const action of module.actions ?? []) for (const principal of Object.values(action.stagedInvocationPrincipals ?? {})) if (!Object.hasOwn(stagedInvocationPrincipals, principal)) throw new Error(`Staged invocation principal "${principal}" referenced by action "${action.actionId}" is not registered with this Platform Host.`);
1586
+ }
1563
1587
  const stagedStore = asStagedInvocationStore(options.store);
1564
1588
  const declaresStaging = options.registry?.orderedModules?.some((module) => (module.actions ?? []).some((action) => (action.stagesInvocations?.length ?? 0) > 0)) ?? false;
1565
1589
  if (declaresStaging && (!stagedStore || !asAtomicMutationStore(options.store))) throw new Error("Staged descendant invocations require a store with transactionalStagedInvocations and transactionWithEvents capabilities.");
@@ -2703,6 +2727,7 @@ function createGovernedActionHost(options) {
2703
2727
  parentAction: action,
2704
2728
  requests: rawStagedInvocations,
2705
2729
  resolveAction: actionResolver,
2730
+ stagedInvocationPrincipals,
2706
2731
  redactParameters: options.redactActionParameters,
2707
2732
  now: now()
2708
2733
  });
@@ -3574,6 +3599,7 @@ function createGovernedActionHost(options) {
3574
3599
  parentAction: action,
3575
3600
  requests: stagedRequests,
3576
3601
  resolveAction: actionResolver,
3602
+ stagedInvocationPrincipals,
3577
3603
  redactParameters: options.redactActionParameters,
3578
3604
  redactedKeys: handlerStagedInvocations.map((request) => request.key),
3579
3605
  now: now()
@@ -4144,6 +4170,8 @@ function createGovernedActionHost(options) {
4144
4170
  if (!parent || parent.status !== "completed") throw new Error("Staged parent is not completed.");
4145
4171
  const child = actionResolver(record.actionId);
4146
4172
  if (!child) throw new StagedDispatchRejectionError("Staged action is no longer registered.");
4173
+ const principal = record.principal !== void 0 && Object.hasOwn(stagedInvocationPrincipals, record.principal) ? stagedInvocationPrincipals[record.principal] : void 0;
4174
+ if (record.principal !== void 0 && !principal) throw new StagedDispatchRejectionError("Staged invocation principal is no longer registered.");
4147
4175
  if (child.execution?.authorityMoment === "capture") throw new StagedDispatchRejectionError("Staged action now requires capture-time authority.");
4148
4176
  if (!child.schema.safeParse(structuredClone(record.parameters)).success) throw new StagedDispatchRejectionError("Staged parameters no longer match the registered action.");
4149
4177
  return submitActionInternal({
@@ -4151,8 +4179,8 @@ function createGovernedActionHost(options) {
4151
4179
  parameters: structuredClone(record.parameters),
4152
4180
  tenantId: parent.tenantId,
4153
4181
  spaceId: parent.spaceId,
4154
- actorId: parent.actorId,
4155
- actorType: parent.actorType,
4182
+ actorId: principal?.actorId ?? parent.actorId,
4183
+ actorType: principal?.actorType ?? parent.actorType,
4156
4184
  correlationId: parent.correlationId,
4157
4185
  causationId: parent.id,
4158
4186
  provenance: {
@@ -4166,7 +4194,8 @@ function createGovernedActionHost(options) {
4166
4194
  id: record.id,
4167
4195
  origin: {
4168
4196
  parentInvocationId: parent.id,
4169
- stageKey: record.key
4197
+ stageKey: record.key,
4198
+ ...record.principal !== void 0 ? { principal: record.principal } : {}
4170
4199
  },
4171
4200
  workerId: lease.workerId,
4172
4201
  leaseToken: lease.leaseToken,
@@ -4836,7 +4865,7 @@ var MemoryPlatformHostStore = class {
4836
4865
  kind: "existing",
4837
4866
  invocation: existing
4838
4867
  };
4839
- if (!this.stagedInvocations.find((record) => record.id === input.stageId && record.tenantId === invocation.tenantId && record.spaceId === invocation.spaceId && record.parentInvocationId === invocation.stagedOrigin.parentInvocationId && record.key === invocation.stagedOrigin.stageKey && record.actionId === invocation.actionId && record.status === "pending" && record.leaseOwner === input.workerId && record.leaseToken === input.leaseToken && record.leaseExpiresAt !== void 0 && record.leaseExpiresAt > now)) return { kind: "lease_lost" };
4868
+ if (!this.stagedInvocations.find((record) => record.id === input.stageId && record.tenantId === invocation.tenantId && record.spaceId === invocation.spaceId && record.parentInvocationId === invocation.stagedOrigin.parentInvocationId && record.key === invocation.stagedOrigin.stageKey && record.actionId === invocation.actionId && record.principal === invocation.stagedOrigin.principal && record.status === "pending" && record.leaseOwner === input.workerId && record.leaseToken === input.leaseToken && record.leaseExpiresAt !== void 0 && record.leaseExpiresAt > now)) return { kind: "lease_lost" };
4840
4869
  const createdAt = /* @__PURE__ */ new Date();
4841
4870
  const record = {
4842
4871
  ...invocation,
@@ -5684,6 +5713,16 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5684
5713
  version: 13,
5685
5714
  name: "agent_proposal",
5686
5715
  sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS agent_proposal jsonb;`
5716
+ },
5717
+ {
5718
+ version: 14,
5719
+ name: "staged_invocation_principals",
5720
+ sql: `
5721
+ ALTER TABLE fabric_platform.staged_invocations
5722
+ ADD COLUMN IF NOT EXISTS principal text;
5723
+ ALTER TABLE fabric_platform.action_invocations
5724
+ ADD COLUMN IF NOT EXISTS staged_origin_principal text;
5725
+ `
5687
5726
  }
5688
5727
  ]);
5689
5728
  }
@@ -5748,9 +5787,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5748
5787
  const current = input.now ?? /* @__PURE__ */ new Date();
5749
5788
  const admitted = invocation.admittedAt !== void 0;
5750
5789
  const admittedColumn = admitted ? ",admitted_at" : "";
5751
- const admittedValue = admitted ? ",$30" : "";
5790
+ const admittedValue = admitted ? ",$31" : "";
5752
5791
  const proposalColumn = invocation.agentProposal !== void 0 ? ",agent_proposal" : "";
5753
- const proposalValue = invocation.agentProposal !== void 0 ? `,$${admitted ? 31 : 30}::jsonb` : "";
5792
+ const proposalValue = invocation.agentProposal !== void 0 ? `,$${admitted ? 32 : 31}::jsonb` : "";
5754
5793
  const values = [
5755
5794
  invocation.id,
5756
5795
  invocation.tenantId,
@@ -5777,6 +5816,7 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5777
5816
  invocation.runtimeEvidence ? JSON.stringify(invocation.runtimeEvidence) : null,
5778
5817
  invocation.stagedOrigin.parentInvocationId,
5779
5818
  invocation.stagedOrigin.stageKey,
5819
+ invocation.stagedOrigin.principal ?? null,
5780
5820
  input.stageId,
5781
5821
  input.workerId,
5782
5822
  input.leaseToken,
@@ -5789,10 +5829,11 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5789
5829
  result = await this.sql.query(`WITH fence AS (
5790
5830
  SELECT stage.id
5791
5831
  FROM fabric_platform.staged_invocations AS stage
5792
- WHERE stage.id=$26 AND stage.tenant_id=$2 AND stage.space_id=$3
5832
+ WHERE stage.id=$27 AND stage.tenant_id=$2 AND stage.space_id=$3
5793
5833
  AND stage.parent_invocation_id=$24 AND stage.stage_key=$25 AND stage.action_id=$4
5794
- AND stage.status='pending' AND stage.lease_owner=$27 AND stage.lease_token=$28
5795
- AND stage.lease_expires_at IS NOT NULL AND stage.lease_expires_at > $29
5834
+ AND stage.principal IS NOT DISTINCT FROM $26
5835
+ AND stage.status='pending' AND stage.lease_owner=$28 AND stage.lease_token=$29
5836
+ AND stage.lease_expires_at IS NOT NULL AND stage.lease_expires_at > $30
5796
5837
  FOR SHARE
5797
5838
  )
5798
5839
  INSERT INTO fabric_platform.action_invocations
@@ -5800,17 +5841,19 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5800
5841
  parameters,result,correlation_id,causation_id,idempotency_key,parameter_digest,
5801
5842
  parameter_digest_algorithm,idempotency_actor_id,idempotency_authorization_binding_id,
5802
5843
  invocation_provenance,authorization_binding,execution_reason,authorization_binding_id,error,
5803
- runtime_evidence,staged_parent_invocation_id,staged_stage_key${admittedColumn}${proposalColumn},created_at,updated_at)
5844
+ runtime_evidence,staged_parent_invocation_id,staged_stage_key,staged_origin_principal${admittedColumn}${proposalColumn},created_at,updated_at)
5804
5845
  SELECT $1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,
5805
- $18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$25${admittedValue}${proposalValue},$29,$29
5846
+ $18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$25,$26${admittedValue}${proposalValue},$30,$30
5806
5847
  FROM fence
5807
5848
  ON CONFLICT (tenant_id,space_id,staged_parent_invocation_id,staged_stage_key)
5808
5849
  WHERE staged_parent_invocation_id IS NOT NULL AND staged_stage_key IS NOT NULL
5809
5850
  DO UPDATE SET id=fabric_platform.action_invocations.id
5810
5851
  RETURNING *`, values);
5811
5852
  } 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 });
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 });
5853
+ const databaseError = error;
5854
+ if (invocation.agentProposal !== void 0 && databaseError?.code === "42703" && typeof databaseError.message === "string" && /\bagent_proposal\b/.test(databaseError.message)) throw new Error("Agent proposals require PostgreSQL migration 13 (agent_proposal); run ensureSchema().", { cause: error });
5855
+ if (databaseError?.code === "42703") throw new Error("A staged child needs PostgreSQL migration 14 (staged_invocation_principals); run ensureSchema() before dispatching staged invocations.", { cause: error });
5856
+ if (databaseError?.code === "42P01") throw new Error("A staged child needs PostgreSQL migration 11 (staged_descendant_invocations); run ensureSchema() before dispatching staged invocations.", { cause: error });
5814
5857
  throw error;
5815
5858
  }
5816
5859
  if (result.rows[0]) {
@@ -5852,33 +5895,39 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
5852
5895
  }
5853
5896
  for (const record of records) {
5854
5897
  if (record.tenantId !== first.tenantId || record.spaceId !== first.spaceId || record.parentInvocationId !== first.parentInvocationId || record.batchDigest !== first.batchDigest) throw new Error(`Invalid staged invocation batch for parent ${first.parentInvocationId}.`);
5855
- await this.sql.query(`INSERT INTO fabric_platform.staged_invocations
5856
- (id,tenant_id,space_id,parent_invocation_id,parent_action_id,stage_key,action_id,
5898
+ try {
5899
+ await this.sql.query(`INSERT INTO fabric_platform.staged_invocations
5900
+ (id,tenant_id,space_id,parent_invocation_id,parent_action_id,stage_key,action_id,principal,
5857
5901
  expected_action_version,parameters,parameter_digest,batch_digest,status,child_invocation_id,
5858
5902
  attempt_count,available_at,lease_owner,lease_expires_at,lease_token,last_error,created_at,updated_at)
5859
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)`, [
5860
- record.id,
5861
- record.tenantId,
5862
- record.spaceId,
5863
- record.parentInvocationId,
5864
- record.parentActionId,
5865
- record.key,
5866
- record.actionId,
5867
- record.expectedActionVersion,
5868
- JSON.stringify(record.parameters),
5869
- record.parameterDigest,
5870
- record.batchDigest,
5871
- record.status,
5872
- record.childInvocationId ?? null,
5873
- record.attemptCount,
5874
- record.availableAt,
5875
- record.leaseOwner ?? null,
5876
- record.leaseExpiresAt ?? null,
5877
- record.leaseToken,
5878
- record.lastError ?? null,
5879
- record.createdAt,
5880
- record.updatedAt
5881
- ]);
5903
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)`, [
5904
+ record.id,
5905
+ record.tenantId,
5906
+ record.spaceId,
5907
+ record.parentInvocationId,
5908
+ record.parentActionId,
5909
+ record.key,
5910
+ record.actionId,
5911
+ record.principal ?? null,
5912
+ record.expectedActionVersion,
5913
+ JSON.stringify(record.parameters),
5914
+ record.parameterDigest,
5915
+ record.batchDigest,
5916
+ record.status,
5917
+ record.childInvocationId ?? null,
5918
+ record.attemptCount,
5919
+ record.availableAt,
5920
+ record.leaseOwner ?? null,
5921
+ record.leaseExpiresAt ?? null,
5922
+ record.leaseToken,
5923
+ record.lastError ?? null,
5924
+ record.createdAt,
5925
+ record.updatedAt
5926
+ ]);
5927
+ } catch (error) {
5928
+ if (error?.code === "42703") throw new Error("Staging an invocation needs PostgreSQL migration 14 (staged_invocation_principals); run ensureSchema() before staging invocations.", { cause: error });
5929
+ throw error;
5930
+ }
5882
5931
  }
5883
5932
  }
5884
5933
  async getActionInvocation(id, tenantId, spaceId) {
@@ -6899,7 +6948,8 @@ function toActionRecord(row) {
6899
6948
  ...row.idempotency_authorization_binding_id ? { idempotencyAuthorizationBindingId: String(row.idempotency_authorization_binding_id) } : {},
6900
6949
  ...row.staged_parent_invocation_id && row.staged_stage_key ? { stagedOrigin: {
6901
6950
  parentInvocationId: String(row.staged_parent_invocation_id),
6902
- stageKey: String(row.staged_stage_key)
6951
+ stageKey: String(row.staged_stage_key),
6952
+ ...row.staged_origin_principal ? { principal: String(row.staged_origin_principal) } : {}
6903
6953
  } } : {},
6904
6954
  ...row.invocation_provenance ? { provenance: row.invocation_provenance } : {},
6905
6955
  ...row.authorization_binding ? { authorizationBinding: row.authorization_binding } : {},
@@ -6945,6 +6995,7 @@ function toStagedInvocationRecord(row) {
6945
6995
  parentActionId: String(row.parent_action_id),
6946
6996
  key: String(row.stage_key),
6947
6997
  actionId: String(row.action_id),
6998
+ ...row.principal ? { principal: String(row.principal) } : {},
6948
6999
  expectedActionVersion: Number(row.expected_action_version),
6949
7000
  parameters: row.parameters,
6950
7001
  parameterDigest: String(row.parameter_digest),
@@ -8444,6 +8495,7 @@ exports.PLATFORM_HOST_HEALTH_CONTRACT_VERSION = PLATFORM_HOST_HEALTH_CONTRACT_VE
8444
8495
  exports.PLATFORM_HOST_METRIC_NAMES = PLATFORM_HOST_METRIC_NAMES;
8445
8496
  exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
8446
8497
  exports.STAGED_INVOCATION_KEY_PATTERN = STAGED_INVOCATION_KEY_PATTERN;
8498
+ exports.STAGED_INVOCATION_PRINCIPAL_PATTERN = STAGED_INVOCATION_PRINCIPAL_PATTERN;
8447
8499
  exports.SubmissionRefusalError = SubmissionRefusalError;
8448
8500
  exports.applyPostgresMigrations = applyPostgresMigrations;
8449
8501
  exports.asStagedInvocationStore = asStagedInvocationStore;