Repository navigation
OpenTelemetry
Experimental: this works and is tested, but may change and gets less hardening than the stable core.
Tracekit speaks OpenTelemetry in both directions.
-
In:
tracekit otel servereceives OTLP/HTTP traces and records the agent-relevant spans as signed ledger events. -
Out:
tracekit export --otelwrites a bundle's events as OTLP/JSON spans, and--otel-endpointsends them to a collector (Jaeger, Tempo, or anything that accepts OTLP).
Experimental. The OTLP receiver (
tracekit otel serve) and the ingest gateway (tracekit ingest serve) are off by default and being rebuilt: each runs only with--experimentaland prints a warning when it starts.tracekit initnever starts them.
tracekit init --dev # once: a local signer
tracekit otel serve --experimental # http://127.0.0.1:4318/v1/traces
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:4318/v1/traces python my_agent.pyAny OTLP/HTTP exporter works: protobuf or JSON bodies, gzip or deflate encoding. For exporters that default to
gRPC, add --grpc-port 4317 (needs pip install "tracekit-ai[grpc]"): the same receiver handles both, a signer outage is
answered with UNAVAILABLE (exporters retry) and a malformed request with INVALID_ARGUMENT. The receiver has no
dependencies of its own; it decodes protobuf itself. It listens on loopback only. Agents on other machines send to
the ingest gateway instead, which serves the same /v1/traces path behind TLS and per-client tokens:
tracekit ingest token build-box --home /var/lib/tracekit # prints a token once
tracekit ingest serve --experimental --home /var/lib/tracekit --host 0.0.0.0 --cert c.pem --key k.pem
# on the agent's machine
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://tracekit.example:8443/v1/traces \
OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer%20tk_..." python my_agent.py| Span | Recognised by | Ledger events |
|---|---|---|
| Model call |
gen_ai.operation.name = chat, text_completion, generate_content; OpenLLMetry llm.request.type; OpenInference LLM
|
model.exchange request + response: model, provider, finish reason, tool calls the model asked for, error, duration; prompt and output hashed (or clear with content_capture: full) |
| Tool execution |
execute_tool; OpenLLMetry traceloop.span.kind=tool; OpenInference TOOL; mcp.method.name = tools/call without agentgateway.outbound.* (agentgateway's request span: named mcp:<mcp.target>/<gen_ai.tool.name>, failed when mcp.error.code is set) |
tool.call, policy.decision, tool.result
|
| Root span | no parent |
run.end (error: ... when the span failed) |
| Anything else | HTTP, DB, framework internals | counted and skipped |
Each trace is one run, otel:<service.name>:<trace_id> (prefixed remote:<client>: through the gateway). A
run.start is written the first time a trace is seen, with the policy snapshot attached.
The Vercel AI SDK's spans need no mapping: with experimental_telemetry enabled they go to the receiver unchanged.
Provider calls (ai.*.doGenerate, ai.*.doStream) become model exchanges with the tool calls they requested and token
usage, and ai.toolCall spans become tool calls with their arguments and results. To gate the AI SDK's tool calls
before they run, use its v2 integration instead (Vercel AI SDK).
-
Application-reported. Events are recorded as
source=sdk: the application said this happened. Nothing proves the spans are complete or true, and the coverage report lists OTel runs separately. -
After the fact. Spans are exported when the work is done, so policy is evaluated retrospectively and nothing is
gated. A call that a
denyoraskrule would have stopped is recorded asflag, withpolicy would have returned deny before executionin its reasons. To block calls before they run, use the hooks, the SDK'stool()or the LangChain adapter. - Same privacy rules. Content is redacted and hashed exactly as for hooks and the SDK (privacy).
-
Not cross-checked as proxy evidence. Model exchanges reported by the application are never used for the signer's
hook/proxy cross-check, which would otherwise raise false
hook_missinggaps.
- Retries do not duplicate evidence. Every (trace, span, event kind) is written at most once per receiver process.
-
Outages are retryable. If the signer is down the receiver answers
503withRetry-After, so the exporter keeps the batch. Events written before the outage are not written again on retry. -
Rejections are partial successes. An event the signer refuses is reported in the OTLP
partialSuccess(rejectedSpans,errorMessage); the rest of the batch still lands. -
Bounded input. Bodies up to 4 MB (16 MB after decompression), 10,000 spans per request, attribute nesting capped
at 16 levels. Malformed bodies get
400, unknown content types415.
tracekit export --otel adds otel.json to the bundle. One trace per run: an invoke_agent root span, a chat span
per model exchange and an execute_tool span per tool call, with GenAI attributes where they exist and Tracekit's
security fields under tracekit.*. Every span carries tracekit.seq and tracekit.entry_hash, the hash of the signed
ledger entry it came from, so a span seen in any backend can be traced back to evidence that tracekit verify checks.
Runs that came in over OpenTelemetry go back out with their original trace id and span ids, so Tracekit can sit between your instrumentation and your existing observability tool without breaking links.

jaeger # Jaeger v2 all-in-one: OTLP on :4318, UI on :16686
python3 examples/custom_agent.py # any traced run
tracekit otel push --endpoint http://localhost:4318 --all # or --follow to send each run as it ends
tracekit export --out run.tkb
python3 examples/otel_jaeger_check.py run.tkb http://localhost:16686Output from Jaeger 2.22 (Linux arm64, Python 3.10, October 2026):
tracekit verify exit: 0
invoke_agent research-bot seq 0 2a3338dd0563ecf2272ac33 in bundle
execute_tool http_get seq 5 efcdcb3bd72585435e8ad1b in bundle
execute_tool http_get seq 8 0209316a33b36671ba4bea3 in bundle
execute_tool parse_table seq 12 8314fd52fa98e994a462ad7 in bundle
execute_tool parse_table seq 13 cd4aa931bcee3d36aa0e19c in bundle
execute_tool Agent seq 2 85f0caa1a99156eea5a2660 in bundle
execute_tool Agent seq 4 e3f58f19031c95f5482ca3f in bundle
execute_tool Bash seq 20 89c43138e53242bf20f7887 in bundle
8/8 spans resolve to a signed record in the verified bundle
The denied sudo cp shows up in Jaeger as an execute_tool Bash span with tracekit.policy.decision=deny. Jaeger is a
view: if a span and the bundle ever disagree, the bundle (checked by tracekit verify) is the record.
With an otlp section in signer.yaml, the signer's HTTPS listener also answers OTLP/HTTP POST /v1/traces
(protobuf or JSON, gzip or deflate). It is never unauthenticated: the same authenticators as the RPC (k8s_sa,
mtls, token) identify the caller before its body is read, and authorize must grant that identity otlp_import
(no identity has it by default). For local development, use a loopback listener with insecure_loopback: true and a
bearer token.
http: {listen: 0.0.0.0:8443, cert: tls/server.pem, key: tls/server.key, authenticators: [mtls], client_ca: {...}}
otlp: {max_spans: 512} # spans per request, default 512
tenants: {"mtls:spiffe://acme/collector": acme}
authorize: {"mtls:spiffe://acme/collector": [otlp_import]}-
One run per (tenant, trace id). The caller's tenant comes from
tenants, as for RPCs; the run id isotel:<trace id>, never derived fromservice.name, so two services in one trace share a run and one service's two traces are two runs. The signer registers it (run.registered,source: import,tier: T3,fidelity: none). -
Records. A tool span becomes
tool.call+tool.result, a model span onemodel.exchange, allsource: import,tier: T3, with the span id inspan_id. Arguments, results and messages are redacted and kept only as commitments (hmac-sha256:), as for the RPC. Other spans are counted and skipped. Imported runs are not reconciled. -
Meaning. T3 proves receipt at the time of the record, nothing about completeness or truth; nothing is gated.
An imported run is not linked to the run that gated the same calls: a framework's span for a call Tracekit denied
(PydanticAI's
execute_tool, when your hook returns the refusal) reads as a tool call that succeeded. Read the gated run for what was allowed; the import is what the framework said it did. -
Retries write nothing twice. A (span id, record type) is written once per run, and the set is part of the run's
state, so it survives restarts and snapshots: a re-sent batch writes nothing, and no second
run.registeredappears. -
Lifecycle. An imported run closes when its trace's root span (one with no parent) arrives, since an exporter
sends a span once it ends and the root ends last:
run.closing{close_run}, thenrun.finalaftergrace_s. A trace whose root never comes (its root is in another process, or not sampled) closes afteridle_swithout spans,run.closing{idle_timeout}. Spans for a final run are rejected (partialSuccess): a span that ends after its root and comes more thangrace_sbehind it is not recorded. -
Refusals. No accepted credential:
401; nootlp_import:403; a malformed body (nested too deep, a protobuf array or attribute list over 512 entries, or any decoding error):400, with nothing written. A request is written whole or not at all. The identity's event rate and open-run quotas apply (503withRetry-After, so exporters retry).
tracekit export --v2 --run RUN --otel writes the run's spans next to the bundle as <name>.otel.json
(--otel-endpoint also sends them). With otel_out: {endpoint, headers} in signer.yaml, the signer sends each run
when it reaches run.final, from its own thread with a queue of 1000 runs; runs it could not deliver are counted in
tracekit_signer_otel_export_dropped_total (observability).
One trace per run: an invoke_agent root span and an execute_tool span per call (tool call id and attempt). Every span
carries tracekit.entry_hash (the hash of its first signed record), tracekit.run_seq and tracekit.tier; tool spans
add tracekit.policy.decision, tracekit.policy.rule_ids, tracekit.args_commitment and tracekit.result.hash.
Argument and result content is never exported: v2 records hold commitments only. An imported run keeps its trace id and
its tool spans their span ids.
Generated from docs/ at v1.4.1. Edit the docs in the repository, not here.
Get started
- Quickstart: the v2 signer
- Quickstarts
- Quickstart: Claude Agent SDK
- Quickstart: a custom agent
- Quickstart: LangChain and LangGraph
- Quickstart: MCP client
- Quickstart: OpenAI Agents SDK
- Coding agents other than Claude Code
- tracekit doctor
- Walkthrough: Tracekit end to end
How it works
- How Tracekit works
- Overview
- The life of a tool call
- The evidence format
- Verification and assurance
- Witnesses and monitoring
- Policy
- Approvals
- Detection and containment
- Capture and completeness
- Custody
- Provenance and tracekit why
- Deployment and isolation
- Storage, durability and performance
- Integrations
- Security model
- Architecture
- tracekit why: architecture
Integrations
- Integrations
- Adapters
- OpenTelemetry
- Remote clients
- Kubernetes agent-sandbox
- agentgateway
- agentsh and Tracekit
- Browser Use
- Claude Agent SDK
- Claude Code's sandbox
- Codex's sandbox
- CrewAI
- Docker MCP Gateway
- E2B
- Falco
- Gemini CLI's sandbox
- Google ADK
- Haystack
- LangChain v1
- LiteLLM
- LlamaIndex
- MCP client
- microsandbox
- Microsoft Agent Framework
- OpenAI Agents SDK
- NVIDIA OpenShell
- Pydantic AI
- sandbox-runtime's proxy on its own
- Strands Agents
- Tetragon
- Vercel AI SDK
- Wassette
Policy and approvals
- Policy reference
- Approvals
- Findings
- Reading the model's reasoning
- Incidents and containment
- Webhooks
Deploy and operate
- Deploying the signer
- The signer as a container
- The signer on Kubernetes
- The compose stack
- Identity
- Tool executor
- MCP gateway
- LLM gateway
- Egress observation
- Witnesses
- Log monitor
- Record-key issuer
- Signer observability
- Viewer
- tracekit why: causal investigation and replay
- Migrating
- Platforms
Verify the evidence
- Reading a v2 verify report
- Evidence format v2
- Auditor guide
- Sample evidence bundle
- Signing
- What Tracekit guarantees
Security and privacy
- Threat model: the v2 signer
- Threat model: the v1 laptop signer
- HTTP security checklist
- Threat-model review packet
- Privacy
- Known limits and trade-offs
- What's stable
Compliance
- Compliance mapping
- EU AI Act
- ISO/IEC 42001
- NIST AI RMF and the Generative AI Profile
- OWASP Top 10 lists for LLM and agentic applications
Evaluation
Demos
- Demos
- Agent approvers within bounds
- Completeness and tamper evidence
- Detection and containment
- Network coverage: an egress observer
- Guardrails: allow, deny, or ask a person
- MCP tools, gated and recorded
- Prompt injection held by provenance
- Why: which input caused the action
Help