# Governed Google ADK in 3 minutes

Add ACP governance to a Google Agent Development Kit (ADK) agent in three minutes. One @governed decorator, one set_context call, every tool call audited and policy-checked. Works with direct Gemini and Vertex AI.

Google's [Agent Development Kit](https://google.github.io/adk-docs/) is the cleanest path to a production agent on Gemini. `LlmAgent`, tools as plain Python callables, runners that handle the eval/deploy lifecycle, and a Plugin system for cross-cutting concerns. Three deployment targets out of the box (Agent Runtime, Cloud Run, GKE Autopilot) via the [Agents CLI](/integrations/google-agents-cli).

What it doesn't ship: per-user identity attribution on tool calls, cross-tenant audit, output redaction, or pluggable policy. Google's Agent Gateway + Model Armor exists for the managed Agent Runtime target, but it enforces Google policies, not yours.

This post is the 3-minute path to closing that gap with [Agentic Control Plane](/) on any ADK deployment — your tools, your code, no framework rewrite.

## The pattern

Tool-layer governance via the `@governed` decorator. Each tool function is wrapped so every call runs `preToolUse → handler → postToolOutput` against ACP. Identity is bound once per request via `set_context`. ADK's tool schema introspection follows `__wrapped__` (preserved by `functools.wraps`), so the decorator is invisible to ADK's signature builder.

LLM calls go direct to Gemini (or Vertex) with your own key. Governance is at the tool layer, not the LLM layer. If you also want LLM-layer governance — for cost attribution, rate limits, or model-call audit — pair this with [LiteLLM pointing at the ACP proxy](/integrations/google-agents-cli), but the tool-layer pattern is the production default.

## Three minutes from blank slate

### 1. Install

```bash
pip install acp-governance google-adk
```

### 2. Wrap your tools

```python
from acp_governance import configure, governed, set_context
from google.adk.agents import Agent
from google.adk.runners import InMemoryRunner
from google.genai import types
import asyncio, os

configure(base_url="https://api.agenticcontrolplane.com")


@governed("lookup_record")
def lookup_record(id: str) -> dict:
    """Look up a record by ID."""
    return db.records.find_one({"id": id})


async def main():
    set_context(
        user_token=os.environ["ACP_USER_TOKEN"],
        agent_name="my-adk-agent",
        agent_tier="background",
    )

    agent = Agent(
        name="my_adk_agent",
        model="gemini-flash-latest",
        instruction="You are an ACP-governed agent. Use the tools available.",
        tools=[lookup_record],
    )

    runner = InMemoryRunner(agent=agent, app_name="acp-demo")
    await runner.session_service.create_session(
        app_name="acp-demo", user_id="user-1", session_id="session-1"
    )

    msg = types.Content(role="user", parts=[types.Part(text="Look up record id=abc-123.")])
    async for event in runner.run_async(
        user_id="user-1", session_id="session-1", new_message=msg
    ):
        if event.is_final_response():
            print(event.content.parts[0].text)
            return


asyncio.run(main())
```

### 3. Run it

```bash
export ACP_USER_TOKEN=gsk_...
export GOOGLE_API_KEY=...   # from aistudio.google.com/apikey
python my_agent.py
```

Open [cloud.agenticcontrolplane.com/activity](https://cloud.agenticcontrolplane.com/activity). One row appears for `lookup_record` — actor, tool, decision, session, latency, input/output preview with redactions applied if your policy says so. That's it. Three minutes, two lines of integration code, full audit.

## What `@governed` does

Every call through the wrapped function:

1. POSTs to ACP's `/govern/tool-use` with the tool name, input arguments, and the user token bound by `set_context`.
2. If ACP denies, returns the string `"tool_error: <reason>"` — Gemini sees this as a tool result and adapts its next turn.
3. If ACP allows, runs your function.
4. POSTs the output to `/govern/tool-output` for audit logging and PII / secret scanning.
5. If ACP redacts, replaces the output with the redacted version (configurable per-handler).
6. If ACP blocks the output post-hoc (a leaked secret pattern, for example), returns `"tool_error: <reason>"`.

The function signature, type hints, and docstring are preserved via `functools.wraps`. ADK's `inspect.signature` walks through `__wrapped__` to build the tool schema correctly — your tool's schema is what you wrote, not what the decorator produced.

## What `set_context` does

`set_context` binds the end-user identity to the current execution context. Every governed call inside the same context reads from it automatically — no threading the user token through every tool function.

```python
set_context(
    user_token=verified_jwt,        # the verified end-user identity
    agent_name="my-adk-agent",      # how this agent appears in the dashboard
    agent_tier="background",        # interactive / subagent / background / api
    session_id=request_id,          # optional, auto-generated if omitted
)
```

`agent_tier` is the lever for tier-aware policy: a destructive verb that's allowed in `interactive` (a human is at the keyboard) gets denied in `background` (autonomous, no human anywhere). Match the tier to actual deployment reality, not what's permissive.

The `user_token` is the authenticated end user — a verified JWT from your IdP, a Firebase ID token, or a `gsk_` workspace key for testing. ACP validates the signature on every governed call and uses the verified subject for audit attribution and policy evaluation.

## Adding more tools

Each tool gets its own `@governed` decorator and gets passed to `Agent(tools=[...])`:

```python
@governed("send_email")
def send_email(to: str, subject: str, body: str) -> dict:
    """Send an email."""
    return mailer.send(to=to, subject=subject, body=body)


@governed("query_db")
def query_db(sql: str) -> list[dict]:
    """Run a SQL query."""
    return db.execute(sql)


agent = Agent(
    ...,
    tools=[lookup_record, send_email, query_db],
)
```

Add or remove tools by editing the `tools` list. Governance is automatic for every `@governed`-wrapped function in scope.

## Why decorator and not a Plugin?

ADK has a `Plugin` class registered on the `Runner` that applies cross-cutting concerns across all agents and tools — and it's a clean API for governance. ACP doesn't ship an `ACPGovernancePlugin(BasePlugin)` for ADK today. The decorator pattern is preferred instead because:

- It's framework-agnostic. The same `@governed` decorator works in CrewAI, LangGraph, Pydantic AI, AutoGen, plain Python — anywhere a Python function is the unit of tool dispatch. If you have multiple frameworks in your stack, you don't relearn governance per framework.
- It's invisible at the tool definition site. ADK's signature inspection works through it transparently.
- It composes with anything. You can stack `@governed` with `@function_tool`, with `@tool` from LangChain, with whatever wrapper your framework expects.

A Plugin is the right pick for ADK-only deployments where you want one-line setup at the runner level. This post decorates.

## Where the audit lands

Every governed call lands at [cloud.agenticcontrolplane.com/activity](https://cloud.agenticcontrolplane.com/activity) with:

- **Actor** — the verified user from `user_token`
- **Tool name** — the string passed to `@governed("...")`
- **Decision** — allow / deny / redact / block / fail-open + reason
- **Session** — groups all calls from one `set_context` scope
- **Findings** — PII or secret patterns detected in input or output
- **Tier** — the `agent_tier` you bound

ADK tool calls sit alongside Claude Code, Cursor, Codex CLI, CrewAI, LangGraph, and any other framework using ACP — one audit log across every agent surface in your stack. Your CISO asks "what did our AI do for user X this week?" and the answer is a single dashboard query, not a six-system join.

## ADK-specific notes

**Vertex AI vs direct Gemini.** The starter uses direct Gemini (`GOOGLE_GENAI_USE_VERTEXAI=False`) with an API key from [aistudio.google.com/apikey](https://aistudio.google.com/apikey). For Vertex, set `GOOGLE_GENAI_USE_VERTEXAI=True`, `GOOGLE_CLOUD_PROJECT=...`, and use Application Default Credentials. The governance code is identical — only the LLM auth changes.

**Single-shot via `InMemoryRunner`.** ADK requires an explicit `Runner` + `SessionService` + event iteration even for one-shot calls. Heavier than CrewAI's `crew.kickoff()` or LangGraph's `agent.invoke()`, but the pattern is canonical. `InMemoryRunner` is fine for local dev; production deploys to Agent Runtime, Cloud Run, or GKE Autopilot via the [Agents CLI](https://github.com/google/agents-cli).

**Agent Gateway interaction.** If you deploy to Google's managed Agent Runtime, Google's Agent Gateway + Model Armor enforces Google-defined policies before your code runs. ACP's `@governed` decorator runs in your code, after Agent Gateway. The two layers are complementary — Agent Gateway for Google's prompt-injection / data-leakage protections, ACP for your customer-pluggable policy, identity attribution, and audit.

## Where this fits

The starter at [acp-governance-sdks/examples/starters/google-adk](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/starters/google-adk) is the runnable reference. Clone, swap the placeholder tool, ship.

[Google Agents CLI integration guide →](/integrations/google-agents-cli) · [Reference architecture →](/reference-architecture) · [Three-minute integrations index →](/series/three-minute-governance) · [Get started →](/getting-started)

