Google ADK / Vertex AI
This chapter shows how to put Warden in front of a
Google Agent Development Kit (ADK) or
Vertex AI Agent Engine agent, so every tools/call is checked against policy
(allow / deny / require-approval), high-risk calls are held for a human, and
each executed call lands in a tamper-evident audit chain tied to a named IAM
principal.
Google IAM already governs access. Warden does not replace it — it adds the two things IAM does not give you for tool invocations: a synchronous human pre-authorization hold and a tamper-evident per-action record. IAM mints the credential; Warden maps and enforces what IAM already asserts, and never invents authority of its own.
The runnable version of everything here lives in
examples/google-adk.
Overview
Warden is an MCP proxy. It sits on the stdio (or HTTP) path between the ADK /
Agent-Engine agent and its tool servers. The only integration change is that the
agent’s MCP toolset launches warden proxy … instead of launching the tool
server directly; Warden then spawns the tool server as its own upstream
subprocess.
ADK / Agent-Engine agent --(MCP)--> warden proxy --(MCP)--> tool servers
| |
| A2A hops extend `act` | verify identity / evaluate policy
| | hold for approval / append to audit
+------------------------------> each call tied to a named IAM principal
The A2A angle
For inter-agent work, Google’s emerging Agent2Agent (A2A) protocol has agent cards (which declare an agent’s capabilities) and its own auth story. Warden is the enforcement point on the A2A tool/skill invocation:
- an agent card’s declared capabilities become the granted
scope; - when one agent calls another, that hop extends the
actchain by one, so the delegation from human → agent A → agent B is carried in the token and folded into the audit record; - each inter-agent delegation is recorded in the accountability chain, so “which agent, acting for which human, invoked this skill” is provable after the fact.
See The integration model for why the token is the only coupling point, and docs/platform-integration.md §4 for the Google / Vertex / A2A design notes.
Identity mapping
Warden’s identity boundary is a single signed
delegation token (RFC 8693): sub is the
accountable human, act is the nested acting chain whose leaf equals the
agent’s wire identity, and the authorization claims (roles, attrs, rel,
scope) are trusted-because-signed. The Google adapter shapes those claims from
IAM; the platform issuer signs.
| Warden claim | Google source |
|---|---|
sub — accountable human | OIDC sub / domain-wide-delegation subject |
act — acting chain (leaf = agent) | service account (workload identity); each A2A hop adds one more act entry |
roles (RBAC) | IAM role bindings (e.g. roles/bigquery.dataViewer) |
attrs (ABAC) | IAM conditions (e.g. resource.type == "bigquery") |
scope (agent grant) | agent-card A2A capabilities |
rel (ReBAC tuples) | resource-level IAM bindings (Zanzibar-style relations) |
IAM’s emphasis on condition expressions is a natural fit for Warden’s policy
when clauses, which read those conditions back out of attrs.
Prerequisites
-
Build Warden and put the
wardenbinary on yourPATH(or setWARDEN_BIN— the SDK resolves the binary in that order). See Install & build. -
Install the SDK and the ADK wiring:
pip install warden-agent-sdk # identity adapter + orchestration shim pip install google-adk # ADK, for the MCP toolset pip install "warden-agent-sdk[jwt]" # only needed for production JWT signing -
A GCP project with the agent running as a service account (or a workload-identity binding), plus the IAM role bindings, conditions, and resource-level bindings you want to project into the token.
Step 1 — Mint a Warden token
Map the agent’s Google workload-identity context into a Warden token with the
identity adapter. Its entry point is
warden_sdk.adapters.google.from_workload_identity(context, *, agent, audience, ttl_seconds=300), which returns a TokenBuilder.
from warden_sdk.adapters import google
# In production this dict is assembled from the service account's IAM bindings
# and conditions plus the agent card's declared capabilities — not hand-written.
context = {
"user": "alice@example.com", # OIDC sub / DWD subject -> sub
"service_account": "prod-agent@my-proj.iam.gserviceaccount.com", # -> act hop
"iam_roles": ["roles/bigquery.dataViewer"], # -> roles (RBAC)
"iam_conditions": {"resource.type": "bigquery"}, # IAM condition -> attrs (ABAC)
"a2a_capabilities": ["query_dataset"], # agent-card capabilities -> scope
"resource_bindings": [ # resource-level IAM -> rel (ReBAC)
{"relation": "dataViewer", "resource": "dataset:analytics"},
],
}
tok = google.from_workload_identity(
context,
agent="prod-agent", # becomes the leaf `act` — must match the proxy's --agent
audience="warden:google",
)
Context keys (exact): user is required (the human sub; missing it raises
ValueError). service_account → the middle act hop. iam_roles (list) →
roles. iam_conditions (dict) → attrs. a2a_capabilities (list) → scope.
resource_bindings (list of {"relation", "resource"}) → rel tuples.
Local (dev envelope)
For offline runs, emit an unsigned/dev-signed envelope. Its signature verifies
under --token-key; it is a development convenience, not an enforcement
mode.
import os
os.makedirs(".warden", exist_ok=True)
tok.write_dev_envelope(".warden/token.json", key="dev-secret")
Or run the example’s minter directly:
python mint_token.py alice@example.com
Production (real signed JWT)
In production, do not use the dev envelope. Have the platform issuer — Google IAM or your IdP — sign a real JWT and have the proxy verify it against the issuer’s JWKS. The adapter only shapes claims; it asks the issuer to sign.
from warden_sdk import JwtSigner
# The private key belongs in a KMS/HSM / Secret Manager; this is the local signer.
signer = JwtSigner.from_file("issuer.pem", "ES256", "k1")
jwt = tok.to_jwt(signer, at_jwt=True) # RFC 9068 at+jwt so --require-at-jwt accepts it
Warden accepts only asymmetric algorithms (RS*/PS*/ES*/EdDSA), which blocks the RS256→HS256 confusion downgrade.
The no-forged-authority rule. From Warden’s perspective the adapter is untrusted client code — it runs on the side Warden exists to police. An adapter orchestrates the platform’s token exchange; it never signs authority itself. The trusted signer stays the platform issuer (IAM / your IdP); Warden verifies against the issuer’s JWKS. An adapter holding its own broad-scope signing key would be a single high-value secret that collapses the accountability model.
Verify the token
Confirm the token is well-formed and the act leaf matches the agent before you
wire anything up:
# dev envelope
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:google --token-key dev-secret
# production JWT
warden token verify --token .warden/token.json \
--agent prod-agent --aud warden:google --jwks path/to/jwks.json
Step 2 — Route ADK tool calls through Warden
ADK consumes tools via an MCP toolset. Use the orchestration shim
warden_sdk.orchestration.google_adk.warden_connection_params(cfg) to build the
stdio parameters that launch warden proxy as that toolset’s server. It takes a
ProxyConfig and returns
{"command": …, "args": [...]}, ready to splat into ADK’s
StdioServerParameters.
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters
from warden_sdk import ProxyConfig
from warden_sdk.orchestration import google_adk as wg
cfg = ProxyConfig(
upstream="python3 tools_server.py", # Warden spawns this as its MCP upstream
agent="prod-agent", # must equal the token's leaf act
policy="warden.policy.toml",
token=".warden/token.json",
audience="warden:google",
audit=".warden/audit.jsonl",
approvals=".warden/approvals.json",
)
params = wg.warden_connection_params(cfg) # -> {"command": ..., "args": [...]}
toolset = MCPToolset(connection_params=StdioServerParameters(**params))
That toolset is the only Warden-aware line in the agent; the rest of your ADK
agent (models, instructions, other tools) is unchanged. Under the hood the shim
resolves the warden binary (warden_bin → WARDEN_BIN → PATH) and expands
to warden proxy --upstream "python3 tools_server.py" --agent prod-agent --policy … --token … --aud warden:google --audit … --approvals ….
For a production JWT flow, drop token/audience for an issuer-verified
config and set jwks_url, issuer, and audience on the ProxyConfig (or run
the proxy by hand with --jwks-url / --iss / --aud).
Step 3 — Write a policy
Policy is TOML: first matching rule wins, otherwise default. Setting
require_identity = true makes every action tie back to a named IAM principal —
no anonymous tool calls. This mirrors
examples/google-adk/warden.policy.toml.
# Warden policy for a Google ADK / Vertex AI Agent Engine agent.
default = "deny"
require_identity = true
# Reads are allowed for principals holding the BigQuery dataViewer IAM role.
# The role arrives as an RBAC claim mapped from the service account's IAM binding.
[[rules]]
tool = "read_*"
decision = "allow"
require_role = "roles/bigquery.dataViewer"
# Dataset queries are gated on a ReBAC relationship: the token must carry a
# `dataViewer` relation to the exact dataset named in the call's `dataset` arg
# (resource-level IAM binding -> rel tuple). No tuple, no query.
[[rules]]
tool = "query_dataset"
decision = "allow"
require_relation = { relation = "dataViewer", resource_arg = "dataset" }
# Optional ABAC tightening: only within the BigQuery resource type. The attr is
# mapped from an IAM condition (iam_conditions -> attrs).
[[rules]]
tool = "describe_dataset"
decision = "allow"
require_role = "roles/bigquery.dataViewer"
when = { field = "subject:resource.type", op = "eq", value = "bigquery" }
# Destructive operations are never allowed unattended, regardless of role.
[[rules]]
tool = "delete_*"
decision = "deny"
reason = "destructive: agents may not delete datasets or tables"
How each rule reads back the mapped claims:
read_*→require_roleconsumesroles, i.e. the IAM role binding (roles/bigquery.dataViewer) projected in Step 1.query_dataset→require_relationconsumesrel. The{ relation, resource_arg }form means: the token must carry adataViewerrelation whose resource exactly equals the value of the call’sdatasetargument. With the sample token that relation is todataset:analytics, so a call withdataset = "dataset:analytics"passes and any other dataset is denied.describe_dataset→whenreadsattrs["resource.type"](mapped from the IAM condition) via thesubject:field prefix.delete_*→denyblocks destructive calls before they reach the tool.
To gate (rather than block) a high-risk call for a human, use
decision = "require_approval"; see the Policy model
and Policy reference.
Step 4 — Run & verify
The ADK toolset launches Warden for you. To exercise the same proxy by hand against the policy above:
warden proxy --upstream "python3 tools_server.py" --agent prod-agent \
--policy warden.policy.toml --token .warden/token.json --aud warden:google \
--audit .warden/audit.jsonl --approvals .warden/approvals.json
With this policy in place: reads pass for the roles/bigquery.dataViewer role;
query_dataset passes only when the token holds a dataViewer relation to the
exact dataset it targets; and any delete_* is denied before it reaches the
tool. A require_approval decision parks the call until a human runs
warden approve <id> / warden deny <id>.
Inspect the record. Every executed or blocked call is on the chain, each tied to the accountable human behind the service account:
warden audit tail # what ran, what was blocked
warden audit verify # prove the chain is untampered
warden audit verify --anchor .warden/anchor.jsonl --anchor-pub anchor.pub # + signed checkpoints
This is the payoff over IAM alone: IAM decides access, but it does not give you a synchronous human hold on a specific tool call, nor a replay-proof record that ties each executed invocation — with the full delegation chain folded into the hash — to a named IAM principal.
Production notes
- DPoP (sender-constrained tokens, RFC 9449). A bearer token can be replayed
if stolen. Bind the token to a key the client holds: set
cnf.jktviaTokenBuilder.dpop_jkt(thumbprint)and present aDPoPproof per request. DPoP applies on the HTTP transport (it needs method + URL), so use it withwarden proxy --http ADDRrather than the stdio path. - Anchor the audit chain. Run the proxy with
--anchor <file> --anchor-key <PEM>so the chain head is periodically signed to a separate file; rewrites or rollbacks below a checkpoint then become detectable, andwarden audit verify --anchor … --anchor-pub …checks them. - Sidecar vs. gateway. Prefer a sidecar — one Warden per agent runtime/session on the MCP path — which keeps revocation surgical (pause → reload one process). A shared gateway fronting many agents is simpler to operate but forfeits surgical revocation and concentrates the trust boundary. See Deployment patterns.
- 12-factor configuration. Drive the proxy from a
[proxy]config table plus environment overrides rather than hard-coded flags; see Configuration. - Keys in Secret Manager. The issuer/anchor private keys belong in Google Secret Manager (or a KMS/HSM), mounted or fetched at start — never baked into the image or the repo. See Operating in production.
Troubleshooting
- Actor mismatch / token rejected. Warden’s leaf actor (the deepest
actsub) must equal the proxy’s--agent. If you mint withagent="prod-agent"but run--agent something-else(or set a differentagentonProxyConfig), verification fails. Reconcile them, and usewarden token verify --agent …to confirm. - ADK can’t launch the proxy. The shim resolves the binary via
warden_bin→WARDEN_BIN→PATHand raisesFileNotFoundErrorif none is found. PutwardenonPATHor setWARDEN_BIN=/abs/path/to/warden(orProxyConfig(warden_bin=…)) in the environment ADK spawns the toolset from. - ReBAC arg exactness.
require_relation’sresource_argmatches the call argument’s value exactly against thereltuple’s resource. The example bindsdataViewertodataset:analytics, so the call must passdataset = "dataset:analytics"—"analytics"alone will not match. Keep the resource-naming convention identical betweenresource_bindings(Step 1) and the tool’s argument values.
See also
- The identity token — the RFC 8693 claim model
- Policy model · Policy reference
- The audit chain
- The Python SDK —
ProxyConfig, adapters, shims - Deployment patterns · Operating in production
- CLI reference · Token & claims spec · Security & threat model
- Sibling guides: LangGraph · AWS Bedrock (AgentCore) · Databricks (Mosaic AI)
- Runnable example:
examples/google-adk