Evidence and replay
audit-export produces one complete tenant prefix and its referenced immutable policy bundles. replay consumes only those files, verifies the chain and bindings, and reconstructs chronological decisions with separate in-memory ledgers. An ELIGIBLE result is an explanation, never executable authority.
Export and trust
Set DATABASE_URL to the restricted gateway connection in the private operator environment. Store a current tenant ADMIN credential in a private text file; never pass it as a command-line argument. The destination parent must already exist, and both output files must be new.
uv run execbound audit-export --tenant <tenant-UUID> --credential-file local-data/admin.token --output local-data/history.json
The default companion checkpoint is history.head.json; --checkpoint selects a different new file. --database <name> reads a restored copy on the same server instead, so a restore is checked without anyone building a second connection string with its password; the runtime role, forced RLS, check_runtime and the ADMIN credential check apply unchanged, and the output is the same either way. Export reads a coherent read-only repeatable-read snapshot and checks current ADMIN authority again in separate transactions immediately before publishing each file. It does not append an export audit event or contact a provider.
Retain the checkpoint independently. A head distributed alongside its own export establishes internal consistency only: replacement of both files by a privileged writer is outside that guarantee. Publication links the checkpoint first and the export last; a machine crash can leave an orphan checkpoint. Existing files are never overwritten on retry.
Replay and comparison
These commands also work with the database and providers stopped. They do not load .env or runtime configuration, authenticate live credentials, resolve current context, activate policy, or consume real approvals and budgets.
uv run execbound replay --input local-data/history.json --trusted-head local-data/history.head.json --output local-data/baseline
uv run execbound replay --input local-data/history.json --trusted-head local-data/history.head.json --candidate local-data/candidate.json --output local-data/comparison
Each output is a new directory containing deterministic report.json and report.md. Inputs are strict JSON: no duplicate keys, floating numbers, unknown versions, foreign references or silently truncated histories. V1 bounds the export to 64 MiB, 10,000 events and 256 referenced bundles; candidate files are bounded to 1 MiB. Oversized, corrupt or secret-bearing history is rejected without exposing raw input or SQL diagnostics. Use a new destination for each run. For a pre-activation view without files, execbound bundle-preview runs the same export, verification and candidate comparison in memory as a current ADMIN and prints the changed decisions.
The console's preview answers a Policy manager too, limited to the decisions recorded in the Spaces where they hold policy.manage: the whole chain is still read in server memory, because its integrity check needs the complete prefix, and only those decisions are counted and listed. bundle-preview and the replay export stay an administrator's. An administrator narrowed to some Spaces, in the console or with bundle-preview, is limited to those Spaces the same way, and the answer's scope says which it was (workspace or spaces). complete and issues stay tenant-wide, and a candidate's limit differences count tenant-wide impact as shared limit scopes intend, so a filtered preview can reflect that other Spaces acted without naming what (THREAT_MODEL.md R37).
Future execution events capture normalized policy inputs, historical identity/approval bindings and actual usage/start/finish times. Audit sequence orders events; semantic time determines freshness and rolling windows. Confirmed units count in (usage_time - window, usage_time]; unresolved reservations remain counted until exact evidence settles them. Approved operations count toward total ceilings and do not count toward autonomous ceilings. New rules, runs, inverse operations or changed policy cannot erase retained impact. Approval captures carry required_approvers and every counted approver; a VERIFIED capture with fewer distinct approvers than required, a repeated principal or a denial is INSUFFICIENT_QUORUM and makes the history incomplete, and approval.decision_recorded marks a partial approval as a consequential decision. Observed decisions are ordinary captures with mode: OBSERVE: the execution ends OBSERVED with outcome OBSERVED, replay counts its OBSERVED impact only for observe-mode evaluations (the projection the gateway used), a candidate bundle re-evaluates it like any enforced decision, and an observed execution that shows an approval, reservation, claim or dispatch, or an OBSERVED transition without an observe-mode capture, is OBSERVED_WITH_EFFECT and makes the history incomplete. tenant.mode_changed is informational.
Limit scopes are reconstructed from captures alone. Every unit the ledger reserves, observes or holds uncertain carries the account and scalar facts of its capture's frozen plan and the memberships of its captured agent identity, and usage filters each rule's units by scope exactly as the store does: any listed action, the asking principal, the rule's account, the selector over the unit's retained facts, or the rule's group in the unit's retained membership. Captures now record the agent's memberships as identity.groups, an empty list when the agent belongs to no group, so replay can tell "none" from "unrecorded"; a capture without the key predates recorded membership and is evaluated as a member of nothing, which is what the kernel did, so baseline decisions still match. UNKNOWN_GROUP_MEMBERSHIP is a candidate-only uncertainty: a candidate rule with a group scope marks a decision UNRESOLVED when the decision's own capture lacks groups (its applicability is unknown) or when a unit that would otherwise have counted by state and window lacks them (the count is only a lower bound). A unit with unknown membership never counts toward a group scope, so the uncertainty is expressed by the marker and never by possible room in the budget, and a denial at the lower bound still stands. Offline candidate validation checks every action a rule lists against the catalog and every selector against the contracts of the candidate's mappings; group and account existence cannot be checked offline, so a candidate group rule for a group no capture recorded applies to nobody. group.created, group.assigned and group.revoked are informational.
The baseline re-evaluates recorded decisions from immutable facts and reconstructs retained outcomes. Legacy events without required inputs or semantic times are incomplete. Mutable current inventory, approval rows and stored counters never fill historical gaps. Unknown consequential semantics and invalid chronology propagate uncertainty rather than making later decisions appear eligible.
Emergency stop periods are reconstructed from matching tenant.stop_activated and tenant.stop_released events (a stop still active at the head is an open period) and listed in both reports. An admission, approval decision or dispatch commitment recorded inside a period, by chain order or by semantic time, is reported as DECISION_DURING_STOP and makes the history incomplete, because the gateway could not have produced it. An activation while a stop is open or a release without a matching activation is unsupported history. Candidate comparison ignores stops: a stop is not policy.
Each decision row carries the frozen arguments when they are non-empty, omitted otherwise, and report.md renders them as an Arguments: line, so a decision that turns on an option is explainable from the report. Verification rebuilds the attempted provider command with those arguments, so an export containing argument-bearing actions verifies against its recorded command digests. Offline replay cannot rebuild the catalog's OperationSpecs, because an export carries no mapping rows: simulate therefore validates a candidate bundle without them, and the action-context check holds only because a compatible candidate must keep the original, store-validated Cedar schema and mappings.
Candidate comparisons initially allow policy and limit changes with the captured operation mappings, accounts and context contracts. Newly required approval or an action that was never observed produces UNRESOLVED; a changed plan cannot reuse an old approval, so a candidate quorum change is a policy change and recorded approvers never satisfy a changed requirement by assumption; the bundle preview reports the candidate's effective requirement per changed decision. Possible impact and same-target dependencies propagate forward. A denial that remains true despite uncertainty can still be reported as DENY. A candidate re-reads frozen security signal records under its own thresholds and admitted sources but keeps every fact's frozen validity, as for inventory and incident facts, so a candidate that shortens freshness replays as a certain DENY and cannot be previewed (threat model R33). A candidate that needs a record the plan never froze, or maps any verdict to clean over a source that does not attest liveness, is UNRESOLVED with SIGNAL_EVIDENCE_UNAVAILABLE, and so is a later decision under a candidate rule selecting on a signal fact whose count rests on an earlier unit the candidate cannot read (Security signals).
An unchanged observed action can be projected only under the report's explicit FIXED_HISTORY_PROJECTION provenance. This is not evidence about alternate external reality. No assumed-success or assumed-approval mode exists. Reports keep recorded and hypothetical outcomes distinct and expose dependency IDs and possible counts.
Terminal evidence retains the gateway's recorded proof and exact command/digest bindings. Offline verification does not independently reauthenticate the provider HMAC, because signing keys are deliberately excluded. Replay exposes no route of its own and never calls a provider.
Regression fixtures
A regression fixture is one versioned file. It holds a sanitized history, its candidate bundles and the outcome replay classified for every decision when the fixture was recorded. replay-fixture builds one from an export, its trusted head and up to eight candidates. replay-fixture-check replays it and fails when any decision comes out differently. Both run offline, as replay does: neither loads .env, opens a database or calls a provider.
uv run execbound replay-fixture --input local-data/history.json --trusted-head local-data/history.head.json --candidate local-data/candidate.json --output local-data/history.fixture.json
uv run execbound replay-fixture-check --fixture local-data/history.fixture.json
uv run execbound replay-fixture --from-fixture tests/fixtures/replay/signal-world-v1.json --output local-data/rerecorded.json
The format. A fixture is strict JSON, written with sorted keys and one-space indentation. It carries:
format(execbound.replay-fixture) andversion(1 or 2). An unknown value of either is refused, as are duplicate keys, floating numbers and unknown fields.sanitization, which counts the identifiers, labels and opaque digests replaced. In version 2 it also hassignatures, which names the published key each kind of retained record is signed under (below).
Versions. A history whose evidence is only the journals of operations ExecBound dispatched is written as version 1, as it always was. One that also holds evidence of work done outside ExecBound is written as version 2. That evidence is an external executor's signed record or ES256 attestation, an executor's word kept as an assertion, a Microsoft Graph connector record, or a checkpoint its provider verified. Version 2 exists because version 1 refused all of these. A version 1 fixture is refused on reading if it holds any of them, and a version 2 fixture is refused if its signatures do not name exactly the kinds it holds. Each settlement keeps its tier (ATTESTED, ASSERTED, PROVIDER_VERIFIED, or none), and the expectations record it beside each decision. So a fixture never presents an executor's word, or ExecBound's own read and comparison, as a provider's enforcement.
trusted_head,export(the sanitized history, with its frozen context, its bundles and its event order) andcandidates.expected:- For the baseline:
complete,issues, and for every decision its sequence, execution, action, stage, recorded effect, mode and settlement tier, with the effect, reasons, obligations, outcome, provenance, counts and dependencies replay gave it. - For each candidate in order: its content hash,
complete,issues, and every decision's result with its differences.
- For the baseline:
A fact that was missing stays explicit as an UNRESOLVED effect with its reasons, so an incomplete history is recorded rather than refused. The check compares those fields and not the wording of report.md. It leaves out plan hashes, but it does compare each candidate's content hash, so a candidate edited without re-recording fails as candidate 1 candidate_hash. A sentence can therefore change without moving a fixture, and a decision cannot.
replay-fixture-check exits 0 when everything matches. It exits 1 when something differs and lists each run, sequence and field that does (baseline sequence 13 effect, candidate 2 sequence 13 reasons). A sequence with a decision missing or recorded twice reads baseline sequence 13 decision. It names no value. It exits 2 when the fixture cannot be read, is refused on reading (below) or does not verify, and names the code, such as PRIVATE_DATA, FIXTURE_NOT_SANITIZED or, for an edited event, INVALID_CHAIN. The bounds are the export's own (64 MiB, 10,000 events and 256 bundles, measured compact), at most eight candidates of 1 MiB each, and a fixture of at most 128 MiB.
What is replaced and what is kept. This is pseudonymization, not anonymization.
-
Identifiers. Every UUID in the export and the candidates is replaced, of whatever kind and wherever it appears. That includes UUIDs inside a retained record, Cedar text or an idempotency key, and a 32-digit hexadecimal token is treated as a UUID's undashed form. An ES256 attestation keeps its header and claims as base64url, where an identifier is not text, so the build opens them into JSON first and replaces what they name like anything else. Identifiers are ranked by their original text, and rank n becomes
00000000-0000-4000-8000-followed by n in twelve hexadecimal digits. So a list that a validator keeps sorted stays sorted. -
Labels. Each open-text value the kept history carries becomes a typed, ranked pseudonym such as
resource-000001:- a resource's provider id;
- a resource's version and a mapping's version, the version a fact's source recorded (a signal fact's is its source's registration number, which is kept), the version a provider journal reports, and the composites a Graph connector record froze and read;
- the key id an external executor or a connector signed a record under, on the record and, for an ES256 attestation, in the header it signed too (
signer-000001); - an operation ref an owner named, where a mapping serves an action under another name (a catalog action's own name, such as
endpoint.isolate, is kept); - a hostname or provider-id target selector;
- a context writer the owner named, which its bundle's context settings permit (the catalog's own writers, such as
soc_analyst, are kept); - a fact's source object;
- a signal record's event id;
- a request's idempotency key and reference (a reference's derived key is derived again);
- the agent's external case, its source, case id and link, which admission writes on an execution's first transition;
- a stop reason, and a transition reason that is not one of the kernel's own codes (
PROVIDER_RESPONSE,HUMAN_DENIED,STALE_AUTHORIZATIONand the rest), however it is shaped, so a change number typed as a reconciliation reason is replaced too; - an approval comment;
- a rule's note;
- a string fact value outside its contract's closed domain (no contract has one today).
The same value is also replaced where it is a whole token inside other text, Cedar text included, so a policy comment naming an owner's mapping version
acme-map-7gets that version's pseudonym. An operation ref or a version with no letter in it, such as7or2026-10-01, is the exception: it is replaced only where it is a whole value or a whole JSON string, as in a journal record, because its digits are also tokens of every time a history holds. A ref the catalog spells inside its own names, such asisolateinendpoint.isolate, is a kept word (below).A label whose text is a word the fixture keeps anyway is replaced only where it was recorded. Those words are every name and closed value the catalog and its contracts define, the Cedar language's own words (
permit,forbid,when,unless,principal,resource,Actionand the rest), the kernel's own transition reasons (PROVIDER_RESPONSEand the rest, whether or not the history writes one), every field name, every value at a field the kernel fills from a closed vocabulary (a state, an effect, a source kind, a reason code, a rule id and the rest), the published journal key's idreplay-fixture, the fixed text of every record a fixture signs again (an attestation's formatjws-es256, its algorithmES256and its domainexecbound.executor.outcome, and a connector record's schemeconnector-record.v1), the fixed text of a case link's pseudonym (https,caseandinvalid), and each whole token of them. So a ref namedcontainafter the vendor's own operation becomesoperation-000001where the owner named it, and the plan's and the journal'scontainstay the vendor's. A comment that readsapprovedorproductionbecomes a pseudonym, while the payload'sapprovedfield and the environment fact'sproductionstay as they are. A comment that readspermitorwhenbecomes a pseudonym and every policy keeps its Cedar; a comment that readsPROVIDER_RESPONSEbecomes one and the settlement keeps that reason. The fixture shows the word anyway and never says which label it was. A comment that reads one of the fixture's own field names or a word of its expectations, such ascompleteorUNRESOLVED, becomes a pseudonym as well, and the expectations keep the word. -
Payloads. An informational event's payload is reduced to the keys replay reads:
execution_id, a bundle event'sbundle_idandcontent_hash, and a dispatch attempt'sprecondition_class. Principal labels, source labels and reconciliation reasons never reach a fixture. -
Opaque digests, such as a signal record's digest, become a pseudonym of forty-eight
es and a counter. -
Recomputed. Every digest that covers a replaced value is computed again: bundle hashes and security versions, intent and plan hashes, each provider journal's command hash, each retained record's signature, the evidence's proof digest, and the audit chain and its head.
-
Signed again under published keys. Replay checks no retained signature offline: each is provenance the gateway kept. A signature over the source's bytes would not cover the fixture's, and anyone holding the signer's public key could test guesses of the original identifiers against it. So every retained record is signed again under a key published for fixtures alone, and its own is never kept:
- a provider's journal and a Graph connector record, under the HMAC key
replay-fixture(a journal also takes that key id); - an external executor's Ed25519 record, under the fixture's published Ed25519 key;
- an external executor's ES256 attestation, under the fixture's published ES256 key, with deterministic ECDSA (RFC 6979), so the same history builds to the same bytes. Where the cryptography library cannot sign deterministically the build refuses rather than write a fixture that differs each time.
A deployment refuses each of these keys: the journal key as any connector's evidence key, Gate account's journal key or sample key (Operations), and the two executor keys at registration and rotation, and as any key a grant could be attested by. Evidence signed under one settles nothing.
- a provider's journal and a Graph connector record, under the HMAC key
-
Kept as they were: event kinds and order, every time, catalog names and the catalog's own writers, contract versions, enumerated fact values, claims and arguments, a signal fact's source version, Cedar text apart from the identifiers and labels in it, rule ids, obligation ids, verdicts, producer names and versions, request numbers and the kernel's reason codes.
Refusals. A build writes nothing when it refuses. It names a code and never a value or a path:
- the source's own verification code, when the source does not verify against its head (
INVALID_CHAIN,HEAD_MISMATCHand the rest); PRIVATE_DATA, when the one secret definition (find_secrets) matches the source, a candidate or the output;INVALID_CANDIDATE, for a candidate that does not parse, names another tenant, exceeds 1 MiB, or is one of more than eight;FIXTURE_UNSUPPORTED_EVIDENCE, for a retained record in none of the envelopes above, which nothing the kernel retains is today;FIXTURE_SIGNING_UNAVAILABLE, for an ES256 attestation where the cryptography library cannot sign deterministically;FIXTURE_NOT_SANITIZED, when any source UUID (dashed or undashed, in any case) is still anywhere in the output; when any collected label that is not a kept word is still anywhere in the sanitized history or the candidates, journal records included, an operation ref or a version with no letter in it being found only as a whole value or a whole JSON string; or when any position a label is collected from holds something other than a pseudonym. Every string is replaced once, from its source text, so no pseudonym is replaced again; a label whose text is the pseudonym another label is given, such as a comment that readshost-000001in a history whose one host is given that name, is therefore refused here, since its text stands in the fixture as the host's. The expectations are not searched for labels: they are replay's own classification of the sanitized history, which must equal the source's (below), so they hold replay's words and what that history holds;FIXTURE_NOT_FAITHFUL, when a retained record no longer validates once its values are replaced, when the sanitized history does not verify, or when its baseline and its candidates do not replay to the source's classified outcomes under the mapping. Times are kept, so a label that is no kept word but is also a whole token of a time, such as a one-word comment00or30, is replaced inside that time too, which breaks it, and the build refuses here. A label inside a policy's text is replaced there (above), and refuses the build only when the rewritten policy no longer decides the same.
The mapping itself is never written anywhere. A fixture is written readable by its owner alone (mode 0600), as the export it may come from is.
Reading a fixture holds it to the build's secret screen and to the shape a build writes. replay-fixture-check and --from-fixture refuse, before replaying anything, with PRIVATE_DATA a fixture that the one secret definition (find_secrets) matches anywhere, its candidates and expectations included, and with FIXTURE_NOT_SANITIZED one holding an identifier that is not a pseudonym or source text at a label position, an attestation's header and claims opened and read like the rest, or any retained record not signed under the fixture's published keys. An export wrapped by hand in the fixture format therefore cannot be checked, re-recorded or committed as one, and a secret typed into a committed candidate's policy text is not re-recorded into a new fixture. Neither screen can tell a tenant's free text typed by hand into policy text, such as a comment naming a host in an edited candidate, from any other policy text: nothing of the source travels with a fixture to compare it with. A pull request that edits a committed fixture's candidate shows that text in its diff, and is reviewed there, as the expectations it re-records are.
Re-recording. --from-fixture records a fixture's expectations again from its own embedded history, after a change that is meant to alter replay outcomes. The history, head, candidates and counts are carried over unchanged, so an unchanged engine writes byte-identical output, and an intended change appears as an ordinary diff of the expectations it moved. A pull request that re-records a committed fixture says why in its description.
Fixtures in this repository come only from synthetic worlds:
tests/fixtures/replay/signal-world-v1.jsoncomes from the signal-replay world. The recorded bundle caps isolations of compromised hosts, two isolations on aMediumverdict give one executed decision and one denied one, and the history also holds a stop and a principal's label. Its three candidates readMediumas suspect, as clean (which isUNRESOLVEDwithSIGNAL_EVIDENCE_UNAVAILABLE), and as suspect under a suspect cap.tests/fixtures/replay/two-provider-tenant-0-v1.jsoncomes from the validation demonstration's tenant-0 export and candidate.
The test suite checks both. The demonstration also builds and checks tenant-<index>.fixture.json for both tenants on every run. A fixture built from a real tenant keeps that tenant's timing and shape and should be handled like the export it came from (threat model R41).
Principal lifecycle events
principal.deactivated and principal.reactivated, a person switching a principal off by hand and back on, are informational: they change only whether that principal can authenticate. A principal that cannot authenticate produces no decision event, and one that can again carries its own capture into every later decision. No decision capture, execution, approval or bundle transition replay reconstructs reads a principal's active state, and the credentials and sessions a deactivation ends are their own credential.revoked and human_session.revoked events, already informational.
principal.labelled, the display name recorded when the console adds a person or an agent or person-invite names a person, is informational too: no policy, capture or admission reads a label. Before October 3, 2026 replay did not know the kind, so every decision after a tenant's first named principal replayed UNRESOLVED with INCOMPLETE_PRIOR_HISTORY; a history exported before then replays completely now, because the events themselves never changed.
Four more kinds that decide nothing were registered the same day, and a history holding them replays completely the same way. agent.bound and agent.unbound record a discovered agent bound to a governed principal, or that binding withdrawn; only Monitoring and the Agents pages read a binding. tenant.assistant_changed turns the assistant on or off for a tenant, and the assistant reads through the asking person's session and writes nothing. policy.rules_changed links a draft to the successor its edited rules were written as: the two drafts' own policy.bundle_retired and policy.bundle_created stand beside it, a draft decides nothing until it is validated and selected, and replay reads every bundle a decision names from the export's own bundles.
monitor.finding_acknowledged, an administrator acknowledging a monitoring finding, is informational from the change that first appends it (October 2026): it records a decision about a finding, only Monitoring reads whether a finding is acknowledged, and no decision capture, execution, approval or bundle transition does.
The assistant's own record is informational for the same reason: assistant.asked (a question answered), assistant.explained (an explanation opened) and assistant.refused (a turn that ended without an answer). The assistant reads through the asking person's session and writes nothing. Until October 4, 2026 replay did not know the three, so a workspace that had used the assistant replayed every later decision unresolved; a history exported before then replays completely now.
Replay also treats as informational the event kinds the hosted service appends for its own tenant lifecycle, agent sleep and signup provisioning, so a history exported from a hosted workspace replays here; they are documented with the hosted service. The audit event reference lists every kind, when it is written and the fields it carries.
Teammate invitations
Teammate invitations register four new kinds, all informational: invitation.created and invitation.resent only start or restart a delivery, the same shape as identity.invited/identity.recheck_refused; invitation.revoked only stops one, the same shape as identity.invitation_revoked. invitation.accepted records who joined and under which grants, but the grants themselves are applied through access_change inside the same call, whose own access.set_user_roles decision is retained separately, and the membership and principal rows the acceptance creates are facts in their own tables, not decisions replay recomputes. No decision capture, execution, approval or bundle transition reads any of the four.
Externally executed operations
A settled checkpoint carries a settlement tier, and the tier is what chooses which envelope the retained record is parsed as, before either is parsed. The transition payload names it beside the evidence rather than inside it, because those bytes are validated as a TerminalEvidence and that model admits no extra field; the executions.result column carries it inside the evidence object as well, so an export is self-describing. At tier ATTESTED the authenticated record is parsed as the external executor's SignedOutcome or, for an ES256 attestation, its retained SignedOutcomeJws flattened-serialization wrapper; recorded_outcome reads either shape back into the same ExecutorRecord, exactly as the executor verifier does when an administrator's settlement presents a retained record again. At tier PROVIDER_VERIFIED it is parsed as a provider's SignedJournal, the same envelope a settlement naming no tier carries for an operation ExecBound dispatched; the tier tells the two apart, because the bytes alone cannot. Neither envelope stands in for another, so a mislabelled settlement is a refused export and never evidence read as the wrong kind. An executor's record binds no execution and no tenant of its own (what ties it to a row is the operation identifier the transition names and the record repeats, which is unique per tenant), and its signature, Ed25519 or ES256, is not rechecked here for the same reason the journal's MAC is not: verifying it needs a public key from executor_keys that no export carries.
An assertion is validated rather than stepped over. It is retained under its own key, never as evidence, so reading it cannot promote it; replay binds it to the operation, the account and both hashes and refuses two shapes no settlement writes: a settlement at tier ASSERTED whose outcome is FAILED_NOT_EXECUTED, which would be a history claiming the executor's bare word refunded a reservation, and a settled state that is not the outcome the assertion itself names — or anything but INDETERMINATE where no tier is named. So the two states a settled assertion may hold are EXECUTED and FAILED_EFFECT_UNKNOWN, and no assertion reaching a settlement can name STALE_AUTHORIZATION, because the pairing table forbids the one reason that would have mapped it there. The new refusal codes are INVALID_SETTLEMENT_TIER, INVALID_ASSERTED_OUTCOME and INVALID_ASSERTED_SETTLEMENT, and both kinds of evidence on one transition is INVALID_TERMINAL_EVIDENCE. MISSING_TERMINAL_EVIDENCE now fires for a terminal transition carrying neither kind, so a settled assertion is not reported as a settlement whose evidence went missing.
An attested checkpoint's provenance is RECORDED_EXECUTOR_EVIDENCE, kept apart from the RECORDED_GATEWAY_EVIDENCE a provider journal gets on the path ExecBound dispatched: a journal there is the provider's own signed record of what it did, made by a different party from the one whose budget is at stake. A binding declared CHECKED_BY_CONNECTOR, such as Microsoft Graph's identity.disable, settles from a connector record instead and gets the same label: that record is ExecBound's own signature over the vendor's answers and the measured window between its read and its write, never a statement the vendor made (Provider), while an executor's record is an authenticated statement about an action ExecBound never dispatched and says only who spoke. A provider-verified checkpoint carries a third provenance of its own, RECORDED_PROVIDER_EVIDENCE: it is settled from the same kind of SignedJournal a dispatched operation's settlement carries, but the party who recorded it is the provider acting for an external executor rather than ExecBound's own gateway, so it is kept apart from RECORDED_GATEWAY_EVIDENCE too, and apart from RECORDED_EXECUTOR_EVIDENCE because it is not the executor's own word. A decision row names its settlement_tier when it has one, serialized only when set so every report of operations ExecBound dispatched is byte-identical, and the rendering names the tier with what it rests on. A report that names a tier carries a fourth limitation line: executor attestation is gateway-recorded provenance, no executor public keys are loaded, and an asserted outcome was never verified; the same line covers a provider-verified checkpoint's journal MAC, which is retained provenance for the same reason a dispatched operation's is. The offline ledger settles an asserted execution as executed, matching the impact the kernel confirmed, and settles an asserted FAILED_EFFECT_UNKNOWN the same way for the same reason: that settlement reads the outcome, and the one outcome it reconstructs a non-execution from is FAILED_NOT_EXECUTED. It models no canonical claim at all, so the claim an assertion leaves held is not something replay reasons about, and the live and replayed pictures of an asserted settlement differ in exactly that place.
A corrected row replays through a second settlement, and the ledger has one method for it. A transition out of FAILED_EFFECT_UNKNOWN is the administrator's correction, and the simulator chooses on the payload's own from_state: Ledger.correct admits CONFIRMED → NONEXECUTION once and refuses everything else, including a second call, while Ledger.settle stays exactly as narrow as it was. Widening settle instead would let any exported history claim a refund, which is the one thing an offline consumer must not be able to invent. Without the method, a tenant that corrected one row would get a report saying it could not reconstruct that window's ledger — INVALID_IMPACT_HISTORY and INCOMPLETE_HISTORY — rather than a refused export. The corrected row's exported settlement_tier and terminal time are the correction's, not the statement's, in both of the directions the correction admits. execution.checkpoint_granted, execution.outcome_reported, execution.claim_released, execution.control_point_contradiction and the four executor_key.* kinds are informational. An event kind the informational set does not name makes a tenant's export UNSUPPORTED with UNKNOWN_EVENT_SEMANTICS permanently, because audit events are append-only, which is why a new kind joins that set in the same change that first appends one.
One thing the history cannot answer. A second, differently signed attested record that restates a settlement's own tier, outcome and reason is verified, found to move nothing, and answered as a no-op: nothing is retained and nothing is appended, and the settlement's retained proof digest stays the first record's. So a tenant cannot afterwards see that two of its registered keys attested one settlement. No budget, claim or execution state moves, so this is a limit on what the chain can be asked and never a settlement that went unrecorded; the qualifying signer is any holder of a key the tenant registered, which may be the administrator's settlement key and not only the executor's.
The security event export (execbound export-events, see Operations) sends destinations exactly the sanitized event representation this export produces, in sequence order and never more; it reads the chain and never writes it.
python3 scripts/ci.py validation reproduces the two-provider demonstration and its evidence package.