Capability-based security for AI agents
Agents today often get broad, long-lived API keys with no real context or fast revocation. VAC gives them task-scoped credentials instead: policies (Datalog) define what’s allowed; receipts prove what was done; a sidecar enforces both and injects the real API key. Revocation is heartbeat-based (seconds, not days).
In short: the agent never sees the API key. It gets a signed Biscuit and, after each allowed call, a signed Receipt. The next call can only be allowed if policy says so and the right receipts are present.
Prerequisites: Rust 1.70+, a Control Plane (use the mock in control-plane/ for testing).
- Generate a root key and create config:
cd sidecar
cargo run --example generate_test_keys # copy the public key
cp ../config.toml.example ../config.toml
# Edit config.toml: set root_public_key and api_key- Run sidecar and Control Plane:
# Terminal 1
cargo run --bin vac-sidecar -- --config-file ../config.toml
# Terminal 2
cd control-plane && cargo runSidecar listens on 0.0.0.0:3000, Control Plane on 0.0.0.0:8081. Optional: run the demo API (demo-api/ or demo-api-python/) on 8080.
- Get a test Biscuit and send a request:
cd sidecar
cargo run --example create_test_biscuit # copy the token
curl -X POST http://localhost:3000/charge \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"amount": 5000, "currency": "usd"}'Response will include X-VAC-Receipt for use in follow-up requests. More options (CLI, env): Deployment.
| Doc | Description |
|---|---|
| Architecture | Components, request flow, state |
| API | HTTP API, headers, Datalog policy |
| Deployment | Config, Docker, Kubernetes |
| Observability | Tracing, OpenTelemetry |
| LangChain integration | Using VAC with LangChain / LangGraph |
| Python SDK | Client library and examples |
- Control Plane (trusted): issues Root Biscuits, runs heartbeats, revocation, kill switch.
- Sidecar (semi-trusted): verifies Biscuits and receipts, evaluates Datalog policy, mints receipts, forwards to upstream with API key. Can be compromised; we use short-lived session keys and heartbeat revocation to limit impact.
- Agent (untrusted): never sees the API key; sends Biscuit + receipts; all traffic goes through the sidecar.
Receipts are signed proofs of completed actions (e.g. “search” done). Policies can require them (e.g. “allow charge only if receipt(search)”). Same workflow is tied together by a correlation ID. Receipts expire in ~5 minutes; session keys rotate on the same order.
| Term | Meaning |
|---|---|
| Root Biscuit | Signed credential from Control Plane; embeds policy |
| Receipt | Signed proof of one completed action; short-lived |
| Correlation ID | Ties a chain of requests (e.g. search → charge) |
| Datalog | Policy language (allow/deny); deterministic |
| Lockdown | Mode where only read-only is allowed (e.g. after heartbeat failures) |
vac/
├── sidecar/ # Main proxy: biscuit, receipt, policy, proxy, heartbeat, revocation, etc.
├── control-plane/ # Mock server (heartbeat, revoke, kill)
├── demo-api/ # Rust demo upstream API
├── demo-api-python/ # FastAPI demo upstream
├── sdks/python/ # Python client (vac_client, example)
├── docs/ # Architecture, API, deployment, observability, LangChain
├── examples/ # langgraph_vac.py, biscuit_spike.rs
├── mcp-server/ # MCP server (vac_request, vac_receipts_count)
└── k8s/ # Kubernetes sidecar deployment
Rust (Axum, Tokio, Reqwest), Biscuit Auth, Ed25519, Clap + config + env, tracing, Wasmtime (WASM adapters), token-bucket rate limiting, optional replay cache. See Architecture.
cd sidecar
cargo test --lib -- --test-threads=1
cargo test --test integration_test --test delegation_chain_integration_test \
--test delegation_depth_test --test heartbeat_test --test revocation_test \
--test security_test --test wasm_adapter_test -- --test-threads=1See CONTRIBUTING.md. For design and layout, Architecture.
MIT. See LICENSE.
Made while eating a lot of 🍊!
