google-adk
Governing Google Agent Development Kit (ADK) agents in Python, zero-code.
Confirmed live against google-adk==2.3.0: FunctionTool has no synchronous call method at
all — every tool call is async and flows through run_async(*, args, tool_context). raxit's
auto-patcher wraps google.adk.tools.function_tool.FunctionTool.run_async, falling back to
BaseTool.run_async for other tool subclasses, so every tool a real Runner dispatches
resolves to permit / defer / deny before it executes, with no governance code in the
agent. Python only — google-adk has no JS/TS SDK.
Run it yourself
For your own google-adk agent, install the SDK and launch it under raxit run:
pip install raxit
raxit run -- python your_agent.pyRun the same file without raxit run and it runs ungoverned — governance is a deploy-time
choice, not a code change.
How it works
Tools are ordinary FunctionTool-wrapped functions with no governance code. The only "wiring"
is each wrapped function's __name__, the policy resource it's governed as:
# your_agent.py — imports NOTHING from raxit
from google.adk.tools.function_tool import FunctionTool
def db_write(payload: str) -> str: ...
db_write.__name__ = "db.write"
agent = Agent(tools=[FunctionTool(db_write)], model=model)apiVersion: raxit.security/v1
agent: google-adk-bot
default: block
rules:
- effect: allow
resource: fetch_docs
- effect: review # unconditional review == defer.policy
resource: db.write
# shell.exec is intentionally UNLISTED -> default-deny (deny.default_or_forbid).A denied or deferred call raises raxit.autopatch.RaxitDenied from inside run_async, carrying
the reason code — the tool body never executes. raxit explain <call_id> shows the full
decision record.
Real captured output from a run:
[PASS] autopatch: install() governed the google-adk FunctionTool.run_async dispatch — patched={'google-adk': ['run_async']}
fetch_docs -> permit -> ran
db.write -> defer -> RaxitDenied raised: reason=defer.policy
shell.exec -> deny -> RaxitDenied raised: reason=deny.default_or_forbid
audit.verify -> ok=True signatures_verified=True signed_count=3 count=3
all checks PASSScope: no MODIFY here
run_async's tool input arrives as the keyword-only args mapping alongside a tool_context
object, not as a bare positional dict or a tool_input/input kwarg — so argument-rewriting
verdicts (MODIFY) aren't exercised here. Same limitation our test suite documents for this
framework. The demo builds a minimal ToolContext from an
in-memory session rather than a real Runner/LLM — ToolContext needs an InvocationContext,
which needs a session_service + session, all constructible offline.
What got denied and why
defer.policy—db.writematched thereviewrule; it's suspended for human approval (see Approvals). The demo runs withRAXIT_DEFER_TIMEOUT=0, so the defer surfaces as a raisedRaxitDeniedrather than blocking inline.deny.default_or_forbid—shell.execappears nowhere in the policy, so it falls todefault: block. The tool body never ran, confirmed by the demo's own call log.
Every decision — the permit, the defer, and the deny — lands in one hash-chained,
Ed25519-signed Decision Provenance log, verified green with client.audit.verify(signatures=True).
See Audit and Decisions for the full
permit / defer / deny vocabulary and reason-code catalog.
Related
- LangGraph (Python) — the same docs/ops-bot story, zero-code autopatch at a different interception point.
- DSPy, smolagents — the same story on other Python frameworks.

