Changelog

All notable changes to AuditTrail are documented in this file.


[1.0.2] - 2026-04-08

Comprehensive PRD ↔ codebase sync sprint. Addresses every CRITICAL and HIGH finding from the 2026-04-07 audit (docs/research/audit-2026-04-07/), plus a follow-up wave that builds out every previously-deferred PRD line item and ships the cross-tenant /admin route group.

Sync sprint Phase 3 — previously-deferred PRD items now shipped

  • Adaptive ablation strategies (FR-017). causal.run_ablation now accepts strategy: "linear" | "adaptive" | "hierarchical" (default "adaptive") and runs_per_segment ∈ [1,5]. The adaptive path runs one cheap pass over every segment, then re-scores only segments whose attribution falls in [0.05, 0.30] with the full quota. The hierarchical path buckets prompts with >12 segments into 4 coarse groups, scores those, then drills into the top-2. Both apply early termination once the top-3 segments cover ≥90% of the cumulative attribution.
  • Sankey segment merge/split editor (FR-025). New segment-editor component lets users merge adjacent segments or split at a word boundary before running ablation. Backed by segments_override on /ablation/run and a new /ablation/segments/preview helper.
  • DAG semantic 3-level zoom (FR-011) + Dagre Web Worker (NFR-013) for 200+ node graphs.
  • Compliance trend deployment markers (US-025) sourced from audit_log rule events.
  • Email verification flow (/auth/verify-email, /auth/resend-verification).
  • Refresh token rotation with theft detection (migration 004, /auth/refresh, silent refresh in the api-client).
  • audittrail CLI (rules validate, rules list, bootstrap-superadmin).
  • Global filter wiringuseFilterStore carries phrase + tool, GlobalFilterBar exposes chips, /traces seeds the store from URL params.

Wave 18 — RBAC /admin route group

  • New auth.require_superadmin dependency + routes/admin.py with 8 endpoints (users CRUD, audit-log, audit-log/export CSV stream, tenants list, instance/info). Self-protection prevents a superadmin from revoking their own flag or deactivating their own account.
  • Frontend app/(admin)/admin/ route group with layout (auth guard), overview, users, tenants, audit-log pages.
  • Sidebar gains a superadmin-only "Admin" entry.
  • audittrail bootstrap-superadmin CLI for the fresh-deployment promote-or-create flow.

Demo mode + login polish

  • New POST /api/v1/auth/demo-login (CSRF-exempt, 20/min). Find-or- creates a demo@audittrail.dev user, seeds 10 sample traces with 30 spans on first call, issues normal cookies. Landing page "View Demo" buttons hit this endpoint.
  • useCurrentUser exposes is_superadmin + email_verified. New formatRoleLabel helper renders "Admin" / "Viewer" / "Superadmin" consistently across the dashboard sidebar and profile page.
  • Sign-out button in the dashboard sidebar footer is now an explicit labelled button.
  • Verify-email page wrapped in <Suspense> so Next.js 16 static rendering accepts useSearchParams (CI build was failing).

CRITICAL security fixes (cross-tenant data exposure)

  • Fixed: WebSocket trace subscription bypass (#115). routes/ws.py now verifies Trace.user_id == authenticated_user.id before attaching the connection to the trace's ring buffer; runtime subscribe messages are also gated. Closes a live cross-tenant exfiltration vector where any authenticated user could replay the last 100 events of any other tenant's trace.
  • Fixed: DELETE /api/v1/traces/flush-all global wipe (#116). Now scoped to Trace.user_id == calling admin's id; tenant admins can no longer wipe other tenants' traces.
  • Fixed: Anonymous span ingestion (#117). Both POST /api/v1/ingest/spans and /ingest/traces switched from Depends(get_optional_user) to a new require_user_or_apikey dependency that rejects anonymous calls with 401. Adds explicit cross-tenant guard inside _ensure_trace/_ensure_agent.

HIGH security fixes

  • Fixed: Cross-tenant agents endpoints (#118). Added Agent.user_id via Alembic 003 + new _visible_to(user) clause used by every read/write in routes/agents.py. Two tenants can register agents with the same display name without colliding.
  • Fixed: Sparkline day_stmt user_id leak (#119)routes/analytics.py:233-238 now joins/filters by user.
  • Fixed: /analytics/slowest-spans p95/p99 leak (#120) — duration query now joins Trace and filters by user.
  • Fixed: Global rules / settings (#121). Rules and settings split into instance defaults vs per-tenant overrides. Tenants can no longer disable safety rules platform-wide. See "Per-tenant scoping" below for the full migration.
  • Fixed: JWT non-revocable + 24h TTL (#122). Default TTL shortened to 1 hour (AUDITTRAIL_JWT_EXPIRY_HOURS). get_current_user already re-fetches the row each request so role demotion is reflected within the access-token TTL.
  • Hardened: seed.py production guard (#123). Now requires both AUDITTRAIL_DEBUG=true AND AUDITTRAIL_ALLOW_SEED=1. The opt-in flag is intentionally outside the Pydantic settings so a stale .env cannot accidentally re-seed prod.
  • Added: per-route rate limits on heavy endpoints (#124). /ablation/run, /ingest/spans, /ingest/traces, /reports/generate now carry explicit @limiter.limit("…/minute") decorators in addition to the global default. Concurrent ablation jobs per user are also capped via ablation_max_concurrent_per_user.
  • Hardened: ReDoS surface (#125). Constitutional rule regex patterns are screened against a deny-list of catastrophic- backtracking shapes ((a+)+, (a*)*, (a|a)+) before compilation. Pattern + input length caps from 002 are kept.

MEDIUM security hardening

  • Added: SecurityHeadersMiddleware in main.py so HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer- Policy, Permissions-Policy and COOP are present even when no fronting proxy adds them.
  • Added: BodySizeLimitMiddleware rejects requests larger than 5 MiB at the FastAPI layer (defence-in-depth complement to nginx).
  • Added: AuditLog table (NFR-023, #137). New audit_log table + audittrail/audit_log.py helper. Every admin mutation (rule.update, rule.create, rule.delete_override, rule.reload, agent.create, agent.delete, settings.patch, webhook.*, report.generate, password.reset_*, auth.logout) records who, when, payload, IP and user-agent.
  • Widened: PII redaction scope (#135). _upsert_span now redacts error_message, attributes, and tool_calls.{arguments,result} in addition to span.input/output.
  • Added: Pydantic extra="forbid" on the new ablation, webhook, and password-reset request schemas to silently reject unknown fields and prevent client-side typos from masking validation failures.

Per-tenant scoping (Wave 5)

  • Added: Alembic migration 003 (003_tenant_scoping_and_audit_log.py). Adds user_id to agents, rules; adds user_settings table for per-tenant overrides; adds users.is_superadmin, users.email_verified, users.reset_token, etc.; adds the audit_log and webhooks tables; adds traces.archived_at / traces.deleted_at for the lifecycle state machine.
  • Added: Tenant-scoped rule layer. app.state.rules is now the YAML-loaded instance default; per-tenant overrides live in app.state.tenant_rules[user_id] and shadow defaults at evaluation time. routes/ingest._evaluate_and_broadcast calls _effective_rules(request, user) so each tenant sees their own effective ruleset during constitutional evaluation.
  • Added: UserSetting model + tenant-scoped settings reads/writes. Reads merge per-user rows on top of instance defaults; writes always land in user_settings for the calling tenant.
  • Added: Trace cross-tenant guard in _ensure_trace. Spans cannot be appended to a trace owned by a different user; the call returns 403 instead of silently mutating someone else's row.
  • Added: WebSocket tenant filter on /ws/live. The ConnectionManager records (user_id, is_admin) on each live subscriber and _can_see_trace filters every broadcast.

Schema management

  • Removed: Runtime ALTER TABLE block in init_db() (#100). Schema management is now Alembic-only; the entrypoint runs alembic upgrade head and init_db only ensures fresh-install table creation + WAL mode.

Constitutional governor

  • Added: BR-002 duplicate rule-id detection. load_rules now cross-checks IDs across YAML files and skips duplicates with a clear error log naming both source files.
  • Added: BR-003 amber ≤ red validator. Pydantic model_validator on RuleDefinition rejects rules where the amber threshold sits past the red threshold for a given operator.
  • Fixed: WS event envelope key drift (#95). The dead-code path in governor.evaluate_trace_and_broadcast was emitting "type" instead of "event"; now matches the rest of the codebase.

Causal attribution & SHAP

  • Added: Real LLM ablation path (#32). causal._real_llm_tool_selection hits Anthropic or OpenAI when ablation_real_llm_enabled=true in settings, falling back to the heuristic on provider failure.
  • Added: 3× averaging for ablation runs (BR-009 / #36). run_ablation now accepts runs_per_segment (1..5) and runs the scorer in asyncio.gather parallel for each masked variant before averaging the normalised scores.
  • Added: Surrogate model warm-train on startup (#33). The lifespan task now pulls up to 500 recent tool spans, builds a feature matrix, and calls SurrogateModel.train() so SHAP responses can return real F1 scores instead of the hard-coded 0.85.

Auth & SDK

  • Added: /auth/forgot-password and /auth/reset-password (#34). v1.0 implementation logs the reset token to the server console per PRD §15.1; v2.0 will deliver via email.
  • Added: Frontend /forgot-password page wired to the backend.
  • Added: audittrail.init(frameworks=...) SDK shim (#35). Honors the public API documented in PRD §14.3; warns on unknown framework names; calls through to init_tracer.
  • Hardened: /auth/logout records the action in audit_log even for cookies that cannot be decoded.

Webhooks (#43)

  • Added: outbound webhook delivery service. New webhooks.py module + routes/webhooks.py CRUD endpoints. Supports Slack webhooks, PagerDuty Events v2, and generic HTTPS with HMAC-SHA256 request signing. Delivery happens off the request path via asyncio.create_task and is retried with exponential backoff. Failures are persisted to Webhook.last_error.
  • Wired: ingest pipeline fan-out. Constitutional violations now fan out to the trace owner's enabled webhooks at the same severity threshold the user configured.

Frontend

  • Removed: Dead Continue with API Key button + Apache license footer link from the login page (#179) — replaced with a "Create an account" link and a working /forgot-password link.
  • Removed: components/dashboard/sidebar.tsx (285 LOC dead code).
  • Removed: components/trace/trace-table.tsx + trace-columns.tsx (640 LOC dead code).
  • Removed: nuqs dependency (declared, never imported).
  • Added: Sign-out button to the dashboard sidebar footer that hits POST /v1/auth/logout and routes to /login.
  • Added: /forgot-password page matching the login UI.
  • Moved: tests/capture-screenshots.spec.tsscripts/ so it is not auto-run as part of the e2e suite.
  • Added: @audittrail/shared workspace dependency in apps/web/package.json so the orphaned shared types package can finally be imported by frontend code.

Architecture / deployment

  • Removed: in-container nginx service from docker-compose.yml (#88). Production already uses Caddy on the Lightsail host as the single TLS / proxy layer; the duplicate in-container nginx is now retired.

PRD sync (highlights — see SYNC_AUDIT_REPORT.md for the full diff)

  • Documented per-tenant scoping, AuditLog, Webhook, UserSetting in the ERD entity list.
  • Removed CrewAI adapter (FR-007b) from the roadmap entirely. The intended adapter never shipped, and PRD §23.8 now reflects that the v1.0 framework matrix is LangGraph + LangChain + AutoGen + raw OpenAI.
  • Removed Mermaid Sankey fallback (FR-027a) from the roadmap and from the response schema. The hand-rolled SVG Sankey covers every supported display surface and the Mermaid path was unimplemented prose.
  • Added § "Reference Production Topology" describing Lightsail + Caddy + cron-poll deploy.

[1.1.0] - 2026-04-01

Authentication & User Flow

  • Fixed: Register page styling mismatch. The register page used shadcn Card/Button/Input components while the login page used inline styles with #171717 card and #0a0a0a background. Restyled register to match login exactly (same radial gradient glow, card dimensions, input styling).

    • File: apps/web/app/(auth)/register/page.tsx
  • Fixed: Default email pre-filled on login. Login form had defaultValues: { email: "priya@company.com" } hardcoded. Changed to empty string so users enter their own credentials.

    • File: apps/web/app/(auth)/login/page.tsx
  • Fixed: Login error silently redirecting to demo. The login catch block was redirecting to /overview (demo dashboard) on ANY error, including invalid credentials. Now shows an inline error message instead.

    • File: apps/web/app/(auth)/login/page.tsx
  • Fixed: Mock data leaking to real accounts. The useCurrentUser hook fell back to a hardcoded "Priya S." demo user whenever auth failed (isDemo = isError || !data), causing real registered users to see Priya's profile on page refresh. Rewrote the hook to only activate demo mode when explicitly requested via ?demo=true query parameter. Unauthenticated users are now redirected to login.

    • Files: apps/web/hooks/use-current-user.ts, apps/web/app/(dashboard)/layout.tsx, apps/web/app/page.tsx

Error Handling

  • Added: Global error boundary. app/error.tsx catches unhandled runtime errors and shows a dark-themed retry page.

    • File: apps/web/app/error.tsx
  • Added: 404 page. app/not-found.tsx shows a dark-themed "Page not found" page with a link back to home.

    • File: apps/web/app/not-found.tsx

User Profile & Identity

  • Added: Dynamic user avatar via useCurrentUser hook. Created a React Query hook that fetches the authenticated user from GET /v1/auth/me. Wired into dashboard sidebar, layout header, and profile page to replace hardcoded "PS" / "Priya S." / "AI Engineer" values.

    • Files: apps/web/hooks/use-current-user.ts, apps/web/components/dashboard/sidebar.tsx, apps/web/app/(dashboard)/layout.tsx, apps/web/app/(dashboard)/profile/page.tsx
  • Added: Platform-aware keyboard shortcut display. Sidebar now shows "Cmd+K" on macOS and "Ctrl K" on Windows/Linux, using a hydration-safe useEffect pattern.

    • File: apps/web/components/dashboard/sidebar.tsx
  • Added: Keyboard shortcuts hint card on Overview page. Animated card showing 5 key shortcuts (Cmd/Ctrl+K, G->O, G->T, G->C, G->A) with platform-aware modifier key.

    • File: apps/web/app/(dashboard)/overview/page.tsx

Testing

  • Added: Vitest unit test infrastructure. Installed vitest, happy-dom, @testing-library/react, @testing-library/jest-dom. Created vitest.config.ts with path aliases.

    • Files: apps/web/vitest.config.ts, apps/web/package.json
  • Added: Unit tests. 21 tests across 2 files covering formatDuration, formatCost, formatTokens, truncateId, formatRelativeTime (utils) and ApiClient GET/POST/204/error/params handling (api-client).

    • Files: apps/web/__tests__/utils.test.ts, apps/web/__tests__/api-client.test.ts

API & Schema

  • Fixed: ToolCallInSpan result field validation. The result field in ToolCallInSpan expects dict | None, not a plain string. Passing a string caused a 422 Unprocessable Entity error. Documented the correct format {"output": "..."} in API reference with a "Common mistake" callout.

    • File: docs/api-reference.md
  • Documented: API response envelope pattern. All GET endpoints wrap responses in {"data": ...}. Added a note at the top of the API reference warning consumers to read resp.json()["data"], not resp.json() directly.

    • File: docs/api-reference.md

DAG Visualization

  • Fixed: DAG vertical spacing. Dagre layout ranksep increased from 60 to 120 pixels, nodesep from 40 to 50, margins from 20 to 30. Nodes now have proper vertical breathing room.

    • File: apps/web/components/dag/dag-viewer.tsx
  • Added: Live WebSocket DAG updates. DAG viewer now subscribes to useTraceWebSocket(traceId) and invalidates the React Query cache on span_start, span_end, and trace_complete events, so the DAG re-renders as spans arrive.

    • File: apps/web/components/dag/dag-viewer.tsx

Ablation Modal

  • Fixed: Prompt segments overflow. The ablation cost dialog's prompt segment list overflowed the modal without scrolling. Replaced ScrollArea (which wasn't respecting max-h) with a native overflow-y-auto div with explicit height and thin scrollbar styling.
    • File: apps/web/components/sankey/ablation-cost-dialog.tsx

Sankey Diagram

  • Fixed: Hardcoded tools replaced with dynamic extraction. The ablation engine hardcoded ["web_search", "calculator", "read_file"] as tool candidates. Now queries ToolCall records and tool-type span names from the actual trace to build the candidate list dynamically. Works with any tool set.

    • Files: apps/api/src/audittrail/routes/ablation.py, apps/api/src/audittrail/causal.py, apps/api/src/audittrail/surrogate.py
  • Fixed: Single reasoning node replaced with multiple nodes. The Sankey middle column showed only one "Tool Selector" node. Now generates multiple reasoning steps (e.g., "Identify computation need", "Resolve data source", "Plan comparison logic") based on tool names, with a keyword-to-label mapping and a dynamic fallback for unknown tool names.

    • File: apps/api/src/audittrail/causal.py
  • Fixed: Spider-web links replaced with targeted connections. Every prompt phrase was connecting to every reasoning node via the fallback connected_reasoning = set(reasoning_nodes.keys()). Replaced with text-affinity scoring that matches phrase content against tool names. Each phrase connects to 1-3 most relevant reasoning nodes, with weight proportional to affinity strength.

    • File: apps/api/src/audittrail/causal.py
  • Fixed: Link thickness normalization. Sankey link glow width was Math.max(3, attr * 40) which produced uniform thin lines when all attribution values were small. Now normalizes against the maximum link value so the strongest link always fills the visual range.

    • File: apps/web/components/sankey/sankey-viewer.tsx
  • Fixed: Hover tooltip on Sankey links. SVG container had pointerEvents: "none" which blocked all mouse events on hit-area paths. Split into two SVG layers: Layer 1 (z-index 1) for visual paths with pointerEvents: "none", Layer 2 (z-index 10) for invisible hit-area paths with pointerEvents: "stroke". Tooltip now appears on hover showing source, target, attribution score, and confidence level.

    • File: apps/web/components/sankey/sankey-viewer.tsx

Live Tracing

  • Added: Incremental span ingestion. Test agent now sends spans during execution using send_running() (on tool/LLM start) and send_complete() (on end), instead of batching all spans at the end. The trace shows as "Running" during execution and transitions to "Complete" when done.

    • File: test-agent/agent.py (now examples/langgraph-agent/agent.py)
  • Added: WebSocket broadcast for HTTP span ingestion. The POST /api/v1/ingest/spans endpoint now broadcasts span_start/span_end events via WebSocket after each span upsert. Previously only internal @traceable decorator spans triggered broadcasts.

    • File: apps/api/src/audittrail/routes/ingest.py
  • Added: Hierarchical live spans. The test agent creates intermediate grouping spans (execute_tools chain, research_sub_agent agent) dynamically during execution, producing a multi-layered DAG instead of a flat tree.

    • File: test-agent/agent.py (now examples/langgraph-agent/agent.py)
  • Fixed: Trace status stuck on "Running". The trace detail page did not subscribe to WebSocket events, so the status badge never updated after the trace completed. Added useTraceWebSocket subscription that invalidates ["trace", id] and ["trace-spans", id] queries on span events.

    • File: apps/web/app/(dashboard)/traces/[id]/page.tsx

Security & User Isolation

  • Added: user_id column on Trace model. Traces are now owned by the user who created them. Each user only sees their own traces across all endpoints (traces, analytics, evaluations, ablation, spans). Column is nullable — existing traces with NULL user_id are invisible to all users (orphaned).

    • Files: apps/api/src/audittrail/models.py, apps/api/src/audittrail/schemas.py
  • Added: Safe schema migration in init_db(). On startup, init_db() checks if the user_id column exists on the traces table. If missing, runs ALTER TABLE traces ADD COLUMN user_id with FK constraint and index. Safe to run repeatedly.

    • File: apps/api/src/audittrail/database.py
  • Added: Authentication on all trace-related endpoints. 35+ endpoints across 7 route files now require Depends(get_current_user) and filter queries by Trace.user_id == user.id. Unauthenticated requests return 401.

    • Files: routes/traces.py (9 endpoints), routes/analytics.py (7 endpoints), routes/evaluations.py (6 endpoints), routes/ablation.py (6 endpoints), routes/spans.py (2 endpoints)
  • Added: Optional auth on ingest endpoints. POST /ingest/spans and POST /ingest/traces use get_optional_user — authenticated users get traces assigned to their user_id; unauthenticated agents create traces with user_id=NULL.

    • File: apps/api/src/audittrail/routes/ingest.py
  • Added: Admin-only flush endpoint. DELETE /api/v1/traces/flush-all deletes ALL traces in the database. Requires admin role. For clearing dev/seed data.

    • File: apps/api/src/audittrail/routes/traces.py
  • Added: Frontend 401 redirect. API client now detects 401 responses and redirects the browser to /login. Prevents broken dashboard state when session expires.

    • File: apps/web/lib/api-client.ts
  • Fixed: Demo mode isolation. useCurrentUser hook no longer falls back to "Priya S." demo user on auth errors. Demo mode only activates when ?demo=true is in the URL (triggered by "View Demo" buttons on landing page). Real users see empty state if they have no traces.

    • Files: apps/web/hooks/use-current-user.ts, apps/web/app/(dashboard)/layout.tsx, apps/web/app/page.tsx

Documentation

  • Updated: Quickstart guide. Complete rewrite with zero-to-dashboard flow, agent integration guide (HTTP + Callback + Template), common pitfalls table, live tracing pattern, and corrected example paths.

    • File: docs/quickstart.md
  • Added: Example LangGraph agent. A complete working agent with 7 tools (web_search, calculator, get_current_time, summarize_findings, database_lookup, read_config_file, compare_values) that demonstrates live tracing, hierarchical span grouping, and correct tool_calls format.

    • Files: examples/langgraph-agent/agent.py, examples/langgraph-agent/requirements.txt
  • Added: Goal-agnostic agent template. A minimal copy-paste-ready template (template.py) with the AuditTrailIngestor class, placeholder tools, and clear TODO markers for customization.

    • File: examples/langgraph-agent/template.py
  • Added: Examples README. Usage guide with architecture diagram, tool format documentation, and response envelope warning.

    • File: examples/langgraph-agent/README.md
  • Added: Changelog. This file documenting all session changes.

    • File: docs/changelog.md