Skip to content

Commit ce95d59

Browse files
author
Imran Siddique
committed
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>
1 parent cb54108 commit ce95d59

3 files changed

Lines changed: 115 additions & 5 deletions

File tree

contributing/samples/agent_governance/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ If `agentmesh-platform` is not installed, the plugin raises `ImportError`
6161
at construction time. Pass `fail_open=True` to degrade gracefully instead
6262
(all calls pass through with a logged warning).
6363

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+
6477
## Links
6578

6679
- [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)

src/google/adk_community/plugins/agent_governance_plugin.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
from __future__ import annotations
2525

26+
import asyncio
27+
import functools
2628
import logging
2729
from pathlib import Path
2830
from typing import Any, Optional
@@ -53,10 +55,15 @@ class AgentGovernancePlugin(BasePlugin):
5355
fail_open: If ``True``, tool calls proceed when ``agentmesh-platform``
5456
is not installed (logs a warning). If ``False`` (default), raises
5557
``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``.
5662
5763
Raises:
5864
ImportError: If ``agentmesh-platform`` is not installed and
5965
``fail_open`` is False.
66+
RuntimeError: If ``strict`` is True and a policy file fails to load.
6067
6168
Example::
6269
@@ -73,15 +80,16 @@ def __init__(
7380
policy_dir: str | Path,
7481
agent_did: str = "did:mesh:adk-agent",
7582
fail_open: bool = False,
83+
strict: bool = False,
7684
) -> None:
7785
super().__init__(name="agent_governance")
7886
self._policy_dir = Path(policy_dir).resolve()
7987
self._agent_did = agent_did
8088
self._engine = None
8189
self._audit = None
82-
self._setup(fail_open=fail_open)
90+
self._setup(fail_open=fail_open, strict=strict)
8391

84-
def _setup(self, *, fail_open: bool) -> None:
92+
def _setup(self, *, fail_open: bool, strict: bool) -> None:
8593
"""Initialize AGT policy engine and audit service."""
8694
try:
8795
from agentmesh.governance.policy import PolicyEngine
@@ -96,6 +104,10 @@ def _setup(self, *, fail_open: bool) -> None:
96104
self._engine.load_yaml(f.read_text())
97105
logger.info("Loaded policy: %s", f.name)
98106
except Exception as exc:
107+
if strict:
108+
raise RuntimeError(
109+
f"Failed to load policy {f.name}: {exc}"
110+
) from exc
99111
logger.warning("Skipped %s: %s", f.name, exc)
100112
else:
101113
logger.warning(
@@ -138,9 +150,14 @@ async def before_tool_callback(
138150
"tool_args": tool_args,
139151
}
140152

141-
result = self._engine.evaluate(
142-
agent_did=self._agent_did,
143-
context=context,
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+
),
144161
)
145162

146163
if self._audit:

tests/plugins/test_agent_governance_plugin.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,83 @@ def test_custom_agent_did(self, policy_dir: Path):
297297
agent_did="did:mesh:custom-agent",
298298
)
299299
assert plugin._agent_did == "did:mesh:custom-agent"
300+
301+
def test_strict_mode_raises_on_bad_policy(self, tmp_path: Path):
302+
"""With strict=True, plugin raises RuntimeError on policy load failure."""
303+
_install_fake_agentmesh()
304+
305+
# Patch _FakePolicyEngine.load_yaml to raise on bad content
306+
original_load = _FakePolicyEngine.load_yaml
307+
308+
def failing_load(self, text):
309+
if "INVALID" in text:
310+
raise ValueError("invalid YAML syntax")
311+
original_load(self, text)
312+
313+
_FakePolicyEngine.load_yaml = failing_load
314+
315+
policies = tmp_path / "policies"
316+
policies.mkdir()
317+
(policies / "bad.yaml").write_text("INVALID policy content")
318+
319+
try:
320+
with pytest.raises(RuntimeError, match="Failed to load policy"):
321+
AgentGovernancePlugin(policy_dir=policies, strict=True)
322+
finally:
323+
_FakePolicyEngine.load_yaml = original_load
324+
325+
def test_non_strict_skips_bad_policy(self, tmp_path: Path):
326+
"""Without strict mode, bad policies are skipped with a warning."""
327+
_install_fake_agentmesh()
328+
329+
original_load = _FakePolicyEngine.load_yaml
330+
331+
def failing_load(self, text):
332+
if "INVALID" in text:
333+
raise ValueError("invalid YAML syntax")
334+
original_load(self, text)
335+
336+
_FakePolicyEngine.load_yaml = failing_load
337+
338+
policies = tmp_path / "policies"
339+
policies.mkdir()
340+
(policies / "good.yaml").write_text("valid policy")
341+
(policies / "bad.yaml").write_text("INVALID policy content")
342+
343+
try:
344+
plugin = AgentGovernancePlugin(policy_dir=policies)
345+
# Good policy loaded, bad one skipped
346+
assert len(plugin._engine._policies) == 1
347+
finally:
348+
_FakePolicyEngine.load_yaml = original_load
349+
350+
@pytest.mark.asyncio
351+
async def test_evaluate_runs_in_thread_pool(self, policy_dir: Path):
352+
"""Policy evaluation is offloaded to a thread pool executor."""
353+
import asyncio
354+
355+
_install_fake_agentmesh()
356+
plugin = AgentGovernancePlugin(policy_dir=policy_dir)
357+
358+
# Track which thread evaluate() runs on
359+
import threading
360+
361+
eval_thread = None
362+
original_evaluate = plugin._engine.evaluate
363+
364+
def tracking_evaluate(**kwargs):
365+
nonlocal eval_thread
366+
eval_thread = threading.current_thread()
367+
return original_evaluate(**kwargs)
368+
369+
plugin._engine.evaluate = tracking_evaluate
370+
main_thread = threading.current_thread()
371+
372+
await plugin.before_tool_callback(
373+
tool=_FakeBaseTool("web_search"),
374+
tool_args={"query": "test"},
375+
tool_context=_FakeToolContext(),
376+
)
377+
378+
assert eval_thread is not None
379+
assert eval_thread != main_thread

0 commit comments

Comments
 (0)