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