Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions contributing/samples/agent_governance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Agent Governance Toolkit plugin for Google ADK

An ADK plugin that enforces policy-as-code rules before tool execution using the
[Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)
(MIT licensed).

## Install

```bash
pip install google-adk-community agentmesh-platform
```

## Usage

```python
from pathlib import Path
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk_community.plugins import AgentGovernancePlugin

plugin = AgentGovernancePlugin(
policy_dir=Path(__file__).parent / "policies",
agent_did="did:mesh:my-agent",
)

agent = Agent(
name="governed-agent",
model="gemini-2.0-flash",
tools=[my_tool],
)

runner = Runner(agent=agent, plugins=[plugin], app_name="my-app")
```

## Policy example (`policies/default.yaml`)

```yaml
apiVersion: governance.toolkit/v1
name: adk-agent-policy
rules:
- name: block-dangerous-tools
condition: "action in ['shell_exec', 'file_delete']"
action: deny
- name: rate-limit-api-calls
condition: "action == 'api_call'"
action: allow
limit: "100/hour"
default_action: allow
```

## How it works

The plugin extends `google.adk.plugins.BasePlugin` and implements
`before_tool_callback`. When a tool call is denied by policy, the callback
returns a dict response that short-circuits execution (per the ADK plugin
contract). Allowed calls return `None`, letting the tool proceed normally.

## Fail-closed by default

If `agentmesh-platform` is not installed, the plugin raises `ImportError`
at construction time. Pass `fail_open=True` to degrade gracefully instead
(all calls pass through with a logged warning).

## Strict mode

By default, the plugin skips policy files that fail to parse and logs a
warning. Pass `strict=True` to raise a `RuntimeError` instead, which is
recommended when every policy file is security-critical:

```python
plugin = AgentGovernancePlugin(
policy_dir=Path(__file__).parent / "policies",
strict=True, # abort if any policy fails to load
)
```

## Links

- [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)
- [ADK Plugin docs](https://google.github.io/adk-docs/plugins/)
48 changes: 48 additions & 0 deletions contributing/samples/agent_governance/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example: Google ADK agent with Agent Governance Toolkit policy enforcement.

Demonstrates:
1. Loading YAML governance policies
2. Evaluating policies before tool calls via the ADK plugin lifecycle
3. Producing tamper-evident audit trails
"""

from pathlib import Path

from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk_community.plugins import AgentGovernancePlugin


def create_governed_runner() -> Runner:
"""Create an ADK runner with governance controls."""
plugin = AgentGovernancePlugin(
policy_dir=Path(__file__).parent / "policies",
agent_did="did:mesh:adk-demo-agent",
)

agent = Agent(
name="governed-research-agent",
model="gemini-2.0-flash",
instruction="You are a research assistant with governance controls.",
)

return Runner(agent=agent, plugins=[plugin], app_name="governed-demo")


if __name__ == "__main__":
runner = create_governed_runner()
print(f"Runner created with governance plugin enabled.")
25 changes: 25 additions & 0 deletions contributing/samples/agent_governance/policies/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: governance.toolkit/v1
name: adk-demo-policy
description: Example governance policy for Google ADK agents
rules:
- name: block-shell-execution
condition: "action in ['shell_exec', 'code_exec', 'file_delete']"
action: deny
description: Block dangerous system-level tool calls
priority: 100

- name: rate-limit-api-calls
condition: "action == 'api_call'"
action: allow
limit: "100/hour"
description: Rate limit external API calls
priority: 50

- name: require-approval-for-payments
condition: "action == 'process_payment'"
action: require_approval
approvers: ["admin@example.com"]
description: Payment actions require human approval
priority: 90

default_action: allow
19 changes: 19 additions & 0 deletions src/google/adk_community/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.adk_community.plugins.agent_governance_plugin import (
AgentGovernancePlugin,
)

__all__ = ["AgentGovernancePlugin"]
185 changes: 185 additions & 0 deletions src/google/adk_community/plugins/agent_governance_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""ADK plugin for Agent Governance Toolkit policy enforcement.

Evaluates policy-as-code rules before tool execution using the Agent
Governance Toolkit (https://github.com/microsoft/agent-governance-toolkit).
Denied tool calls are short-circuited with a policy violation response.

Requires: ``pip install agentmesh-platform``
"""

from __future__ import annotations

import asyncio
import functools
import logging
from pathlib import Path
from typing import Any, Optional

from google.adk.plugins.base_plugin import BasePlugin
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.tool_context import ToolContext

logger = logging.getLogger(__name__)


class _GovernanceUnavailableError(ImportError):
"""Raised when agentmesh-platform is not installed and fail_open=False."""


class AgentGovernancePlugin(BasePlugin):
"""ADK plugin that enforces governance policies before tool execution.

Uses the Agent Governance Toolkit to evaluate YAML/OPA/Cedar policies.
When a tool call is denied by policy, the plugin returns a dict response
that short-circuits tool execution (per the ADK plugin contract).

Args:
policy_dir: Absolute or relative path to the directory containing
``*.yaml`` policy files. Resolved relative to the caller's
working directory. Must be provided explicitly.
agent_did: Decentralized identifier for the agent.
fail_open: If ``True``, tool calls proceed when ``agentmesh-platform``
is not installed (logs a warning). If ``False`` (default), raises
``ImportError`` at construction time.
strict: If ``True``, raises on policy load errors instead of
skipping the file. Useful when every policy file is
security-critical and a partial load is unacceptable.
Defaults to ``False``.

Raises:
ImportError: If ``agentmesh-platform`` is not installed and
``fail_open`` is False.
RuntimeError: If ``strict`` is True and a policy file fails to load.

Example::

from google.adk_community.plugins import AgentGovernancePlugin

plugin = AgentGovernancePlugin(
policy_dir=Path(__file__).parent / "policies",
)
runner = Runner(agent=my_agent, plugins=[plugin], ...)
"""

def __init__(
self,
policy_dir: str | Path,
agent_did: str = "did:mesh:adk-agent",
fail_open: bool = False,
strict: bool = False,
) -> None:
super().__init__(name="agent_governance")
self._policy_dir = Path(policy_dir).resolve()
self._agent_did = agent_did
self._engine = None
self._audit = None
self._setup(fail_open=fail_open, strict=strict)

def _setup(self, *, fail_open: bool, strict: bool) -> None:
"""Initialize AGT policy engine and audit service."""
try:
from agentmesh.governance.policy import PolicyEngine
from agentmesh.services.audit import AuditService

self._engine = PolicyEngine()
self._audit = AuditService()

if self._policy_dir.exists():
for f in sorted(self._policy_dir.glob("*.yaml")):
try:
self._engine.load_yaml(f.read_text())
logger.info("Loaded policy: %s", f.name)
except Exception as exc:
if strict:
raise RuntimeError(
f"Failed to load policy {f.name}: {exc}"
) from exc
logger.warning("Skipped %s: %s", f.name, exc)
else:
logger.warning(
"Policy directory does not exist: %s", self._policy_dir
)

logger.info(
"AgentGovernancePlugin initialized (agent=%s, policies=%s)",
self._agent_did,
self._policy_dir,
)
except ImportError:
if not fail_open:
raise _GovernanceUnavailableError(
"agentmesh-platform is required for governance enforcement. "
"Install with: pip install agentmesh-platform"
)
logger.warning(
"agentmesh-platform not installed; governance checks disabled. "
"Install with: pip install agentmesh-platform"
)

async def before_tool_callback(
self,
*,
tool: BaseTool,
tool_args: dict[str, Any],
tool_context: ToolContext,
) -> Optional[dict]:
"""Evaluate governance policy before a tool call.

Returns ``None`` to allow the tool to proceed, or a dict response
to short-circuit execution when the policy denies the call.
"""
if self._engine is None:
return None

context = {
"action": tool.name,
"tool_args": tool_args,
}

loop = asyncio.get_running_loop()
result = await loop.run_in_executor(
None,
functools.partial(
self._engine.evaluate,
agent_did=self._agent_did,
context=context,
),
)

if self._audit:
self._audit.log_policy_decision(
agent_did=self._agent_did,
action=tool.name,
decision=result.action,
policy_name=result.policy_name or "",
data={"tool_args": tool_args, "reason": result.reason},
)

if not result.allowed:
logger.warning(
"Policy denied tool '%s': %s (rule: %s)",
tool.name,
result.reason,
result.matched_rule,
)
return {
"error": "policy_denied",
"reason": result.reason,
"matched_rule": result.matched_rule,
}

return None
13 changes: 13 additions & 0 deletions tests/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Loading
Loading