Skip to content

Commit 9a2492c

Browse files
Imran SiddiqueCopilot
andcommitted
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 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Imran Siddique <imran.siddique@microsoft.com>
1 parent 647c6c0 commit 9a2492c

7 files changed

Lines changed: 639 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
## Links
65+
66+
- [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)
67+
- [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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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 logging
27+
from pathlib import Path
28+
from typing import Any, Optional
29+
30+
from google.adk.plugins.base_plugin import BasePlugin
31+
from google.adk.tools.base_tool import BaseTool
32+
from google.adk.tools.tool_context import ToolContext
33+
34+
logger = logging.getLogger(__name__)
35+
36+
37+
class _GovernanceUnavailableError(ImportError):
38+
"""Raised when agentmesh-platform is not installed and fail_open=False."""
39+
40+
41+
class AgentGovernancePlugin(BasePlugin):
42+
"""ADK plugin that enforces governance policies before tool execution.
43+
44+
Uses the Agent Governance Toolkit to evaluate YAML/OPA/Cedar policies.
45+
When a tool call is denied by policy, the plugin returns a dict response
46+
that short-circuits tool execution (per the ADK plugin contract).
47+
48+
Args:
49+
policy_dir: Absolute or relative path to the directory containing
50+
``*.yaml`` policy files. Resolved relative to the caller's
51+
working directory. Must be provided explicitly.
52+
agent_did: Decentralized identifier for the agent.
53+
fail_open: If ``True``, tool calls proceed when ``agentmesh-platform``
54+
is not installed (logs a warning). If ``False`` (default), raises
55+
``ImportError`` at construction time.
56+
57+
Raises:
58+
ImportError: If ``agentmesh-platform`` is not installed and
59+
``fail_open`` is False.
60+
61+
Example::
62+
63+
from google.adk_community.plugins import AgentGovernancePlugin
64+
65+
plugin = AgentGovernancePlugin(
66+
policy_dir=Path(__file__).parent / "policies",
67+
)
68+
runner = Runner(agent=my_agent, plugins=[plugin], ...)
69+
"""
70+
71+
def __init__(
72+
self,
73+
policy_dir: str | Path,
74+
agent_did: str = "did:mesh:adk-agent",
75+
fail_open: bool = False,
76+
) -> None:
77+
super().__init__(name="agent_governance")
78+
self._policy_dir = Path(policy_dir).resolve()
79+
self._agent_did = agent_did
80+
self._engine = None
81+
self._audit = None
82+
self._setup(fail_open=fail_open)
83+
84+
def _setup(self, *, fail_open: bool) -> None:
85+
"""Initialize AGT policy engine and audit service."""
86+
try:
87+
from agentmesh.governance.policy import PolicyEngine
88+
from agentmesh.services.audit import AuditService
89+
90+
self._engine = PolicyEngine()
91+
self._audit = AuditService()
92+
93+
if self._policy_dir.exists():
94+
for f in sorted(self._policy_dir.glob("*.yaml")):
95+
try:
96+
self._engine.load_yaml(f.read_text())
97+
logger.info("Loaded policy: %s", f.name)
98+
except Exception as exc:
99+
logger.warning("Skipped %s: %s", f.name, exc)
100+
else:
101+
logger.warning(
102+
"Policy directory does not exist: %s", self._policy_dir
103+
)
104+
105+
logger.info(
106+
"AgentGovernancePlugin initialized (agent=%s, policies=%s)",
107+
self._agent_did,
108+
self._policy_dir,
109+
)
110+
except ImportError:
111+
if not fail_open:
112+
raise _GovernanceUnavailableError(
113+
"agentmesh-platform is required for governance enforcement. "
114+
"Install with: pip install agentmesh-platform"
115+
)
116+
logger.warning(
117+
"agentmesh-platform not installed; governance checks disabled. "
118+
"Install with: pip install agentmesh-platform"
119+
)
120+
121+
async def before_tool_callback(
122+
self,
123+
*,
124+
tool: BaseTool,
125+
tool_args: dict[str, Any],
126+
tool_context: ToolContext,
127+
) -> Optional[dict]:
128+
"""Evaluate governance policy before a tool call.
129+
130+
Returns ``None`` to allow the tool to proceed, or a dict response
131+
to short-circuit execution when the policy denies the call.
132+
"""
133+
if self._engine is None:
134+
return None
135+
136+
context = {
137+
"action": tool.name,
138+
"tool_args": tool_args,
139+
}
140+
141+
result = self._engine.evaluate(
142+
agent_did=self._agent_did,
143+
context=context,
144+
)
145+
146+
if self._audit:
147+
self._audit.log_policy_decision(
148+
agent_did=self._agent_did,
149+
action=tool.name,
150+
decision=result.action,
151+
policy_name=result.policy_name or "",
152+
data={"tool_args": tool_args, "reason": result.reason},
153+
)
154+
155+
if not result.allowed:
156+
logger.warning(
157+
"Policy denied tool '%s': %s (rule: %s)",
158+
tool.name,
159+
result.reason,
160+
result.matched_rule,
161+
)
162+
return {
163+
"error": "policy_denied",
164+
"reason": result.reason,
165+
"matched_rule": result.matched_rule,
166+
}
167+
168+
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)