Repository navigation
Security Checklist
Every HTTP surface of Tracekit passes this checklist. Each item names the tests that enforce it;
tests/test_security_checklist.py fails when a handler is added without being listed below and probed, or when a test
named here no longer exists.
| Handler | Serves | Kind |
|---|---|---|
tracekit/observe.py:Handler |
tracekit view (v2 store, or with view.logs every log of a central deployment at /runs, /api/runs, /api/run and /api/bundle; token, or OIDC login at /login and /callback; an approver's approval pages at /approvals; with why logs, tracekit view --why and tracekit why serve, the why read API /api/why/workspace and /api/why/runs/<id> for the token only, the replay POSTs /api/why/runs/<id>/tests and /api/why/runs/<id>/attribute, and POST /v1/ingest for a bearer token only: an --ingest-token, which reads nothing, else the token) and tracekit observe or tracekit view --ledger/--bundle (v0.2 ledger) |
browser; read-only but for the approval pages and the why replay POSTs; ingest API |
tracekit/transport/http.py:_Handler |
signer POST /v2/rpc and OTLP/HTTP POST /v1/traces; tracekit issuer serve
|
API |
tracekit/gateway.py:_Handler |
tracekit gateway serve (LLM gateway) |
API |
tracekit/executor.py:_Executor |
tracekit executor serve (GET /v1/actions, POST /v1/actions/<name>: actions run with credentials the agent never holds) |
API |
tracekit/mcp_gateway.py:_McpGateway |
tracekit mcp-gateway serve (POST /mcp: MCP over streamable HTTP to the configured upstream servers) |
API |
tracekit/egress.py:_Handler |
tracekit egress serve (egress observer: CONNECT host:port and absolute-form http:// requests, forwarded to the outside; no TLS interception) |
API, proxy |
tracekit/signer/metrics.py:Handler |
signer /metrics, /logs/v0 and tlog tiles |
API, public data |
tracekit/otlp.py:H |
tracekit otel serve (loopback OTLP receiver) |
API |
tracekit/ingest.py:H |
tracekit ingest serve (remote ingest) |
API |
tracekit/witness_server.py:H |
tracekit witness serve |
API |
tracekit/public_witness_server.py:H |
tracekit public-witness serve (C2SP tlog-witness add-checkpoint, open register, aggregate stats) |
API, open: a log proves its key by signing; limits per address and origin |
tracekit/proxy.py:Handler |
tracekit-proxy (Anthropic proxy) |
API |
tracekit/slack_approvals.py:Handler |
tracekit approvals slack serve (Slack interactivity callbacks) |
API, Slack-signed |
The browser surface is the only one that serves HTML or holds a session. The viewer's approval pages and its why replay POSTs (which run allow-listed programs and add test results to a run) are the only requests a session can change state with; the why logs are the token's only (an OIDC session gets 403: they belong to no tenant), and their ingest endpoint takes the bearer token only, never the cookie. API surfaces authenticate every request by header or client certificate (bearer token, mTLS, Kubernetes service-account or OIDC token, run token) and never read a cookie, so a browser cannot ride a session on them. The public witness is open by design: it takes no credential, and a log registers by signing a checkpoint with the key it registers (one key per origin, the first). The Slack bridge authenticates each callback by Slack's v0 request signature instead, and answers approvals only through the signer as the Slack user who clicked.
| # | Item | Enforced by |
|---|---|---|
| 1 | HTML and attribute sinks: every value interpolated into the page goes through esc() (& < > " ') |
tests/test_observe_render.py::EscapingStaticCheck::test_terminal_html_escapes_every_interpolation, tests/test_view.py::View::test_hostile_tool_names_are_data_and_the_page_escapes_every_sink
|
| 2 | JS sinks: run data reaches the page only as JSON from the API (exports: as escaped script data), never as code; no eval, Function, document.write; the approval and run pages set every value as text, never as markup |
tests/test_security_checklist.py::Viewer::test_hostile_run_data_is_served_as_json_data, tests/test_security_checklist.py::Page::test_approval_page_shows_data_as_text_with_format_characters_escaped, tests/test_security_checklist.py::Page::test_no_url_or_code_sink_takes_data, tests/test_observe_render.py::HostileBundleRendersInert::test_replay_embeds_hostile_strings_only_as_script_data
|
| 3 | URL sinks: no href, src or action takes data, so a javascript: value stays text; the one href set is the downloaded bundle's object URL |
tests/test_security_checklist.py::Page::test_no_url_or_code_sink_takes_data |
| 4 | Unicode tricks: bidi controls and invisible format characters in tool names, args or tenant names are shown as \uXXXX
|
tests/test_security_checklist.py::Page::test_esc_neutralises_markup_quotes_and_format_characters, tests/test_security_checklist.py::Page::test_approval_page_shows_data_as_text_with_format_characters_escaped
|
| 5 | Strict CSP: default-src 'none', script-src a fresh nonce per response, no unsafe-inline script, no inline handlers, base-uri and form-action 'none'
|
tests/test_observe_render.py::ObserverHttp::test_csp_uses_a_fresh_nonce_and_no_unsafe_inline_script, tests/test_security_checklist.py::Viewer::test_page_headers, tests/test_security_checklist.py::Approvals::test_page_headers, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
|
| 6 | frame-ancestors 'none' |
tests/test_security_checklist.py::Viewer::test_page_headers, tests/test_security_checklist.py::Approvals::test_page_headers, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
|
| 7 |
Referrer-Policy: no-referrer and X-Content-Type-Options: nosniff on every browser-surface answer, errors included |
tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_security_checklist.py::Viewer::test_page_headers
|
| 8 | Cookie auth: the session cookie is HttpOnly; SameSite=Strict, Secure over HTTPS, an 8-hour expiry with a MAC under the server's own random key (nothing of the token); an OIDC login's is a random id of a server-side session |
tests/test_observe_render.py::ObserverHttp::test_token_is_exchanged_for_a_cookie_and_leaves_the_url, tests/test_view.py::View::test_https_serves_with_a_secure_cookie, tests/test_identity_oidc.py::TestViewerLogin::test_auditor_session_sees_its_tenant_only, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
|
| 9 | API surfaces: bearer, mTLS, service-account or OIDC credentials only, checked on every request; Slack callbacks: a v0 signature over the body, a timestamp within 5 minutes and a signature never seen before |
tests/test_slack_approvals.py::TestSignature::test_bad_signature_old_timestamp_and_replay_are_refused, tests/test_http_transport.py::TestLimits::test_bearer_token_and_keep_alive, tests/test_identity_oidc.py::TestValidate::test_refusals, tests/test_gateway.py::TestAuth::test_refusals_never_reach_the_upstream, tests/test_ingest.py::EndToEnd::test_http_level_rejections, tests/test_view.py::WhyIngest::test_rejects_bad_token_tamper_and_gaps
|
| 10 | CSRF: the only state-changing requests served to a cookie session are the approval pages' POSTs, from an approver's OIDC session, each with that session's CSRF token in X-CSRF-Token (a header a cross-site request cannot set) and a JSON body; without approval pages the viewer answers GET only. The why replay POSTs need the token (cookie or bearer), a JSON body (which a cross-site page cannot send without a preflight nobody answers), a same-origin Origin if any, and an expected Host unless the token comes as a bearer header. The signer decides each answer for the person, under the approval rules. OIDC login: authorization code with PKCE, a state the browser's login cookie must match, used once |
tests/test_security_checklist.py::Viewer::test_no_state_changing_request_is_served_to_a_session, tests/test_security_checklist.py::Approvals::test_posts_need_the_session_csrf_token, tests/test_e13_approvals.py::TestWeb::test_csrf_role_and_bridge_grant, tests/test_identity_oidc.py::TestViewerLogin::test_refusals, tests/test_view.py::WhyPage::test_replay_posts_need_the_token_and_a_same_origin_json_body
|
| 11 | Tenant scoping on every read: an identity of tenant B cannot read or act on tenant A's runs or approvals; a run token works only for its run, identity and tenant. The viewer's token is one operator credential over its store or logs; an OIDC session (auditor or approver role) reads its tenant's runs only, in every query of the central viewer (list, search, run, bundle, by guessed id too), an operator-admin's every tenant's, read-only, and an approver's sees and answers its tenant's approvals only |
tests/test_view_central.py::CentralViewer::test_every_endpoint_reads_the_sessions_tenant_only, tests/test_identity_oidc.py::TestViewerLogin::test_operator_admin_reads_every_tenant_and_gets_no_approval_pages, tests/test_identity_oidc.py::TestViewerLogin::test_auditor_session_sees_its_tenant_only, tests/test_e13_approvals.py::TestWeb::test_cross_tenant_approval_is_refused, tests/test_http_transport.py::TestCrossTenant::test_matrix, tests/test_gateway.py::TestAuth::test_refusals_never_reach_the_upstream, tests/test_view.py::View::test_loopback_viewer_prints_a_token_url_and_needs_it
|
| 12 | No secret or token stays in a URL: the viewer's (and tracekit why serve's) printed ?token= is exchanged once for the cookie and answered with a redirect to a URL without it; only the page path exchanges it |
tests/test_observe_render.py::ObserverHttp::test_token_is_exchanged_for_a_cookie_and_leaves_the_url, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
|
| 13 | No secret or token in logs: handlers log no request line; refusals echo no credential |
tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_remote_tokens.py::TestRemoteSigner::test_refusals_carry_no_log_state
|
| 14 | Request size limits: an oversized body is refused with 413 before it is read |
tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_security_checklist.py::Approvals::test_posts_need_the_session_csrf_token, tests/test_http_transport.py::TestLimits::test_oversize_body_is_refused, tests/test_gateway.py::TestExchange::test_bodies_are_capped, tests/test_view.py::WhyPage::test_replay_posts_need_the_token_and_a_same_origin_json_body
|
| 15 | Rate and connection limits: failed authentications limited per address and in total; per-client rate limit on ingest; registrations per address, checkpoints per origin and origins in total on the public witness; slow clients cut off; the central viewer's pages bounded (50 runs, one search query per log plus a reader of each log with matching runs; 500 records) |
tests/test_view_central.py::CentralViewer::test_pages_are_bounded_and_filters_apply, tests/test_remote_tokens.py::TestFailedAuth::test_limited_per_address_and_bounded, tests/test_ingest.py::EndToEnd::test_rate_limit, tests/test_public_witness.py::Server::test_limits, tests/test_http_transport.py::TestLimits::test_slow_client_is_cut_off
|
| 16 | Error bodies without internals: no traceback, source path or credential |
tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_otlp.py::Wire::test_hostile_input_is_refused_cleanly
|
| 17 | HTTPS required off loopback: the viewer, the signer's http section, the issuer, ingest, the witnesses, tracekit why serve and the Slack bridge refuse plain HTTP beyond loopback (ingest, the witnesses and tracekit why serve take --insecure-http, the Slack bridge insecure_http: true, for TLS terminated in front); the observer (tracekit observe, tracekit view --ledger/--bundle) and the OTLP receiver serve loopback only; metrics need allow_remote: true and serve public data only |
tests/test_view.py::View::test_beyond_loopback_needs_https, tests/test_view.py::OneViewer::test_v1_ledger_defaults_to_its_home_and_stays_on_loopback, tests/test_http_transport.py::TestConfig::test_tls_required_except_explicit_insecure_loopback, tests/test_security_checklist.py::PlainHttp::test_plain_http_beyond_loopback_is_refused, tests/test_signer_metrics.py::TestListen::test_non_loopback_needs_allow_remote, tests/test_slack_approvals.py::TestConfig::test_secrets_come_from_files_and_beyond_loopback_needs_tls
|
| 18 | Host check: the browser surface serves only the names it expects, other names only with the token (DNS rebinding) |
tests/test_observe_render.py::ObserverHttp::test_loopback_without_token_still_refuses_foreign_hosts, tests/test_observe_render.py::ObserverHttp::test_wildcard_bind_accepts_its_own_host_only_with_the_token, tests/test_view.py::WhyPage::test_foreign_host_is_refused_without_the_token
|
| 19 | Verdicts the server computes are labelled "operator-side view — re-verify with the signed release for evidence", each run's in the central viewer's list and page too, and its bundle is served to re-verify offline; findings and what the model said (tracekit analyze --v2, agent text) are shown beside a run's integrity, never in it |
tests/test_view_central.py::CentralViewer::test_every_verdict_is_labelled_and_a_failed_run_shows_no_records, tests/test_view_central.py::CentralViewer::test_downloaded_bundle_verifies_offline, tests/test_view.py::View::test_dev_run_is_listed_with_its_counts_and_verified_while_the_signer_holds_the_lock, tests/test_view.py::View::test_tampered_record_shows_only_the_failure, tests/test_view.py::View::test_asked_and_said_show_beside_the_call_and_findings_beside_the_integrity
|
| 20 | The viewer refuses a run that fails verification: only the failure report is shown |
tests/test_view_central.py::CentralViewer::test_every_verdict_is_labelled_and_a_failed_run_shows_no_records, tests/test_view.py::View::test_tampered_record_shows_only_the_failure, tests/test_observe_render.py::ObserveBundle::test_tampered_bundle_is_refused
|
| 21 | Every HTTP handler is listed here and probed |
tests/test_security_checklist.py::Checklist::test_every_http_handler_is_listed, tests/test_security_checklist.py::Surfaces::test_every_surface_has_probes
|
| 22 | Signed findings (review_finding on POST /v2/rpc): written only into a findings run, which only an identity in signer.yaml analyzers registers, bound to a run of its tenant, with that run's token; the request schema is closed, so no text of the run (excerpts, paths, reasons) reaches a signed record, and the signer writes the rule's fixed title |
tests/test_reviews.py::TestReviewFinding::test_refused_outside_a_findings_run_and_for_an_identity_not_in_analyzers, tests/test_reviews.py::TestReviewFinding::test_the_closed_verdict_takes_no_text_of_the_run, tests/test_signer_lifecycle.py::TestSignerOnlyRecords::test_findings_only_from_a_configured_analyzer_bound_to_a_run_of_its_tenant
|
| 23 | Live constraints (constraint_add on POST /v2/rpc): any client of a run may add one, and it only ever raises a later call of that run to an ask a person answers (TK-CONSTRAINT), never lowering a deny or an ask; the record holds a commitment to the file, never its name; an equal one adds nothing; past 32 file constraints per run a new one is a signed constraint_refused gap, the hook is told, and every later write is held (a kind that names no file is always added); the writer checks at most 256 names per call |
tests/test_constraints.py::Live::test_a_decision_is_never_lowered, tests/test_constraints.py::Live::test_a_made_up_constraint_only_adds_holds, tests/test_constraints.py::Live::test_the_record_holds_a_commitment_never_the_name, tests/test_constraints.py::Live::test_refusals, tests/test_constraints.py::Live::test_an_equal_constraint_is_the_one_the_run_has, tests/test_constraints.py::Live::test_past_the_cap_a_file_constraint_is_a_signed_gap_and_every_write_is_held, tests/test_constraints.py::Live::test_the_writer_checks_at_most_max_names_files_per_constraint
|
Add the handler to SURFACES and Surfaces.PROBES in tests/test_security_checklist.py and to the Surfaces table
above, and make it pass every item that applies to its kind.
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