Evidence sources
The run ledger has two ways of learning that a step happened.
| Source | How it arrives | What the gateway can say |
|---|---|---|
gateway |
The call was proxied. The gateway authenticated it, applied guardrails, policy and budgets, signed the chain link and recorded the outcome. | Witnessed. Every control that applied is on the row, and the audit hash chain covers it. |
otel |
The system exported an OpenTelemetry GenAI span of its own and posted it to the inlet. | Reported. The gateway did not sit in the path, enforced nothing, and holds only what the exporter chose to send. |
Both feed the same ledger, the same learned baselines and the same liveness expectations, so a system that never routes through the gateway can still be observed — which is the precondition for it being judged at all. What never happens is one being quietly promoted to the other: the provenance travels on every row, into every rollup, and onto the Assurance Report.
The inlet
Section titled “The inlet”POST /v1/ingest/otel/v1/tracesContent-Type: application/json # OTLP/HTTP JSON; protobuf is refused with 415Authorization: Bearer sk_brutor_api_… # a gateway API keyX-Tenant-ID: <tenant>X-Brutor-AI-System: <ai-system id> # optional — see attribution belowThe body is a standard ExportTraceServiceRequest. Point an OpenTelemetry Collector at it with the otlphttp exporter in JSON mode (the exporter appends /v1/traces itself):
exporters: otlphttp/brutor: endpoint: http://gateway.internal:8100/v1/ingest/otel encoding: json headers: Authorization: "Bearer ${BRUTOR_API_KEY}" X-Tenant-ID: acme X-Brutor-AI-System: sys-claims-agent
service: pipelines: traces: receivers: [otlp] exporters: [otlphttp/brutor]Only spans carrying GenAI semantic-convention attributes become evidence:
gen_ai.operation.name |
Becomes | Read from the span |
|---|---|---|
chat, text_completion, generate_content, embeddings |
a model call | gen_ai.request.model, gen_ai.response.model, gen_ai.provider.name (or legacy gen_ai.system), gen_ai.usage.input_tokens / output_tokens (legacy prompt_tokens / completion_tokens) |
execute_tool |
a tool call | gen_ai.tool.name |
invoke_agent |
an agent call | gen_ai.agent.name |
A span with no operation name is classified from what it carries (a tool name → tool call; a request model → model call). Spans with no gen_ai.* attributes at all — HTTP server spans, database calls — are skipped, not rejected: the exporter can send its whole trace and the inlet keeps what is evidence, while using the rest as topology.
One trace is one run. The run id is otel:<trace id>; steps keep their span and parent ids, and a step’s depth counts only its GenAI ancestors, so a chat span under an HTTP span is a root step. The run closes when the trace’s top-level span arrives in an export — errored if that span’s status is ERROR, otherwise completed. If the root never arrives, the idle sweeper closes the run exactly as it would for a proxied client that never sent X-Brutor-Run-End. Late spans reopen the run the same way late steps do.
The response is the OTLP partialSuccess envelope plus what the inlet did with the batch, so a collector pipeline treats it like any other OTLP endpoint:
{ "partialSuccess": { "rejectedSpans": 0, "errorMessage": "" }, "accepted": 3, "skipped": 1, "runs": ["otel:aaaa0000aaaa0000aaaa0000aaaa0000"], "aiSystemId": "sys-claims-agent"}| Status | Meaning |
|---|---|
400 |
Not JSON, or not an ExportTraceServiceRequest (no resourceSpans) |
401 |
Missing or invalid API key |
413 |
More than 5 000 spans in one request — batch smaller |
415 |
Protobuf body — set the exporter to encoding: json |
Attribution
Section titled “Attribution”The run is attributed to an AI System through the same resolver every proxied call uses, in order: the X-Brutor-AI-System header on the request, the brutor.ai_system attribute on a span or on the resource, then the resource group the API key is bound to. A claim the key cannot reach is refused and recorded, and a batch that resolves to nothing lands in Unattributed runs on Mission Control rather than being dropped.
What ingested evidence is, and is not
Section titled “What ingested evidence is, and is not”An ingested run carries:
source = otelon every step and on the run — ormixedwhen some steps were proxied, such as an agent whose model calls run through the gateway but whose tool spans arrive by export;trace_continuity = externalon every step, andchain_integrity = observed_externalon the run — complete as reported. It counts toward observation coverage and may inform a baseline, because a whole run teaches a baseline what normal looks like. It never counts as gateway-signed or client-asserted linkage: nothing about it was signed or bound to a credential.
It does not carry prompts, responses, headers, costs, or any control decision. The inlet never reads gen_ai.prompt, gen_ai.completion or event bodies. Retention, erasure and body permissions apply to these rows like any other (see Data handling), and there is nothing in them for store_bodies to withhold.
How it shows up
Section titled “How it shows up”- Runs table — the Chain column reads
externalin its own tone, and the run’ssourceis on the row. - Run signals and the status strip —
external_pctis disclosed besideasserted_pct; both sit insideobserved_pct. - Assurance Report — What this report does not cover names the ingested share and states that no control applied. Recommended controls raises
route_through_gateway(high) whenever any run was ingested. - Verdict — a period evidenced only by ingested runs carries
basis: reported_telemetry, which caps the badge at Conditionally assured even under a signed operating envelope. The gateway cannot assure what it did not witness. - Standing checks —
sourceis a run fact ('gateway' | 'otel' | 'mixed'), so a Tier-A check can require witnessed evidence, or fire only on reported runs.
Related
Section titled “Related”- The run ledger — what a run records, and the chain-integrity vocabulary
- Traffic Data Import — vendor account telemetry, at aggregate level
- Data handling — retention, redaction and erasure across both sources

