Repository navigation
A2A
Experimental: this works and is tested, but may change and gets less hardening than the stable core.
tracekit a2a-gateway serve is a reverse proxy for configured A2A (Agent2Agent, JSON-RPC over HTTP) servers. Send requests to it instead of to the receiver. It asks the signer before forwarding a request, holds an ask for a person, uses credentials held only by the gateway, and records the result as source: gateway, tier T2. If the signer is unavailable, it forwards nothing.
The gateway is planned as Tracekit's A2A binding of PCM 0.1 (proof-carrying messages); today it does not check PCM parts. A PCM part in an A2A message has media type application/vnd.pcm+json.
tracekit a2a-gateway serve --config a2a-gateway.yamlhttp:
listen: 0.0.0.0:8092
cert: tls/a2a.pem
key: tls/a2a.key
authenticators: [mtls, k8s_sa, token]
signer: https://signer.internal:8443
approval_wait_s: 300
max_body: 1048576
upstreams:
- name: researcher # [a-z0-9-]{1,64}
url: https://researcher.internal/a2a
card: https://researcher.internal/.well-known/agent-card.json
headers: {authorization: secrets/researcher.header}Paths are relative to the config file. The gateway reads every Agent Card at startup and refuses to start if it cannot read one. It refuses plain HTTP beyond loopback before it fetches a card. It reaches the signer as itself (TRACEKIT_SIGNER_TOKEN_FILE, or TRACEKIT_SIGNER_CERT and _KEY); grant that identity as a gateway:
gateways: ["mtls:spiffe://acme.org/tracekit-a2a-gateway"]
authorize: {"mtls:spiffe://acme.org/tracekit-a2a-gateway": [decide, complete, approval_request, approval_wait, approval_consume, observe]}coding.yaml maps a2a:* to class a2a; dev.yaml, server.yaml and strict.yaml extend it. A policy composed only from server.yaml's component packs needs that mapping itself. server.yaml and strict.yaml ask for every A2A call. To let an approved pair through unattended, a deployment replaces the inherited TK-S011 ask rule by id with an equivalent route allowlist; a deny rule's unless only exempts that deny rule.
| Field | Meaning |
|---|---|
sender, receiver
|
The authenticated caller and configured upstream name. |
route |
sender->receiver. A transfer adds via projector. |
skills |
Sorted skill ids offered by the receiver's Agent Card. |
kinds, hosts
|
Message part kinds and hosts of file-part URIs. |
size |
Canonical JSON-RPC params bytes, or transfer bytes. |
modality, projector, weights
|
Transfer modality, adapter id and measured fuser-weight hash. |
Names (sender, receiver, projector, modality, weights, skill ids and part kinds) are read in lower case. Rules with class: a2a therefore match names in any case. A name that is not printable ASCII without spaces is refused with TK-A2A-NAME, whatever other rules say; its fixed reason and remedy do not repeat the name. Signed arguments retain the original spelling.
extends: server.yaml # or strict.yaml
ask:
# Replaces server.yaml's TK-S011: the listed pair does not ask.
- id: TK-S011
class: a2a
field: route
pattern: '.'
unless: '^mtls:spiffe://acme\.org/planner->(researcher|writer)$'
tools:
'a2a:*': a2a
deny:
- id: A2A-PAIRS
class: a2a
field: route
pattern: '.'
unless: '^mtls:spiffe://acme\.org/planner->(researcher|writer)$'
- id: A2A-SIZE
class: a2a
field: size
pattern: '^[0-9]{6,}$'A2A messages do not name the skill requested. A skill rule sees the skills the receiver's card offers, not a requested skill.
POST JSON-RPC to https://<gateway>:8092/a2a/<upstream>, with the agent identity and X-Tracekit-Run: <run_id>.<run_token> as at the LLM gateway. The gateway does not serve Agent Cards (GET is 405); point the client at the gateway endpoint, for example with a copy of the receiver card whose URL is the gateway URL.
Only these methods are relayed. Others receive -32601, with no decision and nothing sent.
| Method | Tool | Record |
|---|---|---|
message/send |
a2a:<upstream>/message/send |
Decision, then answer as tool.result. |
message/stream |
a2a:<upstream>/message/stream |
Decision; each event as an observed input; all events as the final result. |
tasks/get |
a2a:<upstream>/tasks/get |
Decision, then task as tool.result. |
The decision commits to {sender, receiver, agent_card, skills, params}. An allowed request sends the signer's copy of params; an ask approved with a T2 executor also sends that signer-held copy. A deny, rejected or unanswered ask, other refusal, or unavailable signer returns -32000 and never reaches the receiver. message/send and message/stream refuse a configuration.pushNotificationConfig with -32602, before a decision or send. Params that name an A2A field in another spelling (another case, or a snake_case or kebab-case name such as task_id or push-notification-config), or hold a key outside ASCII, anywhere outside metadata and a data part's data, are refused with -32602 before a decision: a receiver may read them as that field (a2a-python reads snake_case names, Go's encoding/json any case, and a decoder that folds case by Unicode may read ıd as id), while the gateway and the policy read only A2A's spelling. Answers complete the call as redacted T2 results. An upstream connection failure says the message was not sent; an unreadable answer after sending says it may have been acted on. Both complete the call as errors. If that result cannot be recorded, message/send still delivers it and the signer leaves a held_call_unresolved gap, as the executor does; message/stream withholds an event it cannot record.
For message/stream, every JSON-RPC event is first recorded as an input.observed with source a2a:<upstream>/message/stream#<tool_call_id> and trust untrusted, then passed to the agent. An error, message, final update, or terminal task state completes the call with all events as its result before the final event goes to the agent.
A stream cut before that final event (upstream ends or is idle for 10 minutes, an event cannot be recorded or is not JSON-RPC, or events exceed max_body) gives the agent a -32603 error event and has no tool.result. It records only the decision and observed events already passed to the agent. At run finalisation, the signer writes the call's capture.gap of kind held_call_unresolved; until then it looks in flight. The cut reason is not recorded.
The caller's headers are not forwarded upstream. Gateway-held receiver credentials never reach the caller or signer, and configured header values are cut from decoded answers, events and errors before recording or returning them.
-
Custody covers only requests routed through this gateway. Keep receiver credentials in it and fence agent egress.
-
Push notifications sent outside the gateway are not seen. It relays only the three methods above; push configuration methods,
tasks/cancelandtasks/resubscribeare refused, and so is a message carrying a push notification config. -
The gateway checks task ownership only for
tasks/get'sidand messages'taskId,contextIdandreferenceTaskIds. A caller owns the task and context ids the receiver's answers gave it: a task'sidandcontextId, a reply message'scontextId. Those ids are non-empty strings of at most 1,024 characters;referenceTaskIdshas at most 64 unique ids. Up to 1,024 task and context ids per caller (and 1,024 callers) are retained; an evicted id cannot be read or continued through the gateway. It forwardsparams.metadataunchanged and does not check values there. Do not put a receiver that reads task references from metadata behind the gateway; otherwise deny such metadata in an A2A policy, for example:deny: - id: R-METADATA-TASK class: a2a field: target pattern: '"metadata":.*"(taskId|contextId|referenceTaskIds)":'
-
Cards are read once at startup. Restart after a card changes.
-
A cut stream's
held_call_unresolvedgap is written at run end; its reason is not in the record. -
Messages name no skill, so rules see the card's offered skills.
-
Names match in case-insensitively for this class; non-printable-ASCII or space-containing names are refused as
TK-A2A-NAME. -
message/sendwaits 60 seconds for an answer. Use a stream or non-blocking send for long tasks.
See limits.
Generated from docs/ at v1.6.0. Edit the docs in the repository, not here.
Get started
- Quickstart: the v2 signer
- Quickstarts
- Quickstart: Claude Agent SDK
- Quickstart: Claude Code
- 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
- Hermes Agent
- 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
- Intent grants: approve costly payments once, with limits
- PCM 0.1: proof-carrying messages
- Findings
- tracekit audit
- 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
- A2A gateway
- Latent transfers and C2C
- MCP gateway
- LLM gateway
- Egress observation
- Witnesses
- Log monitor
- Record-key issuer
- Signer observability
- Viewer
- Event Horizon
- Viewer 1.4
- tracekit why: causal investigation and replay
- Migrating
- Platforms
- Deploy the public witness
Verify the evidence
- Tracekit for auditors
- Reading a v2 verify report
- Evidence format v2
- Auditor guide
- Sample evidence bundle
- Signing
- tracekit graph: what agents did across runs
- 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
- What the public witness sees
- 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
Use cases and demos
- Use cases
- Demos
- Agent approvers within bounds
- A production bucket
- Completeness and tamper evidence
- Detection and containment
- Network coverage: an egress observer
- Guardrails: allow, deny, or ask a person
- MCP tools, gated and recorded
- A production volume
- Prompt injection held by provenance
- Why: which input caused the action
Help