Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Point your agent’s MCP client at warden proxy instead of the tool server.
  2. Give Warden a signed token that says who the agent acts for (or run audit-only to start).
  3. Write a policy: which tools are allowed, denied, or need a human.
  4. 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

PartRole
Proxy / decision pipelineIntercept every tools/call, decide, forward or block, record.
Identity tokenA signed statement of who the agent acts for (RFC 8693 delegation).
PolicyAllow / deny / require-approval by tool, condition, role, relationship, and budget.
Audit chainAn 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.

  1. Handshake — optionally require the MCP initialize before any call.
  2. Pause — an admin warden pause stops forwarding, at Warden, not by trusting the agent.
  3. Revocation — a signed, append-only revocation feed can deny by token jti, agent, or human (negative authority), checked live.
  4. 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.
  5. Policy — the first matching rule decides: scope narrowing, RBAC/ABAC/ReBAC gates, when conditions, and per-run budgets. Otherwise the default applies.
  6. Approval — a require_approval decision holds the call until a human releases it (optionally with a signed approver assertion), or it times out.
  7. 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.
  8. 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), with GET /healthz and GET /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:

ClaimMeaningPolicy use
rolesRBAC roles/groupsrequire_role
attrsABAC attributeswhen { field = "subject:…" }
relReBAC relationship tuples {relation, resource}require_relation
scopethe agent’s delegated tool grantscope narrowing (an agent may only call tools in scope)
resource_attrstrusted per-resource attributeswhen { field = "resource:…" }
cnf.jktDPoP 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. aud and iss are 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.
  • when conditions — 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 whose when doesn’t match falls through to the next rule — which is what lets threshold rules (amount < X allow vs amount ≥ X hold) compose.

Identity gates (require a verified token)

FieldGate
require_identity = truedeny 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 verify warns 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-version in Cargo.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

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 (or warden 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:

  1. An issuer/verifier config — trust the platform’s token issuer (its JWKS endpoint or public key, and the expected aud).
  2. 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 claimDatabricksAWSGoogleAzure
sub (accountable human)OBO userhuman behind the role chainOIDC sub / DWD subjectEntra oid/upn
act (delegation)service principalSTS sessionservice accountmanaged identity (OBO)
roles (RBAC)UC groupsIAM rolesIAM rolesEntra app roles
attrs (ABAC)workspace/catalogSTS session tagsIAM conditionsdirectory attrs
rel (ReBAC)UC grants on securablesresource tagsresource-level IAMAzure RBAC assignments
scope (agent grant)registered UC functionssession policyagent-card capabilitiesdelegated scopes

Two kinds of adapter (composed, not four interchangeable ones)

KindExamplesJob
Identity adapterDatabricks, AWS, Google, AzureShape the Warden token from the platform’s native credential
Orchestration shimLangGraph, Google ADKRoute 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

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/call carries its own bearer delegation token (--request-identity), so every call is attributed to its accountable human even though one gateway serves many users. Combine with require_identity so 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/SIGINT drain in-flight calls (--drain-timeout) then sign a final audit checkpoint; SIGHUP hot-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")
AdapterEntry pointNative source
awsfrom_sts_sessionSTS AssumeRole + session tags (Bedrock AgentCore)
databricksfrom_oboon-behalf-of-user + Unity Catalog grants
googlefrom_workload_identityworkload identity / service account (Vertex ADK / A2A)
azurefrom_entra_oboEntra 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

  1. CLI flagswarden proxy --upstream "…" --agent prod-agent …
  2. WARDEN_* environment variables — every flag is settable from the environment (factor III). The name is the flag upper-cased with dashes as underscores: --jwks-urlWARDEN_JWKS_URL.
  3. [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.

  1. 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.
  2. Enforce high-risk tools once you’ve validated policy in your environment (deny the destructive ones, require_approval the outbound ones).
  3. 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); schedule warden 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 deny in your build; pin a release.
  • Policy — run warden policy lint in CI; start restrictive; iterate with warden policy test and 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 /metrics on the HTTP transport; --metrics FILE and 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.

GuideOrchestrationIdentity sourceStart here if…
LangGraphLangGraph ReAct agentdev token, or any cloud adapteryou build agents with LangGraph (runs fully locally)
AWS BedrockBedrock AgentCoreSTS AssumeRole + session tagsyour agents run on AgentCore Gateway/Identity
DatabricksMosaic AI Agent Frameworkon-behalf-of-user + Unity Catalogyour tools are UC functions / Databricks-hosted MCP
Google ADKADK / Vertex Agent Engineworkload identity / service accountyou use ADK or Vertex Agent Engine (or A2A)
Azure AI FoundryFoundry Agent ServiceEntra ID managed identity + OBOyour 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:

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:

ToolNaturePolicy decision
read_recordsread-onlyallow
create_ticketoutbound side effectrequire_approval
delete_databasedestructivedeny

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 warden is not on PATH, set WARDEN_BIN=/path/to/warden or pass ProxyConfig(..., 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-key verifies 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:

  1. read_records(table="customers")allowed. The tool result comes back: read 3 records from customers.
  2. 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.json and --aud warden:prod, and keep require_identity = true. The token now comes from your cloud’s identity adapter (STS session, Databricks OBO, workload identity, Entra), not from mint_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 with warden audit verify --anchor .warden/anchor.jsonl --anchor-pub anchor.pub.

  • Deployment topology. The example is a sidecar-per-agent (one warden proxy per 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 via WARDEN_BIN, then PATH. Either export PATH="$PWD/target/release:$PATH", set WARDEN_BIN=/abs/path/to/warden, or pass ProxyConfig(..., warden_bin=...). Because the MCP client launches warden as a subprocess, a bare name that isn’t on the child process’s PATH fails silently as a spawn error — prefer an absolute path when in doubt.

  • no tools discovered / client hangs on get_tools(). Warden launches the upstream from its own working directory. Run the agent from examples/langgraph so python3 tools_server.py resolves, or make --upstream an 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 = true the proxy fails closed unless a verified token is attached. Ensure token=, audience=, and the verifier (--token-key for dev, --jwks-url/--aud for prod) are all set, and that warden token verify succeeds standalone.

  • actor mismatch / token rejected. Warden requires the token’s leaf actor (the deepest sub in the act chain) to equal the proxy’s --agent. If you mint with TokenBuilder(..., agent="demo-agent") you must run the proxy with agent="demo-agent". Intermediate hops (service principal, assumed role) go through .via(...); the leaf is always the wire identity.

  • --token and --request-identity conflict. They are mutually exclusive: --token pins one session principal for every call (sidecar); --request-identity takes per-request identity (shared gateway). Use one.

See also

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 claimAWS sourceMeaning
subaccountable human behind the role chainwho 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_attrsresource tags on what the tools touchrelationship / 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 keyTypeMaps to
accountable (required)strsub — the human behind the role chain
session_namestrmiddle act hop (STS RoleSessionName)
session_tagsdictattrs (ABAC), addressable as subject:<tag>
iam_roleslist[str]roles (RBAC)
session_policy_actionslist[str]scope (the grant)
resource_tagsdict[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 RS256HS256 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_role matches against roles, which the adapter fills from iam_roles (the assumed IAM role ARN).
  • field = "subject:team" reads the team STS session tag carried in attrs — native ABAC, no separate attribute store.
  • arg = "amount" inspects the tool call’s arguments; the require_approval decision 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 via TokenBuilder.dpop_jkt(...) (RFC 7800 cnf.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 with warden 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 deepest sub in the token’s act chain must equal the --agent the proxy runs as. If you mint with agent="prod-agent", run the proxy with --agent prod-agent. The middle act hop (session_name) is fine; it is the leaf that must match.
  • Missing / mismatched aud. If you set audience= when minting, you must pass the same --aud to warden 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 --token will not reflect the caller. Use --request-identity and have each call carry its own bearer; on a sidecar, the fixed --token is correct.
  • algorithm ... is not asymmetric. JwtSigner (and the verifier) reject HS*. Use ES256/RS256/EdDSA and a matching key type.
  • require_identity = true blocks everything. That is by design when no token verifies. Confirm warden token verify ... prints token OK first, then run the proxy with the same --aud and key/JWKS flags.

See also

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 claimMeaningDatabricks source
subaccountable humanOBO user principal
actdelegation chain (leaf = agent)service principalagent
roles (RBAC)group membershipuser’s Unity Catalog groups
attrs (ABAC)environment contextworkspace / catalog
rel (ReBAC)relationships on resourcesUC grants on securables (relation = privilege lower-cased)
scopewhat the agent may invokeregistered 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 set WARDEN_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.jkt when minting (TokenBuilder.dpop_jkt(...)) and require the proof at the proxy. See Operating in production.
  • Anchor the audit chain — periodically --anchor the 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 leaf act.sub does not equal the proxy’s --agent. The service principal is the middle hop; the agent (prod-agent) must be the leaf. Confirm with warden token verify --agent prod-agent ... and check via(service_principal) was called before the agent was set as leaf.
  • ReBAC rule denies a call you expected to allow — the call’s resource_arg value must equal the token’s rel resource string exactly. The token holds select@table:main.sales; the table argument must be exactly table:main.sales (same securable prefix, catalog, and name). A bare main.sales or sales will not match.
  • Every call is denied with no matching ruledefault = "deny" plus require_identity = true fails closed. Make sure a verified token is reaching the proxy (--token for dev envelopes, or --jwks-url/--aud for JWTs) and that --request-identity is paired with a valid token source.
  • JWT rejected — Warden accepts only asymmetric algorithms; an HS256 token is refused. Check --aud and --iss match the token’s aud/iss, and that the signing kid is present in the JWKS.

See also

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 act chain 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 claimGoogle source
sub — accountable humanOIDC 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 warden binary on your PATH (or set WARDEN_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_binWARDEN_BINPATH) 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_role consumes roles, i.e. the IAM role binding (roles/bigquery.dataViewer) projected in Step 1.
  • query_datasetrequire_relation consumes rel. The { relation, resource_arg } form means: the token must carry a dataViewer relation whose resource exactly equals the value of the call’s dataset argument. With the sample token that relation is to dataset:analytics, so a call with dataset = "dataset:analytics" passes and any other dataset is denied.
  • describe_datasetwhen reads attrs["resource.type"] (mapped from the IAM condition) via the subject: field prefix.
  • delete_*deny blocks 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.jkt via TokenBuilder.dpop_jkt(thumbprint) and present a DPoP proof per request. DPoP applies on the HTTP transport (it needs method + URL), so use it with warden proxy --http ADDR rather 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, and warden 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 act sub) must equal the proxy’s --agent. If you mint with agent="prod-agent" but run --agent something-else (or set a different agent on ProxyConfig), verification fails. Reconcile them, and use warden token verify --agent … to confirm.
  • ADK can’t launch the proxy. The shim resolves the binary via warden_binWARDEN_BINPATH and raises FileNotFoundError if none is found. Put warden on PATH or set WARDEN_BIN=/abs/path/to/warden (or ProxyConfig(warden_bin=…)) in the environment ADK spawns the toolset from.
  • ReBAC arg exactness. require_relation’s resource_arg matches the call argument’s value exactly against the rel tuple’s resource. The example binds dataViewer to dataset:analytics, so the call must pass dataset = "dataset:analytics""analytics" alone will not match. Keep the resource-naming convention identical between resource_bindings (Step 1) and the tool’s argument values.

See also

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 claimAzure / 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 jwt extra:
    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 keyRequiredMaps to
useryessub (accountable human)
managed_identitynomiddle act hop (via(...))
app_roles (list)noroles
attributes (dict)noattrs
scopes (list)noscope
role_assignments (list of {role, resource})norel — 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’s roles (mapped from Entra app roles / groups) include Analyst.
  • query_reports + require_relation — the token must carry the reader relation to the exact resource named in the call’s resource argument (resource_arg). If the call passes resource = "storage:reports", the token needs reader@storage:reports (from an Azure RBAC Reader assignment 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 in attrs.

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 principalalice@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.jkt at mint time with TokenBuilder.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 with warden 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 sub in the act chain) must equal the proxy’s --agent. If you minted with agent="prod-agent", run the proxy with --agent prod-agent. Confirm the chain with warden token verify (chain: ... > prod-agent).
  • Missing / wrong aud. If verification fails on audience, the token’s aud and the proxy’s --aud disagree. Pass audience="warden:azure" to from_entra_obo(...) and --aud warden:azure to the proxy. Entra access tokens set aud to 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 scope and must preserve the user identity (oid/upn) as sub — if the downstream token comes back as an app-only (client credentials) token, there is no accountable human and require_identity will fail. Ensure the managed identity has the app roles / Azure RBAC assignments you reference; those are the source for roles and rel, and a missing assignment shows up as a denied require_role / require_relation rule, not a token error.
  • require_relation denials. The relation is role.lower()@<resource>, and the resource must match the call’s resource_arg value exactly. Reader on storage:reports yields reader@storage:reports; a call passing a different resource string will not match.
  • HS256 rejected. Warden accepts only asymmetric JWT algorithms. Sign with ES256/RS256/EdDSA via JwtSigner, not an HMAC secret (dev envelopes are the only symmetric path, and only under --token-key).

See also

CLI reference

Run warden --help for the authoritative list. Options resolve flag ▸ WARDEN_* env ▸ --config TOML ▸ default (see Configuration).

Commands

CommandPurpose
warden demoSelf-contained walkthrough (no API key, no external server).
warden proxy …Run as an MCP proxy (stdio, or HTTP with --http).
warden approvals listList 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 resumeStop/resume forwarding at Warden; resume reloads policy.
warden revoke (--jti X | --agent Y | --human Z) --revoke-key PEMAppend a signed revocation event.
warden token verify --token FILE …Verify a token the way the proxy would (conformance check).
warden audit tailShow the audit trail.
warden audit verify [--anchor FILE --anchor-pub PEM]Verify the chain (and signed checkpoints).
warden policy show | lint | testInspect, statically check, or dry-run a policy.

warden proxy — key flags

Core

FlagMeaning
--upstream "<cmd>"The real MCP tool server to launch and front (required).
--agent NAMEThis agent’s wire identity (must match the token’s leaf actor).
--policy FILEPolicy file (default warden.policy.toml).
--config FILEA [proxy] TOML table; flags override it.
--http ADDRServe MCP Streamable-HTTP instead of stdio.
--log-format jsonStructured decision logs to stderr.
--metrics FILEWrite a metrics snapshot on drain.
--drain-timeout SECSBound the graceful-shutdown drain.

Identity

FlagMeaning
--token FILESession delegation token (one principal for the process).
--request-identityPer-request bearer identity (shared gateway). Mutually exclusive with --token.
--aud AUD / --iss ISSRequired audience / issuer allowlist (comma-separated).
--jwks FILE / --jwks-url URL / --issuer-url URLJWKS by file, over HTTPS, or via OIDC discovery.
--issuer-key PEMA single PEM public key (instead of JWKS).
--token-key KEYDev-envelope keyed digest (local only).
--leeway SECSClock-skew tolerance.
--require-at-jwtRequire typ: at+jwt (RFC 9068).

Evidence & control

FlagMeaning
--audit FILE / --approvals FILEAudit log / approval queue paths.
--anchor FILE --anchor-key PEM [--anchor-interval N]Sign chain-head checkpoints.
--ocsf FILEOCSF event sink (SIEM).
--redact PROFILES [--redact-scan-values]PII/secret redaction (gdpr/hipaa/pci/secrets).
--budget FILEDurable per-run budget counts.
--http-auth-token TOKENRequire a bearer on the HTTP surface.
--approver-jwks FILERequire signed approvals from an allowlisted key set.
--revocations FILE --revocation-pub PEMSubscribe to a signed revocation feed.
--control FILEEnable the admin pause/resume control plane.
--require-handshakeReject any tools/call before MCP initialize.
--upstream-timeout SECSPer-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

KeyTypeMeaning
default"allow" | "deny" | "require_approval"Decision when no rule matches.
require_identityboolDeny 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
FieldMeaning
toolMatch: exact name, prefix* wildcard, or * (all). Case-exact.
decisionallow / deny / require_approval.
reasonHuman-readable, recorded in the audit entry.
require_roleToken must carry this role (implies require_identity).
require_relationToken must hold relation@<value of resource_arg>.
whenCondition tree that must hold for the rule to apply.
max_per_runCap 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 token attrs), resource: (trusted token resource_attrs, tied to the rule’s require_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

  1. require_identity / scope narrowing (authenticated agents may only call tools in scope).
  2. Rules in order: tool match → when selects (non-match falls through) → require_role / require_relation gates → max_per_run → the rule’s decision.
  3. 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>" }
}
ClaimRequiredMeaning
subAccountable human. Empty ⇒ fail closed.
actDelegation chain; the deepest sub must equal --agent.
audwhen --aud setAudience; must match.
isswhen --iss setIssuer; must be in the allowlist.
exp / nbfexp in JWT modeValidity window (with --leeway).
jtirecommendedToken id; used for revocation and audit.
rolesRBAC roles.
attrsABAC attributes (subject: conditions).
relReBAC tuples {relation, resource}.
scopeDelegated tool grant (scope narrowing).
resource_attrsTrusted per-resource attributes (resource: conditions).
cnf.jktDPoP proof-key thumbprint (RFC 7800/9449).

Verification rules

  • Algorithm — asymmetric only (RS*, PS*, ES*, EdDSA). HMAC and none are 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 of act must equal the agent on the wire.
  • Freshnessexp/nbf with 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/iss required when configured; RFC 9068 at+jwt enforcement; 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-key dev-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