Audit Trail
Append-only log of administrative actions across the platform. Records who did what for compliance, incident investigation, and security monitoring.
Concepts
| Term | Description |
|---|---|
| Event type | Category of action (agent_lifecycle, authentication, etc.) |
| Actor | Who performed the action (user, agent, mcp_client, system) |
| Credential | Which key acted, when the call was authenticated with an MCP API key — its id, name and scope, recorded beside the owner |
| Target | What was affected (agent, user, schedule, etc.) |
| Source | Where the action originated (api, mcp, scheduler) |
| Hash chain | Optional SHA-256 chain over consecutive entries — proves the log wasn't tampered with between two checkpoints |
What Gets Logged
| Event Type | Actions |
|---|---|
| agent_lifecycle | create, start, stop, delete, rename, recover |
| authentication | login_success, login_failure, logout |
| authorization | permission_granted, permission_denied |
| configuration | settings_changed, quota_updated |
| credentials | injected, exported, imported |
| mcp_operation | tool_call — every MCP tool call, captured via a transparent wrapper, including calls the tool refused |
| git_operation | sync, pull, push |
| system | startup, shutdown, migration |
All entry-emitting code paths now write to the audit log; the earlier “Phase 1 coverage” caveat no longer applies.
An action taken with an MCP API key is recorded against its owner (actor_type stays user, since the owner is the accountable party) and against the key: mcp_key_id, mcp_key_name and mcp_scope say which credential acted. That is what makes “what did that leaked key touch?” answerable — filter the list on mcp_key_id. A browser session records no key fields.
Refused MCP Calls
An MCP tool that refuses a call — for example chat_with_agent, fan_out or run_agent_loop naming an agent the caller has no permission for — still answers the caller, and its mcp_operation entry records the refusal. The entry's details read success: false and denied: true, with the reason in error:
{
"tool": "chat_with_agent",
"duration_ms": 32,
"success": false,
"error": "Permission denied: Agent 'agent-a' is not permitted to communicate with 'agent-b'. Configure permissions in the Trinity UI.",
"denied": true
}A backend 403 that surfaces through a tool is marked denied too. An error that is not a refusal carries no deniedflag, so you can tell a refused call from a broken one. Where the caller only sees a generic “not found or not accessible” (for example a loop id), the entry carries the specific reason.
How It Works
Dashboard
Admins get a searchable dashboard at Enterprise → Audit Log in the UI. The dashboard is part of the OSS bundle but is gated by an auditentitlement on the route — instances without the entitlement bounce to the dashboard catalogue.
The dashboard surfaces:
/distinct/event-types and /distinct/actor-types endpoints, so the UI never goes stale.Retention
Audit entries are kept for 365 days by default (AUDIT_LOG_RETENTION_DAYS). A daily background job at 04:15 UTC prunes entries older than the retention window. The retention floor is 365 days — the database trigger refuses DELETE on younger rows.
For Agents
API Endpoints: see the Backend API Docs (/docs) for full schemas. All endpoints are admin-only.
| Endpoint | Method | Description |
|---|---|---|
| /api/audit-log | GET | List entries (filterable, paginated) |
| /api/audit-log/{event_id} | GET | Single entry by UUID |
| /api/audit-log/stats | GET | Aggregate counts by event_type and actor_type |
| /api/audit-log/heatmap | GET | Day-of-week × hour-of-day grid |
| /api/audit-log/calendar | GET | Per-day activity grid |
| /api/audit-log/distinct/event-types | GET | Sorted unique event types (for filter dropdowns) |
| /api/audit-log/distinct/actor-types | GET | Sorted unique actor types |
| /api/audit-log/export | GET | CSV or JSON export of a time window |
| /api/audit-log/verify | POST | Verify SHA-256 hash chain over an id range |
| /api/audit-log/hash-chain/enable | POST | Toggle hash chain computation for new entries |
Common query parameters (apply across list, stats, heatmap, calendar, export):
event_type, actor_type, actor_id, target_type, target_id, sourcestart_time, end_time (ISO 8601)limit (default 100, max 1000), offsetThe list endpoint also takes request_id (joins the MCP and backend rows of one call), mcp_key_id, and mcp_scope (user, agent, system, connector, portal_delegate, ops).
Entry Format
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "agent_lifecycle",
"event_action": "create",
"actor_type": "user",
"actor_id": "42",
"actor_email": "admin@example.com",
"mcp_key_id": null,
"mcp_key_name": null,
"mcp_scope": null,
"target_type": "agent",
"target_id": "my-agent",
"timestamp": "2026-05-14T10:30:00Z",
"source": "api",
"endpoint": "/api/agents",
"request_id": "9a7c…",
"details": { "template": "github:Org/repo" }
}Data Integrity
The audit log is append-only at the database level:
previous_hash and entry_hash; the verify endpoint walks the chain and reports the first broken link.