Skip to content

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.

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)
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
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)
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
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
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

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
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

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.

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.