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_ablationnow acceptsstrategy: "linear" | "adaptive" | "hierarchical"(default"adaptive") andruns_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_overrideon/ablation/runand a new/ablation/segments/previewhelper. - 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_logrule 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). audittrailCLI (rules validate,rules list,bootstrap-superadmin).- Global filter wiring —
useFilterStorecarries phrase + tool,GlobalFilterBarexposes chips,/tracesseeds the store from URL params.
Wave 18 — RBAC /admin route group
- New
auth.require_superadmindependency +routes/admin.pywith 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-superadminCLI 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 ademo@audittrail.devuser, seeds 10 sample traces with 30 spans on first call, issues normal cookies. Landing page "View Demo" buttons hit this endpoint. useCurrentUserexposesis_superadmin+email_verified. NewformatRoleLabelhelper 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 acceptsuseSearchParams(CI build was failing).
CRITICAL security fixes (cross-tenant data exposure)
- Fixed: WebSocket trace subscription bypass (#115).
routes/ws.pynow verifiesTrace.user_id == authenticated_user.idbefore attaching the connection to the trace's ring buffer; runtimesubscribemessages 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-allglobal wipe (#116). Now scoped toTrace.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/spansand/ingest/tracesswitched fromDepends(get_optional_user)to a newrequire_user_or_apikeydependency 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_idvia Alembic 003 + new_visible_to(user)clause used by every read/write inroutes/agents.py. Two tenants can register agents with the same display name without colliding. - Fixed: Sparkline
day_stmtuser_id leak (#119) —routes/analytics.py:233-238now joins/filters by user. - Fixed:
/analytics/slowest-spansp95/p99 leak (#120) — duration query now joinsTraceand 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_useralready re-fetches the row each request so role demotion is reflected within the access-token TTL. - Hardened:
seed.pyproduction guard (#123). Now requires bothAUDITTRAIL_DEBUG=trueANDAUDITTRAIL_ALLOW_SEED=1. The opt-in flag is intentionally outside the Pydantic settings so a stale.envcannot accidentally re-seed prod. - Added: per-route rate limits on heavy endpoints (#124).
/ablation/run,/ingest/spans,/ingest/traces,/reports/generatenow carry explicit@limiter.limit("…/minute")decorators in addition to the global default. Concurrent ablation jobs per user are also capped viaablation_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:
SecurityHeadersMiddlewareinmain.pyso 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:
BodySizeLimitMiddlewarerejects requests larger than 5 MiB at the FastAPI layer (defence-in-depth complement to nginx). - Added: AuditLog table (NFR-023, #137). New
audit_logtable +audittrail/audit_log.pyhelper. 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_spannow redactserror_message,attributes, andtool_calls.{arguments,result}in addition tospan.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). Addsuser_idtoagents,rules; addsuser_settingstable for per-tenant overrides; addsusers.is_superadmin,users.email_verified,users.reset_token, etc.; adds theaudit_logandwebhookstables; addstraces.archived_at/traces.deleted_atfor the lifecycle state machine. - Added: Tenant-scoped rule layer.
app.state.rulesis now the YAML-loaded instance default; per-tenant overrides live inapp.state.tenant_rules[user_id]and shadow defaults at evaluation time.routes/ingest._evaluate_and_broadcastcalls_effective_rules(request, user)so each tenant sees their own effective ruleset during constitutional evaluation. - Added:
UserSettingmodel + tenant-scoped settings reads/writes. Reads merge per-user rows on top of instance defaults; writes always land inuser_settingsfor 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. TheConnectionManagerrecords(user_id, is_admin)on each live subscriber and_can_see_tracefilters every broadcast.
Schema management
- Removed: Runtime
ALTER TABLEblock ininit_db()(#100). Schema management is now Alembic-only; the entrypoint runsalembic upgrade headandinit_dbonly ensures fresh-install table creation + WAL mode.
Constitutional governor
- Added: BR-002 duplicate rule-id detection.
load_rulesnow 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_validatoronRuleDefinitionrejects 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_broadcastwas 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_selectionhits Anthropic or OpenAI whenablation_real_llm_enabled=truein settings, falling back to the heuristic on provider failure. - Added: 3× averaging for ablation runs (BR-009 / #36).
run_ablationnow acceptsruns_per_segment(1..5) and runs the scorer inasyncio.gatherparallel 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-coded0.85.
Auth & SDK
- Added:
/auth/forgot-passwordand/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-passwordpage 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 toinit_tracer. - Hardened:
/auth/logoutrecords the action inaudit_logeven for cookies that cannot be decoded.
Webhooks (#43)
- Added: outbound webhook delivery service. New
webhooks.pymodule +routes/webhooks.pyCRUD endpoints. Supports Slack webhooks, PagerDuty Events v2, and generic HTTPS with HMAC-SHA256 request signing. Delivery happens off the request path viaasyncio.create_taskand is retried with exponential backoff. Failures are persisted toWebhook.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 Keybutton + Apache license footer link from the login page (#179) — replaced with a "Create an account" link and a working/forgot-passwordlink. - Removed:
components/dashboard/sidebar.tsx(285 LOC dead code). - Removed:
components/trace/trace-table.tsx+trace-columns.tsx(640 LOC dead code). - Removed:
nuqsdependency (declared, never imported). - Added: Sign-out button to the dashboard sidebar footer that
hits
POST /v1/auth/logoutand routes to/login. - Added:
/forgot-passwordpage matching the login UI. - Moved:
tests/capture-screenshots.spec.ts→scripts/so it is not auto-run as part of the e2e suite. - Added:
@audittrail/sharedworkspace dependency inapps/web/package.jsonso 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
#171717card and#0a0a0abackground. Restyled register to match login exactly (same radial gradient glow, card dimensions, input styling).- File:
apps/web/app/(auth)/register/page.tsx
- File:
-
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
- File:
-
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
- File:
-
Fixed: Mock data leaking to real accounts. The
useCurrentUserhook 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=truequery 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
- Files:
Error Handling
-
Added: Global error boundary.
app/error.tsxcatches unhandled runtime errors and shows a dark-themed retry page.- File:
apps/web/app/error.tsx
- File:
-
Added: 404 page.
app/not-found.tsxshows a dark-themed "Page not found" page with a link back to home.- File:
apps/web/app/not-found.tsx
- File:
User Profile & Identity
-
Added: Dynamic user avatar via
useCurrentUserhook. Created a React Query hook that fetches the authenticated user fromGET /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
- Files:
-
Added: Platform-aware keyboard shortcut display. Sidebar now shows "Cmd+K" on macOS and "Ctrl K" on Windows/Linux, using a hydration-safe
useEffectpattern.- File:
apps/web/components/dashboard/sidebar.tsx
- File:
-
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
- File:
Testing
-
Added: Vitest unit test infrastructure. Installed vitest, happy-dom, @testing-library/react, @testing-library/jest-dom. Created
vitest.config.tswith path aliases.- Files:
apps/web/vitest.config.ts,apps/web/package.json
- Files:
-
Added: Unit tests. 21 tests across 2 files covering
formatDuration,formatCost,formatTokens,truncateId,formatRelativeTime(utils) andApiClientGET/POST/204/error/params handling (api-client).- Files:
apps/web/__tests__/utils.test.ts,apps/web/__tests__/api-client.test.ts
- Files:
API & Schema
-
Fixed: ToolCallInSpan
resultfield validation. Theresultfield inToolCallInSpanexpectsdict | 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
- File:
-
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 readresp.json()["data"], notresp.json()directly.- File:
docs/api-reference.md
- File:
DAG Visualization
-
Fixed: DAG vertical spacing. Dagre layout
ranksepincreased from 60 to 120 pixels,nodesepfrom 40 to 50, margins from 20 to 30. Nodes now have proper vertical breathing room.- File:
apps/web/components/dag/dag-viewer.tsx
- File:
-
Added: Live WebSocket DAG updates. DAG viewer now subscribes to
useTraceWebSocket(traceId)and invalidates the React Query cache onspan_start,span_end, andtrace_completeevents, so the DAG re-renders as spans arrive.- File:
apps/web/components/dag/dag-viewer.tsx
- File:
Ablation Modal
- Fixed: Prompt segments overflow. The ablation cost dialog's prompt segment list overflowed the modal without scrolling. Replaced
ScrollArea(which wasn't respectingmax-h) with a nativeoverflow-y-autodiv with explicit height and thin scrollbar styling.- File:
apps/web/components/sankey/ablation-cost-dialog.tsx
- File:
Sankey Diagram
-
Fixed: Hardcoded tools replaced with dynamic extraction. The ablation engine hardcoded
["web_search", "calculator", "read_file"]as tool candidates. Now queriesToolCallrecords 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
- Files:
-
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
- File:
-
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
- File:
-
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
- File:
-
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 withpointerEvents: "none", Layer 2 (z-index 10) for invisible hit-area paths withpointerEvents: "stroke". Tooltip now appears on hover showing source, target, attribution score, and confidence level.- File:
apps/web/components/sankey/sankey-viewer.tsx
- File:
Live Tracing
-
Added: Incremental span ingestion. Test agent now sends spans during execution using
send_running()(on tool/LLM start) andsend_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(nowexamples/langgraph-agent/agent.py)
- File:
-
Added: WebSocket broadcast for HTTP span ingestion. The
POST /api/v1/ingest/spansendpoint now broadcastsspan_start/span_endevents via WebSocket after each span upsert. Previously only internal@traceabledecorator spans triggered broadcasts.- File:
apps/api/src/audittrail/routes/ingest.py
- File:
-
Added: Hierarchical live spans. The test agent creates intermediate grouping spans (
execute_toolschain,research_sub_agentagent) dynamically during execution, producing a multi-layered DAG instead of a flat tree.- File:
test-agent/agent.py(nowexamples/langgraph-agent/agent.py)
- File:
-
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
useTraceWebSocketsubscription that invalidates["trace", id]and["trace-spans", id]queries on span events.- File:
apps/web/app/(dashboard)/traces/[id]/page.tsx
- File:
Security & User Isolation
-
Added:
user_idcolumn 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
- Files:
-
Added: Safe schema migration in init_db(). On startup,
init_db()checks if theuser_idcolumn exists on thetracestable. If missing, runsALTER TABLE traces ADD COLUMN user_idwith FK constraint and index. Safe to run repeatedly.- File:
apps/api/src/audittrail/database.py
- File:
-
Added: Authentication on all trace-related endpoints. 35+ endpoints across 7 route files now require
Depends(get_current_user)and filter queries byTrace.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)
- Files:
-
Added: Optional auth on ingest endpoints.
POST /ingest/spansandPOST /ingest/tracesuseget_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
- File:
-
Added: Admin-only flush endpoint.
DELETE /api/v1/traces/flush-alldeletes ALL traces in the database. Requires admin role. For clearing dev/seed data.- File:
apps/api/src/audittrail/routes/traces.py
- File:
-
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
- File:
-
Fixed: Demo mode isolation.
useCurrentUserhook no longer falls back to "Priya S." demo user on auth errors. Demo mode only activates when?demo=trueis 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
- Files:
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
- File:
-
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
- Files:
-
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
- File:
-
Added: Examples README. Usage guide with architecture diagram, tool format documentation, and response envelope warning.
- File:
examples/langgraph-agent/README.md
- File:
-
Added: Changelog. This file documenting all session changes.
- File:
docs/changelog.md
- File: