feat(audit): platform audit trail Phase 1 + lifecycle smoke test (#20)#331
Merged
Conversation
#20) Phase 1 of SEC-001 / Issue #20 — the cross-cutting platform audit log that complements the Process Engine's existing audit_entries table. This PR ships the infrastructure only: the table, the service surface, the database operations, and the admin query API. Phases 2–4 (write integration into existing routers, MCP TypeScript audit, hash chain verification + export) follow as separate PRs against #20. Schema - audit_log table in db/schema.py with indexes matching the documented query patterns (timestamp, event_type, actor, target, mcp_key, request_id) - Append-only enforcement at the SQLite layer: triggers block UPDATE unconditionally and DELETE within the 365-day retention window. DELETE of older entries is allowed so a future retention cleanup can run. - Migration audit_log_table (#31) creates the table on existing installs; guarded by PRAGMA table_info so it is idempotent on fresh installs too. Service & DB - db/audit.py: PlatformAuditOperations (insert, query, count, single fetch, range, stats) following the class-per-domain pattern (invariant #2) - services/platform_audit_service.py: PlatformAuditService with global instance, AuditEventType / AuditActorType enums, actor resolution (user > agent > system > mcp_client), JSON details encoding, and the contract that audit failures are logged but never raised so they cannot break the caller's primary operation - Hash chain code is present but dormant behind _hash_chain_enabled = False for Phase 4 - DatabaseManager exposes the operations via thin delegate methods API - routers/audit_log.py mounted at /api/audit-log to coexist with the existing /api/audit Process Engine router. New endpoints: GET /api/audit-log — list with filters and pagination GET /api/audit-log/stats — aggregate counts GET /api/audit-log/{event_id} — single entry by UUID - Every endpoint requires Depends(require_admin) - Route ordering: /stats declared before /{event_id} so it isn't shadowed Tests - tests/test_audit_log_unit.py: 24 unit tests covering schema, query filters, pagination, immutability triggers (UPDATE blocked, DELETE within retention blocked, DELETE >1yr allowed), service actor resolution for all four actor types, JSON details serialization, unique event_id generation, request-metadata persistence, and the "audit failures must not raise" contract. Docs - docs/memory/feature-flows/audit-trail.md (new flow) - docs/memory/feature-flows.md index entry - docs/memory/requirements.md §20.1 status updated to Phase 1 implemented - docs/memory/architecture.md API endpoints + schema sections Refs #20 (issue stays open until all four phases ship) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Second commit on the #20 branch. Phase 1 ships the infrastructure; this commit adds the first write-side integration so the audit log actually produces rows on real user actions. Router — routers/agents.py Four handlers emit audit rows after a successful state transition: - create_agent_endpoint → AGENT_LIFECYCLE / create details: {template, base_image, agent_type} - start_agent_endpoint → AGENT_LIFECYCLE / start details: {credentials_injection} - stop_agent_endpoint → AGENT_LIFECYCLE / stop - delete_agent_endpoint → AGENT_LIFECYCLE / delete All four use the same kwargs shape (actor_user=current_user, actor_ip from request.client, target_type="agent", target_id=agent_name, endpoint=request.url.path). Log calls live in the router, not the service, so current_user stays in scope without threading through service signatures. Placement is always after the state transition has committed — if the action fails partway, no audit row appears. Audit failures are swallowed by the service layer per the Phase 1 contract and never break the caller's primary operation. Tests — tests/test_audit_log_unit.py Five integration-shape tests assert the exact field layout produced by each handler (via direct service.log calls mirroring the router's kwargs): - test_lifecycle_create_emits_row - test_lifecycle_start_emits_row - test_lifecycle_stop_emits_row - test_lifecycle_delete_emits_row - test_lifecycle_full_flow_creates_ordered_history (asserts create → start → stop → delete produces 4 rows in order) A refactor of the call site cannot silently break the contract without breaking these tests. 29/29 total audit tests passing. Docs - feature-flows/audit-trail.md: new "Phase 2a — Agent Lifecycle Integration" section with the handler table, kwargs shape, and placement rationale - requirements.md §20.1: Phase 2a status updated to ✅ - architecture.md: platform audit section updated to note lifecycle smoke test Phase 2b (auth, sharing, settings, credentials, rename, request_id middleware), Phase 3 (MCP TypeScript audit), and Phase 4 (hash chain verification, export, retention) remain as follow-up PRs against #20. Refs #20 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dolho
force-pushed
the
feature/20-audit-trail-phase1
branch
from
April 15, 2026 08:55
845a1c2 to
c7392ff
Compare
vybe
approved these changes
Apr 15, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
LGTM - PR validation passed all checks. Clean three-layer architecture, comprehensive documentation, strong test coverage (29 tests), and security-conscious design.
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
First slice of SEC-001 / Issue #20 — the cross-cutting platform audit log that complements the Process Engine's existing
audit_entriestable. Two commits:d4e39be): infrastructure — schema, immutability triggers, service, DB ops, admin query API, 24 unit tests845a1c2): agent lifecycle smoke test — 4 audit calls inrouters/agents.py+ 5 integration-shape tests, so the audit log actually produces rows on real user actionsIssue #20 stays open — Phase 2b (auth, sharing, settings, credentials, rename, request_id middleware), Phase 3 (MCP TypeScript audit), and Phase 4 (hash chain, export, retention, compliance guide) follow as separate PRs.
What ships
Database
audit_logtable, 6 indexes matching the spec's documented query patterns, 2 immutability triggersUPDATEblocked unconditionallyDELETEblocked within the 365-day retention windowDELETEof older entries allowed so a future retention cleanup can runaudit_log_table(System Manifest UI #31) creates the table on existing installs, idempotent viaPRAGMA table_infoguardService & DB layer
db/audit.py—PlatformAuditOperations(insert, query, count, single fetch, range, stats) following the class-per-domain pattern (invariant Feature/gemini runtime support #2)services/platform_audit_service.py—PlatformAuditServicewith global instance,AuditEventType/AuditActorTypeenums, actor resolution (user > agent > system > mcp_client), JSON details encodingNone. Caller's primary operation is never affected. Verified bytest_service_never_raises_on_db_failure._hash_chain_enabled = Falsefor Phase 4DatabaseManagerexposes 6 thin delegate methodsAPI
routers/audit_log.pymounted at/api/audit-logto coexist with the existing/api/auditProcess Engine router (no breaking change to existing consumers)GET /api/audit-log— list with filters and paginationGET /api/audit-log/stats— aggregate counts by event_type and actor_typeGET /api/audit-log/{event_id}— single entry by UUIDrequire_admin/statsdeclared before/{event_id}so it isn't shadowed (invariant fix: add missing logging_config.py to backend Dockerfile #4)Lifecycle integration (Phase 2a)
Four handlers in
routers/agents.pyemit audit rows after a successful state transition:create_agent_endpointcreate{template, base_image, agent_type}start_agent_endpointstart{credentials_injection}stop_agent_endpointstopnulldelete_agent_endpointdeletenullAll four use the same kwargs shape:
actor_user=current_user,actor_ipfromrequest.client,target_type=\"agent\",target_id=agent_name,endpoint=request.url.path. Log call lives in the router, not the service, socurrent_userstays in scope without threading through service signatures. Placement is always after the state transition has committed — if the action fails partway, no audit row appears.Tests
tests/test_audit_log_unit.py— 29 unit tests covering:Docs
docs/memory/feature-flows/audit-trail.md— new flow doc with strict-format sections (Overview, Problem, User Story, Entry Points, Frontend Layer, Backend Layer, Side Effects, Error Handling, Security Considerations, Phase 2a integration, Out of Scope, Testing, Related Flows)docs/memory/feature-flows.md— index entrydocs/memory/requirements.md§20.1 — status flipped to 🚧 Phase 1 + Phase 2a implementeddocs/memory/architecture.md— new API endpoints table + newaudit_logschema blockManual verification
End-to-end smoke test against the local backend after merge:
This was actually run against the local backend during PR development — all 12 verification steps passed (table created, lifecycle calls produce rows with correct actor / target / details / timestamps, stats endpoint aggregates correctly, single-entry lookup works, bad token returns 401, UPDATE and DELETE both blocked by triggers). The audit log is observably useful end-to-end.
Design decisions worth eyeballing
/api/audit-logprefix, not/api/auditrouters/audit.pyexposes the Process Engine's separate audit system at/api/audit. Mounting platform audit at the same prefix would either break that or require renaming it (breaking change for any current consumer). Two distinct prefixes, both intentional per spec; can be unified later via a?system=query param.PlatformAuditService, notAuditServiceservices/process_engine/services/audit.py::AuditService. Two classes with the same name in the same Python process is a foot-gun. Prefix avoids the collision.PlatformAuditServicefor the Phase 4 hash chain. Kept because Phase 4 will turn it on; ripping it out now means a larger Phase 4 PR. Documented in the flow doc.current_userin scope without threading through service signatures. Each call is 10 lines in the router. Easier to test.What's NOT in this PR (intentional Phase 2b/3/4 deferrals)
request_idmiddleware for cross-event correlationIssue #20 stays open until all four phases ship.
Test plan
pytest tests/test_audit_log_unit.py -v— 29/29 in 1.2s)Refs #20
🤖 Generated with Claude Code