Phase 6: audit logging for LLM + tool invocations#6
Merged
Conversation
- New audit_log table with immutability triggers (UPDATE/DELETE blocked at the DB layer); migration in backend/migrations/audit_log.sql and appended to backend/schema.sql. - backend/src/lib/audit.ts: AuditEntry shape, recordAudit() fire-and-forget insert, hashContent() SHA-256 helper, AUDIT_LOG_ENABLED feature flag. - Tool dispatcher (lib/tools/registry.ts) records a tool_call row per invocation with duration, input/output hashes, and resolved document IDs from args + side effects; errors are recorded then re-thrown. - streamChatWithTools wraps the per-provider stream and records an llm_call row on success or error. Audit context flows through runLLMStream and the tabular generate path. - GET /audit-log returns the caller's own entries with filters (project_id, event_type, from, to, limit, offset). - Unit tests cover hashContent determinism, recordAudit insert shape, feature-flag no-op, and error swallowing.
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.
Summary
Phase 6 of the Mike → GordonOSS finance fork build plan: append-only audit logging for every LLM call and every tool invocation. Required groundwork before paid data connectors (Phase 8) so we can prove who saw what data and which model handled it.
What changed
audit_logtable with immutability triggers (UPDATE/DELETE raiseaudit_log entries are immutable). Migration inbackend/migrations/audit_log.sqland appended tobackend/schema.sql. Indexes on(user_id, created_at),(project_id, created_at), andevent_type.backend/src/lib/audit.ts—AuditEntryshape,recordAudit()(fire-and-forget; never throws),hashContent()SHA-256 helper,AUDIT_LOG_ENABLEDfeature flag.lib/tools/registry.ts) records onetool_callrow per invocation withduration_ms, input/output hashes, and resolveddocument_ids(extracted from both args and side effects). Errors are recorded then re-thrown.lib/llm/index.ts) records onellm_callrow perstreamChatWithToolscall with provider, model, hashes, and duration. Audit context flows throughrunLLMStream(used by/chat,/projects/:id/chat,/tabular-review/:id/chat) and the tabulargeneratepath.GET /audit-log— user-scoped reader withproject_id,event_type,from,to,limit(default 100, max 1000), andoffsetfilters.hashContentdeterminism, insert shape, feature-flag no-op, and error swallowing.What's NOT in this PR
The schema's
event_typeCHECK reservesconnector_fetch,document_upload, anddocument_downloadbut no code path emits them yet — they land in Phase 8 (connectors) and a follow-up that instruments the documents/downloads routes. No migration needed when those wire up.Reviewer notes
recordAuditcatches all errors andconsole.errors them. The build plan calls this out — audit logging must never break a user request.user_emailis denormalized:user_idisON DELETE SET NULLso historical entries survive a user deletion; the email column preserves attribution for investigators.streamChatWithToolsmay internally drive multiple provider iterations (tool-use loops). We record one row per outer call rather than one per iteration — keeps the table compact and matches "one user message = one llm_call row."Test plan
npx vitest runinbackend/— 65/65 passing (7 new inaudit.test.ts)npx tsc --noEmitcleannpx eslint src— 0 errors (pre-existing warnings unrelated)backend/migrations/audit_log.sqlto Supabase, send a chat message, confirmaudit_logcontains bothllm_callandtool_callrowsUPDATE audit_log SET status='error' WHERE id = ...should raise the immutability exceptionGET /audit-logreturns only the caller's own rows🤖 Generated with Claude Code