feat(insights): audit-record checks — the control-plane log against the state it narrates - #309
Open
ophiocus wants to merge 1 commit into
Open
feat(insights): audit-record checks — the control-plane log against the state it narrates#309ophiocus wants to merge 1 commit into
ophiocus wants to merge 1 commit into
Conversation
…he state it narrates Read-only checks over the audit log, returning findings; nothing gates and nothing is surfaced. `audit_events` is an append log ordered by createdAt only, so on its own it cannot tell a removed event from a quiet hour. Where an event names its entity, the log and the state can be read against each other both ways: - keys: every API key has a key.issued event; every revoked key has a key.revoked event; every such event names a key that exists and is in that state. - projects: an archived project has a project.archived event, and that event names a project that is archived. - budgets: a project with a budget has a budget.updated event, and that event names a project that has one. Not evaluable and not claimed: actions whose target is the route rather than the entity (member.*, role.*, repo.*, project.created, org.updated), actions that leave no durable state (auth.logout), and state written by bootstrap paths outside audited routes. The module says so. Keyset-paginated over keys and over paired events; projects and budgets are org-scoped and read whole; every lookup is an IN over the page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ophiocus
force-pushed
the
feat/audit-integrity-observe
branch
from
September 7, 2026 14:11
ca4e2dd to
d35df83
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR — Read-only checks that the audit log agrees with the state it describes, in both directions: every API key has its
key.issued, every revocation itskey.revoked, every archived project itsproject.archived, every budget itsbudget.updated— and every such event names state that exists and is in that condition. One module that returns findings; it surfaces nothing, gates nothing, needs no migration. 10 tests. Opens with a question rather than a claim.The question first.
audit_eventscame through #289 as a plain append log —actor,action,target,payload,createdAt, indexed by time. It has no ordering of its own, so an event removed from the middle leaves no trace the log itself can show; a gap and a quiet hour look the same. That may well be the intended shape now that trust is architectural and the log is a record rather than an instrument — in which case this PR is a small read that costs nothing, and the question is answered. If the log is meant to stand as the account of who did what to the control plane, then what follows is the least that makes a gap visible without giving the log any machinery of its own.Sibling of #308 (the delivery-record checks), same shape, same rule: every check enumerates from a ledger other than the one it verifies. Here the second ledger is the state the log narrates.
What the log already gives us
Three explicit audits carry the entity as target —
key.issuedandkey.revokedwith{type: "key", id}(me-members-roles.ts),auth.logoutwith the principal. Fifteen more arrive through the genericonResponsehook on routes that declareauditAction; those carry the project as target when the route has one, otherwise the route path. So the log names an entity for keys, and a project forproject.archivedandbudget.updated. Those are the pairs this reads.api_keysrowkey.issuedfor that idkey.issuednames a key that existsapi_keys.revoked_atsetkey.revokedfor that idkey.revokednames a key that is revokedprojects.status = archivedproject.archivedfor that projectproject.archivednames a project that is archivedproject_budgetsrowbudget.updatedfor that projectbudget.updatednames a project with a budgetWhat it does not claim
member.*,role.*,repo.*,project.created,org.updatedaudit with the route as target, not the entity, so they cannot be paired by id. They are outside this read, stated rather than approximated.auth.logoutleaves no durable state (it clears a cookie); nothing to pair.createdAtonly. This PR does not add a sequence or a chain; it makes gaps visible from the state side, which is the only side that survives a deletion.Proof — every shape is "break the pair, watch the report"
services/api/test/audit-record.integration.test.ts(10 tests, scoped fixture orgs):keys: state-without-event(key.issued)keys: state-without-event(key.revoked)key.issuedin the log, key row deletedkeys: event-without-state— the deletion is visible from the log that still expects the rowkey.revokedin the log, key still activekeys: event-without-stateprojects: state-without-eventproject.archivedin the log, project is activeprojects: event-without-state— "the project is active"budgets: state-without-eventbudget.updatedin the log, no budgetbudgets: event-without-statepageSize: 2Suite:
services/api24 files / 149 tests green (1 pre-existing opt-in skip);tscandbiomeclean.Bounded by construction
Read-only; keyset-paginated over
api_keysand over the paired events; projects and budgets are org-scoped and read whole; every lookup is anINover the page. Counts are reported over everything read —checkedKeys,checkedProjects,checkedBudgets,checkedEvents— never over survivors.Non-goals
No sequence column, no hash, no chain, no trigger, no gating, no surface, no migration. The log stays exactly what it is; this only reads it against the state beside it.
Files
services/api/src/insights/audit-record.ts—verifyAuditRecord(new)services/api/test/audit-record.integration.test.tsVerify locally
🤖 Generated with Claude Code