@fabricorg/platform-host 8.2.0 → 8.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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f1b6def: Add the parked action invocation ID and durable invocation record to `authorizeApproval` input so applications can authorize decisions for the specific proposal being resumed.
8
+
9
+ ## 8.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 749c507: Add host-owned staged descendant invocations. Actions declare an allowlist and return typed child
14
+ requests; Platform Host validates and redacts them atomically, releases them only after parent
15
+ completion, and dispatches them through fresh entitlement, authorization, admission, runtime
16
+ evidence, policy, and HITL decisions.
17
+
18
+ PostgreSQL migration 11 adds the staged-invocation ledger and caller-unforgeable child-origin unique
19
+ index. Child identity is a Host-only parent-invocation/stage-key origin. Child creation is
20
+ atomically fenced on the stage's pending status, lease owner, lease token, and unexpired lease;
21
+ PostgreSQL locks that stage row `FOR SHARE` while the fenced insert is in flight.
22
+
23
+ Each attempt settles a stage through one sequence. It first looks the child up by origin: a child
24
+ created by an earlier attempt is handed to the dispatcher again with its stable workflow ID while it
25
+ remains pending, without pre-submission checks or repeated governance, even after a deploy changes
26
+ the child's schema. Otherwise Host submits the child. When that submission did not itself return
27
+ the child (a failed check, a refusal, an error, or a timeout), Host rotates the lease token through
28
+ the store operation `rotateStagedInvocationLease` before an authoritative lookup, so the attempt's
29
+ own timed-out submission can no longer create the child. A stage is rejected only when that lookup
30
+ finds no child, and a dead-lettered stage records the child ID whenever the attempt found or created
31
+ a child. A stage becomes dispatched only after confirmed handoff, when no dispatcher is configured,
32
+ or when the child has already left pending. A failed child lookup settles the stage for retry and
33
+ does not dead-letter it. Settlements are compare-and-set on the lease owner and current token and
34
+ keep a child ID recorded earlier. A store error on one stage is reported through the sweep's
35
+ `onError` option (the Platform worker passes its own) and does not stop the rest of the batch.
36
+ Handoff failures retry after a constant one-second delay by default; callers provide `retryDelayMs`
37
+ for backoff. Public entitlement and authorization refusals now expose `SubmissionRefusalError` with
38
+ stable `code: "ACTION_SUBMISSION_REFUSED"`. The child inherits the parent's actor type: a
39
+ natural-person parent therefore does not enter agent HITL, while an agent parent follows the normal
40
+ agent approval route.
41
+
3
42
  ## 8.2.0
4
43
 
5
44
  ### Minor Changes
package/README.md CHANGED
@@ -738,6 +738,8 @@ An approval workflow calls `resumeApprovedInvocation()` instead of invoking the
738
738
  host authorizes the approver (using `authorizeApproval` when provided), then atomically persists the
739
739
  decision and changes `waiting_for_approval` to either leased `running` or terminal `failed`. This keeps
740
740
  the decision in the mutation ledger and prevents an approval/worker race.
741
+ `authorizeApproval` receives the parked `actionInvocationId` and durable, post-redaction `invocation`
742
+ record so the application can verify the approver's authority over that specific invocation.
741
743
 
742
744
  ```ts
743
745
  await host.resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, {
@@ -917,6 +919,11 @@ Only JSON-data parameters can be admitted. Anything else is refused as `invalid_
917
919
  directly after `authorize`, before the composition callback and the check run. A SharedArrayBuffer or a view over one is refused this way,
918
920
  since the caller could still write to its memory (test "a parameter backed by a SharedArrayBuffer …").
919
921
 
922
+ Entitlement and submission-authorization refusals throw `SubmissionRefusalError`. It remains an
923
+ `Error` with the existing message and adds stable `code: "ACTION_SUBMISSION_REFUSED"`, `actionId`,
924
+ and `refusal: "entitlement" | "authorization"` fields so internal dispatchers can distinguish a
925
+ definitive refusal from a retryable failure.
926
+
920
927
  Only `{ admitted: true }`, read as an own data property, admits. Each of these makes
921
928
  `submitAction` throw `ActionAdmissionError` (`code: "ACTION_ADMISSION_REFUSED"`, `refusal`, and
922
929
  `refusalCode` for `refused`):
@@ -1206,11 +1213,13 @@ cast in an index expression is not immutable and PostgreSQL refuses it.
1206
1213
 
1207
1214
  ## Tenant isolation
1208
1215
 
1209
- `tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` run eight checks against a host
1216
+ `tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` run nine checks against a host
1210
1217
  and its store. They prove scope on the invocation reads and writes, idempotency keys, event streams,
1211
- worker claims, external completion, an optional governed read, and adapter checkpoint reads and
1212
- writes they probe. The subject supplies a host, a recoverable store, a registered action, and two
1213
- scopes in different tenants. Adapter checks create probes in dedicated spaces and retire them afterwards.
1218
+ worker claims, staged-invocation lists and claims, external completion, an optional governed read,
1219
+ and adapter checkpoint reads and writes they probe. The subject supplies a host, a recoverable
1220
+ store, a registered action, and two scopes in different tenants. Adapter and staged checks create
1221
+ scoped probes and retire their parent invocations afterwards. The staged check is skipped for a
1222
+ store that does not advertise transactional staged invocations.
1214
1223
 
1215
1224
  Host passes `AdapterInvocationScope` from the store-loaded action invocation to
1216
1225
  `getAdapterInvocation(id, scope)` and `updateAdapterInvocation(id, patch, scope)`. Custom stores
@@ -1345,6 +1354,129 @@ CI certifies concurrent migrations, command idempotency, and schema behavior aga
1345
1354
  Lakebase failover, backup, and restore remain adopter certification gates and are not implied by the
1346
1355
  PostgreSQL CI result.
1347
1356
 
1357
+ ## Staged descendant invocations
1358
+
1359
+ An action declaring `stagesInvocations` may return up to 32 child requests from its handler or
1360
+ `after_adapters` finalizer. A request key is 1–128 characters and matches
1361
+ `[A-Za-z0-9][A-Za-z0-9._:-]*`. The Host accepts at most 64 KiB of canonical JSON per request and
1362
+ 256 KiB for the canonical batch. Keys are unique within the combined handler/finalizer batch.
1363
+ Every action ID must be allowlisted and registered, and each parameter object must pass the current
1364
+ child schema before the parent transaction commits. `redactActionParameters` runs during staging;
1365
+ the redacted, schema-valid parameters are the only parameters written to the stage record and later
1366
+ used to create the child invocation.
1367
+
1368
+ Staging requires `transactionWithEvents`, a transaction-scoped `stageInvocations`, and a store whose
1369
+ `transactionalStagedInvocations` flag is true. Host construction checks the advertised staged-store
1370
+ operations and atomic-store capability: scoped origin lookup, lease-fenced child creation, claim,
1371
+ renewal, lease-token rotation, settlement, cancellation, listing, and `transactionWithEvents`. The transaction object itself is available only when execution begins, so
1372
+ a missing transaction-scoped `stageInvocations` method is caught by the first staging action's
1373
+ execution preflight and interrupts that invocation. For `before_adapters`, the handler's batch
1374
+ commits with the handler mutation. For `after_adapters`, handler and finalizer requests commit with
1375
+ events and the completed parent verdict. A finalizing action retains its handler requests in the
1376
+ durable finalization checkpoint. Replaying an identical parent batch is a no-op; any changed key,
1377
+ action, version evidence, parameter digest, parameter value, batch size, or batch digest moves the
1378
+ parent to reconciliation without changing stored requests.
1379
+
1380
+ A pending stage is claimable only while its immutable parent scope matches and the parent is
1381
+ `completed`. Parents ending `failed`, `blocked_by_policy`, or `validation_failed` have pending stages
1382
+ cancelled by the sweep. `reconciliation_required` and nonterminal parents leave stages pending and
1383
+ unclaimed. `host.dispatchStagedInvocations` uses renewable leases, a monotonic lease token,
1384
+ per-submission timeout, retry availability, a default ten-row claim batch, and a default ten-attempt
1385
+ dead-letter budget. The normal Platform worker runs this sweep by default only when its Host registry
1386
+ contains an action with a non-empty `stagesInvocations` allowlist. `dispatchStagedInvocations: true`
1387
+ forces a sweep and `dispatchStagedInvocations: false` disables it. A deployment that has no staging
1388
+ declarations therefore does not query the migration-11 tables by default.
1389
+
1390
+ The child has a Host-only `stagedOrigin` of parent invocation ID plus stage key, backed by a separate
1391
+ unique index. It carries no idempotency key, so a public caller cannot pre-claim its identity with a
1392
+ staged-looking string. A crash after parent commit is recovered by a later sweep. A crash after child
1393
+ creation but before the stage checkpoint resolves the same child by `stagedOrigin`. PostgreSQL
1394
+ migration 11 creates `staged_invocations`, adds the nullable origin columns and partial unique index,
1395
+ and adds the claim and parent indexes.
1396
+
1397
+ Each claimed row goes through one settlement sequence (`dispatchStagedRow` in `src/host.ts`):
1398
+
1399
+ 1. Host renews the claim's lease, skipping the row if the lease is gone, and looks the child up by
1400
+ `stagedOrigin` in the parent's tenant and space. Every claim replaces the lease token, so a
1401
+ creation still in flight from an earlier attempt holds an older token and fails the creation
1402
+ fence; this lookup therefore settles what earlier attempts did. A child found here goes to step 4
1403
+ without the pre-submission checks or any entitlement, authorization, or admission call.
1404
+ 2. With no child, Host checks that the parent is still `completed` and that the child action is
1405
+ still registered, does not require capture-time authority, and accepts the stored parameters. It
1406
+ then submits through the shared submission pipeline under the claim's lease token, bounded by
1407
+ `submissionTimeoutMs`.
1408
+ 3. If that submission's own fenced create returned the child, Host settles with its ID: `dispatched`
1409
+ when the handoff succeeded or no dispatcher is configured, otherwise a retry that records the ID
1410
+ (`dead_lettered` once the attempt budget is spent). In every other case (a failed pre-submission
1411
+ check, a refusal, an error, or a timeout) Host first rotates the lease token with
1412
+ `rotateStagedInvocationLease`. The only other possible creator is this attempt's own timed-out
1413
+ submission, which still holds the old token, so after the rotation it can no longer create the
1414
+ child and the lookup that follows is authoritative. A child found there goes to step 4. With none,
1415
+ a definitive refusal settles `rejected`, and anything else settles for retry or, at the attempt
1416
+ limit, `dead_lettered`.
1417
+ 4. For a known child that is still `pending`, Host calls the configured dispatcher again with the
1418
+ stable workflow ID `action-invocation-<id>`, bounded by `submissionTimeoutMs`. A confirmed
1419
+ handoff, no dispatcher, or a child that has left `pending` settles `dispatched`; a failed or
1420
+ unconfirmed handoff settles for retry or `dead_lettered`. Both record the child ID.
1421
+
1422
+ Each settlement is one compare-and-set on the row's lease owner and current token, and a settlement
1423
+ that carries no child ID keeps one recorded earlier (both stores; shared contract test "keeps a
1424
+ recorded child id when a settlement carries none"). A settlement that finds the lease gone writes
1425
+ nothing, and the row's next claim starts again at step 1. A child lookup that fails settles the row
1426
+ for retry with the error `Staged child lookup failed.`, not as `rejected` or `dead_lettered`, even
1427
+ past the attempt limit. That attempt still counts, so the next attempt that can read the lookup and
1428
+ fails is dead-lettered if the budget is spent. A store
1429
+ error from renewal, rotation, or settlement ends only that row: the sweep counts it as failed,
1430
+ passes the error to `onError`, and continues with the next row. The Platform worker passes its own
1431
+ `onError`. Without a dispatcher, the submission that creates a child also executes it inline, as
1432
+ `submitAction` does; the stage settles `dispatched` once the child exists, even when that execution
1433
+ outlasts `submissionTimeoutMs`.
1434
+
1435
+ Child creation is fenced in the store operation that inserts the invocation. The stage must still be
1436
+ `pending`, owned by the submitting worker at the matching lease token, and unexpired. PostgreSQL
1437
+ locks the qualifying stage row `FOR SHARE` in a CTE before `INSERT ... SELECT`. A token rotation of
1438
+ that row therefore waits for an in-flight insert to commit, and an insert under the old token that
1439
+ reaches the row while a rotation is uncommitted waits for it and then inserts nothing; the live
1440
+ PostgreSQL contract tests observe both orders. The memory store's fence, insert, rotation, and
1441
+ settlements run without an intervening await. An existing child is returned regardless of the fence,
1442
+ but a stale attempt with no child reports lease loss and writes nothing. While a submission or a
1443
+ handoff runs, a heartbeat renews the row's current lease token.
1444
+
1445
+ Dispatch inherits the immutable parent's tenant, space, actor ID/type, and correlation ID; causation
1446
+ is the parent invocation ID. It does not copy the parent's authorization binding or approval decision.
1447
+ The child receives current entitlement, submission authorization, `admitSubmission`, runtime
1448
+ evidence, dispatcher, execution authorization, policy, and HITL handling as a fresh governed
1449
+ decision. Consequently, a natural-person parent produces a natural-person child and does not trigger
1450
+ agent HITL; an agent parent produces an agent child and follows the normal agent route. A definitive
1451
+ admission refusal, a submission authorization or entitlement refusal, or a failed pre-submission
1452
+ check settles `rejected` only after the token rotation and an authoritative lookup that finds no
1453
+ child. When that lookup finds one, as after a deploy that changes the schema of a child whose earlier
1454
+ handoff failed, the stage takes the existing-child path instead, so a `rejected` stage has no child.
1455
+ Admission failures/timeouts, store failures, and submission timeouts before child creation retry and
1456
+ eventually become `dead_lettered`. The default retry delay is a constant one second; callers provide
1457
+ `retryDelayMs(attemptCount)` when they need linear, exponential, jittered, or another backoff policy.
1458
+ A stage becomes `dispatched` only when the child exists and the current dispatcher handoff succeeds,
1459
+ no dispatcher is configured, or the child is no longer `pending`. A dispatcher failure after creation
1460
+ leaves the stage pending with the child ID and consumes the attempt. Exhausting the attempt budget
1461
+ moves it to `dead_lettered` while retaining that child ID: this terminal state means Host could not
1462
+ confirm dispatcher handoff, not that no child was created. A `dead_lettered` stage without a child ID
1463
+ was settled after an authoritative lookup found none. Later child policy blocks, approval waits,
1464
+ validation failures, or execution failures do not rewrite a dispatched stage. Stored stage errors
1465
+ are fixed messages and do not include request parameters or caught secret text:
1466
+
1467
+ - `Staged invocation dispatch failed.`
1468
+ - `Staged invocation submission was definitively refused.`
1469
+ - `Staged child dispatcher handoff was not confirmed.`
1470
+ - `Staged child lookup failed.`
1471
+
1472
+ `dispatchStagedInvocations` returns counters for the sweep. Each claimed row is counted once, as
1473
+ `dispatched`, `rejected`, or `failed`; `failed` also covers rows whose lease was lost before
1474
+ settlement and rows whose processing threw, and `deadLettered` is the part of `failed` settled as
1475
+ `dead_lettered`.
1476
+
1477
+ This release does not provide operator requeue of dead-lettered stages, lifecycle events for stage
1478
+ transitions, a parent-result field listing children, or per-stage release options.
1479
+
1348
1480
  ## Durable outbox egress
1349
1481
 
1350
1482
  Configure `outbox` on `createGovernedActionHost()` only with an `OutboxPlatformHostStore`.