Skip to content

Commit 73f448c

Browse files
committed
feat(examples): add governance engine and skill_run tool filter
1 parent 4d3ef68 commit 73f448c

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Code review tool filters package."""
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Tool-level filter guarding LLM-initiated skill_run calls."""
7+
from trpc_agent_sdk.abc import FilterType
8+
from trpc_agent_sdk.filter import BaseFilter
9+
10+
from review.governance import GovernanceEngine
11+
12+
13+
class GovernanceToolFilter(BaseFilter):
14+
"""Blocks skill_run commands the GovernanceEngine does not allow."""
15+
16+
def __init__(self, engine: GovernanceEngine, on_event=None):
17+
super().__init__()
18+
self._type = FilterType.TOOL
19+
self._name = "cr_governance"
20+
self._engine = engine
21+
self._on_event = on_event
22+
23+
async def _before(self, ctx, req, rsp):
24+
command = ""
25+
if isinstance(req, dict):
26+
command = str(req.get("command", "") or "")
27+
if not command:
28+
return
29+
decision = self._engine.check_command(command)
30+
if self._on_event is not None:
31+
self._on_event(decision)
32+
if decision.decision != "allow":
33+
rsp.rsp = {
34+
"error": f"blocked by governance filter ({decision.rule})",
35+
"decision": decision.decision,
36+
"reason": decision.reason,
37+
}
38+
rsp.is_continue = False
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Governance policy: script allowlist, forbidden paths, network policy,
7+
risk heuristics and execution budget."""
8+
import posixpath
9+
from dataclasses import dataclass, field
10+
11+
DEFAULT_ALLOWED_SCRIPTS = (
12+
"parse_diff.py",
13+
"check_security.py",
14+
"check_async_leak.py",
15+
"check_db_lifecycle.py",
16+
"check_tests_missing.py",
17+
"check_secrets.py",
18+
)
19+
20+
NETWORK_TOOLS = frozenset({"curl", "wget", "pip", "pip3", "ssh", "scp", "nc", "git", "apt", "apt-get"})
21+
RISK_TOKENS = frozenset({"sudo", "docker", "chmod", "chown", "mount", "rm", "mkfs"})
22+
PYTHON_EXECUTABLES = frozenset({"python", "python3"})
23+
24+
25+
@dataclass
26+
class GovernanceDecision:
27+
"""Outcome of one governance check."""
28+
29+
target: str
30+
decision: str # allow | deny | needs_human_review
31+
rule: str = ""
32+
reason: str = ""
33+
34+
35+
@dataclass
36+
class GovernanceEngine:
37+
"""Stateful policy engine; deny/needs_human_review must never reach the sandbox."""
38+
39+
allowed_scripts: tuple = DEFAULT_ALLOWED_SCRIPTS
40+
max_runs: int = 20
41+
max_sandbox_seconds: float = 300.0
42+
_runs: int = field(default=0, init=False)
43+
_elapsed: float = field(default=0.0, init=False)
44+
45+
def record_run(self, duration_s: float) -> None:
46+
self._runs += 1
47+
self._elapsed += max(0.0, duration_s)
48+
49+
def _check_budget(self, target: str):
50+
if self._runs >= self.max_runs or self._elapsed >= self.max_sandbox_seconds:
51+
return GovernanceDecision(target, "deny", "budget_exceeded",
52+
f"budget: {self._runs} runs / {self._elapsed:.1f}s used")
53+
return None
54+
55+
@staticmethod
56+
def _check_paths(target: str, argv):
57+
for arg in argv:
58+
if arg.startswith("/") or arg.startswith("~") or ".." in arg:
59+
return GovernanceDecision(target, "deny", "forbidden_path",
60+
f"path escapes workspace: {arg!r}")
61+
return None
62+
63+
def check_script(self, script: str, argv) -> GovernanceDecision:
64+
target = f"{script} {' '.join(argv)}".strip()
65+
blocked = self._check_budget(target) or self._check_paths(target, argv)
66+
if blocked:
67+
return blocked
68+
if posixpath.basename(script) not in self.allowed_scripts:
69+
return GovernanceDecision(target, "deny", "script_allowlist",
70+
f"script {script!r} is not allowlisted")
71+
return GovernanceDecision(target, "allow")
72+
73+
def check_command(self, command: str) -> GovernanceDecision:
74+
tokens = command.split()
75+
if not tokens:
76+
return GovernanceDecision(command, "deny", "empty_command", "empty command")
77+
head = posixpath.basename(tokens[0])
78+
if head in NETWORK_TOOLS or any(posixpath.basename(t) in NETWORK_TOOLS for t in tokens):
79+
return GovernanceDecision(command, "deny", "network_policy",
80+
"non-whitelisted network access")
81+
if head in RISK_TOKENS or any(posixpath.basename(t) in RISK_TOKENS for t in tokens):
82+
return GovernanceDecision(command, "needs_human_review", "risk_command",
83+
"high-risk command requires human review")
84+
if head in PYTHON_EXECUTABLES and len(tokens) >= 2:
85+
return self.check_script(tokens[1], tokens[2:])
86+
return GovernanceDecision(command, "needs_human_review", "unknown_command",
87+
f"executable {head!r} is not recognized")
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Tests for governance decisions and the tool filter."""
7+
from trpc_agent_sdk.abc import FilterResult
8+
9+
from review.governance import GovernanceEngine
10+
from filters_cr.governance_filter import GovernanceToolFilter
11+
12+
13+
def test_allowlisted_script_allowed():
14+
eng = GovernanceEngine()
15+
d = eng.check_script("check_security.py", ["work/inputs/changes.diff"])
16+
assert d.decision == "allow"
17+
18+
19+
def test_unknown_script_denied():
20+
d = GovernanceEngine().check_script("steal_data.py", ["work/inputs/changes.diff"])
21+
assert d.decision == "deny"
22+
assert d.rule == "script_allowlist"
23+
24+
25+
def test_forbidden_path_denied():
26+
eng = GovernanceEngine()
27+
assert eng.check_script("check_security.py", ["/etc/passwd"]).decision == "deny"
28+
assert eng.check_script("check_security.py", ["../../secrets"]).decision == "deny"
29+
30+
31+
def test_budget_exceeded_denied():
32+
eng = GovernanceEngine(max_runs=1)
33+
eng.record_run(0.1)
34+
d = eng.check_script("check_security.py", ["work/inputs/changes.diff"])
35+
assert d.decision == "deny"
36+
assert d.rule == "budget_exceeded"
37+
38+
39+
def test_command_network_tool_denied():
40+
d = GovernanceEngine().check_command("curl http://evil.example.com")
41+
assert d.decision == "deny"
42+
assert d.rule == "network_policy"
43+
44+
45+
def test_command_risk_token_needs_review():
46+
d = GovernanceEngine().check_command("sudo rm -rf /")
47+
assert d.decision == "needs_human_review"
48+
49+
50+
def test_command_allowlisted_python_script_allowed():
51+
d = GovernanceEngine().check_command(
52+
"python3 skills/code-review/scripts/check_security.py work/inputs/changes.diff")
53+
assert d.decision == "allow"
54+
55+
56+
def test_command_unknown_executable_needs_review():
57+
d = GovernanceEngine().check_command("make all")
58+
assert d.decision == "needs_human_review"
59+
60+
61+
async def test_filter_blocks_denied_command():
62+
events = []
63+
filt = GovernanceToolFilter(GovernanceEngine(), on_event=events.append)
64+
rsp = FilterResult()
65+
await filt._before(None, {"skill": "code-review", "command": "curl http://x"}, rsp)
66+
assert rsp.is_continue is False
67+
assert "blocked" in str(rsp.rsp)
68+
assert events and events[0].decision == "deny"
69+
70+
71+
async def test_filter_allows_good_command():
72+
filt = GovernanceToolFilter(GovernanceEngine())
73+
rsp = FilterResult()
74+
await filt._before(None, {
75+
"skill": "code-review",
76+
"command": "python3 skills/code-review/scripts/parse_diff.py work/inputs/changes.diff",
77+
}, rsp)
78+
assert rsp.is_continue is True

0 commit comments

Comments
 (0)