Repository navigation
Integrations Tetragon
Experimental: this works and is tested, but may change and gets less hardening than the stable core. Linux only. Tested: event lines shaped as Tetragon's source writes them, imported into a real signer; not yet run on a live Tetragon (#312).
Tetragon is an eBPF sensor: it sees, in the kernel, the processes a workload
starts, the connections it opens and the files it writes, and exports them as JSON lines. tracekit sensor import
reads that export and sends each event of an agent's process to the v2 signer, which signs it into the agent's run as
an effect.observed record. It works whatever sandbox the agent runs in, or none.
| Tetragon | Tracekit | |
|---|---|---|
| What it does | Sees processes, connections and file writes in the kernel, and exports them | Signs each one into the run of the agent that caused it, joined to the tool call when the process names one, and signs a gap for what Tetragon says it dropped |
| Where | A DaemonSet (or a host service) with eBPF programs in the kernel | The import next to Tetragon, the signer outside the agent |
| What a third party can check later | Nothing signed: an export file, rotated away | A signed, hash-chained bundle, verified offline |
Tracekit doesn't check or change what Tetragon sees: a record says what Tetragon exported, under the import's own signer identity. The same import reads Falco's alerts.
1. Tetragon. Export to a file, keep the agents' events and the throttle events, and, for Codex, keep the one environment variable that names the tool call (Helm values):
tetragon:
exportAllowList: |-
{"namespace":["agents"],"event_set":["PROCESS_EXEC","PROCESS_EXIT","PROCESS_KPROBE"]}
{"event_set":["PROCESS_THROTTLE"]}
extraArgs:
enable-process-environment-variables: "true"
filter-environment-variables: CODEX_TOOL_CALL_IDThe chart writes the export to /var/log/tetragon/tetragon.log on each node. Outside Kubernetes, the same flags are
--export-filename, --export-allowlist, --enable-process-environment-variables and
--filter-environment-variables. Field filters (--field-filters) must keep process with its pod,
environment_variables, and the kprobe args.
2. A TracingPolicy for connections and for writes outside the workspace. Exec and exit events need none: Tetragon always sends them.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
name: tracekit-agent-effects
namespace: agents
spec:
kprobes:
- call: security_socket_connect # every connect, after the kernel copied the address
syscall: false
args:
- index: 1
type: sockaddr
selectors:
- matchArgs:
- index: 1
operator: Family
values: [AF_INET, AF_INET6]
- call: security_file_permission # every read and write path (write, sendfile, io_uring, ...)
syscall: false
args:
- index: 0
type: file
- index: 1
type: int
selectors:
- matchArgs:
- index: 0
operator: FileType
values: [reg] # regular files: no pipes or sockets
- index: 0
operator: NotPrefix
values: [/workspace/, /tmp/] # the agent's own directories
- index: 1
operator: Mask
values: ["2"] # MAY_WRITE
matchActions:
- action: Post
rateLimit: 1m # one event a minute per process and file: every write() call would flood
rateLimitScope: process # the export, and a repeat names no file the first didn'tThree filters on one selector need a 5.3 or later kernel. Put your own workspace path in NotPrefix. The rate limit
drops a process's repeat writes to one file within the minute, so tracekit analyze --v2 may find a second write call
to that file in the minute with no effect (TK-X207): read it against the first.
3. The signer. Give each node's import its own identity and the observe_effect grant, never a default, and keep
them out of every agent's hands:
authorize: {"mtls:spiffe://acme.org/tracekit-sensor/node-1": [observe_effect],
"mtls:spiffe://acme.org/tracekit-sensor/node-2": [observe_effect]}
tenants: {"mtls:spiffe://acme.org/tracekit-sensor/node-1": acme, # the tenant whose runs it covers
"mtls:spiffe://acme.org/tracekit-sensor/node-2": acme}One identity is one process: when a second process of it calls, the signer takes it for a restart of the first and
refuses the first from then on (conflict), so imports on two nodes under one identity stop each other. Run an import
per node, each with its own identity, or one import for the whole cluster reading every node's export.
4. The import, on each node, next to Tetragon's export (sensor.yaml):
sensor: tetragon
signer: https://signer.internal:8443 # its identity: TRACEKIT_SIGNER_TOKEN_FILE or _CERT/_KEY
runs:
- match: {namespace: agents, pod: "coder-*"} # globs over namespace, pod, container (id), uid
caller: k8s_sa:system:serviceaccount:agents:coder # the agent's signer identitytracekit sensor import --config sensor.yaml --follow /var/log/tetragon/tetragon.log
kubectl logs -n kube-system <the node's tetragon pod> -c export-stdout -f | tracekit sensor import --config sensor.yamlkubectl logs ds/tetragon reads one pod of the DaemonSet, so one node's events: name each node's pod, as above.
--follow reads from the file's end on and keeps reading across Tetragon's rotation. Without --follow it reads the
file once and exits; without a file, stdin.
The first runs entry whose match fits the event's process (its pod's namespace and name, its container id, its
uid) attributes it: the signer gives the event to the latest open run of that entry's caller registered by the
event's time (Tetragon's time; attribution: identity in the record), so an event read late goes to the run it
happened in, and one older than every open run of the caller (an export read again) to none. With no older run open,
an event up to 5 s before a run registered (the node's clock behind the signer's) goes to that run. An event of a process no entry matches is not an agent's: the import skips
it. An event of a matched process whose caller has no open run is a signed capture.gap{effect_outside_run} in the
tenant's signer run. The import covers the runs of its entries' callers only (it names them in every call), so a run
of any other identity never says its processes were observed, and the import's losses are no gaps in it.
One effect.observed per event, in the run (source: signer, format 1.4,
format v2):
| Tetragon event | kind |
Fields |
|---|---|---|
process_exec |
exec |
binary, pid, exec_id, parent_exec_id (the process that started it), args_commitment (the arguments, committed to, never stored) |
process_exit |
exit |
status, signal
|
process_kprobe with a sockaddr_arg (security_socket_connect) or sock_arg (tcp_connect) |
connect |
host (the address), port
|
process_kprobe with a file_arg or path_arg
|
file |
path |
Every record also has observer (the import's identity), sensor: tetragon, ts (Tetragon's time) and
attribution. Other events (other kprobes, tracepoints, LSM hooks) aren't recorded.
run.final's coverage matrix says processes observed (tetragon) when an import covered the run throughout: the
run's identity is a caller of its runs, and the import was alive when the run registered, sent a heartbeat at least
every 30 s, reported no loss, and recorded at least one of the run's events. An import per node can't tell which runs
are its node's: only the one that recorded a run's events says it observed it.
Codex puts the id of the tool call that started a command into its environment as CODEX_TOOL_CALL_ID, the id Tracekit
records as the call's tool_call_id (coding agents). With Tetragon keeping that variable
(step 1), each record of the command's processes carries it as tool_call_id. The id is the agent's claim: the
process can change, remove or reuse it, so the signer records it as it came and never takes it as explaining the
record. tracekit analyze --v2 checks it (TK-X206, below): the call ran, the record falls in its window, and its tool
causes that kind of effect.
Claude Code puts no such id into the commands it runs, so its runs' records carry none.
tracekit analyze --v2 compares the records with the run's calls (findings):
-
TK-X206, effect with no action (high): a process, connection or file write no call explains. A process is a
call's when it names the call (
tool_call_id, re-checked: the call ran, the effect is in its window, and its tool causes that kind of effect), when the process that started it is the call's (parent_exec_id: a server a call left running, and what it starts later), or when it started while a call that runs processes was running. A process naming a Read call, or a call long finished, is a finding. -
TK-X207, action with no effect (medium): a call that returned ok without what it should have caused: a Bash
command's programs (and curl's or wget's connection), a fetch tool's connection, a write tool's file outside the
directories your TracingPolicy leaves out (pass them:
--sensor-skip /workspace/ --sensor-skip /tmp/for the sample above). Checked only whenrun.finalsays the import covered the run throughout.
The agent's harness runs from before the run, so its own connections (its model calls) and files aren't checked,
except a file a write call named. Shell and Python processes started outside any call, as Tracekit's own hooks are,
aren't either. The checks need the sensor's clock and the signer's to agree within --clock-skew (0.5 s): keep both
on NTP; a finding says when they don't, and by how much.
Tetragon numbers no events, so a missing one can't be seen from the stream. What it reports in the stream becomes a signed gap:
-
rate_limit_info: its export rate limit (--export-rate-limit) dropped that many events. -
process_throttle(THROTTLE_START): its cgroup rate limit (--cgroup-rate) stopped sending a cgroup's exec and exit events. - A
process_execflaggedprocFS: Tetragon (re)started and reported the processes already running, read from/proc; what happened while it was down isn't in its export.
The import adds lines it can't read and an agent's events it can't record. It skips, and counts on stderr when it
exits, events it never records: a connect to no IP address (a unix socket: any family but AF_INET and AF_INET6),
other kinds of event, and events of no agent; none of these is a loss. It sends the losses to the signer as lost,
which writes a capture.gap{observer_lost} in each run the import covers (in the tenant's signer run when it covers
none), and their matrix then says processes not observed; they stay watched, so the import's silence after it is a gap
too. An import that stops sending for 90 s, or restarts, is the same gap, as for the
egress observer. With an import per node, each of these is a gap in the open
runs of every node: an import can't tell which runs are its node's.
Tetragon counts some drops only in its metrics (--metrics-server): tetragon_observer_ringbuf_events_lost_total,
tetragon_observer_ringbuf_queue_events_lost_total, tetragon_events_export_failed_total. Alert on them in your
monitoring: the import doesn't read them.
- Sees: what Tetragon exported for the agents' processes, as Tetragon reported it, and the losses Tetragon reports in its export.
- Doesn't see: what Tetragon didn't export: events its policies didn't select, drops it counts only in metrics, events of a container that started before Tetragon applied a namespaced policy to it (without Tetragon's OCI runtime hooks, policies apply best effort), and anything before the import's first heartbeat.
- Doesn't check: that Tetragon's events are true. The records are the import's assertion of what Tetragon said; a root user on the node can change Tetragon, its export or the import.
Limits lists these with what to do. A bundle with these records needs verifier
1.4.0 or later; an older one reports it UNVERIFIABLE (needs tracekit >= 1.4.0). Verify it with
uvx --from "tracekit-ai>=1.4.0" tracekit verify run.tkb --trust trust.json,
npx -y @cygnuxlabs/tracekit@latest verify run.tkb --trust trust.json, or the browser page
(verdicts).
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