Skip to main content

Google ADK Plugin

The watchlight-adk plugin brings Watchlight AI Agent Runtime Governance to any Google Agent Development Kit (ADK) application. Install it, register it on your Runner, and every tool call is policy-evaluated, every action is logged to the execution graph.

Quick Start

pip install watchlight-adk google-adk
from google.adk.agents import Agent
from google.adk.runners import Runner
from watchlight_adk import WatchlightPlugin

# Create your agent
agent = Agent(
name="my-agent",
model="gemini-2.5-flash",
tools=[get_data, run_analysis],
)

# Add Watchlight governance
plugin = WatchlightPlugin(apdp_url="http://localhost:8081")
runner = Runner(agent=agent, app_name="my-app", plugins=[plugin])

That's it. Every tool call is now policy-evaluated, every agent invocation is trust-checked, and every action is logged to the execution graph.

Configuration

WatchlightPlugin(
# Required: WL-APDP endpoint
apdp_url="http://localhost:8081",

# Action verb mapping (tool name → governance action)
action_map={
"get_market_data": "read",
"execute_trade": "execute",
"search_docs": "search",
},

# Optional: WL-Registry for tool discovery
registry_url="http://localhost:8080",
registry_api_key="agent-api-key",

# Behavior
fail_closed=True, # Deny if APDP unreachable (default: True)
log_decisions=True, # Log Allow/Deny to stdout (default: True)
tenant_id="acme-corp", # Multi-tenant scope
)

Action Verb Model

The plugin classifies every tool call with a governance action verb. This determines which policies apply and whether the action aligns with the agent's declared intent.

Convention-Based Defaults

When a tool is not in the explicit action_map, the plugin resolves the action from the tool name:

Tool Name PrefixActionRationale
get_*, fetch_*, load_*readData retrieval, no side effects
list_*, show_*listCollection enumeration
search_*, find_*, query_*searchFiltered retrieval
create_*, add_*, new_*createResource creation
update_*, set_*, modify_*updateResource mutation
delete_*, remove_*, drop_*deleteResource destruction
execute_*, run_*, trigger_*executeSide-effecting execution
analyze_*analyzeData analysis
summarize_*summarizeSummarization
generate_*generateContent generation
approve_*, confirm_*approveApproval workflow
review_*reviewReview workflow
embed_*embedVector embedding
(no match)executeConservative default

Override Precedence

1. Explicit action_map entry (highest priority)
2. Convention-based prefix matching
3. Default: "execute" (most restrictive)

The default execute is intentionally conservative — most intent categories (Research, DataAnalysis, SecurityCompliance) exclude it, so an unmapped tool won't accidentally get broader access.

LLM Access

LLM calls use Action::"chat" with Model:: resource type. This aligns with the authorization model where chat is a distinct action that can be independently governed.

Policy Examples

// All agents can read data tools
permit(principal, action == Action::"read", resource)
when {
(principal == Agent::"research-analyst" ||
principal == Agent::"portfolio-manager" ||
principal == Agent::"compliance-officer") &&
(resource == Tool::"get_market_data" ||
resource == Tool::"get_portfolio" ||
resource == Tool::"get_audit_log")
};

// Only portfolio-manager can execute trades
permit(
principal == Agent::"portfolio-manager",
action == Action::"execute",
resource == Tool::"execute_trade"
);

// All agents can chat with the LLM
permit(principal, action == Action::"chat", resource == Model::"gemini-chat")
when {
principal == Agent::"research-analyst" ||
principal == Agent::"portfolio-manager" ||
principal == Agent::"compliance-officer"
};

// Defense-in-depth: deny untrusted agents
forbid(principal, action, resource)
when {
principal has trust_state && principal.trust_state != "trusted"
};

What Happens on Denial

When a tool call is denied by policy:

  1. The plugin returns a successful response (not an error) to the agent
  2. The message clearly states the denial is a permanent policy restriction
  3. The agent is instructed not to retry and to inform the user
  4. The denial is logged in the execution graph with the determining policy name
Agent response: "This action was denied by Watchlight AI Agent Runtime
Governance. The research-analyst role is not authorized to execute
execute_trade."

How Events Flow

Plugin                          WL-APDP                    Execution Graph
│ │ │
├─ POST /authorize ────────────▶│ │
│ ├─ PolicyDecisionMade ────────▶│
│ │ (agent_id, action, │
│◀─ {decision, policy_name} ────┤ resource, decision) │
│ │ │
├─ POST /v1/events ────────────▶│ │
│ (execution_started, ├─ publish to NATS ───────────▶│
│ tool_call_observed, │ │
│ execution_completed) │ │
  • Policy decisions are emitted by APDP when it evaluates /authorize
  • Lifecycle events are batched by the plugin and flushed to APDP's /v1/events endpoint at execution end
  • APDP publishes all events to NATS → execution graph materializes them

The plugin never connects to NATS directly. APDP is the single gateway.

Tool Registration

When registry_url is configured, the plugin automatically registers its tools in WL-Registry on agent startup:

plugin = WatchlightPlugin(
apdp_url="http://localhost:8081",
registry_url="http://localhost:8080",
registry_api_key="agent-key",
action_map={
"get_market_data": "read",
"execute_trade": "execute",
},
)

Each tool is registered with:

  • Name — function name
  • Action verb — governance classification (read, execute, etc.)
  • Description — from function docstring
  • Frameworkgoogle_adk

Tools are visible in the WL-Registry dashboard and API.

ADK Callback Hooks

The plugin implements these ADK BasePlugin callbacks:

CallbackAction
before_agent_callbackTrust check, emit execution_started, register tools
before_tool_callbackPolicy evaluation (Allow/Deny)
after_tool_callbackEmit tool_call_observed
before_model_callbackLLM access policy check
after_agent_callbackEmit execution_completed, flush events

Agent Naming

ADK requires underscores in agent names (research_analyst). The plugin automatically converts to hyphens for the Cedar principal (research-analyst). Policies should use the hyphenated form:

permit(principal == Agent::"research-analyst", ...)

Fail-Closed Behavior

By default, the plugin denies all actions when APDP is unreachable:

# Default: fail closed (recommended for production)
plugin = WatchlightPlugin(apdp_url="...", fail_closed=True)

# Fail open (for development only)
plugin = WatchlightPlugin(apdp_url="...", fail_closed=False)

When fail_closed=False and APDP is unreachable, actions are allowed with a warning logged.