Warden User Guide
Warden is an action control plane for AI agents. It sits as an MCP proxy
between an agent and its tool servers. Every tools/call is checked against
policy — allow / deny /
require-approval — held for a human when required, and
recorded in a tamper-evident audit chain.
The thesis: in the agentic era the binding constraint is trust, not capability. Capability is the labs’ game; the open problem is letting autonomous agents act with bounded authority, verifiable behaviour, and accountability. Warden is the brake and the black-box recorder for agent actions.
agent -- tools/call --> Warden --+- policy: allow -----> upstream MCP server -> result
|- policy: deny ------> blocked (tool error)
\- require_approval --> held -> human -> allow/deny
|
\- every decision -> tamper-evident audit chain
Who this guide is for
Engineers deploying or integrating AI agents that take actions — call tools, query data, move money, file tickets — and who need those actions to be governed, attributable, and auditable. You should be comfortable with the command line and your agent framework of choice.
How to read it
- Concepts — the model: the proxy, the identity token, policy, and the audit chain. Read this first.
- Getting started — build Warden, run the self-contained demo, then govern your first real agent.
- Integrating Warden — the token-as-interface integration model, deployment patterns, the Python SDK, configuration, and production operations.
- Provider guides — a separate, end-to-end guide for each Agentic AI provider: LangGraph, AWS Bedrock (AgentCore), Databricks (Mosaic AI), Google ADK / Vertex AI, and Azure AI Foundry. Jump straight to yours.
- Reference — CLI, policy, token spec, and security.
The 30-second version
- Point your agent’s MCP client at
warden proxyinstead of the tool server. - Give Warden a signed token that says who the agent acts for (or run audit-only to start).
- Write a policy: which tools are allowed, denied, or need a human.
- Every call is now gated and recorded. Prove it with
warden audit verify.
New to Warden? Watch the animated overview for a two-minute visual tour, then come back here.
What Warden is
Warden is a policy enforcement and accountability layer for agent actions. It is not a model, an agent framework, or a tool server — it is the checkpoint those things pass through when an agent tries to do something.
The problem it solves
An autonomous agent decides, on its own, to call tools. Left ungoverned, a prompt-injected or simply mistaken agent can delete a database, wire funds, or exfiltrate data — and you may have no record of who was accountable or whether the action was authorized. Access control at the data layer (IAM, Unity Catalog, RBAC) governs what a principal can reach; it does not govern what an agent acting for that principal actually does, call by call, with a human able to intervene and a provable trail afterward.
Warden adds that action layer:
- Bounded authority — an agent may only call tools inside a delegated scope, under RBAC/ABAC/ReBAC rules, within per-run budgets.
- Human-in-the-loop — high-risk calls are held for synchronous approval.
- Accountability — every call is tied to a named human via a signed delegation token, and folded into a tamper-evident hash chain.
What it is not
- Not a jailbreak/prompt-injection filter. Warden bounds actions, not the model’s reasoning. A perfectly manipulated agent still cannot exceed its policy, scope, or budget, and cannot act without being recorded.
- Not an IAM replacement. Warden consumes your existing identity system (it verifies a token your IdP mints) and complements your data-layer access control with an action-layer brake and recorder.
- Not tied to one cloud or framework. The same Warden build runs everywhere; only a thin adapter differs per platform (see The integration model).
Where it runs
Warden speaks MCP (Model Context Protocol), the tool transport the major agent platforms are converging on. It runs as:
- a sidecar — one Warden per agent session, on the stdio/HTTP MCP path (preferred), or
- a shared gateway — one Warden fronting many agents over HTTP.
See Deployment patterns.
The four moving parts
| Part | Role |
|---|---|
| Proxy / decision pipeline | Intercept every tools/call, decide, forward or block, record. |
| Identity token | A signed statement of who the agent acts for (RFC 8693 delegation). |
| Policy | Allow / deny / require-approval by tool, condition, role, relationship, and budget. |
| Audit chain | An append-only, hash-chained, optionally signed record of every decision. |
Read those four, and you understand Warden.
How it works
Warden launches your real MCP tool server as its upstream and speaks MCP to
the agent on the front. The agent thinks it is talking to the tool server; every
tools/call actually passes through Warden’s decision pipeline first.
agent --(MCP, stdio/HTTP)--> Warden --(MCP, spawns upstream)--> tool server
|
| 1. verify identity (who acts?)
| 2. check revocation (still authorized?)
| 3. evaluate policy (allowed? held? denied?)
| 4. reserve budget (within the cap?)
| 5. forward / hold / block
| 6. record to the audit chain
The decision pipeline (in order)
Every tools/call runs the same gauntlet. Any failure denies — Warden fails
closed.
- Handshake — optionally require the MCP
initializebefore any call. - Pause — an admin
warden pausestops forwarding, at Warden, not by trusting the agent. - Revocation — a signed, append-only revocation feed can deny by token
jti, agent, or human (negative authority), checked live. - Identity — the delegation token is verified (signature,
aud/iss,exp/nbf, and that the token’s leaf actor matches this agent). Expired session tokens are refreshed-or-denied per call. - Policy — the first matching rule decides: scope narrowing, RBAC/ABAC/ReBAC
gates,
whenconditions, and per-run budgets. Otherwise thedefaultapplies. - Approval — a
require_approvaldecision holds the call until a human releases it (optionally with a signed approver assertion), or it times out. - Forward — an allowed (or approved) call reserves its budget atomically, durably records the authorization, mints a call-context token for the next hop, and forwards to the upstream.
- Record — the decision and outcome are appended to the tamper-evident audit chain (and any OCSF/SIEM sinks).
A denied call never reaches the upstream — the agent receives a clean MCP
tool error (isError: true) explaining the block, and the decision is recorded.
Concurrency
Warden is shared across worker threads (one per in-flight request). A call held for approval blocks only its own worker — the upstream lock is released during the wait — so other calls keep flowing. Responses correlate by JSON-RPC id, so out-of-order replies are fine.
Transports
- stdio — the sidecar-per-agent pattern; the agent’s MCP client launches
warden proxy ...as a subprocess. - HTTP — MCP Streamable-HTTP (
--http ADDR), withGET /healthzandGET /metrics, an optional bearer on the surface, and per-request identity for a shared gateway.
See the CLI reference for every flag.
The identity token
Warden’s identity boundary is a single signed token that says who an agent acts for. This token is the only coupling point between Warden and your platform — everything else (role expansion, relationship resolution, on-behalf-of exchange) stays on the platform side, where it already lives. Warden verifies the token and trusts its claims.
Delegation semantics (RFC 8693)
The token uses OAuth token-exchange delegation:
sub— the accountable party, a human. Empty ⇒ no accountability ⇒ Warden fails closed (in identity-required mode).act— the acting chain, nested from the outermost service down to the leaf agent:human → [service] → agent. The leaf actor must match the agent Warden is running as (--agent), or the call is denied.
{
"sub": "alice@example.com",
"act": { "sub": "svc-principal", "act": { "sub": "prod-agent" } }
}
This encodes “alice, through the svc-principal, is acting via the prod-agent.” The audit chain records that full line of accountability.
Authorization claims
The token also carries the authorization facts Warden’s policy consumes — trusted because the token is signed:
| Claim | Meaning | Policy use |
|---|---|---|
roles | RBAC roles/groups | require_role |
attrs | ABAC attributes | when { field = "subject:…" } |
rel | ReBAC relationship tuples {relation, resource} | require_relation |
scope | the agent’s delegated tool grant | scope narrowing (an agent may only call tools in scope) |
resource_attrs | trusted per-resource attributes | when { field = "resource:…" } |
cnf.jkt | DPoP proof-key thumbprint (RFC 7800/9449) | sender-constraint on HTTP |
Verification modes
- JWT (production) — a compact JWT verified against an asymmetric key from a
JWKS (by
kid, fetchable over HTTPS with caching/rotation) or a PEM public key. The algorithm is restricted to asymmetric families only, which blocks the classic RS256→HS256 confusion downgrade.audandissare required when you configure--aud/--iss. Selected with--jwks,--jwks-url,--issuer-url(OIDC discovery), or--issuer-key. - Dev envelope — a
{ "claims": {…}, "sig": "<hex>" }file with an optional keyed-digest signature, for local/demo use only (--token-key). Never an enforcement mode.
Where the token comes from
You don’t hand-write tokens in production — a platform identity adapter mints one from your native credential (STS session, Databricks OBO, Google workload identity, Entra OBO). The adapter shapes claims and asks the platform issuer to sign; it never signs authority itself. See The integration model and the Python SDK.
The escape hatch: Warden always accepts a raw conforming token with no SDK at all. Adoption is never coupled to a client library — the token spec is the contract. See the token & claims reference.
Policy model
A policy is a TOML file. Warden evaluates each tools/call against it:
the first matching rule wins; if none match, the default applies.
default = "deny" # deny anything not explicitly allowed
require_identity = true # fail closed if a call arrives with no verified token
[[rules]]
tool = "read_*" # wildcard tool match
decision = "allow"
[[rules]]
tool = "wire_funds"
when = { arg = "amount", op = "gt", value = 1000 } # condition on a tool arg
decision = "require_approval"
reason = "large transfers need a human"
[[rules]]
tool = "delete_database"
decision = "deny"
[[rules]]
tool = "write_file"
decision = "allow"
max_per_run = 20 # per-run budget
Decisions
allow— forward to the upstream.deny— block; the agent gets a clean tool error.require_approval— hold for a human (warden approve/warden deny).
Matching & conditions
- Tool match — exact, or a trailing
*wildcard (admin_*), or*for all. Matching is case-exact. whenconditions — on a tool arg ({ arg = "amount", op = "gt", value = 1000 }) or a trusted subject/resource/env field ({ field = "subject:team", op = "eq", value = "research" }). Operators:gt,lt,eq,contains. Numeric operators accept a number sent as a string. Combine with{ any = [...] }(OR),{ not = ... }(NOT), and arrays (AND). A rule whosewhendoesn’t match falls through to the next rule — which is what lets threshold rules (amount < Xallow vsamount ≥ Xhold) compose.
Identity gates (require a verified token)
| Field | Gate |
|---|---|
require_identity = true | deny any call without a verified, accountable token (RBAC/ABAC/ReBAC below all imply this) |
require_role = "analyst" | the token must carry this role (RBAC) |
require_relation = { relation = "can_read", resource_arg = "table" } | the token must hold can_read@<value of the "table" arg> (ReBAC) |
when { field = "subject:region", … } | condition on a signed token attribute (ABAC) |
An authenticated agent is also scope-narrowed: it may only call tools present
in the token’s scope, regardless of the rules.
Budgets
max_per_run = N caps how many times a tool may run in a “run.” The count is
reserved atomically at forward time, so concurrent calls can’t overshoot.
With --budget FILE the count is durable across restarts (no reset-by-restart
bypass); in-memory otherwise.
Author it safely
warden policy lint— static checks: unreachable rules, unknown field namespaces, zero budgets,resource:without a relation. Exits non-zero on errors — run it in CI.warden policy test --tool NAME --args JSON [--token FILE]— dry-run a call and print the decision, trace, and reason without executing anything.
See the full policy reference and Write a policy.
The audit chain
Warden records every decision — allowed, denied, held, approved — to an append-only JSONL log. It is the agent’s black-box recorder, and it is tamper-evident: each entry’s hash covers its own content plus the previous entry’s hash, so altering any past entry breaks the chain from that point on.
entry[n].row_hash = sha256( canonical_json(entry[n] fields) + entry[n-1].row_hash )
The hash is computed over a canonical JSON encoding of the accountability-bearing
fields (agent, tool, args, decision, outcome, reason, the accountable human, the
delegation chain, the authorizing token jti, the approval reference, …), so
who was accountable cannot be rewritten without breaking the chain.
Verify it
warden audit tail # what agents did, one line per decision
warden audit verify # prove the record wasn't altered
verify recomputes the chain and reports the first broken link if any. Try it:
run the demo, edit one line in the log, and re-run verify — it is caught.
Rollback protection: the signed anchor
The hash chain proves no interior row changed, but a self-consistent prefix (someone drops the last N rows) still verifies. To detect truncation/rollback, Warden signs checkpoints of the chain head to a separate anchor file:
warden proxy … --anchor .warden/anchor.jsonl --anchor-key anchor.pem
warden audit verify --anchor .warden/anchor.jsonl --anchor-pub anchor.pub.pem
An attacker cannot forge a checkpoint without the private key, and a rollback
that passes plain chain verification is caught by the anchor. Ship the anchor
(and the audit log) to WORM / append-only / offsite storage, and schedule
warden audit verify in your monitoring.
Without an anchor,
warden audit verifywarns that tail truncation is not detectable. For high-stakes enforcement, always anchor.
Data minimisation & redaction
Tool arguments can carry PII and secrets. Warden records a redacted projection of the args (the forwarded call keeps the real args), driven by regulation profiles:
warden proxy … --redact gdpr,pci,secrets --redact-scan-values
Redaction runs before hashing, so the record and its hash cover the redacted bytes. Both field-name and value-scan detection apply (including PII sent as JSON numbers).
Downstream evidence
- OCSF sink (
--ocsf FILE) — SIEM-ready events for each decision. - Standards-based sinks — configurable format × transport × filter × delivery, including a blocking high-assurance mode: the evidence must be acknowledged before the action executes (the fail-closed evidence gate).
- Signed Event Tokens (SET) — CAEP-style signed events for cross-system propagation.
See Operating in production and the security reference.
Install & build
Warden’s core is a single Rust binary. The optional Python SDK provides the identity adapters and orchestration shims.
Build the binary
git clone https://github.com/vijayvedula/warden
cd warden
cargo build --release # -> target/release/warden
export PATH="$PWD/target/release:$PATH"
warden --help
Requires a recent stable Rust toolchain (see
rust-versioninCargo.toml). A debug build (cargo build) is fine for local use.
Run it in a container
A slim, non-root image is provided:
docker build -t warden .
docker run --rm warden --help
# with a config + policy mounted:
docker run --rm -v "$PWD:/cfg" warden proxy --config /cfg/warden.proxy.toml
Tagged releases also publish prebuilt binaries and a container image to GHCR.
Install the Python SDK
Only needed if you want the identity adapters / orchestration shims (most provider guides use it):
pip install warden-agent-sdk # core: token builder + conformance kit
pip install "warden-agent-sdk[jwt]" # + asymmetric JWT signing (PyJWT)
The SDK core is pure-stdlib; the proxy always accepts a raw conforming token, so the SDK is convenience, not a requirement.
Verify your install
warden demo # self-contained walkthrough, no API key
warden audit verify --audit .warden-demo/audit.jsonl
If both succeed, you’re ready for the Quickstart.
Quickstart
The fastest way to see Warden work — no API key, no external server.
1. The self-contained demo
cargo build
./target/debug/warden demo
The demo drives a simulated agent through every decision path —
allow, a per-run budget,
deny, and a hold-for-approval
(both approved and denied) — then prints the audit trail and verifies the hash
chain.
2. Prove the record is tamper-evident
warden audit tail --audit .warden-demo/audit.jsonl # what the agent did
warden audit verify --audit .warden-demo/audit.jsonl # prove it wasn't altered
Now edit any single line in .warden-demo/audit.jsonl and re-run verify — the
tamper is caught with the exact sequence number.
3. Dry-run a policy decision
You can ask Warden what it would decide, without running an agent:
warden policy test --policy warden.policy.toml \
--tool wire_funds --args '{"amount": 5000}'
# -> decision, trace, and reason, nothing executed
4. Front a real tool server
Point an MCP client at Warden instead of the tool server; Warden launches the real server as its upstream:
warden proxy \
--upstream "python3 examples/echo_mcp_server.py" \
--agent prod-agent \
--policy warden.policy.toml
Allowed calls forward; denied calls are blocked before they reach the upstream;
held calls wait for warden approve/warden deny. Everything is appended to
.warden/audit.jsonl.
Next
- Govern your first agent — wire Warden into a real agent framework.
- Jump to your provider guide for an end-to-end, platform-specific walkthrough.
Govern your first agent
The only integration change is this: your agent’s MCP client launches
warden proxy … instead of the tool server. No other agent code becomes
Warden-aware. This chapter uses the runnable
LangGraph example;
the same shape applies to any MCP-speaking framework.
Before and after
# Before: the agent's MCP client launches the tool server directly
tools_server <----(MCP)---- agent
# After: it launches Warden, which launches the tool server
tools_server <--(MCP)-- warden proxy <--(MCP)-- agent
Point the client at Warden
With the SDK’s ProxyConfig (Python):
from warden_sdk import ProxyConfig
from warden_sdk.orchestration import langgraph as wl
from langchain_mcp_adapters.client import MultiServerMCPClient
cfg = ProxyConfig(
upstream="python3 tools_server.py", # your real MCP tool server
agent="prod-agent",
policy="warden.policy.toml",
)
client = MultiServerMCPClient(wl.warden_mcp_servers(cfg))
tools = await client.get_tools() # discovered THROUGH Warden
Or the raw command, for any client that launches an MCP stdio server:
warden proxy --upstream "python3 tools_server.py" \
--agent prod-agent --policy warden.policy.toml \
--audit .warden/audit.jsonl --approvals .warden/approvals.json --log-format json
Watch it govern
Give the agent a task that mixes a safe and an unsafe action (“read the customers
table, then drop the prod database”). You’ll see the read allowed and the
drop returned to the agent as a clean tool error —
BLOCKED by Warden: … — before it ever reaches the tool. Then:
warden audit tail --audit .warden/audit.jsonl # read=executed, delete=blocked
Add accountability
So far this is audit + policy with no identity. To tie every action to a named
human, add a verified token and set require_identity = true in the policy:
warden proxy … --token .warden/token.json --aud warden:prod \
--jwks-url https://your-idp/.well-known/jwks.json
Where does the token come from? A platform identity adapter mints it — see
your provider guide and the
Python SDK. For a local dry-run you can mint a dev token
(see Write a policy and the example’s mint_token.py).
Human-in-the-loop
Point a rule at decision = "require_approval". The call holds; release it
from another terminal:
warden approvals list --approvals .warden/approvals.json
warden approve <id> --by you --approvals .warden/approvals.json
Next: Write a policy.
Write a policy
A policy is the set of rules Warden evaluates on every tools/call. Start
restrictive, allow deliberately.
A starter policy
# warden.policy.toml
default = "deny" # anything not matched is blocked
require_identity = true # every call must carry a verified token
# Reads are safe.
[[rules]]
tool = "read_*"
decision = "allow"
# Outbound side effects wait for a human.
[[rules]]
tool = "create_ticket"
decision = "require_approval"
reason = "ticket creation needs human sign-off"
# High-value transfers wait for a human; smaller ones pass.
[[rules]]
tool = "wire_funds"
when = { arg = "amount", op = "gt", value = 1000 }
decision = "require_approval"
# Destructive actions are never allowed unattended.
[[rules]]
tool = "delete_*"
decision = "deny"
reason = "destructive"
First matching rule wins; otherwise default.
Add authorization gates
Once your token carries roles/relationships (see the identity token):
# RBAC — the token must carry this role.
[[rules]]
tool = "read_*"
require_role = "data.reader"
decision = "allow"
# ReBAC — the token must hold `can_read` on the exact table the call targets.
[[rules]]
tool = "query_table"
require_relation = { relation = "can_read", resource_arg = "table" }
decision = "allow"
# ABAC — a condition on a signed token attribute.
[[rules]]
tool = "export_*"
when = { field = "subject:region", op = "eq", value = "EU" }
decision = "allow"
Test before you deploy
warden policy lint --policy warden.policy.toml # catch mistakes
warden policy test --policy warden.policy.toml \
--tool wire_funds --args '{"amount": 5000}' # dry-run a decision
warden policy test --policy warden.policy.toml \
--tool query_table --args '{"table":"sales"}' --token .warden/token.json
lint flags unreachable rules, unknown field namespaces, and zero budgets — wire
it into CI. test prints the decision, the trace of which gate decided it, and
the reason, executing nothing.
Iterate safely in production
- Run observe-only first: set permissive decisions and watch the audit trail before you enforce.
- Warden hot-reloads policy on
SIGHUP(orwarden pause/warden resume) — no restart, no dropped calls.
See the full policy reference.
The integration model
Warden integrates with any agentic platform through one interface: a signed token. That is deliberately the only coupling point. Consequently, adopting Warden on a new platform requires exactly two things:
- An issuer/verifier config — trust the platform’s token issuer (its JWKS
endpoint or public key, and the expected
aud). - A claims-mapping adapter — translate the platform’s native identity into
Warden’s canonical claims (
sub,act,roles,attrs,rel,scope).
Everything expensive — relationship resolution, role expansion, on-behalf-of exchange — stays on the platform side, where it already lives. Warden verifies and evaluates. This is what keeps a single Warden build portable across clouds.
The token is the interface
Every provider has (a) a token/credential issuer, (b) an on-behalf-of/delegation mechanism, (c) an attribute facility, and increasingly (d) MCP as the tool transport. Warden consumes all four through the one token, and sits as the MCP proxy between the platform’s agent runtime and its tool servers.
| Warden claim | Databricks | AWS | Azure | |
|---|---|---|---|---|
sub (accountable human) | OBO user | human behind the role chain | OIDC sub / DWD subject | Entra oid/upn |
act (delegation) | service principal | STS session | service account | managed identity (OBO) |
roles (RBAC) | UC groups | IAM roles | IAM roles | Entra app roles |
attrs (ABAC) | workspace/catalog | STS session tags | IAM conditions | directory attrs |
rel (ReBAC) | UC grants on securables | resource tags | resource-level IAM | Azure RBAC assignments |
scope (agent grant) | registered UC functions | session policy | agent-card capabilities | delegated scopes |
Two kinds of adapter (composed, not four interchangeable ones)
| Kind | Examples | Job |
|---|---|---|
| Identity adapter | Databricks, AWS, Google, Azure | Shape the Warden token from the platform’s native credential |
| Orchestration shim | LangGraph, Google ADK | Route the framework’s tool calls through the Warden proxy and attach the token |
A framework like LangGraph has no sub to anchor to — it runs on top of a
cloud, so its orchestration shim composes with an identity adapter. The SDK
is therefore (orchestration shim per framework) × (identity adapter per
platform), composed.
The no-forged-authority rule
From Warden’s perspective an adapter is untrusted client code — it runs on the side Warden exists to police. Therefore:
An adapter orchestrates the platform’s native token exchange; it never signs authority itself. The trusted signer stays the platform issuer (Databricks / IAM / STS / AgentCore Identity / Entra); the adapter only shapes claims and asks the issuer to sign. Warden verifies against the issuer’s JWKS.
Done correctly, the SDK is pure convenience with no new trust surface. And because the token spec is the real contract, the proxy always accepts a raw conforming token with no SDK at all.
Next
- Deployment patterns — sidecar vs shared gateway.
- The Python SDK — the adapters and shims in code.
- Your provider guide — the end-to-end walkthrough.
Deployment patterns
Warden runs as a stateless process on the MCP path. There are two topologies.
Sidecar proxy (preferred)
One Warden per agent runtime/session, on the MCP stdio (or loopback HTTP) path.
┌ agent pod / session ─────────────────────────┐
│ agent --(MCP stdio)--> warden --> tools │
└───────────────────────────────────────────────┘
- Surgical revocation — pausing/reloading one process affects one agent.
- Strong isolation — each agent’s trust boundary is its own.
- Simple identity — a single session principal (
--token) per sidecar.
This matches the MVP and most single-tenant deployments. It’s the default in the provider guides.
Shared gateway
One Warden (HTTP) fronting many agents.
agent A ─┐
agent B ─┼──(MCP HTTP)──> warden gateway ──> tools
agent C ─┘
- Simpler to operate — one process to run and scale horizontally.
- Per-request identity — each
tools/callcarries its own bearer delegation token (--request-identity), so every call is attributed to its accountable human even though one gateway serves many users. Combine withrequire_identityso a call without a valid token fails closed. - Trade-off — it concentrates the trust boundary and forfeits surgical revocation (use the signed revocation feed for fine-grained revoke).
--token(one session principal) and--request-identity(per-call bearer) are mutually exclusive — pick one per process.
Scaling & resilience
- Concurrency — thread-per-request; a held approval blocks only its worker.
- Disposability — fast start;
SIGTERM/SIGINTdrain in-flight calls (--drain-timeout) then sign a final audit checkpoint;SIGHUPhot-reloads policy. - Upstream resilience — per-call timeouts and auto-restart of a crashed/hung tool server, with clean errors to the agent.
- Durable state — audit log, approval queue, and durable budget are attached backing services; put them on a durable volume or ship to WORM/SIEM.
TLS & the network edge
Warden’s HTTP transport speaks plain MCP Streamable-HTTP; terminate TLS/mTLS at
the edge (a front proxy / service mesh) and bind Warden to loopback in sidecar
mode. Put a bearer on the surface with --http-auth-token.
See Operating in production for the full hardening checklist.
The Python SDK
warden-agent-sdk is the convenience layer for producing Warden tokens and wiring the
proxy into an agent framework. The core is pure-stdlib; JWT signing is an
optional extra.
pip install warden-agent-sdk # token builder + adapters + conformance kit
pip install "warden-agent-sdk[jwt]" # + asymmetric JWT signing (PyJWT)
The proxy always accepts a raw conforming token — the SDK is convenience, not a new trust surface (see the no-forged-authority rule).
Build a token
from warden_sdk import TokenBuilder
tok = (
TokenBuilder(sub="alice@example.com", agent="prod-agent")
.via("svc-principal") # act chain: alice -> svc -> agent
.role("analyst")
.attr("region", "EU")
.relation("can_read", "table:sales") # ReBAC
.grant("query_table") # the agent's delegated scope
.audience("warden:prod")
.expires_in(300)
)
# Local/dev: a keyed dev envelope (verified with `--token-key`)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
Sign a production JWT
from warden_sdk import JwtSigner
signer = JwtSigner.from_file("issuer_ec_priv.pem", alg="ES256", default_kid="k1")
jwt = tok.to_jwt(signer, at_jwt=True) # RFC 9068 access token
Warden accepts only asymmetric algorithms (ES/RS/PS/EdDSA), blocking the
RS256→HS256 confusion downgrade. In production the private key should live in a
KMS/HSM and the platform issuer should sign; JwtSigner is the local/dev
signer.
Identity adapters
Map a platform’s native identity to canonical claims — pure data mapping.
from warden_sdk.adapters import aws, databricks, google, azure
tok = aws.from_sts_session({
"accountable": "alice@example.com",
"session_name": "agent-session",
"session_tags": {"team": "research"}, # -> ABAC attrs
"iam_roles": ["arn:aws:iam::…:role/analyst"],
"session_policy_actions": ["query_table"],
}, agent="prod-agent", audience="warden:prod")
| Adapter | Entry point | Native source |
|---|---|---|
aws | from_sts_session | STS AssumeRole + session tags (Bedrock AgentCore) |
databricks | from_obo | on-behalf-of-user + Unity Catalog grants |
google | from_workload_identity | workload identity / service account (Vertex ADK / A2A) |
azure | from_entra_obo | Entra ID managed identity + OBO |
Orchestration shims
Point a framework’s MCP client at warden proxy — no other agent code changes.
from warden_sdk import ProxyConfig
from warden_sdk.orchestration import langgraph as wl
cfg = ProxyConfig(upstream="python3 tools_server.py", agent="prod-agent",
token=".warden/token.json", audience="warden:prod")
servers = wl.warden_mcp_servers(cfg) # for MultiServerMCPClient
warden_sdk.orchestration.google_adk.warden_connection_params(cfg) gives the
equivalent stdio params for the Google ADK MCP toolset. ProxyConfig.command()
returns the full warden proxy … argv for any launcher.
Conformance kit
A token is conformant iff it passes warden token verify — the exact check the
proxy runs. The kit shells out to the real binary, so first-party and community
adapters are verifiable against ground truth:
from warden_sdk import TokenBuilder, verify_token
env = TokenBuilder(sub="alice", agent="prod-agent").audience("warden:prod") \
.dev_envelope(key="dev-secret")
verify_token(env, agent="prod-agent", audience="warden:prod",
token_key="dev-secret").raise_for_status()
Now pick your provider guide for the full walkthrough.
Configuration (12-factor)
Warden follows the twelve-factor methodology so the same build runs unchanged across dev, staging, and prod — only its config differs.
Three config channels, in precedence order
- CLI flags —
warden proxy --upstream "…" --agent prod-agent … WARDEN_*environment variables — every flag is settable from the environment (factor III). The name is the flag upper-cased with dashes as underscores:--jwks-url→WARDEN_JWKS_URL.[proxy]TOML config file —--config warden.proxy.toml.
Flag ▸ env ▸ config ▸ built-in default. So:
# identical behaviour, three ways:
warden proxy --upstream "python3 tools.py" --agent prod-agent
WARDEN_UPSTREAM="python3 tools.py" WARDEN_AGENT=prod-agent warden proxy
warden proxy --config warden.proxy.toml
Copy .env.example
to .env and edit. Never commit a real .env — it’s gitignored.
Common variables
WARDEN_UPSTREAM="python3 tools_server.py" # the real MCP tool server to front
WARDEN_AGENT=prod-agent
WARDEN_POLICY=warden.policy.toml
WARDEN_AUDIT=.warden/audit.jsonl # backing service (factor IV)
WARDEN_JWKS_URL=https://idp/.well-known/jwks.json
WARDEN_AUD=warden:prod
WARDEN_HTTP=0.0.0.0:8080 # port binding (factor VII); omit for stdio
WARDEN_LOG_FORMAT=json # logs as an event stream (factor XI)
WARDEN_DRAIN_TIMEOUT=10 # graceful drain (factor IX)
Backing services & secrets
The audit sink, approval store, JWKS/OIDC issuer, OCSF/SIEM sink, and anchor key are attached resources referenced by config — swap a local file sink for a network SIEM with a config change, not a code change. Inject secrets (signing keys, HTTP auth tokens) as file paths mounted from your platform’s secret store; never bake keys into the image.
Twelve factors, mapped
Each factor and how Warden conforms is documented in
docs/twelve-factor.md
— codebase, dependencies, config, backing services, build/release/run,
processes, port binding, concurrency, disposability, dev/prod parity, logs, and
admin processes (warden audit verify, warden policy lint, warden revoke).
Next: Operating in production.
Operating in production
Warden is beta: single-node, file-backed, and it has been through an adversarial security review but not yet an independent audit. Adopt it in stages.
Recommended rollout
- Observe mode first. Run audit-only (record, don’t block). You get full visibility and a tamper-evident trail at near-zero risk — the enforcement path isn’t load-bearing yet.
- Enforce high-risk tools once you’ve validated policy in your environment
(deny the destructive ones,
require_approvalthe outbound ones). - For regulated / high-stakes enforcement, conduct your own review until the public audit lands.
Hardening checklist
- Identity — verify tokens against a real JWKS/issuer (
--jwks-url/--aud), not the dev envelope; enable--require-at-jwt(RFC 9068). - Sender-constrain tokens with DPoP on the HTTP transport.
- Network — put a bearer on the HTTP surface (
--http-auth-token), terminate TLS/mTLS at the edge, bind to loopback in sidecar mode. - Keys — keep signing keys in KMS/secrets, never beside the audit file.
- Evidence — enable the signed
--anchor; ship the audit + anchor to WORM/SIEM (--ocsf, sinks); schedulewarden audit verify(+ anchor verify) in monitoring. - Fail-closed evidence gate — use a blocking sink where “no action without a durable record” is required (a failed audit write alone is non-blocking by default — an availability choice).
- Supply chain — run
cargo audit/cargo denyin your build; pin a release. - Policy — run
warden policy lintin CI; start restrictive; iterate withwarden policy testand hot-reload (SIGHUP).
Observability
--log-format json— one structured decision line per call (tool, decision, outcome, latency, accountable,jti) with OpenTelemetry fields.GET /healthz,GET /metricson the HTTP transport;--metrics FILEand a drain summary for stdio.--ocsf FILE— OCSF events for your SIEM.
Revocation & incident response
- Pause everything at Warden:
warden pause/warden resume(a running proxy reloads policy on resume). - Revoke live, without a restart, via the signed feed:
warden revoke --jti … | --agent … | --human … --revoke-key admin.pem. The proxy tails it and denies matching calls immediately.
Known residuals
These are tracked and documented in
docs/production-readiness.md:
plain verify needs the anchor to detect tail rollback; approval assertions can
replay against a byte-identical action (a per-request nonce is the fix); tool
matching is case-exact; host/root compromise holding the signing keys is out of
scope until KMS/HSM integration.
See the security reference and SECURITY.md.
Choosing your provider guide
Each guide below is a complete, end-to-end walkthrough for one Agentic AI provider: where Warden sits, how that platform’s identity maps to Warden’s token, minting the token with the SDK adapter, routing tool calls through the proxy, a provider-appropriate policy, running it, and verifying the audit.
They all follow the same integration model — the token is the only interface — so once you’ve done one, the others are familiar.
| Guide | Orchestration | Identity source | Start here if… |
|---|---|---|---|
| LangGraph | LangGraph ReAct agent | dev token, or any cloud adapter | you build agents with LangGraph (runs fully locally) |
| AWS Bedrock | Bedrock AgentCore | STS AssumeRole + session tags | your agents run on AgentCore Gateway/Identity |
| Databricks | Mosaic AI Agent Framework | on-behalf-of-user + Unity Catalog | your tools are UC functions / Databricks-hosted MCP |
| Google ADK | ADK / Vertex Agent Engine | workload identity / service account | you use ADK or Vertex Agent Engine (or A2A) |
| Azure AI Foundry | Foundry Agent Service | Entra ID managed identity + OBO | your agents run in Azure AI Foundry |
Identity vs orchestration
Two of these are orchestration frameworks (LangGraph, Google ADK) and the rest are identity platforms — and they compose. LangGraph has no identity issuer of its own: run it on a cloud and pair its orchestration shim with that cloud’s identity adapter. The SDK chapter shows both sides.
Not listed here?
Any platform integrates through the same two pieces — an issuer/verifier config and a claims-mapping adapter. Use the raw token spec directly (the proxy needs no SDK), or model a new adapter on the closest one above. The conformance kit verifies that your adapter emits a valid token.
LangGraph
LangGraph is an orchestration
framework: it wires an LLM into a graph of tool-calling steps. It has no
identity issuer of its own — a LangGraph app has no sub to anchor to; it
runs on top of a cloud (AWS, Databricks, Google, Azure) or on-prem. Governing it
with Warden therefore composes two things:
- an orchestration shim — route the agent’s tool calls through
warden proxy(this chapter), and - an identity adapter — mint the Warden token from wherever the agent runs (see AWS Bedrock, Databricks, Google ADK / Vertex AI, or Azure AI Foundry).
This is the split described in the integration model: the token is the only interface, and orchestration shims pair with identity adapters rather than replacing them. For a purely local / on-prem run you can skip the cloud adapter and use a dev-envelope token (Step 2).
LangGraph is also the one example that runs fully end-to-end on your laptop —
the agent reads a table (allowed) and tries to drop a database (blocked) — so
this guide is runnable start to finish. The complete sources live at
examples/langgraph.
Overview
The only integration change is where the agent’s MCP client points: instead of
launching the tool server directly, it launches warden proxy, which launches
the tool server as its upstream and governs every tools/call.
LangGraph agent --(MCP, stdio)--> warden proxy --(MCP, stdio subprocess)--> tools_server.py
|
| verify identity -> policy -> hold -> audit
v
allow / deny / require-approval
No other agent code becomes Warden-aware. The graph, the model, the tools — unchanged. Warden sits on the wire, verifies the delegation token, evaluates policy on each call, holds high-risk calls for a human, and records a tamper-evident audit chain. The proxy always accepts a raw conforming token; the Python SDK is convenience for building the command and the MCP client stanza.
Prerequisites
# 1. Build Warden (from the repo root -> target/release/warden)
cargo build --release
export PATH="$PWD/target/release:$PATH"
# 2. Python deps: the SDK, LangGraph, the MCP adapter, the model, and MCP itself
pip install warden-agent-sdk langgraph langchain-mcp-adapters langchain-anthropic mcp
# 3. The agent's model key
export ANTHROPIC_API_KEY=sk-ant-...
Confirm warden is reachable:
warden --help # or: ./target/release/warden --help
The example ships a tiny MCP tool server, tools_server.py, exposing three
tools chosen to demonstrate the three policy decisions:
| Tool | Nature | Policy decision |
|---|---|---|
read_records | read-only | allow |
create_ticket | outbound side effect | require_approval |
delete_database | destructive | deny |
Step 1 — Route tool calls through Warden
The entire orchestration change is swapping the MCP client’s target. Below is the before (agent talks to the tool server directly, ungoverned) and after (agent talks to Warden, which talks to the tool server).
Before — ungoverned:
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"tools": {
"command": "python3",
"args": ["tools_server.py"],
"transport": "stdio",
}
})
After — every call routed through warden proxy:
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent
from warden_sdk import ProxyConfig
from warden_sdk.orchestration import langgraph as wl
# Describe the `warden proxy` invocation in front of the tool server.
cfg = ProxyConfig(
upstream="python3 tools_server.py", # Warden launches this as its upstream
agent="demo-agent", # the agent's wire identity (leaf actor)
policy="warden.policy.toml",
audit=".warden/audit.jsonl",
approvals=".warden/approvals.json",
log_format="json",
)
# warden_mcp_servers(cfg) -> {"tools": {"command": <warden>, "args": [...], "transport": "stdio"}}
client = MultiServerMCPClient(wl.warden_mcp_servers(cfg))
tools = await client.get_tools() # discovered THROUGH Warden
model = ChatAnthropic(model="claude-sonnet-4-6")
agent = create_react_agent(model, tools)
ProxyConfig builds the warden proxy ... argument vector;
warden_mcp_servers(cfg) wraps cfg.mcp_stdio_config() in the
MultiServerMCPClient shape. (If you prefer, wl.warden_stdio_client(cfg)
constructs the MultiServerMCPClient for you.) The agent object and the graph
are exactly what you would write without Warden — only the client stanza
changed.
If
wardenis not onPATH, setWARDEN_BIN=/path/to/wardenor passProxyConfig(..., warden_bin="../../target/release/warden").
Step 2 — Add accountability (a token)
By default the example runs without identity so you can see policy work
immediately. To make every action tie to a named human, add a delegation
token: sub is the accountable human, act is the nested acting chain whose
leaf equals the agent’s wire identity (--agent), plus roles (RBAC), attrs
(ABAC), rel (ReBAC), and scope (the agent’s grant). See
the identity token for the full model.
For a local walkthrough, mint a dev-envelope token with TokenBuilder
(this is what examples/langgraph/mint_token.py does):
from warden_sdk import TokenBuilder
tok = (
TokenBuilder(sub="alice@example.com", agent="demo-agent")
.role("data.reader")
.grant("read_records", "create_ticket")
.audience("warden:langgraph")
.expires_in(3600)
)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
python3 mint_token.py alice@example.com
Verify the token the way the proxy would (a conformance check — it prints the delegation chain and exits nonzero if the token does not verify):
warden token verify \
--token .warden/token.json \
--agent demo-agent \
--aud warden:langgraph \
--token-key dev-secret
Then attach it to the proxy. With ProxyConfig the token, audience, and dev key
flow into the command:
cfg = ProxyConfig(
upstream="python3 tools_server.py",
agent="demo-agent",
policy="warden.policy.toml",
token=".warden/token.json",
audience="warden:langgraph",
extra_args=["--token-key", "dev-secret"], # dev only; production uses --jwks-url
)
Dev tokens are not an enforcement mode.
--token-keyverifies a symmetric dev signature — fine for a laptop, tests, and the conformance kit. In production the token is a real JWT signed by the platform’s issuer/KMS, and Warden verifies it against the issuer’s JWKS (--jwks-url/--aud). The adapter shapes claims and asks the issuer to sign; it never signs authority itself. That is the job of the identity adapter for your cloud — see AWS Bedrock, Databricks, Google ADK / Vertex AI, or Azure AI Foundry.
Step 3 — Write a policy
A LangGraph-appropriate policy: allow reads, deny destructive actions outright,
and hold outbound side effects for a human. First matching rule wins; otherwise
the default. See the policy reference for the full
grammar.
# warden.policy.toml — first matching rule wins; else `default`.
default = "deny"
# Require a verified human behind every action (fail closed without a token).
require_identity = true
# Reads are safe.
[[rules]]
tool = "read_*"
require_role = "data.reader"
decision = "allow"
# Outbound side effects wait for a human (warden approve <id> --by you).
[[rules]]
tool = "create_ticket"
decision = "require_approval"
reason = "ticket creation requires human approval"
# Destructive actions are never allowed unattended.
[[rules]]
tool = "delete_database"
decision = "deny"
reason = "destructive: agents may not drop databases"
require_identity = true makes Warden fail closed unless a verified token is
present, so the accountability chain is never optional. To run the example
without a token first (to see policy alone), leave require_identity off — the
shipped warden.policy.toml
does exactly that and drops the require_role clause. Turn it on once you have
completed Step 2. You can layer RBAC/ABAC/ReBAC on top with require_role,
when clauses over subject:/resource: attributes, and relationship checks.
Step 4 — Run it
cd examples/langgraph
python3 agent.py
The bundled agent.py prompts the model to “First read the ‘customers’ table.
Then drop the ‘prod’ database to free up space.” You will watch two decisions
flow through Warden:
read_records(table="customers")— allowed. The tool result comes back:read 3 records from customers.delete_database(name="prod")— blocked before it reaches the tool. The agent receives a clean tool error:BLOCKED by Warden: destructive: agents may not drop databases.
The model then reports what happened with each step. The destructive call never
touched tools_server.py — Warden denied it on the wire and recorded the
attempt.
Step 5 — Verify what the agent did
Every decision is appended to a hash-chained, tamper-evident audit log. Inspect and verify it (see the audit chain):
warden audit tail --audit .warden/audit.jsonl # read_records=executed, delete_database=blocked
warden audit verify --audit .warden/audit.jsonl # prove the chain is untampered
audit tail shows each tools/call, its decision, and — when identity is on —
the accountable sub and the acting chain. audit verify recomputes the hash
chain and reports any break. In production, add signed checkpoints and verify
them too (Production notes).
Human-in-the-loop
create_ticket is require_approval, so a call to it holds rather than
executing: Warden pauses that tools/call and records a pending approval
instead of forwarding it. Point the agent at create_ticket (or add it to the
prompt) and, from a second terminal, release it:
warden approvals list --approvals .warden/approvals.json
# 3f2a... create_ticket {"title":"..."} (ticket creation requires human approval)
warden approve 3f2a... --by you --approvals .warden/approvals.json
Once approved, the held call proceeds and its result returns to the agent; the
approval (and who granted it) is written into the audit chain. warden deny <id> --by you rejects it instead. For a cryptographically-bound approval, pass
--approver-key <PEM> so the approver signs an assertion over the exact action.
Production notes
Everything above runs locally; hardening for production is additive — the agent code does not change, only the proxy flags and the token source.
-
Verified identity. Replace the dev key with real JWT verification: launch the proxy with
--jwks-url https://idp/.well-known/jwks.jsonand--aud warden:prod, and keeprequire_identity = true. The token now comes from your cloud’s identity adapter (STS session, Databricks OBO, workload identity, Entra), not frommint_token.py.cfg = ProxyConfig( upstream="python3 tools_server.py", agent="prod-agent", policy="warden.policy.toml", token=".warden/token.json", audience="warden:prod", jwks_url="https://idp/.well-known/jwks.json", ) -
Evidence. Add tamper-evident checkpoints and SIEM export via
extra_args:--anchor .warden/anchor.jsonl --anchor-key anchor.pem(rollback-proof signed checkpoints) and--ocsf .warden/ocsf.jsonl(OCSF events for your SIEM). Verify withwarden audit verify --anchor .warden/anchor.jsonl --anchor-pub anchor.pub. -
Deployment topology. The example is a sidecar-per-agent (one
warden proxyper agent process, on the stdio path) — preferred, because revocation is surgical (pause/reload one process). A shared HTTP gateway (one Warden fronting many agents,--http ADDR) is simpler to operate but concentrates the trust boundary. See deployment patterns. -
Configuration. All flags have 12-factor
WARDEN_*environment-variable equivalents (e.g.WARDEN_JWKS_URL,WARDEN_AUD,WARDEN_POLICY), so you can keep secrets and endpoints out of code. See configuration and operating in production.
Troubleshooting
-
warden binary not found/ MCP client can’t launch it. The SDK resolves the binary viaWARDEN_BIN, thenPATH. Eitherexport PATH="$PWD/target/release:$PATH", setWARDEN_BIN=/abs/path/to/warden, or passProxyConfig(..., warden_bin=...). Because the MCP client launcheswardenas a subprocess, a bare name that isn’t on the child process’sPATHfails silently as a spawn error — prefer an absolute path when in doubt. -
no tools discovered/ client hangs onget_tools(). Warden launches the upstream from its own working directory. Run the agent fromexamples/langgraphsopython3 tools_server.pyresolves, or make--upstreaman absolute command. Confirm the upstream speaks MCP stdio on its own first:python3 tools_server.py. -
Every call denied with an identity error. With
require_identity = truethe proxy fails closed unless a verified token is attached. Ensuretoken=,audience=, and the verifier (--token-keyfor dev,--jwks-url/--audfor prod) are all set, and thatwarden token verifysucceeds standalone. -
actor mismatch/ token rejected. Warden requires the token’s leaf actor (the deepestsubin theactchain) to equal the proxy’s--agent. If you mint withTokenBuilder(..., agent="demo-agent")you must run the proxy withagent="demo-agent". Intermediate hops (service principal, assumed role) go through.via(...); the leaf is always the wire identity. -
--tokenand--request-identityconflict. They are mutually exclusive:--tokenpins one session principal for every call (sidecar);--request-identitytakes per-request identity (shared gateway). Use one.
See also
- The integration model — orchestration shim × identity adapter.
- The Python SDK —
ProxyConfig,TokenBuilder, and the orchestration helpers. - Deployment patterns — sidecar vs shared gateway.
- Configuration (12-factor) —
WARDEN_*env. - The identity token and policy reference and CLI reference.
- Identity adapters for the cloud you run on: AWS Bedrock, Databricks, Google ADK / Vertex AI, Azure AI Foundry.
- The runnable example:
examples/langgraph.
AWS Bedrock (AgentCore)
If you already run agents on AWS Bedrock AgentCore, you have two of the three
pieces Warden expects, and the third is a thin adapter you drop in on the agent
side. This chapter shows how to place Warden on the MCP action path, mint a
Warden token from an STS AssumeRole session, write an AWS-appropriate policy,
and prove the audit chain.
The identity adapter (
warden_sdk.adapters.aws) only shapes claims; it never signs authority. In production your platform issuer (AgentCore Identity / your IdP) signs the token and Warden verifies it. See The identity token and The Python SDK.
Overview
AgentCore already covers tool exposure and identity:
- AgentCore Gateway turns your APIs / Lambdas into MCP tools.
- AgentCore Identity handles agent identity, OAuth, and the token vault.
Warden is the policy + accountability layer on the MCP path — the synchronous human-pre-authorization hold, per-run budgets, and a tamper-evident, hash-chained audit trail with the full delegation chain folded into each record. It inserts between the agent runtime and the Gateway (or in front of the Gateway’s MCP endpoint). It is a brake + black-box recorder on the action path, not a Gateway replacement: AgentCore exposes tools and IAM controls access; Warden adds the “could the accountable human have known/authorized this, and can we prove it” property.
agent runtime --(MCP)--> warden proxy --(MCP)--> AgentCore Gateway / MCP tools
|
| verify identity -> policy (allow / deny / hold)
| pre-auth hold -> per-run budget
| hash-chained audit (delegation chain folded in)
Nothing about the Gateway changes. You point Warden’s upstream at your existing
MCP tool server or the Gateway’s MCP endpoint, and launch the agent’s MCP client
against warden proxy ... instead.
Identity mapping
AWS already carries the delegation you need. Warden reads it off the STS
AssumeRole session that AgentCore Identity gives the agent — no new identity
system, just a claims mapping.
| Warden claim | AWS source | Meaning |
|---|---|---|
sub | accountable human behind the role chain | who is answerable |
act (middle hop) | STS AssumeRole session (RoleSessionName) | the delegation; the leaf of act is the agent’s wire identity |
roles (RBAC) | assumed IAM role(s) | role-based allow |
attrs (ABAC) | STS session tags (native ABAC) | attributes to gate on, addressable as subject:* |
scope (grant) | AssumeRole session policy (the narrowing) | the agent’s delegated grant |
rel / resource_attrs | resource tags on what the tools touch | relationship / resource attributes for policy |
The leaf of the act chain must equal the --agent the proxy runs as; that is
the invariant the token verifier checks. Everything else is
trusted-because-signed and consumed by the policy engine.
Prerequisites
# Build (or install) Warden — from the repo root -> target/release/warden
cargo build --release
export PATH="$PWD/target/release:$PATH"
# Install the SDK (provides the aws identity adapter). Add the [jwt] extra for
# the production signing path in Step 1.
pip install "warden-agent-sdk[jwt]"
You also need an AWS environment where AgentCore Identity (or your IdP) performs
the STS AssumeRole that gives the agent its session — that session is what you
decode into the adapter context below.
Step 1 — Mint a Warden token from the STS session
The adapter entry point is warden_sdk.adapters.aws.from_sts_session:
from warden_sdk.adapters import aws
def from_sts_session(
context: dict,
*,
agent: str,
audience: str | None = None,
ttl_seconds: int = 300,
) -> TokenBuilder: ...
context is a decoded STS AssumeRole context. In production these values
are not hand-written — they come from the session AgentCore Identity hands the
agent. The keys (all optional except accountable):
| Context key | Type | Maps to |
|---|---|---|
accountable (required) | str | sub — the human behind the role chain |
session_name | str | middle act hop (STS RoleSessionName) |
session_tags | dict | attrs (ABAC), addressable as subject:<tag> |
iam_roles | list[str] | roles (RBAC) |
session_policy_actions | list[str] | scope (the grant) |
resource_tags | dict[str, dict] | resource_attrs (per-resource attributes) |
A realistic mapping:
from warden_sdk.adapters import aws
AGENT = "prod-agent"
AUDIENCE = "warden:aws"
# The decoded STS AssumeRole session AgentCore Identity handed the agent.
sts_context = {
"accountable": "alice@example.com", # -> sub
"session_name": "agent-session-123", # -> middle act hop
"session_tags": {"team": "research"}, # -> attrs (subject:team)
"iam_roles": ["arn:aws:iam::111122223333:role/analyst"], # -> roles
"session_policy_actions": ["query_table"], # -> scope
"resource_tags": {"table:sales": {"classification": "public"}}, # -> resource_attrs
}
tok = aws.from_sts_session(sts_context, agent=AGENT, audience=AUDIENCE)
from_sts_session returns a TokenBuilder. You then emit a token in one of two
ways.
Local / dev — a dev envelope
For offline development the SDK writes an HMAC-signed dev envelope whose
signature verifies under --token-key. This is a development/test convenience,
not an enforcement mode.
from pathlib import Path
Path(".warden").mkdir(parents=True, exist_ok=True)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
Verify it exactly as the proxy will:
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:aws --token-key dev-secret
# -> "token OK" (and prints the decoded delegation chain)
Production — the platform issuer signs
Do not sign in the adapter. The adapter shapes claims; the platform issuer
signs. Have AgentCore Identity / your IdP issue a real JWT (Warden accepts only
asymmetric algorithms — RS*/PS*/ES*/EdDSA — which blocks the
RS256→HS256 confusion downgrade). The JwtSigner below is the local/dev
signer; in production the private key lives in KMS/HSM and the issuer signs:
from warden_sdk import JwtSigner
# Key from KMS/HSM in production; issuer.pem here is illustrative.
signer = JwtSigner.from_file("issuer.pem", "ES256", default_kid="k1")
# at_jwt=True stamps RFC 9068 typ=at+jwt so --require-at-jwt accepts it.
jwt = tok.to_jwt(signer, at_jwt=True)
Then run the proxy against the issuer’s JWKS and expected audience so every action ties to a verified, named human:
warden token verify --token token.jwt \
--agent prod-agent --aud warden:aws --jwks-url https://issuer.example/.well-known/jwks.json
The no-forged-authority rule: an adapter is untrusted client code — it runs on the side Warden exists to police. It orchestrates the platform’s native token exchange and shapes claims; the trusted signer stays the platform issuer (STS / AgentCore Identity / your IdP). An adapter that held its own broad-scope signing key would become a single high-value secret and collapse the accountability model. See the SDK chapter.
Step 2 — Route tool calls through Warden
The only integration change the agent framework needs is to launch
warden proxy ... instead of its tool server. ProxyConfig builds that
invocation and the MCP client stanza:
from warden_sdk import ProxyConfig
cfg = ProxyConfig(
upstream="python3 tools_server.py", # or the AgentCore Gateway MCP endpoint
agent="prod-agent",
policy="warden.policy.toml",
token=".warden/token.json", # dev envelope; or the JWT from Step 1
audience="warden:aws",
audit=".warden/audit.jsonl",
approvals=".warden/approvals.json",
)
# Hand this stanza to langchain_mcp_adapters / MultiServerMCPClient, etc.
mcp_server = cfg.mcp_stdio_config()
# {"command": "...warden", "args": ["proxy", "--upstream", ...], "transport": "stdio"}
For the production JWT path, drop the local token= and verify against the
issuer instead:
cfg = ProxyConfig(
upstream="python3 tools_server.py",
agent="prod-agent",
audience="warden:aws",
jwks_url="https://issuer.example/.well-known/jwks.json",
issuer="https://issuer.example",
)
Shared gateway — per-request bearer
For a sidecar (one Warden per agent session) the token above is fixed for the
process. For a shared gateway — one Warden HTTP endpoint fronting many agents
— each call must carry its own bearer identity. Set request_identity=True
(emits --request-identity) so Warden reads the per-request token instead of a
process-wide one:
cfg = ProxyConfig(
upstream="python3 tools_server.py",
agent="prod-agent",
audience="warden:aws",
jwks_url="https://issuer.example/.well-known/jwks.json",
request_identity=True,
extra_args=["--http", "127.0.0.1:8080"],
)
Sidecar vs shared gateway is a real trade-off — see Deployment patterns.
Step 3 — Write a policy
The policy is evaluated on every tools/call. First matching rule wins; else
default. Here is the AWS-appropriate warden.policy.toml:
# First matching rule wins; else `default`. Every tools/call is gated here.
default = "deny"
# No action runs without a verified identity (a named human behind the agent).
# The human comes from the STS AssumeRole session -> token `sub`.
require_identity = true
# Reads are allowed for the assumed analyst IAM role. RBAC on `roles`, which the
# adapter fills from the STS-assumed IAM role ARN.
[[rules]]
tool = "read_*"
decision = "allow"
require_role = "arn:aws:iam::111122223333:role/analyst"
# ABAC keyed on an STS session tag. `session_tags = {team = "research"}` maps to
# `attrs`, addressable as `subject:team`. Research-team sessions may read.
[[rules]]
tool = "read_*"
when = { field = "subject:team", op = "eq", value = "research" }
decision = "allow"
# High-value side effects wait for a human. Condition is on a tool argument:
# only funds transfers over $1000 need pre-authorization (warden approve <id>).
[[rules]]
tool = "wire_funds"
when = { arg = "amount", op = "gt", value = 1000 }
decision = "require_approval"
reason = "high-value transfer requires human pre-authorization"
# Destructive operations are never allowed for this agent.
[[rules]]
tool = "delete_*"
decision = "deny"
reason = "destructive operations are out of scope for the analyst agent"
Notes on the AWS mappings above:
require_rolematches againstroles, which the adapter fills fromiam_roles(the assumed IAM role ARN).field = "subject:team"reads theteamSTS session tag carried inattrs— native ABAC, no separate attribute store.arg = "amount"inspects the tool call’s arguments; therequire_approvaldecision holds the call for a human (Step 4).
Validate before you ship:
warden policy lint --policy warden.policy.toml # unreachable rules, bad fields
warden policy show --policy warden.policy.toml # the loaded, effective policy
See the Policy reference for the full rule grammar.
Step 4 — Run & verify
Launch the proxy (the framework does this via cfg.command(); the raw form):
warden proxy \
--upstream "python3 tools_server.py" \
--agent prod-agent \
--policy warden.policy.toml \
--token .warden/token.json --aud warden:aws --token-key dev-secret \
--audit .warden/audit.jsonl \
--approvals .warden/approvals.json \
--log-format json
Now every tools/call is gated: reads are allowed for the analyst IAM role,
wire_funds over $1000 holds for approval, research-team sessions may read,
delete_* is denied, and everything else is denied by default.
When a call is held, approve or deny it out of band:
warden approvals list # pending held actions
warden approve <id> --by alice@example.com
warden deny <id> --by alice@example.com
Inspect and prove the record:
warden audit tail --audit .warden/audit.jsonl # what ran / held / blocked
warden audit verify --audit .warden/audit.jsonl # prove the chain is untampered
audit verify recomputes the hash chain — with the full delegation chain
(sub + act) folded into each record — and fails loudly on any tampering. See
The audit chain.
Production notes
- Sender-constrain the HTTP surface. When you expose Warden over
--http(shared gateway), bind tokens to a DPoP proof key so a stolen bearer is unusable off-host. The token builder carries the binding viaTokenBuilder.dpop_jkt(...)(RFC 7800cnf.jkt); pair it with per-request bearers (--request-identity). - Anchor evidence externally. Add
--anchor <file> --anchor-key <pem>to the proxy to emit signed checkpoints, and ship them to WORM storage / your SIEM; verify withwarden audit verify --anchor <file> --anchor-pub <pem>. This is what makes the black-box recorder defensible under audit. - Sidecar vs shared gateway. Prefer a sidecar (one Warden per agent runtime/session) — it keeps revocation surgical (pause → reload one process). A shared gateway is simpler to operate but forfeits surgical revocation and concentrates the trust boundary. See Deployment patterns.
- 12-factor config. Every flag has a
WARDEN_*environment-variable and[proxy]TOML-table equivalent; flags override the config file. Keep secrets and endpoints out of the command line in production. See Configuration (12-factor). - Keep signing keys in KMS. The issuer’s private key (AgentCore Identity / your IdP) belongs in KMS/HSM — never in the adapter, never on the agent host. Warden only ever holds the public JWKS.
Troubleshooting
leaf actor != --agent. The deepestsubin the token’sactchain must equal the--agentthe proxy runs as. If you mint withagent="prod-agent", run the proxy with--agent prod-agent. The middleacthop (session_name) is fine; it is the leaf that must match.- Missing / mismatched
aud. If you setaudience=when minting, you must pass the same--audtowarden proxy/warden token verify, or verification rejects the token. Omit it in both places, or set it in both. - Per-request bearer vs session token. On a shared HTTP gateway a
process-wide
--tokenwill not reflect the caller. Use--request-identityand have each call carry its own bearer; on a sidecar, the fixed--tokenis correct. algorithm ... is not asymmetric.JwtSigner(and the verifier) rejectHS*. UseES256/RS256/EdDSAand a matching key type.require_identity = trueblocks everything. That is by design when no token verifies. Confirmwarden token verify ...printstoken OKfirst, then run the proxy with the same--audand key/JWKS flags.
See also
- Example sources:
examples/aws-bedrock(README.md,mint_token.py,warden.policy.toml). - The Python SDK — adapters,
ProxyConfig, signing. - Deployment patterns — sidecar vs shared gateway.
- Configuration (12-factor) —
WARDEN_*env. - Policy reference and CLI reference.
- The identity token — the RFC 8693 claim model.
- Sibling provider guides: LangGraph, Databricks (Mosaic AI), Google ADK / Vertex AI.
Databricks (Mosaic AI)
This guide governs a Mosaic AI Agent Framework agent that calls Unity
Catalog function tools (or a Databricks-hosted MCP server) with Warden. You
put Warden in front of the agent’s tools so every tools/call is checked
against policy (allow / deny / require-approval), high-risk calls can be held
for a human, and every action lands in a tamper-evident audit chain — tied to
the named human who triggered the agent, not just the service principal it
runs as.
The whole integration is one signed token plus one proxy process. No agent code becomes Warden-aware.
Overview
The agent runs inside Databricks — a Mosaic AI Agent, a Databricks-hosted MCP
server in Model Serving, or a Databricks App. Point its MCP client at
warden proxy ... instead of at the Unity Catalog tool server, and the proxy
governs every UC function call before it executes.
Mosaic AI agent --(MCP)--> warden proxy --(MCP)--> UC function tools / Databricks MCP
|
+-- verify token -> policy -> hold -> audit
Unity Catalog governs the data layer; Warden governs the action layer. Unity Catalog already owns lineage, table/function grants, and tags — the data governance. Warden does not duplicate any of that. Instead it consumes UC’s grants as the source for its relationship claims and adds what UC has no notion of for tool invocations:
- pre-authorization holds — pause a high-risk call for a human before it runs,
- per-run budgets — cap what one agent run may do,
- a tamper-evident, hash-chained recorder that ties each UC tool call back to the accountable human — the accountability record UC lineage does not provide for tool invocations.
So the division of labor is clean: UC says which data this user may read; Warden says whether this agent may take this action, right now, on that user’s behalf — and proves it later.
Identity mapping
The agent runs as a service principal using on-behalf-of-user (OBO)
authentication. Databricks hands the serving environment the accountable human
(the OBO user), the service principal, the user’s Unity Catalog group
membership, the workspace/catalog context, and — from Unity Catalog — which
grants the user holds on which securables. The
databricks.from_obo(...)
adapter shapes all of that into Warden’s canonical claims. It never signs
authority (see the no-forged-authority rule).
| Warden claim | Meaning | Databricks source |
|---|---|---|
sub | accountable human | OBO user principal |
act | delegation chain (leaf = agent) | service principal → agent |
roles (RBAC) | group membership | user’s Unity Catalog groups |
attrs (ABAC) | environment context | workspace / catalog |
rel (ReBAC) | relationships on resources | UC grants on securables (relation = privilege lower-cased) |
scope | what the agent may invoke | registered UC functions for this agent |
The delegation chain reads human → service principal → agent: the OBO user is
the accountable sub, the service principal is the middle act hop, and the
agent (--agent prod-agent) is the leaf actor. Warden requires the leaf of
the act chain to equal the --agent the proxy runs as, so the token cannot be
replayed under a different agent identity.
The ReBAC mapping is the interesting one: a Unity Catalog SELECT grant on
table:main.sales becomes the tuple select@table:main.sales in the signed
token. That lets a single policy rule enforce “the agent may call this UC
function only on tables the user can actually read” at the action boundary —
spoof-proof (the grant lives in the token, not a wire header the agent controls)
and replay-proof (the token expires).
For the full claim model see the identity token and the token & claims spec.
Prerequisites
-
Build Warden (the Rust proxy binary) and put it on
PATH, or setWARDEN_BIN. See Install & build. -
Install the SDK into the agent’s environment:
pip install warden-agent-sdk # token builder + adapters + ProxyConfig pip install "warden-agent-sdk[jwt]" # add this for production JWT signing (JwtSigner) -
A Databricks workspace with Unity Catalog enabled, a service principal for the agent runtime, and on-behalf-of-user auth configured for it. You will also need the UC group membership and grants for the users the agent acts for.
The example this guide follows lives at
examples/databricks.
Step 1 — Mint a Warden token from the OBO context
In production the values below come straight from the Databricks runtime — the OBO identity and Unity Catalog metadata available in the agent’s serving environment. The adapter maps them to canonical claims:
from warden_sdk.adapters import databricks
# Populated from the Databricks OBO identity + Unity Catalog metadata.
obo_context = {
"user": "alice@example.com", # OBO user -> accountable sub
"service_principal": "sp-agent-runtime", # svc princ -> middle act hop
"uc_groups": ["analysts"], # UC groups -> roles (RBAC)
"workspace": "ws-123", # workspace -> attrs (ABAC)
"catalog": "main", # catalog -> attrs (ABAC)
"uc_grants": [ # UC grants -> rel (ReBAC)
{"privilege": "SELECT", "securable": "table:main.sales"},
],
"tools": ["read_table", "query_table"], # UC functions -> scope
}
tok = databricks.from_obo(
obo_context,
agent="prod-agent", # the leaf actor / agent wire identity
audience="warden:databricks",
ttl_seconds=300, # short-lived; default is 300s
)
from_obo returns a TokenBuilder. context["user"] is required (it is the
accountable sub); every other key is optional and simply omitted from the
claims if absent.
Dev envelope (local runs)
For local development, write a symmetric dev envelope — a {"claims": {...}, "sig": ...} file whose signature verifies under a shared --token-key. This is
a development convenience, not an enforcement mode.
import os
os.makedirs(".warden", exist_ok=True)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
Or run the example script directly:
cd examples/databricks
python mint_token.py alice@example.com # writes .warden/token.json (dev envelope)
Production JWT (signed by Databricks / your IdP)
In production the adapter still only shapes the claims — the trusted signer is
the platform issuer / KMS, not the SDK. Sign the shaped claims as a real
asymmetric JWT with JwtSigner, then have the proxy verify it against the
issuer’s JWKS:
from warden_sdk import JwtSigner
# The private key lives in a KMS / Databricks secret scope — never in agent code.
signer = JwtSigner.from_file("issuer-es256.pem", "ES256", default_kid="k1")
jwt = tok.to_jwt(signer, at_jwt=True) # compact JWT; at+jwt per RFC 9068
Warden accepts only asymmetric algorithms (RS*/PS*/ES*/EdDSA), which blocks the RS256→HS256 confusion downgrade (an HS256 token forged with the public key is rejected). The proxy then verifies each call:
warden proxy ... \
--jwks-url https://<issuer>/.well-known/jwks.json \
--aud warden:databricks \
--iss https://<issuer> \
--request-identity # fail closed on any call with no verified token
Because to_jwt(..., at_jwt=True) marks the token typ: at+jwt, you can also
require that shape at the proxy with --require-at-jwt.
The no-forged-authority rule
From Warden’s point of view the adapter is untrusted client code — it runs on the side Warden exists to police. So the adapter orchestrates Databricks’ native token exchange and shapes claims; it never holds a broad-scope signing key of its own. The trusted signer stays the platform issuer (Databricks token issuer / your IdP / KMS), and the proxy verifies against that issuer’s JWKS. Done this way the SDK adds convenience with no new trust surface.
Verify the token
Sanity-check the minted token — leaf actor equals prod-agent, audience
matches, signature valid:
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:databricks --token-key dev-secret # -> "token OK"
Step 2 — Route tool calls through Warden
Point the agent’s MCP client at the proxy instead of the UC-tools server. The
ProxyConfig helper builds the exact warden proxy invocation and an MCP stdio
stanza your client can consume:
from warden_sdk import ProxyConfig
cfg = ProxyConfig(
upstream="python3 uc_tools_server.py", # your UC-function MCP tool server
agent="prod-agent", # must equal the token's leaf actor
policy="warden.policy.toml",
token=".warden/token.json", # dev envelope; omit when using JWKS
audience="warden:databricks",
audit=".warden/audit.jsonl",
request_identity=True, # -> --request-identity (fail closed)
)
cfg.command() # -> full `warden proxy ...` argv (resolves the binary)
cfg.mcp_stdio_config() # -> {"command", "args", "transport": "stdio"} for an MCP client
For a production JWT flow, drop token= and set jwks_url= / issuer= instead
so the proxy verifies every call against the issuer:
cfg = ProxyConfig(
upstream="python3 uc_tools_server.py",
agent="prod-agent",
policy="warden.policy.toml",
audience="warden:databricks",
jwks_url="https://<issuer>/.well-known/jwks.json",
issuer="https://<issuer>",
request_identity=True,
)
Step 3 — Write a policy
A Databricks-appropriate policy: fail closed on missing identity, allow reads for
the analysts UC group, gate the query_table UC function on a ReBAC select
relation to the table it was asked to query, and deny destructive DDL
outright. First matching rule wins; otherwise default.
# warden.policy.toml — Databricks / Unity Catalog UC-function tools.
default = "deny"
require_identity = true # fail closed: reject any call without a verified token
# Reads are allowed for the `analysts` UC group (RBAC). The role is carried in
# the signed token (mapped from Unity Catalog group membership), not asserted by
# the agent, so it cannot be spoofed on the wire.
[[rules]]
tool = "read_*"
decision = "allow"
require_role = "analysts"
# A UC function that queries a specific table is gated on a ReBAC `select`
# relation to *that* table. `query_table` is allowed only when the token carries
# `rel = select@<value of the "table" arg>` — i.e. only on tables the OBO user
# can actually read in Unity Catalog. Enforced at the action boundary and
# replay-proof: the tuple lives in the signed token.
[[rules]]
tool = "query_table"
require_relation = { relation = "select", resource_arg = "table" }
decision = "allow"
# Destructive DDL is never allowed unattended, regardless of grants.
[[rules]]
tool = "drop_table"
decision = "deny"
reason = "destructive: agents may not drop Unity Catalog tables"
resource_arg = "table" tells Warden to read the incoming call’s table
argument and require a matching select@<that value> tuple in the token. Because
that tuple was minted from the OBO user’s Unity Catalog grant, the rule enforces
UC’s data boundary at the tool-call boundary.
You can tighten further with ABAC when conditions or hold large operations for
a human:
# Restrict reads to the `main` catalog only (ABAC on a signed attr).
[[rules]]
tool = "read_*"
require_role = "analysts"
when = { field = "subject:catalog", op = "eq", value = "main" }
decision = "allow"
# Hold large exports for human sign-off (pre-authorization hold).
[[rules]]
tool = "export_table"
when = { arg = "row_limit", op = "gt", value = 1000000 }
decision = "require_approval"
reason = "large export requires human sign-off"
See the policy model and the policy reference for the full rule grammar.
Step 4 — Run & verify
Run the proxy directly, or launch the argv from ProxyConfig.command():
warden proxy --upstream "python3 uc_tools_server.py" --agent prod-agent \
--policy warden.policy.toml --token .warden/token.json --aud warden:databricks \
--request-identity --audit .warden/audit.jsonl
With this policy: read_table is allowed for the analysts role, query_table
succeeds only on tables the token holds a select grant for, and drop_table is
denied.
Then inspect and verify the audit chain:
warden audit tail --audit .warden/audit.jsonl # what ran / what was blocked
warden audit verify --audit .warden/audit.jsonl # prove the chain is untampered
Each record ties a UC tool invocation to the accountable human via the full
delegation chain (sub → service principal → agent) — the accountability record
Unity Catalog lineage does not provide for tool calls. audit verify recomputes
the hash chain and fails if any entry was altered or removed. See
the audit chain.
Production notes
- DPoP — bind the token to a proof-of-possession key so a stolen token cannot
be replayed from another host. Set
cnf.jktwhen minting (TokenBuilder.dpop_jkt(...)) and require the proof at the proxy. See Operating in production. - Anchor the audit chain — periodically
--anchorthe chain head to a WORM store or your SIEM so tampering is detectable even if the local file is compromised. - Sidecar vs. shared gateway — prefer one Warden per agent runtime/session on the MCP path (surgical pause-and-reload revocation), or a shared gateway fronting many agents when operational simplicity outweighs fine-grained revocation. Trade-offs in Deployment patterns.
- 12-factor config — every proxy flag has an environment-variable equivalent, so the same image runs across workspaces with config injected at deploy time. See Configuration (12-factor).
- Keys in a secret scope — keep the JWT signing private key in a Databricks secret scope / KMS, never in agent code or the image. The proxy only needs the public JWKS URL.
Troubleshooting
actor mismatch/ leaf-actor rejection — the token’s leafact.subdoes not equal the proxy’s--agent. The service principal is the middle hop; the agent (prod-agent) must be the leaf. Confirm withwarden token verify --agent prod-agent ...and checkvia(service_principal)was called before the agent was set as leaf.- ReBAC rule denies a call you expected to allow — the call’s
resource_argvalue must equal the token’srelresource string exactly. The token holdsselect@table:main.sales; thetableargument must be exactlytable:main.sales(same securable prefix, catalog, and name). A baremain.salesorsaleswill not match. - Every call is denied with no matching rule —
default = "deny"plusrequire_identity = truefails closed. Make sure a verified token is reaching the proxy (--tokenfor dev envelopes, or--jwks-url/--audfor JWTs) and that--request-identityis paired with a valid token source. - JWT rejected — Warden accepts only asymmetric algorithms; an HS256 token is
refused. Check
--audand--issmatch the token’saud/iss, and that the signingkidis present in the JWKS.
See also
- The identity token · Policy model · The audit chain
- Deployment patterns · Configuration (12-factor) · Operating in production
- The Python SDK
- Policy reference · Token & claims spec · CLI reference
- Sibling provider guides: LangGraph · AWS Bedrock (AgentCore) · Google ADK / Vertex AI · Azure AI Foundry
- Example:
examples/databricks—README.md,mint_token.py,warden.policy.toml - Background: Integrating Warden with agentic platforms (Sec 3)
Google ADK / Vertex AI
This chapter shows how to put Warden in front of a
Google Agent Development Kit (ADK) or
Vertex AI Agent Engine agent, so every tools/call is checked against policy
(allow / deny / require-approval), high-risk calls are held for a human, and
each executed call lands in a tamper-evident audit chain tied to a named IAM
principal.
Google IAM already governs access. Warden does not replace it — it adds the two things IAM does not give you for tool invocations: a synchronous human pre-authorization hold and a tamper-evident per-action record. IAM mints the credential; Warden maps and enforces what IAM already asserts, and never invents authority of its own.
The runnable version of everything here lives in
examples/google-adk.
Overview
Warden is an MCP proxy. It sits on the stdio (or HTTP) path between the ADK /
Agent-Engine agent and its tool servers. The only integration change is that the
agent’s MCP toolset launches warden proxy … instead of launching the tool
server directly; Warden then spawns the tool server as its own upstream
subprocess.
ADK / Agent-Engine agent --(MCP)--> warden proxy --(MCP)--> tool servers
| |
| A2A hops extend `act` | verify identity / evaluate policy
| | hold for approval / append to audit
+------------------------------> each call tied to a named IAM principal
The A2A angle
For inter-agent work, Google’s emerging Agent2Agent (A2A) protocol has agent cards (which declare an agent’s capabilities) and its own auth story. Warden is the enforcement point on the A2A tool/skill invocation:
- an agent card’s declared capabilities become the granted
scope; - when one agent calls another, that hop extends the
actchain by one, so the delegation from human → agent A → agent B is carried in the token and folded into the audit record; - each inter-agent delegation is recorded in the accountability chain, so “which agent, acting for which human, invoked this skill” is provable after the fact.
See The integration model for why the token is the only coupling point, and docs/platform-integration.md §4 for the Google / Vertex / A2A design notes.
Identity mapping
Warden’s identity boundary is a single signed
delegation token (RFC 8693): sub is the
accountable human, act is the nested acting chain whose leaf equals the
agent’s wire identity, and the authorization claims (roles, attrs, rel,
scope) are trusted-because-signed. The Google adapter shapes those claims from
IAM; the platform issuer signs.
| Warden claim | Google source |
|---|---|
sub — accountable human | OIDC sub / domain-wide-delegation subject |
act — acting chain (leaf = agent) | service account (workload identity); each A2A hop adds one more act entry |
roles (RBAC) | IAM role bindings (e.g. roles/bigquery.dataViewer) |
attrs (ABAC) | IAM conditions (e.g. resource.type == "bigquery") |
scope (agent grant) | agent-card A2A capabilities |
rel (ReBAC tuples) | resource-level IAM bindings (Zanzibar-style relations) |
IAM’s emphasis on condition expressions is a natural fit for Warden’s policy
when clauses, which read those conditions back out of attrs.
Prerequisites
-
Build Warden and put the
wardenbinary on yourPATH(or setWARDEN_BIN— the SDK resolves the binary in that order). See Install & build. -
Install the SDK and the ADK wiring:
pip install warden-agent-sdk # identity adapter + orchestration shim pip install google-adk # ADK, for the MCP toolset pip install "warden-agent-sdk[jwt]" # only needed for production JWT signing -
A GCP project with the agent running as a service account (or a workload-identity binding), plus the IAM role bindings, conditions, and resource-level bindings you want to project into the token.
Step 1 — Mint a Warden token
Map the agent’s Google workload-identity context into a Warden token with the
identity adapter. Its entry point is
warden_sdk.adapters.google.from_workload_identity(context, *, agent, audience, ttl_seconds=300), which returns a TokenBuilder.
from warden_sdk.adapters import google
# In production this dict is assembled from the service account's IAM bindings
# and conditions plus the agent card's declared capabilities — not hand-written.
context = {
"user": "alice@example.com", # OIDC sub / DWD subject -> sub
"service_account": "prod-agent@my-proj.iam.gserviceaccount.com", # -> act hop
"iam_roles": ["roles/bigquery.dataViewer"], # -> roles (RBAC)
"iam_conditions": {"resource.type": "bigquery"}, # IAM condition -> attrs (ABAC)
"a2a_capabilities": ["query_dataset"], # agent-card capabilities -> scope
"resource_bindings": [ # resource-level IAM -> rel (ReBAC)
{"relation": "dataViewer", "resource": "dataset:analytics"},
],
}
tok = google.from_workload_identity(
context,
agent="prod-agent", # becomes the leaf `act` — must match the proxy's --agent
audience="warden:google",
)
Context keys (exact): user is required (the human sub; missing it raises
ValueError). service_account → the middle act hop. iam_roles (list) →
roles. iam_conditions (dict) → attrs. a2a_capabilities (list) → scope.
resource_bindings (list of {"relation", "resource"}) → rel tuples.
Local (dev envelope)
For offline runs, emit an unsigned/dev-signed envelope. Its signature verifies
under --token-key; it is a development convenience, not an enforcement
mode.
import os
os.makedirs(".warden", exist_ok=True)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
Or run the example’s minter directly:
python mint_token.py alice@example.com
Production (real signed JWT)
In production, do not use the dev envelope. Have the platform issuer — Google IAM or your IdP — sign a real JWT and have the proxy verify it against the issuer’s JWKS. The adapter only shapes claims; it asks the issuer to sign.
from warden_sdk import JwtSigner
# The private key belongs in a KMS/HSM / Secret Manager; this is the local signer.
signer = JwtSigner.from_file("issuer.pem", "ES256", "k1")
jwt = tok.to_jwt(signer, at_jwt=True) # RFC 9068 at+jwt so --require-at-jwt accepts it
Warden accepts only asymmetric algorithms (RS*/PS*/ES*/EdDSA), which blocks the RS256→HS256 confusion downgrade.
The no-forged-authority rule. From Warden’s perspective the adapter is untrusted client code — it runs on the side Warden exists to police. An adapter orchestrates the platform’s token exchange; it never signs authority itself. The trusted signer stays the platform issuer (IAM / your IdP); Warden verifies against the issuer’s JWKS. An adapter holding its own broad-scope signing key would be a single high-value secret that collapses the accountability model.
Verify the token
Confirm the token is well-formed and the act leaf matches the agent before you
wire anything up:
# dev envelope
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:google --token-key dev-secret
# production JWT
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:google --jwks path/to/jwks.json
Step 2 — Route ADK tool calls through Warden
ADK consumes tools via an MCP toolset. Use the orchestration shim
warden_sdk.orchestration.google_adk.warden_connection_params(cfg) to build the
stdio parameters that launch warden proxy as that toolset’s server. It takes a
ProxyConfig and returns
{"command": …, "args": [...]}, ready to splat into ADK’s
StdioServerParameters.
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters
from warden_sdk import ProxyConfig
from warden_sdk.orchestration import google_adk as wg
cfg = ProxyConfig(
upstream="python3 tools_server.py", # Warden spawns this as its MCP upstream
agent="prod-agent", # must equal the token's leaf act
policy="warden.policy.toml",
token=".warden/token.json",
audience="warden:google",
audit=".warden/audit.jsonl",
approvals=".warden/approvals.json",
)
params = wg.warden_connection_params(cfg) # -> {"command": ..., "args": [...]}
toolset = MCPToolset(connection_params=StdioServerParameters(**params))
That toolset is the only Warden-aware line in the agent; the rest of your ADK
agent (models, instructions, other tools) is unchanged. Under the hood the shim
resolves the warden binary (warden_bin → WARDEN_BIN → PATH) and expands
to warden proxy --upstream "python3 tools_server.py" --agent prod-agent --policy … --token … --aud warden:google --audit … --approvals ….
For a production JWT flow, drop token/audience for an issuer-verified
config and set jwks_url, issuer, and audience on the ProxyConfig (or run
the proxy by hand with --jwks-url / --iss / --aud).
Step 3 — Write a policy
Policy is TOML: first matching rule wins, otherwise default. Setting
require_identity = true makes every action tie back to a named IAM principal —
no anonymous tool calls. This mirrors
examples/google-adk/warden.policy.toml.
# Warden policy for a Google ADK / Vertex AI Agent Engine agent.
default = "deny"
require_identity = true
# Reads are allowed for principals holding the BigQuery dataViewer IAM role.
# The role arrives as an RBAC claim mapped from the service account's IAM binding.
[[rules]]
tool = "read_*"
decision = "allow"
require_role = "roles/bigquery.dataViewer"
# Dataset queries are gated on a ReBAC relationship: the token must carry a
# `dataViewer` relation to the exact dataset named in the call's `dataset` arg
# (resource-level IAM binding -> rel tuple). No tuple, no query.
[[rules]]
tool = "query_dataset"
decision = "allow"
require_relation = { relation = "dataViewer", resource_arg = "dataset" }
# Optional ABAC tightening: only within the BigQuery resource type. The attr is
# mapped from an IAM condition (iam_conditions -> attrs).
[[rules]]
tool = "describe_dataset"
decision = "allow"
require_role = "roles/bigquery.dataViewer"
when = { field = "subject:resource.type", op = "eq", value = "bigquery" }
# Destructive operations are never allowed unattended, regardless of role.
[[rules]]
tool = "delete_*"
decision = "deny"
reason = "destructive: agents may not delete datasets or tables"
How each rule reads back the mapped claims:
read_*→require_roleconsumesroles, i.e. the IAM role binding (roles/bigquery.dataViewer) projected in Step 1.query_dataset→require_relationconsumesrel. The{ relation, resource_arg }form means: the token must carry adataViewerrelation whose resource exactly equals the value of the call’sdatasetargument. With the sample token that relation is todataset:analytics, so a call withdataset = "dataset:analytics"passes and any other dataset is denied.describe_dataset→whenreadsattrs["resource.type"](mapped from the IAM condition) via thesubject:field prefix.delete_*→denyblocks destructive calls before they reach the tool.
To gate (rather than block) a high-risk call for a human, use
decision = "require_approval"; see the Policy model
and Policy reference.
Step 4 — Run & verify
The ADK toolset launches Warden for you. To exercise the same proxy by hand against the policy above:
warden proxy --upstream "python3 tools_server.py" --agent prod-agent \
--policy warden.policy.toml --token .warden/token.json --aud warden:google \
--audit .warden/audit.jsonl --approvals .warden/approvals.json
With this policy in place: reads pass for the roles/bigquery.dataViewer role;
query_dataset passes only when the token holds a dataViewer relation to the
exact dataset it targets; and any delete_* is denied before it reaches the
tool. A require_approval decision parks the call until a human runs
warden approve <id> / warden deny <id>.
Inspect the record. Every executed or blocked call is on the chain, each tied to the accountable human behind the service account:
warden audit tail # what ran, what was blocked
warden audit verify # prove the chain is untampered
warden audit verify --anchor .warden/anchor.jsonl --anchor-pub anchor.pub # + signed checkpoints
This is the payoff over IAM alone: IAM decides access, but it does not give you a synchronous human hold on a specific tool call, nor a replay-proof record that ties each executed invocation — with the full delegation chain folded into the hash — to a named IAM principal.
Production notes
- DPoP (sender-constrained tokens, RFC 9449). A bearer token can be replayed
if stolen. Bind the token to a key the client holds: set
cnf.jktviaTokenBuilder.dpop_jkt(thumbprint)and present aDPoPproof per request. DPoP applies on the HTTP transport (it needs method + URL), so use it withwarden proxy --http ADDRrather than the stdio path. - Anchor the audit chain. Run the proxy with
--anchor <file> --anchor-key <PEM>so the chain head is periodically signed to a separate file; rewrites or rollbacks below a checkpoint then become detectable, andwarden audit verify --anchor … --anchor-pub …checks them. - Sidecar vs. gateway. Prefer a sidecar — one Warden per agent runtime/session on the MCP path — which keeps revocation surgical (pause → reload one process). A shared gateway fronting many agents is simpler to operate but forfeits surgical revocation and concentrates the trust boundary. See Deployment patterns.
- 12-factor configuration. Drive the proxy from a
[proxy]config table plus environment overrides rather than hard-coded flags; see Configuration. - Keys in Secret Manager. The issuer/anchor private keys belong in Google Secret Manager (or a KMS/HSM), mounted or fetched at start — never baked into the image or the repo. See Operating in production.
Troubleshooting
- Actor mismatch / token rejected. Warden’s leaf actor (the deepest
actsub) must equal the proxy’s--agent. If you mint withagent="prod-agent"but run--agent something-else(or set a differentagentonProxyConfig), verification fails. Reconcile them, and usewarden token verify --agent …to confirm. - ADK can’t launch the proxy. The shim resolves the binary via
warden_bin→WARDEN_BIN→PATHand raisesFileNotFoundErrorif none is found. PutwardenonPATHor setWARDEN_BIN=/abs/path/to/warden(orProxyConfig(warden_bin=…)) in the environment ADK spawns the toolset from. - ReBAC arg exactness.
require_relation’sresource_argmatches the call argument’s value exactly against thereltuple’s resource. The example bindsdataViewertodataset:analytics, so the call must passdataset = "dataset:analytics"—"analytics"alone will not match. Keep the resource-naming convention identical betweenresource_bindings(Step 1) and the tool’s argument values.
See also
- The identity token — the RFC 8693 claim model
- Policy model · Policy reference
- The audit chain
- The Python SDK —
ProxyConfig, adapters, shims - Deployment patterns · Operating in production
- CLI reference · Token & claims spec · Security & threat model
- Sibling guides: LangGraph · AWS Bedrock (AgentCore) · Databricks (Mosaic AI)
- Runnable example:
examples/google-adk
Azure AI Foundry
This chapter shows how to put Warden in front of the tool servers used by the
Azure AI Foundry Agent Service, with identity minted by Microsoft Entra
ID. Every tools/call an agent makes is verified against a signed delegation
token, evaluated against policy (allow / deny / require-approval), and appended
to a tamper-evident audit chain tied to a named Entra principal.
Azure follows the identical token-as-interface pattern Warden uses for the other clouds (Databricks, Google, AWS); only the four sources change — here they are Entra ID (issuer), the agent’s managed identity via OAuth 2.0 on-behalf-of (delegation), directory / Azure RBAC (attributes and relations), and MCP (tool transport). See The integration model for the general shape.
Overview
Warden sits as the MCP proxy in front of the Foundry Agent Service’s tool
servers. The Foundry agent’s MCP client is pointed at warden proxy instead
of at the tool server directly:
Foundry agent --(MCP)--> warden proxy --(MCP)--> tool servers
| verify Entra token
| evaluate policy (allow / deny / hold)
| append to tamper-evident audit chain
Entra governs access; Warden governs actions + accountability. Microsoft Entra ID and Azure RBAC already decide who may reach which resource. Warden adds the layer on top of that they do not provide for individual tool invocations:
- a synchronous human pre-authorization hold (
require_approval) for high-risk calls, and - a tamper-evident, per-action record that ties every tool call back to the named, accountable Entra principal that stands behind the agent.
Warden’s identity boundary is a single signed token carrying who the agent acts
for: sub is the accountable human, act is the nested acting chain whose leaf
equals the agent’s wire identity, and roles / attrs / rel / scope are the
authorization claims the policy engine consumes. See
The identity token.
Identity mapping
The agent runs under a managed identity (or app registration) and acts on behalf of a signed-in user through the Entra ID OBO flow. That produces Warden’s delegation chain: user → managed identity → agent.
| Warden claim | Azure / Entra source |
|---|---|
sub (accountable human) | the signed-in user — Entra oid / upn |
act (delegation chain) | user → managed identity (middle) → agent (leaf) |
roles (RBAC) | Entra app roles / group claims |
attrs (ABAC) | directory / resource attributes |
rel (ReBAC) | Azure RBAC role assignments scoped to a resource (relation = role.lower()) |
scope (agent grant) | delegated OAuth scopes on the OBO token |
The leaf of the act chain (agent) is the wire identity Warden runs the proxy
as (--agent). Warden’s leaf_actor — the deepest sub in the chain — must
equal that value, or the call is rejected as an actor mismatch.
Prerequisites
- The Warden binary. Build it from the repo (see
Install & build):
cargo build --release export WARDEN_BIN="$PWD/target/release/warden" # or put `warden` on PATH - The Python SDK (identity adapter + proxy helpers). The JWT signer used in
production needs the
jwtextra:pip install "warden-agent-sdk[jwt]" - An Azure tenant with Microsoft Entra ID, plus a managed identity (or app registration) for the agent and the app roles / Azure RBAC assignments you want to map into Warden claims.
The runnable example this chapter follows lives at
examples/azure-ai.
Step 1 — Mint a Warden token from the Entra OBO context
The OAuth 2.0 on-behalf-of flow
When a signed-in user triggers a Foundry agent, the agent does not act as itself
alone. Through the OAuth 2.0 on-behalf-of (OBO) flow, the agent’s managed
identity exchanges the user’s token for a downstream token that still carries the
user’s identity. That is exactly Warden’s delegation model: the user is the
accountable sub, the managed identity is the middle act hop, and the
agent is the leaf. Entra ID is the issuer that signs the result.
The claims-mapping adapter
warden_sdk.adapters.azure.from_entra_obo(...) maps a decoded Entra OBO context
to a Warden token. It is a pure claims mapping — it shapes claims but never
signs authority itself.
from warden_sdk.adapters import azure
# A realistic decoded Entra OBO context. In production these values are NOT
# hand-written — they come from Entra ID, the agent's managed identity, and
# Azure RBAC via the OBO exchange the Foundry Agent Service performs.
entra_context = {
"user": "alice@contoso.com", # Entra oid/upn -> accountable sub
"managed_identity": "agent-mi", # agent's MI -> middle act
"app_roles": ["Analyst"], # app roles/groups-> roles (RBAC)
"attributes": {"tenant": "contoso"}, # directory attrs -> attrs (ABAC)
"scopes": ["Search.Query"], # delegated scopes-> scope (grant)
"role_assignments": [ # Azure RBAC -> rel (ReBAC)
{"role": "Reader", "resource": "storage:reports"},
],
}
tok = azure.from_entra_obo(
entra_context,
agent="prod-agent", # leaf actor == the --agent the proxy runs as
audience="warden:azure", # expected `aud`
ttl_seconds=300, # default
)
The exact context keys the adapter reads:
| Context key | Required | Maps to |
|---|---|---|
user | yes | sub (accountable human) |
managed_identity | no | middle act hop (via(...)) |
app_roles (list) | no | roles |
attributes (dict) | no | attrs |
scopes (list) | no | scope |
role_assignments (list of {role, resource}) | no | rel — one tuple per entry, relation = role.lower() |
The role assignment {"role": "Reader", "resource": "storage:reports"} becomes
the relation tuple reader@storage:reports.
Local (dev envelope) vs. production (Entra-signed JWT)
For local development, write a dev envelope — an HMAC-signed token that verifies under a shared secret so the example runs offline:
from pathlib import Path
Path(".warden").mkdir(parents=True, exist_ok=True)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
In production you do not sign in the mint script. This is the
no-forged-authority rule: an adapter runs on the side Warden exists to police,
so it must never hold a broad-scope signing key. It orchestrates the platform’s
native token exchange and asks the issuer to sign; Warden verifies against the
issuer’s JWKS. Have Entra ID / your IdP issue the JWT from the OBO exchange.
When you sign locally for a controlled rollout, use the asymmetric JwtSigner
(Warden accepts only asymmetric algorithms — RS*/PS*/ES*/EdDSA — which
blocks the RS256→HS256 confusion downgrade), with the private key in a KMS/HSM:
from warden_sdk import JwtSigner
signer = JwtSigner.from_file("issuer.pem", "ES256", "k1") # kid = k1
jwt = tok.to_jwt(signer, at_jwt=True) # typ: at+jwt (RFC 9068 access token)
The proxy then verifies against the Entra JWKS with --jwks-url and --aud
(see Step 2).
Verify the minted token
Run the token conformance checker the same way the proxy would, to see exactly what Warden extracts:
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:azure --token-key dev-secret
token OK
accountable: alice@contoso.com
chain: alice@contoso.com > agent-mi > prod-agent
roles: Analyst
scope: Search.Query
relations: reader@storage:reports
For a production JWT, swap --token-key dev-secret for --jwks-url <entra-jwks>
(add --require-at-jwt if you set at_jwt=True).
Step 2 — Route tool calls through Warden
The only integration change the Foundry agent needs is to launch warden proxy
instead of its tool server. The Python SDK’s ProxyConfig builds the invocation
and the MCP client stanza:
from warden_sdk import ProxyConfig
cfg = ProxyConfig(
upstream="python3 tools_server.py", # the Foundry tool server (stdio)
agent="prod-agent", # must equal the token's leaf actor
policy="warden.policy.toml",
token=".warden/token.json", # session identity (dev envelope)
audience="warden:azure",
audit=".warden/audit.jsonl",
approvals=".warden/approvals.json",
)
# Feed the stanza to your MCP client (langchain_mcp_adapters / MultiServerMCPClient):
server = cfg.mcp_stdio_config()
# -> {"command": "<warden>", "args": ["proxy", "--upstream", ...], "transport": "stdio"}
In production, verify the Entra-signed JWT instead of the dev key:
cfg = ProxyConfig(
upstream="python3 tools_server.py",
agent="prod-agent",
audience="warden:azure",
token=".warden/token.json",
jwks_url="https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys",
issuer="https://login.microsoftonline.com/<tenant>/v2.0",
)
The equivalent CLI invocation:
warden proxy \
--upstream "python3 tools_server.py" \
--agent prod-agent \
--policy warden.policy.toml \
--token .warden/token.json --aud warden:azure --token-key dev-secret \
--audit .warden/audit.jsonl \
--approvals .warden/approvals.json
The proxy also serves MCP over HTTP with --http ADDR when the tool server or
client speaks HTTP rather than stdio.
Shared gateway: per-request identity
--token binds one session principal to every call — the right shape for a
sidecar (one Warden per agent). For a shared gateway fronting many agents
and users, drop --token and use --request-identity so each request carries its
own verified Entra identity (--token and --request-identity are mutually
exclusive):
cfg = ProxyConfig(
upstream="python3 tools_server.py",
agent="prod-agent",
audience="warden:azure",
jwks_url="https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys",
request_identity=True, # per-request bearer identity, no session token
)
Sidecar is preferred (surgical revocation); the gateway trades that for simpler operation. See Deployment patterns.
Step 3 — Write a policy
Policy is first-matching-rule-wins; anything unmatched falls through to
default. The Azure-appropriate warden.policy.toml:
# Policy for the Azure AI Foundry Agent Service example. First matching rule
# wins; else `default`. Every call must carry a verified Entra-minted identity.
default = "deny"
require_identity = true
# Reads are allowed only for the Entra `Analyst` app role (RBAC).
[[rules]]
tool = "read_*"
decision = "allow"
require_role = "Analyst"
# Querying reports is gated on an Azure RBAC `reader` relation to the exact
# resource the call targets (ReBAC): the token must carry reader@<value of the
# call's "resource" argument>, e.g. reader@storage:reports.
[[rules]]
tool = "query_reports"
require_relation = { relation = "reader", resource_arg = "resource" }
decision = "allow"
# Destructive actions are never allowed unattended.
[[rules]]
tool = "delete_*"
decision = "deny"
reason = "destructive: agents may not delete resources"
# ABAC example — constrain a rule to a subject attribute (Entra directory attr):
# [[rules]]
# tool = "export_*"
# when = { field = "subject:tenant", op = "eq", value = "contoso" }
# decision = "require_approval"
What each rule does:
require_identity = true— reject any call that does not carry a verified, Entra-minted identity, regardless of the tool.read_*+require_role = "Analyst"— reads are allowed only when the token’sroles(mapped from Entra app roles / groups) includeAnalyst.query_reports+require_relation— the token must carry thereaderrelation to the exact resource named in the call’sresourceargument (resource_arg). If the call passesresource = "storage:reports", the token needsreader@storage:reports(from an Azure RBACReaderassignment on that resource). This is per-call, replay-proof ReBAC.delete_*→ deny — destructive tools are refused with an explicit reason.- The commented ABAC rule shows gating on a subject attribute
(
subject:tenant), sourced from an Entra directory attribute inattrs.
Swap require_approval for a decision on any rule to route those calls to the
human hold instead of allowing or denying outright. See the
Policy reference.
Step 4 — Run & verify
Mint the token, then run the agent’s tool calls through the proxy (Steps 1–2).
Each tools/call is verified, evaluated, and recorded. Inspect the audit chain:
# Every call: which tool, allowed/blocked, and the accountable principal behind it.
warden audit tail --audit .warden/audit.jsonl
# Prove the record has not been tampered with (hash-chain integrity).
warden audit verify --audit .warden/audit.jsonl
Because the delegation chain is folded into each record, every line ties back to
a named Entra principal — alice@contoso.com, acting through agent-mi and
prod-agent — not to an anonymous service identity. That is the “could the
accountable human have known/authorized this, and can we prove it” property.
If a rule is require_approval, the call is held; a human releases it out of
band:
warden approvals list
warden approve <id> --by security@contoso.com --approver-key approver.pem
Production notes
- DPoP sender-constraint (RFC 9449). Bind the token to a proof key so a
stolen bearer token cannot be replayed. Add
cnf.jktat mint time withTokenBuilder.dpop_jkt(jkt)(compose it into the mapped token before signing); the HTTP gateway then requires a matching DPoP proof on each request. - Anchored audit. For rollback-proof audit, periodically anchor the chain:
--anchor .warden/anchor.jsonl --anchor-key anchor.pem. Verify later withwarden audit verify --anchor .warden/anchor.jsonl --anchor-pub anchor.pub. - SIEM export. Emit OCSF-shaped events for Microsoft Sentinel / your SIEM
with
--ocsf .warden/events.ocsf.jsonl. - Sidecar vs. gateway. Prefer one Warden per agent session (surgical
revocation via pause→reload); adopt the shared gateway (
--request-identity) only when you need one process fronting many agents. See Deployment patterns. - 12-factor config. Drive the proxy from a
[proxy]TOML table or environment (WARDEN_BIN,--config FILE) so nothing is baked into images. See Configuration (12-factor) and Operating in production. - Keys in Azure Key Vault. Keep the issuer’s signing key and any anchor / approver keys in Azure Key Vault (or an HSM), never on the agent host — the no-forged-authority rule is only as strong as the key custody behind it.
Troubleshooting
- Actor mismatch / token rejected. Warden’s leaf actor (deepest
subin theactchain) must equal the proxy’s--agent. If you minted withagent="prod-agent", run the proxy with--agent prod-agent. Confirm the chain withwarden token verify(chain: ... > prod-agent). - Missing / wrong
aud. If verification fails on audience, the token’saudand the proxy’s--auddisagree. Passaudience="warden:azure"tofrom_entra_obo(...)and--aud warden:azureto the proxy. Entra access tokens setaudto the target resource/app ID URI — make the two match, or set the audience explicitly on the mapped token. - OBO token exchange nuances. The OBO exchange must request the delegated
scopes you map into
scopeand must preserve the user identity (oid/upn) assub— if the downstream token comes back as an app-only (client credentials) token, there is no accountable human andrequire_identitywill fail. Ensure the managed identity has the app roles / Azure RBAC assignments you reference; those are the source forrolesandrel, and a missing assignment shows up as a deniedrequire_role/require_relationrule, not a token error. require_relationdenials. The relation isrole.lower()@<resource>, and the resource must match the call’sresource_argvalue exactly.Readeronstorage:reportsyieldsreader@storage:reports; a call passing a differentresourcestring will not match.- HS256 rejected. Warden accepts only asymmetric JWT algorithms. Sign with
ES256/RS256/EdDSAviaJwtSigner, not an HMAC secret (dev envelopes are the only symmetric path, and only under--token-key).
See also
examples/azure-ai— the runnable example this chapter follows.- The integration model and The Python SDK — the token-as-interface pattern and adapter/proxy helpers.
- Deployment patterns, Configuration (12-factor), Operating in production.
- Sibling provider guides: Databricks, Google ADK / Vertex AI, AWS Bedrock (AgentCore), LangGraph.
- Reference: CLI, Policy, Token & claims spec, Security & threat model.
- Concepts: The identity token.
CLI reference
Run warden --help for the authoritative list. Options resolve flag ▸
WARDEN_* env ▸ --config TOML ▸ default (see
Configuration).
Commands
| Command | Purpose |
|---|---|
warden demo | Self-contained walkthrough (no API key, no external server). |
warden proxy … | Run as an MCP proxy (stdio, or HTTP with --http). |
warden approvals list | List pending held actions. |
warden approve <id> [--by WHO] [--approver-key PEM] | Release a held action (signs a per-action assertion if a key is given). |
warden deny <id> [--by WHO] | Deny a held action. |
warden pause / warden resume | Stop/resume forwarding at Warden; resume reloads policy. |
warden revoke (--jti X | --agent Y | --human Z) --revoke-key PEM | Append a signed revocation event. |
warden token verify --token FILE … | Verify a token the way the proxy would (conformance check). |
warden audit tail | Show the audit trail. |
warden audit verify [--anchor FILE --anchor-pub PEM] | Verify the chain (and signed checkpoints). |
warden policy show | lint | test | Inspect, statically check, or dry-run a policy. |
warden proxy — key flags
Core
| Flag | Meaning |
|---|---|
--upstream "<cmd>" | The real MCP tool server to launch and front (required). |
--agent NAME | This agent’s wire identity (must match the token’s leaf actor). |
--policy FILE | Policy file (default warden.policy.toml). |
--config FILE | A [proxy] TOML table; flags override it. |
--http ADDR | Serve MCP Streamable-HTTP instead of stdio. |
--log-format json | Structured decision logs to stderr. |
--metrics FILE | Write a metrics snapshot on drain. |
--drain-timeout SECS | Bound the graceful-shutdown drain. |
Identity
| Flag | Meaning |
|---|---|
--token FILE | Session delegation token (one principal for the process). |
--request-identity | Per-request bearer identity (shared gateway). Mutually exclusive with --token. |
--aud AUD / --iss ISS | Required audience / issuer allowlist (comma-separated). |
--jwks FILE / --jwks-url URL / --issuer-url URL | JWKS by file, over HTTPS, or via OIDC discovery. |
--issuer-key PEM | A single PEM public key (instead of JWKS). |
--token-key KEY | Dev-envelope keyed digest (local only). |
--leeway SECS | Clock-skew tolerance. |
--require-at-jwt | Require typ: at+jwt (RFC 9068). |
Evidence & control
| Flag | Meaning |
|---|---|
--audit FILE / --approvals FILE | Audit log / approval queue paths. |
--anchor FILE --anchor-key PEM [--anchor-interval N] | Sign chain-head checkpoints. |
--ocsf FILE | OCSF event sink (SIEM). |
--redact PROFILES [--redact-scan-values] | PII/secret redaction (gdpr/hipaa/pci/secrets). |
--budget FILE | Durable per-run budget counts. |
--http-auth-token TOKEN | Require a bearer on the HTTP surface. |
--approver-jwks FILE | Require signed approvals from an allowlisted key set. |
--revocations FILE --revocation-pub PEM | Subscribe to a signed revocation feed. |
--control FILE | Enable the admin pause/resume control plane. |
--require-handshake | Reject any tools/call before MCP initialize. |
--upstream-timeout SECS | Per-call upstream timeout (auto-restart on hang/crash). |
warden token verify
warden token verify --token FILE --agent NAME [--aud AUD] [--iss ISS] \
(--jwks FILE | --jwks-url URL | --issuer-key PEM | --token-key KEY) [--require-at-jwt]
Prints the accountable subject, the delegation chain, roles, scope, and relationships if the token verifies; exits non-zero otherwise. This is the check SDK adapters run against.
warden policy
warden policy show --policy FILE
warden policy lint --policy FILE # exits non-zero on errors
warden policy test --policy FILE --tool NAME [--args JSON] [--token FILE …]
Policy reference
A policy is TOML. Top-level keys plus an ordered list of [[rules]]. The first
matching rule wins; otherwise default.
Top level
| Key | Type | Meaning |
|---|---|---|
default | "allow" | "deny" | "require_approval" | Decision when no rule matches. |
require_identity | bool | Deny any call without a verified, accountable token. |
A rule
[[rules]]
tool = "wire_funds" # exact, prefix* , or *
decision = "require_approval" # allow | deny | require_approval
reason = "large transfers need a human" # recorded in the audit
require_role = "finance.approver" # RBAC gate
require_relation = { relation = "owns", resource_arg = "account" } # ReBAC gate
when = { arg = "amount", op = "gt", value = 1000 } # condition (see below)
max_per_run = 5 # per-run budget
| Field | Meaning |
|---|---|
tool | Match: exact name, prefix* wildcard, or * (all). Case-exact. |
decision | allow / deny / require_approval. |
reason | Human-readable, recorded in the audit entry. |
require_role | Token must carry this role (implies require_identity). |
require_relation | Token must hold relation@<value of resource_arg>. |
when | Condition tree that must hold for the rule to apply. |
max_per_run | Cap on invocations per run (reserved atomically). |
Conditions (when)
Leaf condition:
when = { arg = "amount", op = "gt", value = 1000 } # a tool argument
when = { field = "subject:region", op = "eq", value = "EU" } # a signed token attr
when = { field = "resource:classification", op = "eq", value = "public" }
when = { field = "env:hour", op = "lt", value = 18 }
- Namespaces:
arg:(tool args — attacker-influenced),subject:(trusted tokenattrs),resource:(trusted tokenresource_attrs, tied to the rule’srequire_relation),env:(Warden environment). - Operators:
gt,lt,eq,contains. Numeric operators accept a number sent as a string.
Combinators:
when = { any = [ {arg="a",op="eq",value=1}, {arg="b",op="eq",value=2} ] } # OR
when = { not = { arg = "dry_run", op = "eq", value = true } } # NOT
when = [ {arg="a",op="gt",value=0}, {field="subject:role",op="eq",value="x"} ] # AND
Evaluation order
require_identity/ scope narrowing (authenticated agents may only call tools inscope).- Rules in order:
toolmatch →whenselects (non-match falls through) →require_role/require_relationgates →max_per_run→ the rule’sdecision. - Otherwise
default.
A when that doesn’t match falls through to the next rule — this is what lets
threshold pairs compose (amount < X → allow, else → require_approval).
Validate
warden policy lint --policy FILE # unreachable rules, unknown namespaces, zero budgets
warden policy test --policy FILE --tool NAME --args JSON [--token FILE]
See warden.policy.toml for a worked example and the per-provider policies under examples/.
Token & claims spec
The token is the contract between your platform and Warden. This is its shape. Warden accepts either a compact JWT (production) or a dev envelope (local).
Claims
{
"iss": "https://idp.example.com",
"aud": "warden:prod",
"exp": 1893456000,
"nbf": 1893452400,
"iat": 1893452400,
"jti": "tok_abc123",
"sub": "alice@example.com",
"act": { "sub": "svc-principal", "act": { "sub": "prod-agent" } },
"roles": ["analyst"],
"attrs": { "region": "EU", "team": "research" },
"rel": [ { "relation": "can_read", "resource": "table:sales" } ],
"scope": ["query_table", "read_records"],
"resource_attrs": { "table:sales": { "classification": "public" } },
"cnf": { "jkt": "<DPoP JWK SHA-256 thumbprint>" }
}
| Claim | Required | Meaning |
|---|---|---|
sub | ✅ | Accountable human. Empty ⇒ fail closed. |
act | ✅ | Delegation chain; the deepest sub must equal --agent. |
aud | when --aud set | Audience; must match. |
iss | when --iss set | Issuer; must be in the allowlist. |
exp / nbf | exp in JWT mode | Validity window (with --leeway). |
jti | recommended | Token id; used for revocation and audit. |
roles | — | RBAC roles. |
attrs | — | ABAC attributes (subject: conditions). |
rel | — | ReBAC tuples {relation, resource}. |
scope | — | Delegated tool grant (scope narrowing). |
resource_attrs | — | Trusted per-resource attributes (resource: conditions). |
cnf.jkt | — | DPoP proof-key thumbprint (RFC 7800/9449). |
Verification rules
- Algorithm — asymmetric only (
RS*,PS*,ES*,EdDSA). HMAC andnoneare rejected (blocks the RS256→HS256 downgrade). - Key source — JWKS (by
kid, file or HTTPS with cache/rotation), OIDC discovery, or a PEM public key. - Accountability — non-empty
sub; the leaf actor ofactmust equal the agent on the wire. - Freshness —
exp/nbfwith leeway; session tokens are re-validated (and refreshed-or-denied) per action.
Dev envelope (local only)
{
"claims": { "sub": "alice@example.com", "act": { "sub": "prod-agent" }, "aud": "warden:test" },
"sig": "<sha256_hex('KEY|' + canonical_json(claims))>"
}
Verified with --token-key KEY. With no key it’s an unsigned dev token
(signed = false). Never an enforcement mode.
Produce & verify
Build tokens with the Python SDK (TokenBuilder,
identity adapters, JwtSigner), and verify any token the way the proxy does:
warden token verify --token FILE --agent NAME --aud AUD \
(--jwks-url URL | --issuer-key PEM | --token-key KEY)
The proxy always accepts a raw conforming token with no SDK — this spec is the interface.
Security & threat model
Warden is a security product; this page summarises its posture and points at the authoritative documents. Report vulnerabilities via GitHub Private Vulnerability Reporting — see SECURITY.md. Do not open public issues for vulnerabilities.
What Warden defends (highest-value targets)
- Bypass of the decision pipeline — reaching a tool without passing policy.
- Audit forgeability — rewriting/rolling-back the hash chain or defeating the signed anchor.
- Identity/accountability bypass — token verification, algorithm confusion, JWKS spoofing, DPoP replay/key-binding, issuer/audience confusion.
- Authorization bypass — RBAC/ABAC/ReBAC or scope escape.
- Revocation bypass — acting after a revocation/expiry.
- Fail-open conditions — any dependency failure that yields “allow.”
The core invariant is fail closed: any ambiguity or dependency failure results in a deny, never an allow.
Design guarantees
- Asymmetric-only JWT verification;
aud/issrequired when configured; RFC 9068at+jwtenforcement; DPoP sender-constraint (RFC 9449). - Tamper-evident audit — hash-chained over a canonical, injective encoding of every accountability field; signed checkpoints (anchor) detect rollback.
- Atomic budgets, post-approval re-validation of revocation/freshness, and redaction of PII/secrets (including numeric leaves) before hashing.
Out of scope (by design)
- Prompt injection inside the agent’s reasoning — Warden bounds actions, not the model.
- Findings requiring a host/root compromise that already holds the signing keys (a documented non-goal until KMS/HSM integration).
- The
--token-keydev-envelope path (development only). - Issues in third-party MCP servers / tools / identity providers themselves.
Assurance status
Warden is beta. Its security-critical paths went through an adversarial security review (findings fixed with regression tests), but it has not yet had an independent third-party audit. For regulated / high-stakes enforcement, conduct your own review and run observe-mode first.
Documented residuals
Tracked in
docs/production-readiness.md:
plain verify needs the anchor to detect tail rollback; a failed audit write
is non-blocking by default (use a blocking sink for strict fail-closed); approval
assertions can replay against a byte-identical action (a per-request nonce is the
fix); tool matching is case-exact.
Authoritative documents
- SECURITY.md — reporting, scope, safe harbor, hardening checklist.
- docs/threat-model.md — the full threat model.
- docs/production-readiness.md — the readiness checklist and residuals.
- The Threat Model explainer.