Skip to content

feat(audit): the audit log as a striped db-core table with its own retention - #323

Merged
iheitlager merged 3 commits into
mainfrom
feat/309-audit-table
Nov 14, 2026
Merged

iheitlager merged 3 commits into
mainfrom
feat/309-audit-table

Conversation

@iheitlager

Copy link
Copy Markdown
Member

Summary

  • The audit log is a striped db-core table, audit, sorted by at, in data/audit or under <FRANK_STORE>/audit (ADR-024 point 3). It has its own retention, FRANK_AUDIT_RETENTION_DAYS (default 400), apart from the sightings'. This supersedes ADR-019 point 3: one object per line in S3 is gone.
  • The server is the table's one writer. A command run next to it (frank user add through podman exec) spools its line to data/audit.spool, and the server moves it in every minute and at shutdown. With the server stopped the command writes the table itself. frank serve refuses to start while another process holds the table.
  • /v1/admin/audit answers from the table with the admin's scope in the SQL WHERE. MCP tool calls are reads: they leave the audit and the access log names the tool (tool).

Changes

Added

  • crates/frank/src/audit.rs: the table, the spool, the scoped query and a one-time import of the JSONL days (files in data/audit/, or audit/YYYY-MM-DD/ objects in the store), kept in audit.jsonl/ and marked IMPORTED. An import cut short resumes without duplicates.
  • FRANK_AUDIT_RETENTION_DAYS (CLI flag and env), in compose and the STACKIT module.
  • make s3-column TABLE=audit reads the audit table (scripts/read-garage-column.sh).

Changed

  • /v1/admin/audit is one query; the response is as before.
  • mcp_call is no longer an audit line: frank::access gets a tool field (ADR-024 point 6).
  • frank token revoke audits the token's 12-digit prefix, not what was typed.
  • rust-version 1.85 → 1.89 (File::lock for the spool). frank-core's unhex uses is_multiple_of, which its MC/DC tests now name by the new snapshot id.
  • Specs 010, 011, 015, 020; ADR-024; README; docs/mcp.md, docs/sightings-storage.md.

Fixed

  • Nothing user-visible.

Changelog

  • The audit log is a database table of its own with its own retention (FRANK_AUDIT_RETENTION_DAYS, 400 days). Commands run beside the server no longer write the audit file directly. The existing JSONL audit days are imported once on start.
  • MCP tool calls are no longer audit lines; they are on the access log with the tool's name.

Testing

  • cargo test --workspace --locked --features frank/tls: 376 passed, 0 failed
  • make test-mcdc: 48/48 decisions covered; make check-mcdc-fresh current
  • cargo clippy --workspace --all-targets --features frank/tls -D warnings, cargo clippy -p frank-wasm --target wasm32-unknown-unknown, cargo fmt --check, make check-mvl-limit, make check-assurance (0 dead links)
  • Import tested both ways: JSONL files and store objects, opened again and with the marker removed
  • Token never in an audit row or in the table's files (admins_manage_their_own_organisation_only)
  • Spool: a command run while frank serve holds the table (a_command_next_to_the_server_spools_its_audit_line); a second server refused (serve_refuses_an_audit_table_held_by_another_process)
  • s3-column table selection checked with a stub column-rs (URL and error paths)
  • make s3-column TABLE=audit against a running Garage: not run (needs make up); the table's read through column-rs is unverified

Related Issues

Related to #309

The open item is the live read above. Once it is run, the body can say Closes #309.


🤖 Generated with Claude Code

iheitlager and others added 3 commits November 14, 2026 20:31
…tention (#309)

The audit is the table `audit` (ADR-024 point 3): `at`, `by`, `action`,
`participant`, `user`, `token` and the rest as JSON, sorted by `at`, in
`data/audit` or under `<FRANK_STORE>/audit`. FRANK_AUDIT_RETENTION_DAYS
(400) drops whole days at the hourly compaction, apart from the sightings'.

- The server is the one writer; a command that finds the table locked
  appends to `data/audit.spool`, which the server drains each tick and at
  shutdown. `frank serve` refuses a table another process holds.
- `/v1/admin/audit` is one query with the admin's scope in the WHERE;
  the response is as before.
- The JSONL days (files, or `audit/YYYY-MM-DD/` objects) are imported
  once, kept in `audit.jsonl/`; an import cut short goes on without
  duplicates.
- `mcp_call` leaves the audit; the access line carries `tool` (point 6).
- `frank token revoke` audits the token's 12 digits, not the prefix given.
- rust-version 1.89 (File::lock); `make s3-column TABLE=audit`; specs
  010, 011, 015, 020, README, deploy settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gate

serve::maintain and audit::import_store were CCN 18 (gate 15). The audit's
per-tick work moves to maintain_audit, the read-cache warm-up to warm_cache,
and the store import to move_day_objects and store_text. No behaviour change;
the MC/DC snapshot follows the moved lines.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@iheitlager
iheitlager merged commit 8210333 into main Nov 14, 2026
6 checks passed
@iheitlager
iheitlager deleted the feat/309-audit-table branch November 14, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant