The Audit Row
Every call through the Core Proxy — LLM, MCP, Virtual MCP, skill, OAuth, A2A, and every span that arrives through the OpenTelemetry inlet — writes one row to proxy_logs. It is the record the run ledger, the Assurance Report, the compliance reports and the audit export are all computed from, so what it holds is the ceiling on what the platform can ever prove.
This page lists the fields by the question they answer. Three words recur, and they are the point of the design:
- Witnessed — the gateway saw it happen. Covered by the row hash.
- Asserted — the caller said so (a trace id, a run id, an outcome). Recorded as a claim, outside the hash, and never “upgraded” into a witnessed fact.
- Derived — computed from other rows at write time for convenience. Outside the hash, because the witnessed rows already prove it.
Absence is recorded as absence: a field the gateway could not establish is null, never a default that reads like a fact.
Identity and time
Section titled “Identity and time”| Field | Meaning |
|---|---|
id |
ULID — time-sortable, so rows order by id inside a millisecond |
tenant_id |
The tenant; every query is scoped by it |
timestamp |
When the call was recorded, microsecond precision, UTC |
created_at |
When the row was inserted (the writer batches, so this can trail timestamp) |
What was called
Section titled “What was called”| Field | Meaning |
|---|---|
proxy_type |
llm, mcp, virtual_mcp, skill, oauth, a2a, portal, batch |
proxy_subtype |
The shape within the type — chat_completions, embeddings, anthropic_messages, mcp_jsonrpc, sse_stream — and governance outcomes such as approval_required, semantic_policy_block, residency_violation |
http_method, endpoint, target_url |
The gateway route, and the upstream URL the call was forwarded to |
server_id |
The MCP or LLM server the call targeted |
mcp_method, mcp_request_id, mcp_capability_name |
JSON-RPC method (tools/call, tools/list, …), the client’s JSON-RPC id, and the tool, resource or prompt name |
skill_id, skill_execution_id |
The skill that initiated the call, when a skill did |
llm_model_requested, llm_model_used, llm_provider |
What the client asked for and what routing resolved it to |
llm_prompt_tokens, llm_completion_tokens, llm_total_tokens, llm_estimated_cost_usd |
The bill |
llm_streaming, llm_temperature, llm_max_tokens, llm_function_calling |
Request shape |
llm_used_fallback, llm_fallback_reason |
Whether a resilience fallback served the call, and why |
llm_cache_hit, llm_cache_layer, llm_tokens_saved, llm_cache_similarity_score |
Response-cache outcome (the Caching tab in Mission Control → Analytics) |
prompt_cache_write_tokens, prompt_cache_read_tokens, prompt_cache_savings_usd |
Provider prompt-cache outcome |
rag_enabled, rag_chunks_retrieved, rag_context_tokens, rag_collection_ids, rag_top_similarity_score, rag_retrieval_duration_ms |
Knowledge-base retrieval that was injected |
prompt_fingerprint |
SHA-256 of the instruction text sent to the model — a hash, never the prompt — so the drift cause ladder can tell “the system prompt changed” from “the model changed” |
media_images_generated, media_audio_chars, media_audio_seconds_input, media_video_seconds_output |
Media metering for image, speech, transcription and video modes |
oauth_flow_type, oauth_scope |
For OAuth proxy rows |
Who called it
Section titled “Who called it”| Field | Meaning |
|---|---|
api_key_id, api_key_name |
The API key that authenticated the call; null on JWT-authenticated calls |
resource_group_id |
The resource group — for an agent, its AI System — the call executed inside |
gateway_user_id, gateway_user_username |
The gateway user, when a user is present |
idp_user_id, idp_user_username, idp_user_email |
The identity-provider subject behind the credential |
actor_agent_id |
The agent identity that performed the action; null for human and plain API-key callers |
subject_id, subject_kind |
Who the agent acted for — a user, an upstream agent, or a service |
end_user_session_id |
The conversation, when the client asserted one (X-Brutor-Session-Id, HMAC-derived so one caller cannot file traffic under another’s session) |
client_ip, user_agent |
The calling process |
is_auth_event |
The row is itself an authentication event (token issue, login) |
How calls link together
Section titled “How calls link together”| Field | Kind | Meaning |
|---|---|---|
correlation_id |
asserted | x-correlation-id from the client, or one the gateway minted; the debugging join from a user complaint to its rows |
request_id, parent_request_id |
asserted | Client request id (X-Request-ID) and, for nested gateway calls, the request that caused this one |
root_task_id |
witnessed / asserted | The run this action belongs to. Minted by the gateway for delegation chains; derived from the client’s x-brutor-run-id (keyed to the credential, so it cannot collide with another principal’s run). See the run ledger |
current_task_id, parent_task_id, delegation_depth |
witnessed | This step’s id, the step that invoked it, and how deep in the delegation it sits; a2a_protocol_version records the dialect |
trace_continuity |
witnessed | How the row joined its chain: minted, verified (gateway-signed), client_dropped, sig_failed, client_asserted, external (OTel inlet) |
trace_root_kind |
witnessed | What started the run: llm_user, agent_invocation, skill_execution, scheduled_job, external_api, external_telemetry |
step_id, step_name |
asserted | The logical phase the client named (X-Brutor-Step-Id); nothing is inferred when absent |
turn_id, turn_seq |
asserted | The pass of the agent loop the client named; when absent, turns are inferred from model-call boundaries and the ledger says so |
run_end_state |
asserted | The terminal state the client declared on this step (X-Brutor-Run-End): completed, completed_degraded, exhausted, errored, blocked_policy, cancelled |
trace_id, span_id |
asserted | The caller’s W3C traceparent ids, validated and lower-cased; on OTel-inlet rows, the span’s own. The join from your tracing tool to the governed record |
upstream_request_id |
witnessed | The provider’s own request id (x-request-id) — the only join between this trail and the vendor’s |
What went in and what came out
Section titled “What went in and what came out”| Field | Kind | Meaning |
|---|---|---|
request_size_bytes, response_size_bytes |
witnessed | Sizes as captured |
request_headers, response_headers |
witnessed | Sanitised header JSON, subject to the retention policy |
request_body_preview, response_body_preview |
witnessed | The content as stored under the tenant’s store_bodies mode — full (capped at 64 KiB), redacted, or absent under none — and removed after body_days. For LLM rows the request is the outbound body: what the gateway sent the provider after routing and translation |
request_content_hash, response_content_hash |
witnessed, hashed | SHA-256 over the content exactly as captured, sealed before any retention mode or prune touches the preview. What the row proves about the content after the content is gone |
http_status_code, success, error_type, error_message, duration_ms |
witnessed | The outcome |
What decided it
Section titled “What decided it”| Field | Meaning |
|---|---|
guardrail_blocked, argument_policy_denied, pii_detected |
Report-level flags; the fields below say which rule and what it did |
guardrail_config_id, guardrail_check, guardrail_action |
The guardrail config that fired, the check (pii, prompt_injection, jailbreak, toxic_content, banned_words, banned_pattern, secrets) and its action (block, redact, warn) |
guardrails_skipped, guardrails_degraded |
Checks configured but not run (a detector failed and the config opted into log-and-allow), and checks that ran on a lower rung of the resilience ladder — so a clean row never silently means “unchecked” |
argument_policy_id, semantic_policy_id |
The argument or semantic policy that produced the decision |
agent_authz_decision, agent_grant_id |
The agent grant outcome — allow, approval_required, deny — and the grant that drove it |
autonomy_level, autonomy_provenance |
The autonomy in force (autonomous, approval_required, restricted, suspended) and how it was obtained: live, last_known_good, or floor — anything but live means enforcement ran on degraded information |
consumed_approval_request_id |
The approval whose one-time token authorised this call, set on the retried call so approval → action is walkable forward |
pre_action_state |
The observed aggregates the quota or budget decision was taken against ({kind, …}); the limits themselves are pinned by the AI System’s contract |
region_routed_to, residency_class |
Where the call actually went and the residency verdict: allowed, blocked, not_evaluated |
compliance_tags |
Regime tags the tagging engine fired (gdpr_art_30, soc2.cc7.2, eu_ai_act.high, …); null when the tenant has no profile, [] when it has one and nothing fired |
Tool provenance and the deciding model
Section titled “Tool provenance and the deciding model”Three fields answer the questions an incident review asks of a tool call first — which definition of the tool was in force, which server, and which model chose to call it:
| Field | Kind | Meaning |
|---|---|---|
tool_definition_hash |
witnessed, hashed | SHA-256 over the canonical JSON (keys sorted) of the tool definition as the upstream last listed it through the gateway — falling back to the latest discovery snapshot when no tools/list has passed through this instance. A changed description or schema under the same name is a different hash; a tool the gateway never saw listed gets no hash rather than a guessed one |
server_version |
witnessed, hashed | The version the MCP server announced in its initialize result, as this proxy instance last saw it |
deciding_model |
derived | For a tool, skill or A2A action: the model whose completion decided it — the run’s most recent model call, same turn preferred. Resolved at write time so the answer is on the row an exporter reads, not in a join they would have to know to write. Never set on model calls, never guessed from another run |
Integrity and provenance
Section titled “Integrity and provenance”| Field | Meaning |
|---|---|
chain_id, chain_seq |
The writer chain (one per proxy process) and the row’s dense position in it; a gap in chain_seq means rows were deleted |
prev_hash, row_hash |
The predecessor’s hash and this row’s own: SHA-256(canonical bytes ‖ prev_hash). Batches are signed into audit_chain_checkpoints with Ed25519 when a signing key is configured |
source |
gateway — the proxy saw the call; otel — reported through the OTel inlet, never witnessed |
bodies_pruned_at |
When the retention sweeper removed the bodies and headers |
erased_at |
When a data-subject erasure scrubbed personal data from the row |
custom_metadata, tags |
Type-specific extras (for OTel rows: span name, operation, service and scope) and searchable tags |
What the hash covers
Section titled “What the hash covers”The canonical bytes are versioned; the tag is the first field inside them, so a row can never pass under a format it was not written in. Rows written before 0.10.71 carry v1; the verifier recomputes both and accepts v1 only for a row that carries none of the v2 fields.
| Format | Covered fields |
|---|---|
| v1 | id, tenant_id, timestamp, proxy_type, proxy_subtype · actor_agent_id, subject_id, subject_kind, api_key_id, resource_group_id · http_method, endpoint, mcp_capability_name, llm_model_used · agent_authz_decision, agent_grant_id, semantic_policy_id, argument_policy_id, guardrail_config_id, guardrail_check, guardrail_action, autonomy_level, autonomy_provenance, consumed_approval_request_id, guardrail_blocked, argument_policy_denied · http_status_code, success · root_task_id, turn_id |
| v2 (0.10.71) | everything in v1, then request_content_hash, response_content_hash, server_version, tool_definition_hash |
Deliberately outside the hash: the body previews and headers (retention must be able to remove them without breaking the chain — the digests stand in for them), everything asserted by the caller (correlation_id, request_id, trace_id, span_id, step_id, turn_seq, run_end_state, end_user_session_id), and everything derived (deciding_model, compliance_tags, cost and cache figures). Editing any of those is not undetectable — it is simply not what the chain is for; the witnessed rows that produced a derived value still are.
Verify a chain from Administration → System → Audit Chains, or POST /v1/admin/audit/chains/{chain_id}/verify. The six verdicts are explained on the assurance journey.
An example row
Section titled “An example row”A tools/call from a support agent, after retention has pruned the bodies. Economics and cache fields omitted.
{ "id": "01K4A2ZK5QH0P4N6R9T2VXW8YB", "tenant_id": "acme", "timestamp": "2026-09-03T14:02:11.482913Z", "proxy_type": "mcp", "proxy_subtype": "mcp_jsonrpc", "endpoint": "mcp-proxy", "server_id": "srv-crm", "server_version": "2.4.1", "mcp_method": "tools/call", "mcp_capability_name": "issue_refund", "tool_definition_hash": "4653538b0e55911dbc661c55961a9f8de1e27c7ae950acb842d82ede364e0319", "deciding_model": "gpt-5.2",
"api_key_id": "key-01J…", "resource_group_id": "sys-support-copilot", "actor_agent_id": "agent-support-copilot", "subject_id": "u-4471", "subject_kind": "user",
"correlation_id": "ticket-8841", "root_task_id": "run-01K4A2Z…", "current_task_id": "01K4A2ZK5Q…", "delegation_depth": 0, "trace_continuity": "client_asserted", "turn_id": "turn-3", "step_id": "settlement", "trace_id": "0af7651916cd43dd8448eb211c80319c", "span_id": "b7ad6b7169203331",
"request_size_bytes": 212, "response_size_bytes": 88, "request_body_preview": null, "response_body_preview": null, "bodies_pruned_at": "2026-10-03T02:00:00Z", "request_content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "response_content_hash": "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752", "http_status_code": 200, "success": true, "duration_ms": 412.7,
"agent_authz_decision": "approval_required", "agent_grant_id": "grant-refunds", "consumed_approval_request_id": "appr-01K4A2Y…", "autonomy_level": "approval_required", "autonomy_provenance": "live", "guardrail_blocked": false, "argument_policy_denied": false, "compliance_tags": ["soc2.cc6.6", "eu_ai_act.high"],
"source": "gateway", "chain_id": "core-1-01K49…", "chain_seq": 184220, "prev_hash": "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", "row_hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}Read the same row in the Admin UI under Operations → Logs & Audit → Proxy Logs (expand the row for the decision record, or open its details), query it with POST /v1/admin/proxy-logs/query, or ship it through the audit export — the export manifest records whether the chain columns were included, because an allowlist that strips row_hash makes a file unverifiable and the recipient cannot tell.

