Skip to content

Commit f9bfa20

Browse files
feat: add AgentGovernancePlugin for tool-call governance (#141)
* feat: add AgentGovernancePlugin for ADK tool-call governance Integrates microsoft/agent-governance-toolkit (AGT) as an ADK BasePlugin for centralized tool-call policy enforcement. - Extends BasePlugin with async before_tool_callback - Evaluates tool calls against YAML policy rules via agentmesh PolicyEngine - Returns None (allow) or dict (deny/short-circuit) per ADK contract - Fail-closed by default; opt-in fail_open=True for graceful degradation - Structured audit logging for all policy decisions - 11 tests covering allow/deny/fail-open/missing-dep/custom-agent-did Signed-off-by: Imran Siddique <imran.siddique@microsoft.com> * feat: add strict mode and async policy evaluation Address review feedback: - Add strict=True parameter that raises RuntimeError on policy load failures instead of silently skipping (for security-critical deployments) - Offload synchronous PolicyEngine.evaluate to thread pool executor via asyncio.run_in_executor to avoid blocking the event loop - Add 3 new tests (strict mode, non-strict skip, thread pool verification) - Update sample README with strict mode documentation Signed-off-by: Imran Siddique <imran.siddique@microsoft.com> --------- Signed-off-by: Imran Siddique <imran.siddique@microsoft.com>
1 parent 647c6c0 commit f9bfa20

7 files changed

Lines changed: 749 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Agent Governance Toolkit plugin for Google ADK
2+
3+
An ADK plugin that enforces policy-as-code rules before tool execution using the
4+
[Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)
5+
(MIT licensed).
6+
7+
## Install
8+
9+
```bash
10+
pip install google-adk-community agentmesh-platform
11+
```
12+
13+
## Usage
14+
15+
```python
16+
from pathlib import Path
17+
from google.adk.agents import Agent
18+
from google.adk.runners import Runner
19+
from google.adk_community.plugins import AgentGovernancePlugin
20+
21+
plugin = AgentGovernancePlugin(
22+
policy_dir=Path(__file__).parent / "policies",
23+
agent_did="did:mesh:my-agent",
24+
)
25+
26+
agent = Agent(
27+
name="governed-agent",
28+
model="gemini-2.0-flash",
29+
tools=[my_tool],
30+
)
31+
32+
runner = Runner(agent=agent, plugins=[plugin], app_name="my-app")
33+
```
34+
35+
## Policy example (`policies/default.yaml`)
36+
37+
```yaml
38+
apiVersion: governance.toolkit/v1
39+
name: adk-agent-policy
40+
rules:
41+
- name: block-dangerous-tools
42+
condition: "action in ['shell_exec', 'file_delete']"
43+
action: deny
44+
- name: rate-limit-api-calls
45+
condition: "action == 'api_call'"
46+
action: allow
47+
limit: "100/hour"
48+
default_action: allow
49+
```
50+
51+
## How it works
52+
53+
The plugin extends `google.adk.plugins.BasePlugin` and implements
54+
`before_tool_callback`. When a tool call is denied by policy, the callback
55+
returns a dict response that short-circuits execution (per the ADK plugin
56+
contract). Allowed calls return `None`, letting the tool proceed normally.
57+
58+
## Fail-closed by default
59+
60+
If `agentmesh-platform` is not installed, the plugin raises `ImportError`
61+
at construction time. Pass `fail_open=True` to degrade gracefully instead
62+
(all calls pass through with a logged warning).
63+
64+
## Strict mode
65+
66+
By default, the plugin skips policy files that fail to parse and logs a
67+
warning. Pass `strict=True` to raise a `RuntimeError` instead, which is
68+
recommended when every policy file is security-critical:
69+
70+
```python
71+
plugin = AgentGovernancePlugin(
72+
policy_dir=Path(__file__).parent / "policies",
73+
strict=True, # abort if any policy fails to load
74+
)
75+
```
76+
77+
## Links
78+
79+
- [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)
80+
- [ADK Plugin docs](https://google.github.io/adk-docs/plugins/)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Example: Google ADK agent with Agent Governance Toolkit policy enforcement.
16+
17+
Demonstrates:
18+
1. Loading YAML governance policies
19+
2. Evaluating policies before tool calls via the ADK plugin lifecycle
20+
3. Producing tamper-evident audit trails
21+
"""
22+
23+
from pathlib import Path
24+
25+
from google.adk.agents import Agent
26+
from google.adk.runners import Runner
27+
from google.adk_community.plugins import AgentGovernancePlugin
28+
29+
30+
def create_governed_runner() -> Runner:
31+
"""Create an ADK runner with governance controls."""
32+
plugin = AgentGovernancePlugin(
33+
policy_dir=Path(__file__).parent / "policies",
34+
agent_did="did:mesh:adk-demo-agent",
35+
)
36+
37+
agent = Agent(
38+
name="governed-research-agent",
39+
model="gemini-2.0-flash",
40+
instruction="You are a research assistant with governance controls.",
41+
)
42+
43+
return Runner(agent=agent, plugins=[plugin], app_name="governed-demo")
44+
45+
46+
if __name__ == "__main__":
47+
runner = create_governed_runner()
48+
print(f"Runner created with governance plugin enabled.")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
apiVersion: governance.toolkit/v1
2+
name: adk-demo-policy
3+
description: Example governance policy for Google ADK agents
4+
rules:
5+
- name: block-shell-execution
6+
condition: "action in ['shell_exec', 'code_exec', 'file_delete']"
7+
action: deny
8+
description: Block dangerous system-level tool calls
9+
priority: 100
10+
11+
- name: rate-limit-api-calls
12+
condition: "action == 'api_call'"
13+
action: allow
14+
limit: "100/hour"
15+
description: Rate limit external API calls
16+
priority: 50
17+
18+
- name: require-approval-for-payments
19+
condition: "action == 'process_payment'"
20+
action: require_approval
21+
approvers: ["admin@example.com"]
22+
description: Payment actions require human approval
23+
priority: 90
24+
25+
default_action: allow
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.adk_community.plugins.agent_governance_plugin import (
16+
AgentGovernancePlugin,
17+
)
18+
19+
__all__ = ["AgentGovernancePlugin"]
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""ADK plugin for Agent Governance Toolkit policy enforcement.
16+
17+
Evaluates policy-as-code rules before tool execution using the Agent
18+
Governance Toolkit (https://github.com/microsoft/agent-governance-toolkit).
19+
Denied tool calls are short-circuited with a policy violation response.
20+
21+
Requires: ``pip install agentmesh-platform``
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import asyncio
27+
import functools
28+
import logging
29+
from pathlib import Path
30+
from typing import Any, Optional
31+
32+
from google.adk.plugins.base_plugin import BasePlugin
33+
from google.adk.tools.base_tool import BaseTool
34+
from google.adk.tools.tool_context import ToolContext
35+
36+
logger = logging.getLogger(__name__)
37+
38+
39+
class _GovernanceUnavailableError(ImportError):
40+
"""Raised when agentmesh-platform is not installed and fail_open=False."""
41+
42+
43+
class AgentGovernancePlugin(BasePlugin):
44+
"""ADK plugin that enforces governance policies before tool execution.
45+
46+
Uses the Agent Governance Toolkit to evaluate YAML/OPA/Cedar policies.
47+
When a tool call is denied by policy, the plugin returns a dict response
48+
that short-circuits tool execution (per the ADK plugin contract).
49+
50+
Args:
51+
policy_dir: Absolute or relative path to the directory containing
52+
``*.yaml`` policy files. Resolved relative to the caller's
53+
working directory. Must be provided explicitly.
54+
agent_did: Decentralized identifier for the agent.
55+
fail_open: If ``True``, tool calls proceed when ``agentmesh-platform``
56+
is not installed (logs a warning). If ``False`` (default), raises
57+
``ImportError`` at construction time.
58+
strict: If ``True``, raises on policy load errors instead of
59+
skipping the file. Useful when every policy file is
60+
security-critical and a partial load is unacceptable.
61+
Defaults to ``False``.
62+
63+
Raises:
64+
ImportError: If ``agentmesh-platform`` is not installed and
65+
``fail_open`` is False.
66+
RuntimeError: If ``strict`` is True and a policy file fails to load.
67+
68+
Example::
69+
70+
from google.adk_community.plugins import AgentGovernancePlugin
71+
72+
plugin = AgentGovernancePlugin(
73+
policy_dir=Path(__file__).parent / "policies",
74+
)
75+
runner = Runner(agent=my_agent, plugins=[plugin], ...)
76+
"""
77+
78+
def __init__(
79+
self,
80+
policy_dir: str | Path,
81+
agent_did: str = "did:mesh:adk-agent",
82+
fail_open: bool = False,
83+
strict: bool = False,
84+
) -> None:
85+
super().__init__(name="agent_governance")
86+
self._policy_dir = Path(policy_dir).resolve()
87+
self._agent_did = agent_did
88+
self._engine = None
89+
self._audit = None
90+
self._setup(fail_open=fail_open, strict=strict)
91+
92+
def _setup(self, *, fail_open: bool, strict: bool) -> None:
93+
"""Initialize AGT policy engine and audit service."""
94+
try:
95+
from agentmesh.governance.policy import PolicyEngine
96+
from agentmesh.services.audit import AuditService
97+
98+
self._engine = PolicyEngine()
99+
self._audit = AuditService()
100+
101+
if self._policy_dir.exists():
102+
for f in sorted(self._policy_dir.glob("*.yaml")):
103+
try:
104+
self._engine.load_yaml(f.read_text())
105+
logger.info("Loaded policy: %s", f.name)
106+
except Exception as exc:
107+
if strict:
108+
raise RuntimeError(
109+
f"Failed to load policy {f.name}: {exc}"
110+
) from exc
111+
logger.warning("Skipped %s: %s", f.name, exc)
112+
else:
113+
logger.warning(
114+
"Policy directory does not exist: %s", self._policy_dir
115+
)
116+
117+
logger.info(
118+
"AgentGovernancePlugin initialized (agent=%s, policies=%s)",
119+
self._agent_did,
120+
self._policy_dir,
121+
)
122+
except ImportError:
123+
if not fail_open:
124+
raise _GovernanceUnavailableError(
125+
"agentmesh-platform is required for governance enforcement. "
126+
"Install with: pip install agentmesh-platform"
127+
)
128+
logger.warning(
129+
"agentmesh-platform not installed; governance checks disabled. "
130+
"Install with: pip install agentmesh-platform"
131+
)
132+
133+
async def before_tool_callback(
134+
self,
135+
*,
136+
tool: BaseTool,
137+
tool_args: dict[str, Any],
138+
tool_context: ToolContext,
139+
) -> Optional[dict]:
140+
"""Evaluate governance policy before a tool call.
141+
142+
Returns ``None`` to allow the tool to proceed, or a dict response
143+
to short-circuit execution when the policy denies the call.
144+
"""
145+
if self._engine is None:
146+
return None
147+
148+
context = {
149+
"action": tool.name,
150+
"tool_args": tool_args,
151+
}
152+
153+
loop = asyncio.get_running_loop()
154+
result = await loop.run_in_executor(
155+
None,
156+
functools.partial(
157+
self._engine.evaluate,
158+
agent_did=self._agent_did,
159+
context=context,
160+
),
161+
)
162+
163+
if self._audit:
164+
self._audit.log_policy_decision(
165+
agent_did=self._agent_did,
166+
action=tool.name,
167+
decision=result.action,
168+
policy_name=result.policy_name or "",
169+
data={"tool_args": tool_args, "reason": result.reason},
170+
)
171+
172+
if not result.allowed:
173+
logger.warning(
174+
"Policy denied tool '%s': %s (rule: %s)",
175+
tool.name,
176+
result.reason,
177+
result.matched_rule,
178+
)
179+
return {
180+
"error": "policy_denied",
181+
"reason": result.reason,
182+
"matched_rule": result.matched_rule,
183+
}
184+
185+
return None

tests/plugins/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.

0 commit comments

Comments
 (0)