@fabricorg/platform-host 8.8.0 → 8.9.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 +321 -0
- package/README.md +760 -9
- package/dist/index.cjs +6239 -393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +560 -36
- package/dist/index.d.ts +560 -36
- package/dist/index.js +6220 -398
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ injected `now` clock is also passed to ordinary policy evaluation.
|
|
|
41
41
|
|
|
42
42
|
Production polling workers can use `createStoreBackedActionDispatcher()` plus
|
|
43
43
|
`runPlatformActionWorker()`. The pending invocation row is the durable queue item; workers claim
|
|
44
|
-
bounded batches with an atomic, renewable lease and `FOR UPDATE SKIP LOCKED`. Every claim advances a
|
|
44
|
+
bounded batches with an atomic, renewable lease and `FOR NO KEY UPDATE SKIP LOCKED`. Every claim advances a
|
|
45
45
|
fencing token. A heartbeat may renew only a lease that has not expired; after expiry, the work must be
|
|
46
46
|
reclaimed so its token advances. An expired worker cannot finalize after another worker takes ownership. Pass a stable
|
|
47
47
|
`idempotencyKey` to `submitAction()` so retries resolve
|
|
@@ -76,7 +76,7 @@ Non-idempotent governance retry requires a durable `deferredBeforeEffectAttempt`
|
|
|
76
76
|
preceding claim count. Each retry must record its own pre-effect deferral to permit another claim.
|
|
77
77
|
A missing or stale marker retains interruption failure and manual reconciliation. Marker-write
|
|
78
78
|
failure ends terminally when the verdict can be persisted; lease loss retains the recoverable
|
|
79
|
-
fencing error. Handler and adapter errors are outside this deferral window.
|
|
79
|
+
fencing error. Handler and adapter errors are outside this governance deferral window; handler rollback retry is described below.
|
|
80
80
|
|
|
81
81
|
PostgreSQL migration 9 (`pre_effect_governance_deferral`) adds the nullable marker column.
|
|
82
82
|
Run `ensureSchema()` before deploying Hosts that may defer non-idempotent actions. Invocation
|
|
@@ -96,6 +96,124 @@ both count worker claims.
|
|
|
96
96
|
A retryable obligation-resolver error fails terminally with the policy evidence from that attempt.
|
|
97
97
|
Preview evaluation does not enter this execution catch.
|
|
98
98
|
|
|
99
|
+
## Retry a rolled-back handler transaction
|
|
100
|
+
|
|
101
|
+
Throw `RetryableHandlerError` from `@fabricorg/platform` (or its `/policies` export) to assert
|
|
102
|
+
that the handler's effects stayed inside the supplied Host transaction and it can run again.
|
|
103
|
+
Host can retry that transaction within the same execution attempt. The default budget is three
|
|
104
|
+
total tries; `handlerRetry.maxAttempts` accepts integers from 1 to 10, with 1 disabling retry.
|
|
105
|
+
The worker claim, `attemptCount`, policy evaluations, obligations and approval decision are retained.
|
|
106
|
+
This is separate from `RetryableExecutionError`, whose handler throws retain terminal failure behavior.
|
|
107
|
+
|
|
108
|
+
Retry requires the atomic `transactionWithEvents` path, a throw from the handler call, and
|
|
109
|
+
`ConfirmedTransactionRollback` from that transaction call with the identical thrown error as `cause`.
|
|
110
|
+
Bare errors, other wrappers, failed rollback and errors at commit do not qualify. Errors from
|
|
111
|
+
transactional authorization, state validation, result/child validation, event extraction/persistence,
|
|
112
|
+
staging or finalization preparation do not qualify; neither does a returned `{ success: false }`.
|
|
113
|
+
`ReconciliationRequiredError`, `RetryableExecutionError` and Host's recoverable fencing errors retain
|
|
114
|
+
their existing outcomes and bypass the classifier.
|
|
115
|
+
|
|
116
|
+
There must be no adapter row from the resume scan (including failed/running rows), durable provider
|
|
117
|
+
effect, or pending completion/handoff. Before a retried try runs application code, its transaction
|
|
118
|
+
locks and checks invocation ownership. Leased execution checks the captured owner and token and
|
|
119
|
+
uses a transaction-scoped lease fence. Inline execution requires an unleased running row without
|
|
120
|
+
lease expiry; built-in workers do not claim such rows. Missing locking/fencing support prevents retry.
|
|
121
|
+
Lease loss raises the existing recoverable fencing error rather than a stale owner's failed verdict.
|
|
122
|
+
|
|
123
|
+
Host reparses copied durable parameters and restores copied provenance, admission time and
|
|
124
|
+
transactional authorization input. Uncopyable inputs, failed reparse or a changed parsed value stop
|
|
125
|
+
retrying. Preparation, adapter inputs and staged requests are published from the committed try.
|
|
126
|
+
A compatible replay checkpoint continues to bypass the handler. Exhaustion settles the last handler
|
|
127
|
+
error through `redactErrorText` with `source: "handler"` and the existing status handling.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { isTransientPostgresTransactionError } from "@fabricorg/platform-host";
|
|
131
|
+
|
|
132
|
+
const host = createGovernedActionHost({
|
|
133
|
+
store,
|
|
134
|
+
registry,
|
|
135
|
+
authorization,
|
|
136
|
+
handlerRetry: {
|
|
137
|
+
maxAttempts: 3,
|
|
138
|
+
classify: (error, { actionId }) =>
|
|
139
|
+
actionId === "records.update" && isTransientPostgresTransactionError(error),
|
|
140
|
+
delayMs: (retry) => Math.random() * Math.min(1000, 25 * 2 ** (retry - 1)),
|
|
141
|
+
onRetry: ({ actionInvocationId, attempt, maxAttempts }) =>
|
|
142
|
+
metrics.recordHandlerRetry(actionInvocationId, attempt, maxAttempts),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`classify` must return literal `true` to opt a raw error in; a thrown classifier refuses retry.
|
|
148
|
+
It asserts safety for the whole retried unit, not just the SQL statement that failed.
|
|
149
|
+
`isTransientPostgresTransactionError` recognizes candidate SQLSTATEs `40001` (serialization),
|
|
150
|
+
`40P01` (deadlock victim) and `55P03` (lock unavailable), following up to 16 errors through `cause`
|
|
151
|
+
with cycle and throwing-getter protection. It is not proof of rollback or absence of effects.
|
|
152
|
+
|
|
153
|
+
Without `delayMs`, Host uses full jitter on a 25 ms exponential backoff capped at 1000 ms.
|
|
154
|
+
A configured number or callback result is clamped to 0–1000 ms; callback retry numbers start at 1.
|
|
155
|
+
A throwing or non-finite delay stops retries with the original handler error. `onRetry` runs before
|
|
156
|
+
the delay; `attempt` identifies the upcoming try (2 through `maxAttempts`). Synchronous throws and
|
|
157
|
+
asynchronous rejections are ignored without awaiting the observer. Delay happens between finished
|
|
158
|
+
transactions; Host does not race a transaction against a timer. Retrying delays the worker's other claimed rows. A crash mid-retry uses existing recovery.
|
|
159
|
+
|
|
160
|
+
With retry enabled, in-transaction authorization, state guards and the other transaction callbacks
|
|
161
|
+
run again per try. They and the handler must keep effects inside the supplied transaction. Avoid
|
|
162
|
+
independent commits, HTTP calls, signals, nontransactional service writes and unawaited work.
|
|
163
|
+
|
|
164
|
+
### Provider rollback evidence
|
|
165
|
+
|
|
166
|
+
Rollback evidence is optional. A provider that propagates bare errors gets no handler retry.
|
|
167
|
+
`PostgresPlatformHostStore` does not infer rollback. Built-in stores expose provider rollback
|
|
168
|
+
confirmation to Host's handler transaction through an internal callback request. Ordinary application
|
|
169
|
+
transactions, store-internal approval/erasure transactions and other Host APIs retain their prior
|
|
170
|
+
error contracts, including callback-error identity and erasure's SQLSTATE mappings, when the
|
|
171
|
+
provider below wraps callback failures.
|
|
172
|
+
The request travels on the callback Host passes to `transactionWithEvents`. Code that decorates a
|
|
173
|
+
built-in store's `transactionWithEvents` and substitutes its own callback drops the request, so
|
|
174
|
+
handler retry stays off for that store; a decorator that passes Host's callback through unchanged
|
|
175
|
+
keeps it.
|
|
176
|
+
An application-owned provider can use this pattern with its transaction-bound domain store:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { ConfirmedTransactionRollback, type PostgresPlatformHostTransactionProvider } from "@fabricorg/platform-host";
|
|
180
|
+
|
|
181
|
+
type ApplicationDb = ReturnType<typeof domainStore.withClient>;
|
|
182
|
+
const transactionProvider: PostgresPlatformHostTransactionProvider<ApplicationDb> = {
|
|
183
|
+
async run(run) {
|
|
184
|
+
const client = await pool.connect();
|
|
185
|
+
try {
|
|
186
|
+
await client.query("BEGIN");
|
|
187
|
+
let result;
|
|
188
|
+
try {
|
|
189
|
+
result = await run({ db: domainStore.withClient(client), sql: client });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
await client.query("ROLLBACK");
|
|
192
|
+
throw new ConfirmedTransactionRollback(error);
|
|
193
|
+
}
|
|
194
|
+
// COMMIT errors remain outside the callback rollback signal.
|
|
195
|
+
try {
|
|
196
|
+
await client.query("COMMIT");
|
|
197
|
+
} catch (error) {
|
|
198
|
+
await client.query("ROLLBACK");
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
} finally {
|
|
203
|
+
client.release();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The provider throws the wrapper after successful `ROLLBACK` of a failed callback. If rollback
|
|
210
|
+
throws, that rejection supplies no confirmation. Memory confirms a failed callback for the Host
|
|
211
|
+
handler request after successful Host-ledger undo and a configured provider's `restore`. Commit-phase
|
|
212
|
+
errors and providerless approval transactions supply no confirmation. Ordinary public Memory
|
|
213
|
+
transaction callbacks retain their original errors. Custom atomic stores that expose the wrapper
|
|
214
|
+
through their public transaction API are supported; Host's non-handler transaction boundary and
|
|
215
|
+
exported conformance rejection checks normalize that transport evidence before classifying errors.
|
|
216
|
+
|
|
99
217
|
**Handler transaction reconciliation.** A handler or store transaction wrapper can throw
|
|
100
218
|
`ReconciliationRequiredError(message, { cause })` from `@fabricorg/platform` (also exported from
|
|
101
219
|
`@fabricorg/platform/policies`) when a domain effect may already have committed and an operator
|
|
@@ -133,7 +251,7 @@ The signal does not change failures in pre-handler governance or validation, sta
|
|
|
133
251
|
exceptions, adapters, or finalization. Application governance errors and definite adapter failures retain their
|
|
134
252
|
`failed` paths; post-success errors follow the recovery rules below. State guards still fail closed.
|
|
135
253
|
Transient `after_adapters` finalization failures escape as `RecoverableFinalizationError` for
|
|
136
|
-
recovery within `recoveryMaxAttempts`.
|
|
254
|
+
recovery within `recoveryMaxAttempts`. A handler error without eligible confirmed rollback still records `failed`. This signal requests an operator decision,
|
|
137
255
|
not an automatic retry or a claim that the current transaction committed.
|
|
138
256
|
|
|
139
257
|
New rows bind that key to the action version, actor/authority binding, and a versioned canonical
|
|
@@ -345,7 +463,7 @@ and accept the weaker stale-worker guarantee. Parent annotations similarly have
|
|
|
345
463
|
|
|
346
464
|
Atomic finalizers require transaction-scoped `writeAdapterInvocation` when the store advertises the
|
|
347
465
|
capability and execution holds a lease. A missing member selects verdict-first fallback before
|
|
348
|
-
transaction evidence writes. PostgreSQL locks the matching parent with `FOR UPDATE` before the
|
|
466
|
+
transaction evidence writes. PostgreSQL locks the matching parent with `FOR NO KEY UPDATE` before the
|
|
349
467
|
adapter mutation in one SQL statement. Memory rechecks buffered lease and adapter predicates at
|
|
350
468
|
commit and excludes claims and other conditional writes while applying the commit. A commit-time
|
|
351
469
|
conflict rolls back and raises `RecoverableFinalizationError`. No schema migration is added by this
|
|
@@ -564,6 +682,35 @@ When `authorizeExecution` is absent, the Host reuses `authorize` at the executio
|
|
|
564
682
|
completed idempotent replay returns its existing result without creating or executing a new
|
|
565
683
|
mutation.
|
|
566
684
|
|
|
685
|
+
Applications whose authority is protected by database locks can also configure
|
|
686
|
+
`authorizeExecutionInTransaction`. Host calls it after state-machine validation and directly before
|
|
687
|
+
the handler (or the handlerless action's event/state-transition work), with the same execution input
|
|
688
|
+
plus the `db` supplied to the handler transaction. Only `true` permits the effect; `false` and
|
|
689
|
+
non-boolean results roll the transaction back and use the same authorization-refusal settlement as
|
|
690
|
+
`authorizeExecution`. A throw rolls the transaction back and settles the invocation as `failed`.
|
|
691
|
+
Unlike `authorizeExecution`, a `RetryableExecutionError` thrown here is not deferred for a later
|
|
692
|
+
attempt, because the hook runs after policy evidence is recorded; retry transient authority-store
|
|
693
|
+
errors, such as a lock timeout, inside the hook. The hook runs on initial execution, approval resume,
|
|
694
|
+
recovery, offline replay and staged dispatch when those paths execute the handler transaction. It is
|
|
695
|
+
intentionally skipped for `authorityMoment: "capture"`, whose durable capture evidence governs instead.
|
|
696
|
+
|
|
697
|
+
The hook guards the handler transaction only. A finalizing action's `finalize` callback runs in a
|
|
698
|
+
later transaction, after adapters, without this hook, and a recovery attempt that resumes from a
|
|
699
|
+
finalization checkpoint skips the handler transaction and therefore the hook. Only the
|
|
700
|
+
pre-transaction `authorizeExecution` check runs on that attempt.
|
|
701
|
+
|
|
702
|
+
With `transactionWithEvents`, the hook shares the application transaction and `db` used by the
|
|
703
|
+
handler. A finalizing action has already locked its invocation row, and state-machine validation has
|
|
704
|
+
already performed its application reads before the hook runs; ordinary actions do not acquire an
|
|
705
|
+
invocation-row lock here. Host does not choose lock strengths for application state or authority
|
|
706
|
+
rows. Applications should document a consistent order from those existing locks to any locks acquired
|
|
707
|
+
by the hook. The legacy `store.transaction` path invokes the hook and handler in the same callback on
|
|
708
|
+
the same `db`, but that base store seam does not itself promise a database transaction. A store
|
|
709
|
+
whose `transaction` is not a real database transaction, such as `PostgresPlatformHostStore` built
|
|
710
|
+
without a transaction provider, gives the hook no atomicity with the handler's writes: a lock the
|
|
711
|
+
hook takes is released before the handler runs. Use a store with `transactionWithEvents` when the
|
|
712
|
+
hook must close the revocation gap.
|
|
713
|
+
|
|
567
714
|
External gateways should persist a compact admission record bound to the tenant, registration, actor,
|
|
568
715
|
action, canonical parameter hash, resource scope, and expiry, then pass its opaque non-secret identifier
|
|
569
716
|
as `authorizationBindingId` to `submitAction()`. Credentials and signed principal proofs do not belong
|
|
@@ -781,13 +928,24 @@ The evaluator returns `auto-execute`, `needs-approval`, `escalate`, or `rejected
|
|
|
781
928
|
from execution claims.
|
|
782
929
|
- `rejected` terminally fails before policies and mutation code run.
|
|
783
930
|
|
|
784
|
-
An approval workflow
|
|
931
|
+
An approval workflow can record a decision for a worker or call `resumeApprovedInvocation()` to execute inline. For inline resume, the
|
|
785
932
|
host authorizes the approver (using `authorizeApproval` when provided), then atomically persists the
|
|
786
933
|
decision and changes `waiting_for_approval` to either leased `running` or terminal `failed`. This keeps
|
|
787
934
|
the decision in the mutation ledger and prevents an approval/worker race.
|
|
788
935
|
`authorizeApproval` receives the parked `actionInvocationId` and durable, post-redaction `invocation`
|
|
789
936
|
record so the application can verify the approver's authority over that specific invocation.
|
|
790
937
|
|
|
938
|
+
`authorizeApprovalInTransaction` is the optional final authority boundary for both inline resume and
|
|
939
|
+
`recordApprovalDecision`. The store locks and classifies the invocation first, then calls the hook with
|
|
940
|
+
the transaction-bound `db` and locked invocation immediately before a decision write. Only `true`
|
|
941
|
+
permits the decision. A refusal leaves inline resume waiting and makes record-only approval return
|
|
942
|
+
`unauthorized` with its configured refusal evidence; a throw rolls back. Applied-decision replay,
|
|
943
|
+
expired decisions, and rows that cannot accept a decision do not call the hook. The built-in
|
|
944
|
+
PostgreSQL store holds the invocation row's `FOR NO KEY UPDATE` lock while the hook runs; the memory store
|
|
945
|
+
holds its serialized mutation unit. Applications choose any authority-row lock strength and should
|
|
946
|
+
acquire authority locks after the invocation lock to keep one lock order. Configuring this hook
|
|
947
|
+
requires `transactionalApprovalAuthorization: true`; Host rejects stores that do not advertise it.
|
|
948
|
+
|
|
791
949
|
```ts
|
|
792
950
|
await host.resumeApprovedInvocation(actionInvocationId, tenantId, spaceId, {
|
|
793
951
|
approved: true,
|
|
@@ -802,6 +960,97 @@ Custom stores remain source-compatible when HITL is unused. To enable HITL they
|
|
|
802
960
|
additive `ApprovalPlatformHostStore` capability. Both built-in stores implement it; Postgres users must
|
|
803
961
|
call `ensureSchema()` so the HITL evidence and approval-decision columns are added.
|
|
804
962
|
|
|
963
|
+
### Record a decision for worker execution
|
|
964
|
+
|
|
965
|
+
`createGovernedActionHost()` also returns `ApprovalDecisionRecordingHost`; the base
|
|
966
|
+
`GovernedActionHost` interface retains its handwritten-host shape. An HTTP endpoint can commit a
|
|
967
|
+
decision without running the handler, adapters or `executeInvocation`:
|
|
968
|
+
|
|
969
|
+
```ts
|
|
970
|
+
const result = await host.recordApprovalDecision(actionInvocationId, tenantId, spaceId, {
|
|
971
|
+
requestId: decisionRequestId,
|
|
972
|
+
approved: true,
|
|
973
|
+
approverId: reviewer.id,
|
|
974
|
+
approverType: "natural_person",
|
|
975
|
+
evidence: { verificationRef: receiptId },
|
|
976
|
+
});
|
|
977
|
+
// result.outcome === "approved": the row is pending for a worker claim.
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
`requestId` must be a non-blank string of at most 256 characters. Keep it stable across transport
|
|
981
|
+
retries. The result contains `outcome`, `actionInvocationId`, current `status`, `replayed`, and the
|
|
982
|
+
stored `decision` for `approved` or `denied`, including replays. Non-applied outcomes omit
|
|
983
|
+
`decision`, protecting another approver's identity, reason and evidence. Host snapshots and validates
|
|
984
|
+
evidence before authorization.
|
|
985
|
+
|
|
986
|
+
| Outcome | Durable effect |
|
|
987
|
+
| --- | --- |
|
|
988
|
+
| `approved` | Commit the decision and queue `pending`, clearing lease/error without changing `attemptCount`. |
|
|
989
|
+
| `denied` | Commit the decision and settle `failed`, using the inline denial patch. |
|
|
990
|
+
| `unauthorized` | Entitlement or approval authorization returned false; invocation unchanged. |
|
|
991
|
+
| `expired` | A due waiting approval settles `failed` with `approvalExpiry`; a terminal row already carrying approval expiry and no decision remains unchanged. The attempted decision is not applied. |
|
|
992
|
+
| `already_decided` | An existing decision belongs to a different request, or the same request changes verdict or approver id/type; invocation unchanged. |
|
|
993
|
+
| `late_terminal` | Terminal row without an approval decision or approval expiry; unchanged. |
|
|
994
|
+
| `not_waiting` | Non-terminal row without a decision that is not waiting for approval; unchanged. |
|
|
995
|
+
| `unsupported` | Waiting row outside Host HITL atomic approval; unchanged. |
|
|
996
|
+
|
|
997
|
+
The store's locked classifier checks an applied request id, verdict and approver id/type before
|
|
998
|
+
checking authorization refusal and row state. A matching retry returns its original `approved` or
|
|
999
|
+
`denied` outcome with `replayed: true` after pending, running, completion or failure, and appends no
|
|
1000
|
+
events. An opposite verdict is `already_decided`. Older decisions without request ids also produce
|
|
1001
|
+
`already_decided` for an authorized request. For a different request id, authorization refusal
|
|
1002
|
+
returns `unauthorized` before existing-decision classification. An authorized late retry returns
|
|
1003
|
+
`expired` from a terminal row's `approvalExpiry`, including when a sweep expired it. It remains
|
|
1004
|
+
`replayed: false`, because this is row-state classification rather than a stored decision receipt.
|
|
1005
|
+
The compatibility authorizers still run before the transaction; thrown callback errors propagate even
|
|
1006
|
+
on a retry. When `authorizeApprovalInTransaction` is configured, its locked result also has to allow
|
|
1007
|
+
the write. An applied-decision replay still runs the compatibility callbacks but skips the
|
|
1008
|
+
in-transaction hook because it writes nothing.
|
|
1009
|
+
|
|
1010
|
+
The store operation atomically classifies, changes state and appends its events/outbox rows.
|
|
1011
|
+
PostgreSQL requires a transaction provider (`transactionWithEvents`); unsupported stores throw a
|
|
1012
|
+
configuration error before writing. Recording does not use `beginApprovalDecision`, whose inline
|
|
1013
|
+
contract remains leased `running`. Missing scoped rows and invalid ids/evidence throw.
|
|
1014
|
+
An unresolved waiting action returns `unsupported` without changing the row, with or without a
|
|
1015
|
+
deadline, including one already due. Approval authorization is skipped when the action cannot
|
|
1016
|
+
resolve; the locked classifier leaves the invocation unchanged in that case. Inline resume keeps
|
|
1017
|
+
its existing unknown-action behavior.
|
|
1018
|
+
|
|
1019
|
+
The stored decision carries `requestId` and `recordedAtAttempt` in the existing decision JSON.
|
|
1020
|
+
The first worker claim after recording has `attemptCount === recordedAtAttempt + 1`. Host computes
|
|
1021
|
+
`approval_resume` for that claim, exempts the non-idempotent interruption guard and compares the
|
|
1022
|
+
recovery budget using `attemptCount - recordedAtAttempt`. The earlier parking claim therefore does
|
|
1023
|
+
not exhaust this first run at `recoveryMaxAttempts: 1`; an inline-origin wait recorded at zero works
|
|
1024
|
+
the same way. Attempt history is retained. Later claims use the existing recovery checks, budget
|
|
1025
|
+
and `recovery` reason, comparing the absolute `attemptCount` against `recoveryMaxAttempts`
|
|
1026
|
+
without subtracting the recorded count. Retryable governance failures on the approved claim retain leased-attempt
|
|
1027
|
+
retry behavior instead of the inline resume override's terminal behavior.
|
|
1028
|
+
|
|
1029
|
+
The pending row is recoverable through `runPlatformActionWorkerCycle` without a separate enqueue.
|
|
1030
|
+
Hosts without a worker must dispatch the pending row themselves. `executeInvocation` exists but is
|
|
1031
|
+
not a concurrent-safe targeted claim. Upgrade workers before using this API: older workers mishandle
|
|
1032
|
+
recorded approvals' attempt and execution-reason semantics. Saga-parent waits are excluded through
|
|
1033
|
+
`ActionDefinition.kind === "saga"`; Host HITL also requires a `needs-approval` or `escalate` checkpoint.
|
|
1034
|
+
|
|
1035
|
+
Set `approvalDecisionAttemptEvents: true` to append `APPROVAL_DECISION_REFUSED_EVENT`
|
|
1036
|
+
(`ApprovalDecisionRefused`) for non-applied outcomes. It defaults off. The payload contains outcome,
|
|
1037
|
+
request id, approver id/type, attempted `approved` verdict, observed `status` and `attemptedAt` ISO
|
|
1038
|
+
time, without evidence, reason text or parameters. Scoped row lookup supplies tenant and space;
|
|
1039
|
+
a wrong scope throws not-found without an event. Refusal event ids combine invocation identity with
|
|
1040
|
+
the SHA-256 digest of the exact request id (`digestParameters`), preserving punctuation through
|
|
1041
|
+
`lifecycleId` sanitization. Store event-id deduplication retains one event for a repeated request.
|
|
1042
|
+
The attempt factory runs after any update with the pre-patch observed row: waiting on first expiry,
|
|
1043
|
+
failed on a later attempt against that expired row. Applied-decision replays append nothing. The
|
|
1044
|
+
classifier appends within its transaction, including on expiry, so append failure rolls back the
|
|
1045
|
+
state patch and events. Host construction requires atomic approval, transaction and outbox support
|
|
1046
|
+
when the option is on. `approvalLifecycleEvents` independently controls applied decision/expiry events.
|
|
1047
|
+
|
|
1048
|
+
After commit, Host emits `invocation.failed` for an applied denial or expiry. Custom recording stores
|
|
1049
|
+
return `applied` alongside outcome, invocation and replay information so Host can distinguish a new
|
|
1050
|
+
transition from a previously expired row. Replays and unchanged rows emit no additional status
|
|
1051
|
+
telemetry. Approval queues pending without a recording-time status event, matching sibling approval
|
|
1052
|
+
paths; the worker emits execution telemetry when it executes the invocation.
|
|
1053
|
+
|
|
805
1054
|
### Approval deadlines, evidence, and lifecycle events
|
|
806
1055
|
|
|
807
1056
|
`HostHitlDecision.approvalExpiresAt` accepts an absolute, valid `Date`. Alternatively,
|
|
@@ -1040,6 +1289,14 @@ an offline replay and a re-submission under an existing idempotency key:
|
|
|
1040
1289
|
`redactActionParameters` changes it (test "the persisted parameters are the copy that was
|
|
1041
1290
|
checked …").
|
|
1042
1291
|
|
|
1292
|
+
When recording refusals is enabled, the recording path first checks request-only metadata:
|
|
1293
|
+
correlation/causation consistency, provenance source and trace attributes, audit attributes when no
|
|
1294
|
+
audit redactor is configured, and parameter-independent authorization-binding fields. A failure
|
|
1295
|
+
throws the validation error and leaves the key free. This validation does not run redaction or trace
|
|
1296
|
+
hooks. Binding digest comparison and capture-time expiry are checked before recording, except
|
|
1297
|
+
that the digest is not compared for actions declaring `admitSubmission`. Audit validation after a
|
|
1298
|
+
configured redactor retains its normal ordering.
|
|
1299
|
+
|
|
1043
1300
|
Only JSON-data parameters can be admitted. Anything else is refused as `invalid_parameters`
|
|
1044
1301
|
directly after `authorize`, before the composition callback and the check run. A SharedArrayBuffer or a view over one is refused this way,
|
|
1045
1302
|
since the caller could still write to its memory (test "a parameter backed by a SharedArrayBuffer …").
|
|
@@ -1059,10 +1316,63 @@ Only `{ admitted: true }`, read as an own data property, admits. Each of these m
|
|
|
1059
1316
|
- any other answer (`invalid_decision`);
|
|
1060
1317
|
- parameters that are not JSON data (`invalid_parameters`).
|
|
1061
1318
|
|
|
1062
|
-
Host writes nothing for a refused submission: no invocation, idempotency key, event, policy record
|
|
1319
|
+
By default, Host writes nothing for a refused submission: no invocation, idempotency key, event, policy record
|
|
1063
1320
|
or error, so the key stays free. The message names the action, the refusal and the check's code
|
|
1064
1321
|
only. Host emits `invocation.admission_refused` telemetry with the refusal.
|
|
1065
1322
|
|
|
1323
|
+
### Opt-in recorded submission refusals
|
|
1324
|
+
|
|
1325
|
+
Set `idempotency: { recordRefusals: true }` to reserve a refused submission's idempotency key.
|
|
1326
|
+
The default is `false`. Activation requires a store implementing `recordSubmissionRefusal`
|
|
1327
|
+
and `conflictMode: "enforce"` (the default); `"audit-only"` is rejected at host creation.
|
|
1328
|
+
This applies to keyed, non-staged submissions from any actor type. With the option on, an
|
|
1329
|
+
empty string is a present key; the default path continues to treat it as keyless. Keyless submissions,
|
|
1330
|
+
including staged children, retain the default behavior.
|
|
1331
|
+
|
|
1332
|
+
Host records entitlement and authorization refusals, and admission kinds `refused` and
|
|
1333
|
+
`invalid_parameters`. Admission `failed`, `timed_out`, and `invalid_decision`, malformed-request
|
|
1334
|
+
errors and store errors do not become recorded refusals. A recording failure surfaces the store
|
|
1335
|
+
error; an error's `recorded: true` confirms that the key is occupied by a refusal matching the
|
|
1336
|
+
caller. A durable insert winner decides whether the key holds a refusal or an executable invocation.
|
|
1337
|
+
|
|
1338
|
+
A refusal occupies one `action_invocations` row with terminal status `submission_refused` and a
|
|
1339
|
+
validated `submissionRefusal` descriptor. Parameters and result are fixed empty objects; agent
|
|
1340
|
+
proposal and provenance attributes are absent. The descriptor carries a fixed class and, for
|
|
1341
|
+
admission, the refusal kind and optional fixed `refusalCode`. An `admitSubmission` refusal code
|
|
1342
|
+
is durable evidence when this option is on: choose it from fixed action codes, without deriving
|
|
1343
|
+
it from submitted values (see `ActionAdmissionDecision` in `packages/platform/actions/index.ts`).
|
|
1344
|
+
Create new refusals through `recordSubmissionRefusal`; generic creation and status updates
|
|
1345
|
+
reject `submission_refused` as an input status. Refusal rows appear in invocation listings.
|
|
1346
|
+
Execution, resume and store mutation paths leave
|
|
1347
|
+
these rows inert. The stored-record erasure operator can redact their payload fields;
|
|
1348
|
+
Host does not schedule automatic retention. See **Erasing stored records**.
|
|
1349
|
+
|
|
1350
|
+
Entitlement, authorization and admission checks still run on replay before Host tries an insert;
|
|
1351
|
+
there is no refusal lookup before the checks. A matching caller receives the stored refusal,
|
|
1352
|
+
including after the checks start passing. Identity compares actor id and type, authorization
|
|
1353
|
+
binding id, action version, and a parameter digest when the stored refusal has one. A refused
|
|
1354
|
+
caller with a different identity receives its own unrecorded refusal. An admitted caller with a
|
|
1355
|
+
different identity receives `IdempotencyConflictError`. A refused caller does not receive the
|
|
1356
|
+
stored result of an executable invocation that won the key.
|
|
1357
|
+
|
|
1358
|
+
For actions without `admitSubmission`, Host digests parameters at the refusal site; a digest
|
|
1359
|
+
failure records nothing and is surfaced. For actions declaring `admitSubmission`, refusal sites
|
|
1360
|
+
do not digest parameters, including entitlement and authorization refusals: the parameters have
|
|
1361
|
+
not passed admission. These refusal rows are not parameter-bound, so changing parameters alone
|
|
1362
|
+
does not free their keys. Use a new key for a new request.
|
|
1363
|
+
|
|
1364
|
+
A caller who fails authorization can occupy a key within the tenant, space and action scope.
|
|
1365
|
+
Applications must select trusted tenant/space context and suitable key namespaces. If an
|
|
1366
|
+
entitlement or authorization callback returns `false` during an outage, that response becomes
|
|
1367
|
+
a permanent refusal when its insert wins. Throw for an unavailable dependency rather than
|
|
1368
|
+
reporting a definitive denial.
|
|
1369
|
+
|
|
1370
|
+
Before enabling the option, apply PostgreSQL migration 15 through `ensureSchema()` and upgrade
|
|
1371
|
+
all hosts sharing the database. Older hosts do not recognize `submission_refused` and could
|
|
1372
|
+
execute a refusal row. Migration 15 adds nullable `submission_refusal`; the existing status
|
|
1373
|
+
column is unconstrained text and parameters accept the fixed empty object. The memory and
|
|
1374
|
+
PostgreSQL stores use the same key scope as ordinary invocation insertion.
|
|
1375
|
+
|
|
1066
1376
|
The deadline is judged when the check's answer settles, not by which of the answer and a timer
|
|
1067
1377
|
wins a race. Host reads the monotonic clock (`performance.now()`) when the check is called and
|
|
1068
1378
|
again in the handler attached to the check's promise. An answer, or a throw, that settles
|
|
@@ -1338,13 +1648,18 @@ cast in an index expression is not immutable and PostgreSQL refuses it.
|
|
|
1338
1648
|
|
|
1339
1649
|
## Tenant isolation
|
|
1340
1650
|
|
|
1341
|
-
`tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` run
|
|
1651
|
+
`tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` run ten checks against a host
|
|
1342
1652
|
and its store. They prove scope on the invocation reads and writes, idempotency keys, event streams,
|
|
1343
1653
|
worker claims, staged-invocation lists and claims, external completion, an optional governed read,
|
|
1344
1654
|
and adapter checkpoint reads and writes they probe. The subject supplies a host, a recoverable
|
|
1345
1655
|
store, a registered action, and two scopes in different tenants. Adapter and staged checks create
|
|
1346
1656
|
scoped probes and retire their parent invocations afterwards. The staged check is skipped for a
|
|
1347
|
-
store that does not advertise transactional staged invocations.
|
|
1657
|
+
store that does not advertise transactional staged invocations. The refusal check is skipped when
|
|
1658
|
+
`recordSubmissionRefusal` is absent. When present, it tests refusal insertion/replay and ordinary
|
|
1659
|
+
invocation creation under one key/action in another tenant and another space. It creates terminal
|
|
1660
|
+
refusal rows and completed probe invocations in the other tenant’s and other space’s fresh probe
|
|
1661
|
+
spaces; refusal rows retain their identity and refusal verdict through the stored-record erasure operator. A PostgreSQL store must have migration
|
|
1662
|
+
15 applied (`ensureSchema()`) before running the kit, or the new check fails.
|
|
1348
1663
|
|
|
1349
1664
|
Host passes `AdapterInvocationScope` from the store-loaded action invocation to
|
|
1350
1665
|
`getAdapterInvocation(id, scope)` and `updateAdapterInvocation(id, patch, scope)`. Custom stores
|
|
@@ -1356,6 +1671,115 @@ refusals and an admitted update in our scope. The two adapter isolation checks
|
|
|
1356
1671
|
fail for stores that ignore scope. Deployments running `runTenantIsolationChecks` against a custom
|
|
1357
1672
|
store will see new failures until that store honours the argument.
|
|
1358
1673
|
|
|
1674
|
+
## Staged invocation conformance
|
|
1675
|
+
|
|
1676
|
+
`stagedInvocationChecks()` and `runStagedInvocationChecks(subject)` expose 11 destructive
|
|
1677
|
+
checks for isolated test stores. `StagedInvocationSubject.makeHarness()` returns a fresh
|
|
1678
|
+
`StagedContractHarness`: a staged/atomic store and optional `raceCreateThenRotate` and
|
|
1679
|
+
`raceRotateThenCreate` hooks. The hooks retain a transaction during the competing operation;
|
|
1680
|
+
without them the checks use ordinary concurrent or ordered store calls.
|
|
1681
|
+
|
|
1682
|
+
The probes cover principal round trips and fencing, lease owner/status/token/expiry fences,
|
|
1683
|
+
token rotation, child identity retention, create/rotation ordering, parent status gating,
|
|
1684
|
+
and batch replay/drift refusal. They do not establish database-clock authority.
|
|
1685
|
+
|
|
1686
|
+
```ts
|
|
1687
|
+
const result = await runStagedInvocationChecks({
|
|
1688
|
+
makeHarness: async () => ({ store: await createIsolatedStagedStore() }),
|
|
1689
|
+
});
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
## Outbox requeue conformance
|
|
1693
|
+
|
|
1694
|
+
`outboxRequeueChecks()` and `runOutboxRequeueChecks(subject)` expose seven destructive
|
|
1695
|
+
checks. `OutboxRequeueSubject.makeStore()` supplies a fresh store implementing the outbox
|
|
1696
|
+
requeue port, including relay operations and readback. Probes cover budget reset and event
|
|
1697
|
+
identity, concurrent requeue, stale authorization generations, detached evidence, relay races,
|
|
1698
|
+
live-record refusal, and tenant/space refusal.
|
|
1699
|
+
|
|
1700
|
+
```ts
|
|
1701
|
+
const result = await runOutboxRequeueChecks({ makeStore: createIsolatedRequeueStore });
|
|
1702
|
+
```
|
|
1703
|
+
|
|
1704
|
+
## Approval conformance
|
|
1705
|
+
|
|
1706
|
+
`approvalChecks()` and `runApprovalChecks(subject)` expose 44 destructive checks, including
|
|
1707
|
+
approval lifecycle parity. `ApprovalSubject.makeStore()` supplies a fresh `ApprovalCheckStore`
|
|
1708
|
+
with approval, recovery, outbox, and transaction support for the host-based probes. These build
|
|
1709
|
+
Host instances internally. Parameterized variants of a named case run within its check.
|
|
1710
|
+
|
|
1711
|
+
Probes cover recording, replay and contradiction, authorization boundaries, expiry, refusal
|
|
1712
|
+
evidence and telemetry, transaction rollback, inline decision parity, and recovery budgets.
|
|
1713
|
+
They exercise the supplied store with the packaged Host; they do not certify application policy.
|
|
1714
|
+
|
|
1715
|
+
```ts
|
|
1716
|
+
const result = await runApprovalChecks({ makeStore: createIsolatedApprovalStore });
|
|
1717
|
+
```
|
|
1718
|
+
|
|
1719
|
+
For these sets, inspect each result's `status`, `evidence`, and optional `error`. Missing optional
|
|
1720
|
+
capabilities yield `skipped`; the recorded approval probes also skip absent transactional
|
|
1721
|
+
approval lifecycle/outbox support. Probe errors and missing readback fail. `passed` means no check
|
|
1722
|
+
failed and can include skips. Checks leave invocation, event, and outbox records under fresh
|
|
1723
|
+
identities/spaces. Use isolated test stores with no competing workers or relays; requeue probes
|
|
1724
|
+
can mark rows published without sending to a real consumer. A store factory must provide the
|
|
1725
|
+
transaction support its advertised capabilities require.
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
## Outbox relay conformance
|
|
1729
|
+
|
|
1730
|
+
`outboxRelayChecks()` and `runOutboxRelayChecks({ makeStore })` expose eight destructive
|
|
1731
|
+
checks. `OutboxRelaySubject.makeStore()` returns a fresh `OutboxPlatformHostStore<unknown>`
|
|
1732
|
+
for each check. The public types are `OutboxRelaySubject`, `OutboxRelayCheck` and
|
|
1733
|
+
`OutboxRelayConformanceResult`; `OutboxRelayResult` remains the relay cycle's result type.
|
|
1734
|
+
|
|
1735
|
+
Probes cover claim eligibility (including equal availability and expiry boundaries), claim
|
|
1736
|
+
owner/expiry/attempt/token effects, a one-row batch limit, concurrent claim exclusivity,
|
|
1737
|
+
owner/token settlement fences, settled-row refusal and renewal fences. Each check uses a base
|
|
1738
|
+
time one hour ahead of the wall clock, with claim and renewal expiry modeled through explicit
|
|
1739
|
+
`now` inputs and a margin for wall-clock settlement guards. New rows must be available by their
|
|
1740
|
+
first claim time, at least one hour after the check starts; availability need not equal `recordedAt`.
|
|
1741
|
+
The relay checks require numeric lease tokens that increase on each claim, including reclaim,
|
|
1742
|
+
even though the general outbox types keep token fields and parameters optional.
|
|
1743
|
+
The `claim-limit` check pins only the batch limit: PostgreSQL orders claim
|
|
1744
|
+
candidates but does not guarantee the order of its returned update rows, and the relay does
|
|
1745
|
+
not rely on returned order. When `renewOutboxLeaseAndRead` is absent, the renewal check still
|
|
1746
|
+
exercises `renewOutboxLease` and does not skip.
|
|
1747
|
+
|
|
1748
|
+
Out of scope: claim/listOutbox returned ordering and settlement of expired but unreclaimed
|
|
1749
|
+
leases (the probes settle while leases are unexpired by the wall clock), relay attempt budgets
|
|
1750
|
+
and backoff policy (`src/outbox.ts`, `runOutboxRelayCycle`),
|
|
1751
|
+
error text, dead-letter requeue (`outboxRequeueChecks`), and first-write-wins event appends
|
|
1752
|
+
(`eventAppendChecks`). These checks ship with the package version, so upgrading Host and
|
|
1753
|
+
rerunning them shows where a custom store has fallen behind.
|
|
1754
|
+
|
|
1755
|
+
```ts
|
|
1756
|
+
const result = await runOutboxRelayChecks({ makeStore: createIsolatedOutboxStore });
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
## Invocation idempotency conformance
|
|
1760
|
+
|
|
1761
|
+
`invocationIdempotencyChecks()` and `runInvocationIdempotencyChecks({ makeStore })` expose
|
|
1762
|
+
seven destructive checks. `InvocationIdempotencySubject.makeStore()` returns a fresh
|
|
1763
|
+
`PlatformHostStore<unknown>` for each check, with optional `recordSubmissionRefusal` support.
|
|
1764
|
+
The other public types are `InvocationIdempotencyCheck` and `InvocationIdempotencyResult`.
|
|
1765
|
+
|
|
1766
|
+
Probes cover first-row replay and readback with changed submission fields, tenant/space/action
|
|
1767
|
+
key scope, distinct unkeyed inserts, empty-string keys, concurrent first-write arbitration,
|
|
1768
|
+
admission timestamp retention and refusal/admitted winner arbitration. The concurrent probe
|
|
1769
|
+
reads each submitted candidate id through the base store port. Only `refusal-winner` skips
|
|
1770
|
+
when `recordSubmissionRefusal` is absent; thrown probes and failed readbacks fail. Inspect
|
|
1771
|
+
individual statuses: `passed` means no check failed and can include that optional skip.
|
|
1772
|
+
|
|
1773
|
+
Out of scope: duplicate-id creates without a key (the stores differ), conflict classification
|
|
1774
|
+
(Host's responsibility), and cross-tenant refusal scoping (already covered by
|
|
1775
|
+
`fabric.tenancy.submission-refusal-scoped.v1`). These checks ship with the package version,
|
|
1776
|
+
so upgrading Host and rerunning them shows where a custom store has fallen behind.
|
|
1777
|
+
Use isolated stores with no competing workers; both sets leave records under fresh ids/spaces.
|
|
1778
|
+
|
|
1779
|
+
```ts
|
|
1780
|
+
const result = await runInvocationIdempotencyChecks({ makeStore: createIsolatedInvocationStore });
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1359
1783
|
## Event append conformance
|
|
1360
1784
|
|
|
1361
1785
|
`eventAppendChecks()` and `runEventAppendChecks(subject)` run six checks for first-write-wins
|
|
@@ -1526,7 +1950,8 @@ declarations therefore does not query the migration-11 tables by default.
|
|
|
1526
1950
|
|
|
1527
1951
|
The child has a Host-only `stagedOrigin` of parent invocation ID plus stage key and, when selected,
|
|
1528
1952
|
the durable principal name. `authorizeExecution` can inspect `invocation.stagedOrigin.principal`
|
|
1529
|
-
alongside the child's durable parameters and actor.
|
|
1953
|
+
alongside the child's durable parameters and actor. When configured, `authorizeExecutionInTransaction`
|
|
1954
|
+
receives the same staged origin on the child's handler transaction. Public `SubmitActionInput` has no
|
|
1530
1955
|
`stagedOrigin`; Host adds it through the internal staged-dispatch path.
|
|
1531
1956
|
The lookup and unique index intentionally remain keyed by tenant, space, parent invocation ID, and
|
|
1532
1957
|
stage key: one parent stage key identifies one child, while the principal is evidence for that
|
|
@@ -1800,3 +2225,329 @@ Apply PostgreSQL migration 13 (`agent_proposal`) with `ensureSchema()` before th
|
|
|
1800
2225
|
proposal submission. Inserts without proposals omit the new column, following the
|
|
1801
2226
|
`admitted_at` rollout pattern. Existing migration SQL is unchanged; readers accept legacy
|
|
1802
2227
|
rows without a proposal. See `src/postgres-store.ts` (`ensureSchema`, `toActionRecord`).
|
|
2228
|
+
|
|
2229
|
+
## Erasing stored records
|
|
2230
|
+
|
|
2231
|
+
`createGovernedActionHost()` also returns the separate `StoredRecordErasureHost`
|
|
2232
|
+
operator capability. `GovernedActionHost` keeps its existing shape. Applications
|
|
2233
|
+
can use `createStoredRecordErasureHost(store, authorization)` without constructing an action
|
|
2234
|
+
runtime. Supply trusted `actorId` and `actorType` from the authenticated application session.
|
|
2235
|
+
`validateErasureInput` requires nonblank actor fields, rejects NUL/lone-surrogate
|
|
2236
|
+
characters in `actorId`, and checks `actorType` with Platform's `isActorType`.
|
|
2237
|
+
The factory's required second argument selects `authorizeStoredRecordErasure` from `PlatformHostAuthorization`; the
|
|
2238
|
+
integrated Host passes its own authorization object.
|
|
2239
|
+
|
|
2240
|
+
After `validateErasureInput`, the operator calls `authorization.authorizeStoredRecordErasure`
|
|
2241
|
+
with exactly `{ actorId, actorType, tenantId, spaceId, requestId, targets }`. Targets are
|
|
2242
|
+
validated, detached copies; the hook receives neither `redact` nor stored payloads.
|
|
2243
|
+
The operator awaits the hook through the authorization object, preserving class receivers,
|
|
2244
|
+
before calling the store, with no prior record reads. Its strict `authorized !== true`
|
|
2245
|
+
guard permits erasure only for a `true` result.
|
|
2246
|
+
|
|
2247
|
+
Absent hooks and non-`true` results throw the exported
|
|
2248
|
+
`StoredRecordErasureAuthorizationError` with `refusal: "not_authorized"`.
|
|
2249
|
+
Absence uses `"Stored-record erasure is refused because no authorizeStoredRecordErasure is configured."`
|
|
2250
|
+
Other refusals use `"Actor <actorId> is not authorized to erase stored records in this scope."`
|
|
2251
|
+
Typed refusals contain no target-existence details. A thrown hook error propagates
|
|
2252
|
+
unchanged before store access, distinguishing a failed check from an explicit denial.
|
|
2253
|
+
Successful operations return `EraseStoredRecordsResult`; validation errors and
|
|
2254
|
+
`ErasureBusyError` remain distinct failures.
|
|
2255
|
+
|
|
2256
|
+
```ts
|
|
2257
|
+
import { createStoredRecordErasureHost } from "@fabricorg/platform-host";
|
|
2258
|
+
|
|
2259
|
+
// Reuse the application's authorization object; no configured erasure hook means refusal.
|
|
2260
|
+
const operator = createStoredRecordErasureHost(store, authorization);
|
|
2261
|
+
const result = await operator.eraseStoredRecords({
|
|
2262
|
+
actorId: "privacy-operator",
|
|
2263
|
+
actorType: "natural_person",
|
|
2264
|
+
requestId: "privacy-request-2026-11-10",
|
|
2265
|
+
lockTimeoutMs: 250, // Optional; target/connection acquisition budget.
|
|
2266
|
+
workTimeoutMs: 5000, // Optional; rewrite work budget.
|
|
2267
|
+
tenantId: "tenant-a",
|
|
2268
|
+
spaceId: "production",
|
|
2269
|
+
targets: [
|
|
2270
|
+
{ kind: "invocation", invocationId: "act_123" },
|
|
2271
|
+
{ kind: "event", eventId: "evt_456" },
|
|
2272
|
+
],
|
|
2273
|
+
redact(record) {
|
|
2274
|
+
// Transform the detached payload fields and return the same record context.
|
|
2275
|
+
// This example removes a known application value, retaining object shapes.
|
|
2276
|
+
return JSON.parse(JSON.stringify(record).replaceAll("value-to-remove", "[erased]"));
|
|
2277
|
+
},
|
|
2278
|
+
});
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
`ErasablePlatformHostStore<TDb>` extends `PlatformHostStore<TDb>` with
|
|
2282
|
+
`eraseStoredRecords(input: EraseStoredRecordsInput): Promise<EraseStoredRecordsResult>`.
|
|
2283
|
+
This store capability is an unauthenticated persistence port, not an authorization boundary.
|
|
2284
|
+
Direct callers must enforce authorization; the Host operator enforces it through the hook.
|
|
2285
|
+
Both built-in stores use `erasureAudit` to retain the request's `actorId` and `actorType`
|
|
2286
|
+
in the `HostRecordsErased` event envelope. These are identifying metadata, outside the
|
|
2287
|
+
redaction view, rather than erased payload values.
|
|
2288
|
+
The base store port has no new required method. Unsupported stores fail with an
|
|
2289
|
+
explicit capability error. The built-in memory and PostgreSQL stores implement
|
|
2290
|
+
this capability; PostgreSQL requires an application transaction provider.
|
|
2291
|
+
`validateErasureInput` rejects missing or blank scope/request identifiers, malformed
|
|
2292
|
+
targets, NUL/lone-surrogate identifiers, duplicate kind/ID pairs, batches outside 1–100 targets, and non-function
|
|
2293
|
+
redactors before invoking the store. IDs shared by different target kinds are distinct.
|
|
2294
|
+
The result retains request target order, reports `erased`, `already_erased`, `active`,
|
|
2295
|
+
or `not_found`, and counts processed records by record kind without returning payloads.
|
|
2296
|
+
Absent and foreign-scope targets both return `not_found`.
|
|
2297
|
+
|
|
2298
|
+
An invocation target covers its invocation payload fields, adapter inputs/outputs/errors
|
|
2299
|
+
and reconciliation, staged parameters/errors linked as parent or child, policy evaluations,
|
|
2300
|
+
policy obligations, execution attestations, external reconciliations, canonical events,
|
|
2301
|
+
outbox envelopes/errors/trace context, and outbox requeue reason/prior-error evidence.
|
|
2302
|
+
An event target covers the canonical event and its outbox/requeue copies. It does not
|
|
2303
|
+
rewrite copies inside the owning invocation's `finalizationCheckpoint.handlerEvents`,
|
|
2304
|
+
`stateTransition`, result or other invocation evidence: checkpoint events have no event ID
|
|
2305
|
+
to join. Use an invocation target to cover those copies of the logical event. The
|
|
2306
|
+
`ERASURE_COLUMNS` source manifest and schema-discovered inventory tests classify
|
|
2307
|
+
text and JSON columns. The event callback runs once per canonical event in a batch;
|
|
2308
|
+
stores copy its redacted payload and disclosed provenance into the outbox envelope.
|
|
2309
|
+
Outbox-specific errors and trace context have their own callback view. Legacy outbox
|
|
2310
|
+
rows lacking a canonical event expose their payload/provenance in that outbox view. Overlapping
|
|
2311
|
+
targets do not invoke the callback twice for the same stored record in one batch.
|
|
2312
|
+
|
|
2313
|
+
`StoredRecordErasureView` discriminates record kinds and exposes identifying context
|
|
2314
|
+
as readonly properties plus payload field names in `fields`. `redactStoredFields` uses
|
|
2315
|
+
`ERASURE_PROTECTED_PATHS` to omit specific Host-owned paths and restore their values.
|
|
2316
|
+
There is no recursive blacklist of key names. For example, policy fact values under
|
|
2317
|
+
`outcome.guidance.factsUsed[].value` remain visible even when they contain `id`, `type`,
|
|
2318
|
+
`status` or `provider`. Application extensions outside the paths below are visible.
|
|
2319
|
+
|
|
2320
|
+
| Record/field | Protected paths relative to that field |
|
|
2321
|
+
| --- | --- |
|
|
2322
|
+
| Invocation/event/outbox `provenance` | `source`, `correlationId`, `causationId` |
|
|
2323
|
+
| Invocation `authorizationBinding` | `id`, `tenantId`, `actorId`, `actionId`, `parameterDigest`, `governingMoment`, `capturedAt`, `expiresAt` |
|
|
2324
|
+
| Invocation `authorizationReconciliation` | `kind`, `governingMoment`, `executionReason`, `authorizationBindingId`, `provenance.source`, `provenance.correlationId`, `provenance.causationId` |
|
|
2325
|
+
| Invocation `adapterReconciliation` | `kind`, `adapterType`, `operation`, `vendor`, `adapterInvocationId`, `externalReference` |
|
|
2326
|
+
| Invocation `approvalDecision` | `approved`, `approverId`, `approverType`, `decidedAt`, `requestId`, `recordedAtAttempt` |
|
|
2327
|
+
| Invocation `pendingCompletion` | `provider`, `adapterType`, `operation`, `adapterInvocationId`, `externalReference`, `acceptedAt`, `dueAt`, `parkedAt`, `overdueAt`; a non-null pending completion prevents erasure |
|
|
2328
|
+
| Invocation `externalCompletion` | `digest`, `externalReference`, `provider`, `outcome`, `observedAt`, `recordedAt` |
|
|
2329
|
+
| Invocation `pendingStateTransition` | `entityType`, `entityId`, `fromState`, `toState` |
|
|
2330
|
+
| Invocation `approvalExpiry` | `expiresAt`, `expiredAt` |
|
|
2331
|
+
| Invocation `submissionRefusal` | `class`, `refusal`, `refusalCode` |
|
|
2332
|
+
| Invocation `finalizationCheckpoint` | `protocol`, `actionVersion`, `steps.length`, `handlerEvents.length`, `stagedInvocations.length`, `steps[].adapterType`, `steps[].operation`, `skipped`; identity fields `eventType`, `eventSchemaVersion`, `subjectType`, `subjectId` and `payload.fromState/toState` on `handlerEvents[]` and `stateTransition`; `stagedInvocations[].key/actionId/expectedActionVersion/principal` |
|
|
2333
|
+
| Adapter `reconciliation` | `kind`, `adapterType`, `operation`, `vendor`, `adapterInvocationId`, `externalReference`, `adapterEventSubject.subjectType`, `adapterEventSubject.subjectId` |
|
|
2334
|
+
| Policy evaluation `outcome` | `policyId`, `policyVersion`, `policyKind`, `result` |
|
|
2335
|
+
| Execution attestation `attestation` | `provider`, `operation`, `externalOperationId`, `satisfiesObligations`, `startedAt`, `completedAt` |
|
|
2336
|
+
| External reconciliation `reconciliation` | `status`, `provider`, `externalOperationId`, `attempt`, `observedAt` |
|
|
2337
|
+
| Event/outbox `payload` | Top-level `fromState`, `toState`; `HostRecordsErased` additionally retains `requestId`, `targets`, `counts` |
|
|
2338
|
+
| Other payload fields | No protected descendants; record headers remain outside the callback view |
|
|
2339
|
+
|
|
2340
|
+
The store rejects changed context or field names, protected-path insertion, invalid JSON,
|
|
2341
|
+
NUL/lone-surrogate strings, incompatible field types and removal of structural containers.
|
|
2342
|
+
Keep array shapes when they contain protected descendants. Null/absent fields remain
|
|
2343
|
+
null/absent. Supply a synchronous, pure JSON transformation; a thrown error aborts the batch.
|
|
2344
|
+
Settled parameters and results need not retain their execution schemas.
|
|
2345
|
+
|
|
2346
|
+
Invocation erasure checks settlement on its locked closure. Event-only owner observations have the concurrency limit below. Pending,
|
|
2347
|
+
running and waiting-for-approval invocations are active. A terminal invocation also
|
|
2348
|
+
remains active with a pending external completion, a live invocation lease, a pending
|
|
2349
|
+
linked stage, or a live linked stage/outbox lease. An event inherits its owning
|
|
2350
|
+
invocation's active refusal and checks its own outbox lease. Completed, failed,
|
|
2351
|
+
blocked-by-policy, validation-failed, submission-refused and reconciliation-required
|
|
2352
|
+
rows without these conditions are settled. Pending stages can make completed parents
|
|
2353
|
+
active: staged dispatch runs after parent completion. A reconciliation-required row
|
|
2354
|
+
without a pending completion can be erased; marker guards fence later evidence writes.
|
|
2355
|
+
|
|
2356
|
+
The PostgreSQL transaction and memory critical section atomically apply accepted
|
|
2357
|
+
rewrites, scope-bound markers and a sequenced `HostRecordsErased` audit event. The audit
|
|
2358
|
+
contains request ID, target kind/ID/outcome and counts, without erased values, value
|
|
2359
|
+
hashes or a free-text reason. Active/missing targets may coexist with erased targets
|
|
2360
|
+
in one successful batch. A callback failure rolls back rewrites, markers and the audit.
|
|
2361
|
+
Repeated requests reapply the callback to stored redacted data and report
|
|
2362
|
+
`already_erased`; `requestId` identifies audit work rather than suppressing retries.
|
|
2363
|
+
|
|
2364
|
+
PostgreSQL erasure requires a transaction provider running READ COMMITTED; it checks
|
|
2365
|
+
`SHOW transaction_isolation` and refuses other levels. Ordinary writers at stricter
|
|
2366
|
+
isolation levels may receive a serialization failure instead of an erased outcome.
|
|
2367
|
+
Apply migration 16, upgrade **all Host instances sharing the database**, then enable
|
|
2368
|
+
erasure. Older Host SQL and direct application SQL bypass these guards and are unsupported
|
|
2369
|
+
once erasure is enabled. Migration 16 adds metadata columns, markers and erasure-lookup
|
|
2370
|
+
indexes without triggers or write-path functions. Like migrations 1–15 it runs in the
|
|
2371
|
+
migration runner's transaction and takes table DDL locks; ordinary index builds block
|
|
2372
|
+
writes for their duration on large tables. Plan an appropriate maintenance window.
|
|
2373
|
+
|
|
2374
|
+
`eraseStoredRecords` processes sorted targets inside individual savepoints. An invocation
|
|
2375
|
+
target locks its invocation and dependent adapter, governance, staged parent/child, event,
|
|
2376
|
+
outbox and requeue rows. It also locks the stage identified by the child's stored
|
|
2377
|
+
`staged_parent_invocation_id` and `staged_stage_key`, even before that stage has recorded
|
|
2378
|
+
its child ID. A pending or leased origin stage keeps the child active; a settled origin
|
|
2379
|
+
stage is redacted with the child. This follows the stage, not the parent invocation's
|
|
2380
|
+
payloads or other stages; target the parent separately for its checkpoint copies. An event target locks its event and delivery/requeue rows, without
|
|
2381
|
+
locking its invocation owner or sibling rows. Canonical delivery copies are found by event
|
|
2382
|
+
ID. Legacy outbox-only rows with null owner columns are found through the indexed envelope
|
|
2383
|
+
`actionInvocationId`; new inserts populate the owner column explicitly. No advisory lock
|
|
2384
|
+
is taken by erasure. The migration runner retains its separate schema advisory lock.
|
|
2385
|
+
|
|
2386
|
+
The first existing target lock may wait within `lockTimeoutMs` (default 250 ms, including
|
|
2387
|
+
provider acquisition). While retaining target locks, subsequent row locks, including the
|
|
2388
|
+
final tenant/space sequence-row lock, use `NOWAIT`; contention rolls back with
|
|
2389
|
+
`ErasureBusyError`. The audit allocation and insert are the final SQL statements before
|
|
2390
|
+
provider commit. Before opening that transaction, a separate provider transaction commits
|
|
2391
|
+
an `event_sequences` row with `next_sequence=0` using `ON CONFLICT DO NOTHING`. It takes no
|
|
2392
|
+
target locks. It attempts the insert only when it sees no committed row for the tenant and
|
|
2393
|
+
space, so it can wait, within the acquisition budget, only for another transaction's
|
|
2394
|
+
uncommitted first insert of that row. The zero seed consumes no event
|
|
2395
|
+
sequence: the first allocation returns 1. The erasure transaction then locks the existing
|
|
2396
|
+
row with `NOWAIT`; it does not try to insert a missing sequence row while holding targets.
|
|
2397
|
+
The seed can remain after a refused or rolled-back erasure without consuming a sequence.
|
|
2398
|
+
Steady event appends in the same tenant and space can starve erasure: it can repeatedly
|
|
2399
|
+
report busy on this final nonwaiting lock. Each retry repeats callbacks and rewrites before
|
|
2400
|
+
attempting that lock; busy rolls those rewrites back.
|
|
2401
|
+
|
|
2402
|
+
Dependent rows are locked before settlement and callbacks. A claim/renewal committed before
|
|
2403
|
+
those locks is observed. Active/missing targets roll back to their savepoints, releasing
|
|
2404
|
+
locks acquired for that target before processing the next. Locks retained from an earlier
|
|
2405
|
+
overlapping accepted target stay held. Invocation settlement is evaluated on locked rows.
|
|
2406
|
+
For an event-only target the owner's status, stages and sibling leases are observed after
|
|
2407
|
+
the event closure is locked, but are not themselves locked. A concurrent change to that
|
|
2408
|
+
owner/sibling activity can therefore race that observation. Target the invocation when
|
|
2409
|
+
settlement of its complete closure must be serialized.
|
|
2410
|
+
|
|
2411
|
+
Ordinary invocation updates predicate on their own `records_erased_at`. Existing dependent
|
|
2412
|
+
updates use their row stamp; insertion paths without a proven transaction-held owner lock
|
|
2413
|
+
use a materialized owner read with `FOR KEY SHARE` inside the insertion statement. Host's
|
|
2414
|
+
ordinary owner locks change from the released version's `FOR UPDATE` to `FOR NO KEY UPDATE`. Transaction-held owner reads are reused
|
|
2415
|
+
for event and reconciliation insertion. Staged creation fences parent and initial child;
|
|
2416
|
+
dispatch/failure attachment fences the newly attached child. An erased child cannot acquire
|
|
2417
|
+
a stage containing old parameters through these methods. Host terminally rejects an
|
|
2418
|
+
unsettled stage if an older/custom store nevertheless returns an erased child by origin,
|
|
2419
|
+
so the attachment refusal cannot keep that stage retrying indefinitely. Replay conflict updates preserve
|
|
2420
|
+
the stored row, and event reappend remains first-write-wins.
|
|
2421
|
+
|
|
2422
|
+
Outbox, stage and invocation claims lock actual candidate rows with `SKIP LOCKED`, without
|
|
2423
|
+
a blocking parent lock. Stage cancellation also selects candidates with `SKIP LOCKED`; transactional approval
|
|
2424
|
+
expiry uses the optional `lockDueApprovals` transaction member for that selection. Delivery of erased outbox records remains usable: claims,
|
|
2425
|
+
renewals, publish/fail acknowledgement and requeue use stored redacted envelopes; failure
|
|
2426
|
+
withholds supplied error text and requeue withholds supplied reason text while retaining
|
|
2427
|
+
stored prior errors. The optional `renewOutboxLeaseAndRead` capability returns the stored
|
|
2428
|
+
envelope atomically with renewal. Relays use that envelope and reject expired responses;
|
|
2429
|
+
custom stores retain the boolean `renewOutboxLease` API. A custom store implementing
|
|
2430
|
+
erasure without optional renew-and-read keeps a window in which the relay can publish an
|
|
2431
|
+
envelope claimed before erasure.
|
|
2432
|
+
|
|
2433
|
+
Callers that do not erase still pay an owner `FOR KEY SHARE` lock for owned inserts outside
|
|
2434
|
+
a supplied mutation transaction, one locking invocation read per transactional Host event
|
|
2435
|
+
append, and one renew-and-read round trip per relayed record. Payload writes that classify
|
|
2436
|
+
a zero-row refusal add a classification `SELECT`, including retried owned duplicate event
|
|
2437
|
+
appends; conditional boolean/lease writes and ownerless duplicate events omit that query.
|
|
2438
|
+
The Host changeset reports the local invocation/adapter insert/update measurements and
|
|
2439
|
+
their scope.
|
|
2440
|
+
|
|
2441
|
+
These row acquisitions keep erasure from waiting on an ordinary row writer while retaining
|
|
2442
|
+
its earlier target locks; it cannot supply the waiting edge of such a row-lock deadlock
|
|
2443
|
+
cycle. They do not promise freedom from indirect waits in arbitrary application transactions,
|
|
2444
|
+
DDL interference, writer-configured timeouts or external transaction-provider behavior.
|
|
2445
|
+
Memory mirrors erased outcomes using its transaction queue and synchronous commit; it does
|
|
2446
|
+
not provide PostgreSQL's row-level lock footprint.
|
|
2447
|
+
|
|
2448
|
+
Indexed selection, callbacks, rewrites and audit work use `workTimeoutMs` (default 5000 ms).
|
|
2449
|
+
Remaining-budget checks and PostgreSQL statement/idle-transaction timeouts bound SQL and idle
|
|
2450
|
+
lock retention. Neither budget preempts synchronous JavaScript or promises a hard real-time
|
|
2451
|
+
network deadline. Timeout options accept integer milliseconds from 1 to 60000. Providers
|
|
2452
|
+
should commit promptly, preserve original rollback errors and discard failed connections.
|
|
2453
|
+
Ordinary query timeouts retain their PostgreSQL identity; database-caused `ErasureBusyError`
|
|
2454
|
+
retains its cause.
|
|
2455
|
+
|
|
2456
|
+
Erasure removes the invocation `parameterDigest` (SQL `NULL`, including
|
|
2457
|
+
`submission_refused` rows). Store-owned rewrites replace
|
|
2458
|
+
`authorizationBinding.parameterDigest`, `externalCompletion.digest` and the
|
|
2459
|
+
`parameterDigest` of rewritten stage rows with exported `ERASED_DIGEST = "erased"`.
|
|
2460
|
+
The sentinel is outside the 64-hex SHA-256 output space and preserves required string
|
|
2461
|
+
fields and the stage column's `NOT NULL` constraint. `ERASURE_PROTECTED_PATHS` keeps
|
|
2462
|
+
these nested fields out of the callback view and rejects their insertion;
|
|
2463
|
+
`redactStoredFields` overwrites them after merging protected structure. The stores
|
|
2464
|
+
remove or replace top-level digests in the row rewrite, alongside its erasure stamp.
|
|
2465
|
+
Repeating erasure leaves these digest fields absent or set to the same sentinel.
|
|
2466
|
+
|
|
2467
|
+
Private proposal/event comparison columns, memory maps and equality ports are removed.
|
|
2468
|
+
Migration 16 is edited in place because it has not been published; migrations 1–15
|
|
2469
|
+
retain their SQL. `packages/platform-host/tests/erasure-digest-cases.ts` checks raw records in both stores,
|
|
2470
|
+
including repeat erasure, and `erasureChecks` checks the four digest outcomes.
|
|
2471
|
+
|
|
2472
|
+
Host keyed resubmission and recorded-refusal paths evaluate retained actor, binding ID
|
|
2473
|
+
and action-version conflicts first. An enforce-mode conflict raises
|
|
2474
|
+
`IdempotencyConflictError`; otherwise an erased row raises `StoredRecordsErasedError`,
|
|
2475
|
+
including in audit-only mode, before the legacy callback or redacted-result return.
|
|
2476
|
+
A caller that is itself refused while the key holds an erased row is handled separately:
|
|
2477
|
+
against an erased recorded refusal, a retained-identity conflict leaves the caller's own
|
|
2478
|
+
refusal in place and a matching identity raises `StoredRecordsErasedError`; an erased row
|
|
2479
|
+
of any other status leaves the caller's own refusal in place.
|
|
2480
|
+
`createDurableSagaParentLifecycle` preserves its event-type conflict check, then refuses
|
|
2481
|
+
an erased prior event before comparing payloads. Erasure-capable custom stores expose
|
|
2482
|
+
`storedRecordsErased` on the mutation transaction's `getEvent` result.
|
|
2483
|
+
Store batch-replay paths refuse erased stage rows before digest comparisons, including
|
|
2484
|
+
an origin stage erased with its child before child-ID attachment.
|
|
2485
|
+
`packages/platform-host/tests/erasure-digest-cases.ts` pins same/different payload refusals and unchanged events.
|
|
2486
|
+
|
|
2487
|
+
Host overwrites only the digests it computes itself. A hash that another package or the
|
|
2488
|
+
application places inside stored evidence is shown to the redactor and is kept unless the
|
|
2489
|
+
redactor removes it. `@fabricorg/policy-opa` stores one by default: `inputHash`, a SHA-256
|
|
2490
|
+
of the policy input, in the policy outcome's engine metadata. Remove it in the redactor
|
|
2491
|
+
when the policy input can contain erased values, or configure the evaluator's `hashInput`.
|
|
2492
|
+
|
|
2493
|
+
The staged `batchDigest` formula now covers parent ID and sorted keys, actions,
|
|
2494
|
+
versions and optional principals, excluding parameters and parameter digests.
|
|
2495
|
+
Migration 16 replaces persisted `batch_digest` values with `LEGACY_BATCH_DIGEST`.
|
|
2496
|
+
Custom `PlatformHostStore` implementations persisting stages should overwrite stored
|
|
2497
|
+
batch digests with that placeholder and skip only the batch-digest comparison for
|
|
2498
|
+
those rows; the remaining request identity and batch-size comparisons still apply.
|
|
2499
|
+
`stagedInvocationRequestMatches` and `LEGACY_BATCH_DIGEST` are exported from
|
|
2500
|
+
`@fabricorg/platform-host` for this comparison.
|
|
2501
|
+
During a rolling upgrade, an old Host can write payload-covering digests after the
|
|
2502
|
+
migration runs. Finish the rollout before relying on this guarantee, and replace
|
|
2503
|
+
any such late-written digests with the placeholder.
|
|
2504
|
+
Erasure replaces external reconciliation IDs with fresh random `rec_` IDs, including
|
|
2505
|
+
on repeated erasure, following the existing reapplication of the redactor.
|
|
2506
|
+
See `packages/platform-host/tests/erasure-digests.test.ts` and
|
|
2507
|
+
`packages/platform-host/tests/staged-invocations.test.ts` for regression coverage.
|
|
2508
|
+
|
|
2509
|
+
Application hashes outside the protected Host paths remain visible to the redactor.
|
|
2510
|
+
|
|
2511
|
+
| Field | Coverage and reader before erasure | Erasure result |
|
|
2512
|
+
| --- | --- | --- |
|
|
2513
|
+
| Invocation `parameterDigest` | Submitted parameters at identity capture, potentially including values removed by write-time redaction; `idempotencyConflict` and `refusalIdentityConflict` | Removed / SQL `NULL` |
|
|
2514
|
+
| `authorizationBinding.parameterDigest` | Parameters bound to captured authority; `validateAuthorizationBinding` validates the incoming binding, rather than comparing the stored binding digest | `ERASED_DIGEST` |
|
|
2515
|
+
| Staged `parameterDigest` | Canonical child parameters; `stagedInvocationRequestIdentity` feeds store exact-batch comparisons; staged dispatch does not read this field | `ERASED_DIGEST` on rewritten stages |
|
|
2516
|
+
| Staged `batchDigest` | Parent ID plus sorted requests; store `stageInvocations` / `applyStagedBatch` drift checks | Payload-free formula; legacy values replaced by migration 16 |
|
|
2517
|
+
| `externalCompletion.digest` | Provider, reference, outcome, result, error and evidence, excluding observation time; completion duplicate/contradiction and conditional-write comparisons | `ERASED_DIGEST` |
|
|
2518
|
+
| Private proposal / saga payload comparisons | Original proposal and saga payload comparisons | Removed, including columns and memory maps |
|
|
2519
|
+
| Contradiction reconciliation ID | `recordCompletionReconciliation` hashes provider/reference/kind, with the kind carrying a completion-digest prefix | Replaced with a fresh random ID |
|
|
2520
|
+
|
|
2521
|
+
An older Host reading the missing invocation digest treats the erased row as a legacy
|
|
2522
|
+
record and can accept a changed request under its existing key. This is an additional
|
|
2523
|
+
reason to upgrade Host instances sharing storage before the first erasure; older writers
|
|
2524
|
+
also bypass the erasure guards.
|
|
2525
|
+
|
|
2526
|
+
Store SQL guards reject invocation/evidence writes after erasure with `StoredRecordsErasedError`
|
|
2527
|
+
(`HOST_RECORDS_ERASED`). Conditional `updateSettledActionInvocation` and
|
|
2528
|
+
`writeAdapterInvocation` return `false` in both built-in stores. Late approval recording
|
|
2529
|
+
returns its normal replay/refusal outcome without appending fresh attempt evidence for an
|
|
2530
|
+
erased record. External completion refuses before reading its supplied payload (`ExternalCompletionError.refusal ===
|
|
2531
|
+
"erased"`, a permanent refusal). Completion processing loads the scoped invocation
|
|
2532
|
+
before validating supplied completion fields. Erasure retains live memory invocation
|
|
2533
|
+
references during its await-free commit so a delayed read observes the marker. First-write-wins event append retains
|
|
2534
|
+
the existing redacted event. Outbox claims hydrate the stored redacted envelope. For an erasable store, the relay
|
|
2535
|
+
renews the lease and uses the returned stored envelope immediately before publisher handoff and withholds stale claims or
|
|
2536
|
+
renewal responses that arrived after the renewed lease expired;
|
|
2537
|
+
requeue retains that envelope and redacted prior-error evidence. Requeue replaces
|
|
2538
|
+
new operator reason text with `[erased]`, and failure settlement discards new publisher
|
|
2539
|
+
error text for erased events. This prevents those late inputs from restoring old text.
|
|
2540
|
+
|
|
2541
|
+
Apply migration 16 with `ensureSchema()` and upgrade **Host instances sharing the
|
|
2542
|
+
database before the first erasure**. Mixed versions can hold previously loaded payloads.
|
|
2543
|
+
A payload handed to a publisher, adapter, caller, cache or downstream consumer before
|
|
2544
|
+
erasure commits cannot be recalled. This operation does not erase external systems,
|
|
2545
|
+
backups, application domain data or consumer projections; arrange those separately.
|
|
2546
|
+
See webhook-delivery's **Event erasure** section for the equivalent delivery limit.
|
|
2547
|
+
|
|
2548
|
+
`erasureChecks()` and `runErasureChecks(subject)` provide eight destructive checks for
|
|
2549
|
+
isolated test stores. Supply `payloadRecords.seed/read` fixture access to verify the
|
|
2550
|
+
record kinds outside the base store's read port. Missing erasure support or fixture
|
|
2551
|
+
access reports the relevant check as `skipped`; callback/check errors report `failed`.
|
|
2552
|
+
The success probes check for exactly one scoped `HostRecordsErased` event with the
|
|
2553
|
+
request's `actorId` and `actorType`. Both built-in stores run the complete kit in their tests.
|