Skip to content

Commit 736abfa

Browse files
committed
fix: address tool safety ci issues
1 parent 4f4a1c2 commit 736abfa

15 files changed

Lines changed: 136 additions & 97 deletions

tests/server/openclaw/tools/test_safety_review.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88

99

1010
def _assert_review(
11-
*,
12-
source: str,
13-
action_type: str,
14-
decision: str,
15-
rule_id: str,
16-
finding: str,
17-
risk_level: str,
18-
tool_name: str = "test_tool",
19-
allowed_domains: tuple[str, ...] = ("api.example.com",),
11+
*,
12+
source: str,
13+
action_type: str,
14+
decision: str,
15+
rule_id: str,
16+
finding: str,
17+
risk_level: str,
18+
tool_name: str = "test_tool",
19+
allowed_domains: tuple[str, ...] = ("api.example.com", ),
2020
) -> None:
2121
reviewer = SafetyReviewer(allowed_domains=allowed_domains)
2222

@@ -173,7 +173,7 @@ def test_infinite_loop_blocks_with_rule_finding_report_and_audit() -> None:
173173
def test_sensitive_information_output_blocks_with_rule_finding_report_and_audit() -> None:
174174
source = "api_key = 'sk-live-secret'\nprint(api_key)"
175175

176-
reviewer = SafetyReviewer(allowed_domains=("api.example.com",))
176+
reviewer = SafetyReviewer(allowed_domains=("api.example.com", ))
177177
review = reviewer.review(source, action_type="python", tool_name="secret_tool")
178178

179179
assert review.decision == "deny"

tests/tools/safety/test_cli_subprocess.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ def test_tool_safety_check_subprocess_exit_codes(tmp_path) -> None:
1717
deny_script.write_text("rm -rf /tmp/demo\n", encoding="utf-8")
1818
review_script.write_text("pip install demo\n", encoding="utf-8")
1919

20-
safe = subprocess.run([sys.executable, str(script_path), str(safe_script)], check=False, capture_output=True,
20+
safe = subprocess.run([sys.executable, str(script_path), str(safe_script)],
21+
check=False,
22+
capture_output=True,
2123
text=True)
22-
deny = subprocess.run([sys.executable, str(script_path), str(deny_script)], check=False, capture_output=True,
24+
deny = subprocess.run([sys.executable, str(script_path), str(deny_script)],
25+
check=False,
26+
capture_output=True,
2327
text=True)
24-
review = subprocess.run([sys.executable, str(script_path), str(review_script)], check=False, capture_output=True,
28+
review = subprocess.run([sys.executable, str(script_path), str(review_script)],
29+
check=False,
30+
capture_output=True,
2531
text=True)
2632

2733
assert safe.returncode == 0

tests/tools/safety/test_examples.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@
1515
"source": "print('hello from tool safety')\n",
1616
},
1717
"deny_bash": {
18-
"path": Path("examples/tool_safety/samples/deny.sh"),
19-
"action_type": "bash",
20-
"source": (
21-
"# Inert sample: rm -rf /tmp/demo\n"
22-
"printf '%s\\n' 'destructive delete sample is intentionally not executed'\n"
23-
),
18+
"path":
19+
Path("examples/tool_safety/samples/deny.sh"),
20+
"action_type":
21+
"bash",
22+
"source": ("# Inert sample: rm -rf /tmp/demo\n"
23+
"printf '%s\\n' 'destructive delete sample is intentionally not executed'\n"),
2424
},
2525
"needs_human_review_bash": {
26-
"path": Path("examples/tool_safety/samples/needs_human_review.sh"),
27-
"action_type": "bash",
28-
"source": (
29-
"# Inert sample: npm install left-pad\n"
30-
"printf '%s\\n' 'dependency install sample is intentionally not executed'\n"
31-
),
26+
"path":
27+
Path("examples/tool_safety/samples/needs_human_review.sh"),
28+
"action_type":
29+
"bash",
30+
"source": ("# Inert sample: npm install left-pad\n"
31+
"printf '%s\\n' 'dependency install sample is intentionally not executed'\n"),
3232
},
3333
}
3434

@@ -64,14 +64,14 @@ def test_example_audit_jsonl_lines_are_valid_json() -> None:
6464
assert {record["decision"] for record in records} >= {"allow", "deny", "needs_human_review"}
6565
for record in records:
6666
for key in {
67-
"tool_name",
68-
"decision",
69-
"risk_level",
70-
"rule_id",
71-
"blocked",
72-
"latency",
73-
"timestamp",
74-
"input_sha256",
67+
"tool_name",
68+
"decision",
69+
"risk_level",
70+
"rule_id",
71+
"blocked",
72+
"latency",
73+
"timestamp",
74+
"input_sha256",
7575
}:
7676
assert key in record
7777
assert isinstance(record["blocked"], bool)
@@ -94,16 +94,16 @@ def test_example_audit_matches_current_reviewer_stable_fields() -> None:
9494
)
9595
record = records_by_case[case_name]
9696
for key in {
97-
"tool_name",
98-
"decision",
99-
"risk_level",
100-
"rule_id",
101-
"blocked",
102-
"desensitized",
103-
"action_type",
104-
"input_sha256",
105-
"allowed_domains",
106-
"rules_evaluated",
97+
"tool_name",
98+
"decision",
99+
"risk_level",
100+
"rule_id",
101+
"blocked",
102+
"desensitized",
103+
"action_type",
104+
"input_sha256",
105+
"allowed_domains",
106+
"rules_evaluated",
107107
}:
108108
assert record[key] == review.audit[key]
109109

tests/tools/safety/test_filter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def test_filter_blocks_needs_human_review_by_default() -> None:
7373

7474

7575
async def test_filter_policy_allows_allowlisted_network_request() -> None:
76-
safety_filter = ToolSafetyFilter(policy=ToolSafetyPolicy(allowed_domains=("api.example.com",)))
76+
safety_filter = ToolSafetyFilter(policy=ToolSafetyPolicy(allowed_domains=("api.example.com", )))
7777
rsp = FilterResult()
7878

7979
with patch("trpc_agent_sdk.tools.safety._filter.get_tool_var", return_value=SimpleNamespace(name="web_fetch")):
@@ -92,7 +92,7 @@ def install_tool(command: str):
9292
called = True
9393
return {"accepted": command}
9494

95-
tool = FunctionTool(install_tool, filters=[ToolSafetyFilter(block_decisions=("deny",))])
95+
tool = FunctionTool(install_tool, filters=[ToolSafetyFilter(block_decisions=("deny", ))])
9696

9797
result = await tool.run_async(tool_context=mock_tool_context, args={"command": "npm install left-pad"})
9898

tests/tools/safety/test_policy.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ def test_load_policy_from_yaml(tmp_path) -> None:
3333

3434
policy = load_tool_safety_policy(policy_file)
3535

36-
assert policy.allowed_domains == ("api.example.com",)
37-
assert policy.blocked_paths_for("read_dotenv") == (".env",)
38-
assert policy.blocked_paths_for("read_ssh") == ("~/.ssh",)
39-
assert policy.allowed_commands == ("python3",)
36+
assert policy.allowed_domains == ("api.example.com", )
37+
assert policy.blocked_paths_for("read_dotenv") == (".env", )
38+
assert policy.blocked_paths_for("read_ssh") == ("~/.ssh", )
39+
assert policy.allowed_commands == ("python3", )
4040
assert policy.max_timeout == 30
4141
assert policy.max_output_size == 4096
4242
assert policy.risk_level_for("network_not_allowlisted") == "critical"
@@ -56,7 +56,7 @@ def test_load_policy_with_missing_fields_uses_defaults(tmp_path) -> None:
5656
policy = load_tool_safety_policy(policy_file)
5757
default_policy = ToolSafetyPolicy.default()
5858

59-
assert policy.allowed_domains == ("api.example.com",)
59+
assert policy.allowed_domains == ("api.example.com", )
6060
assert policy.blocked_paths == default_policy.blocked_paths
6161
assert policy.allowed_commands == default_policy.allowed_commands
6262
assert policy.max_timeout == default_policy.max_timeout
@@ -114,7 +114,7 @@ def test_allowed_domains_policy_changes_network_decision_without_code_changes(tm
114114

115115

116116
def test_allowed_domains_policy_does_not_short_circuit_other_rules() -> None:
117-
policy = ToolSafetyPolicy(allowed_domains=("api.example.com",))
117+
policy = ToolSafetyPolicy(allowed_domains=("api.example.com", ))
118118

119119
review = SafetyReviewer(policy=policy).review(
120120
"curl https://api.example.com/download && rm -rf /tmp/demo",

tests/tools/safety/test_telemetry.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@ async def run_async(self, *, tool_context: InvocationContext, args: dict) -> dic
4444
@pytest.mark.parametrize(
4545
("args", "decision", "risk_level", "rule_id"),
4646
[
47-
({"query": "hello"}, "allow", "none", "safe_python"),
48-
({"command": "rm -rf /tmp/demo"}, "deny", "critical", "dangerous_delete"),
49-
({"command": "npm install left-pad"}, "needs_human_review", "medium", "npm_install"),
47+
({
48+
"query": "hello"
49+
}, "allow", "none", "safe_python"),
50+
({
51+
"command": "rm -rf /tmp/demo"
52+
}, "deny", "critical", "dangerous_delete"),
53+
({
54+
"command": "npm install left-pad"
55+
}, "needs_human_review", "medium", "npm_install"),
5056
],
5157
)
5258
async def test_tool_safety_filter_traces_review_attributes(args, decision, risk_level, rule_id) -> None:
@@ -91,9 +97,18 @@ async def test_code_executor_wrapper_traces_review_attributes(code_input, decisi
9197
@pytest.mark.parametrize(
9298
("args", "decision", "risk_level", "rule_id"),
9399
[
94-
({"skill": "demo", "command": "python scripts/run.py"}, "allow", "none", "safe_python"),
95-
({"skill": "demo", "command": "rm -rf /tmp/demo"}, "deny", "critical", "dangerous_delete"),
96-
({"skill": "demo", "command": "npm install left-pad"}, "needs_human_review", "medium", "npm_install"),
100+
({
101+
"skill": "demo",
102+
"command": "python scripts/run.py"
103+
}, "allow", "none", "safe_python"),
104+
({
105+
"skill": "demo",
106+
"command": "rm -rf /tmp/demo"
107+
}, "deny", "critical", "dangerous_delete"),
108+
({
109+
"skill": "demo",
110+
"command": "npm install left-pad"
111+
}, "needs_human_review", "medium", "npm_install"),
97112
],
98113
)
99114
async def test_skill_wrapper_traces_review_attributes(args, decision, risk_level, rule_id) -> None:

tests/tools/safety/test_wrappers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def test_code_executor_wrapper_returns_human_review_result() -> None:
9292

9393

9494
async def test_code_executor_wrapper_uses_provided_reviewer() -> None:
95-
reviewer = SafetyReviewer(allowed_domains=("api.example.com",))
95+
reviewer = SafetyReviewer(allowed_domains=("api.example.com", ))
9696
inner = RecordingCodeExecutor()
9797
wrapper = SafetyReviewedCodeExecutor(inner, reviewer=reviewer)
9898

@@ -143,7 +143,7 @@ async def test_skill_wrapper_returns_human_review_result() -> None:
143143

144144

145145
async def test_skill_wrapper_can_wrap_callable_and_reuse_reviewer() -> None:
146-
reviewer = SafetyReviewer(allowed_domains=("api.example.com",))
146+
reviewer = SafetyReviewer(allowed_domains=("api.example.com", ))
147147
called = False
148148

149149
async def runner(tool_context: InvocationContext, args: dict) -> dict:

trpc_agent_sdk/_tool_safety.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,7 @@ def _review_network(
273273
started_at: float,
274274
) -> SafetyReview | None:
275275
disallowed_hosts = [
276-
_normalize_host(urlparse(url).hostname or "")
277-
for url in urls
276+
_normalize_host(urlparse(url).hostname or "") for url in urls
278277
if not _host_allowed(_normalize_host(urlparse(url).hostname or ""), self.policy.allowed_domains)
279278
]
280279
if disallowed_hosts:
@@ -361,18 +360,28 @@ def _build_review(
361360
"evidence": evidence,
362361
}
363362
audit = {
364-
"decision": decision,
365-
"rule_id": rule_id,
366-
"risk_level": risk_level,
367-
"tool_name": tool_name,
368-
"blocked": blocked,
369-
"latency": latency,
370-
"desensitized": desensitized,
371-
"action_type": action_type,
372-
"input_sha256": source_hash,
373-
"allowed_domains": list(self.policy.allowed_domains),
374-
"rules_evaluated": ["safe_python", "network_allowlist", "network_not_allowlisted"] +
375-
list(_PATH_RULES) + [rule.rule_id for rule in _RULES],
363+
"decision":
364+
decision,
365+
"rule_id":
366+
rule_id,
367+
"risk_level":
368+
risk_level,
369+
"tool_name":
370+
tool_name,
371+
"blocked":
372+
blocked,
373+
"latency":
374+
latency,
375+
"desensitized":
376+
desensitized,
377+
"action_type":
378+
action_type,
379+
"input_sha256":
380+
source_hash,
381+
"allowed_domains":
382+
list(self.policy.allowed_domains),
383+
"rules_evaluated": ["safe_python", "network_allowlist", "network_not_allowlisted"] + list(_PATH_RULES) +
384+
[rule.rule_id for rule in _RULES],
376385
}
377386
return SafetyReview(
378387
decision=decision,

trpc_agent_sdk/_tool_safety_policy.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class SafetyPolicyError(ValueError):
2323

2424
_DEFAULT_ALLOWED_DOMAINS: tuple[str, ...] = ()
2525
_DEFAULT_BLOCKED_PATHS: dict[str, tuple[str, ...]] = {
26-
"read_dotenv": (".env",),
26+
"read_dotenv": (".env", ),
2727
"read_ssh": ("~/.ssh", ".ssh/"),
2828
}
2929
_DEFAULT_ALLOWED_COMMANDS: tuple[str, ...] = ()
@@ -67,16 +67,18 @@ class ToolSafetyPolicy:
6767
risk_levels: Mapping[str, str] | None = None
6868

6969
def __post_init__(self) -> None:
70-
object.__setattr__(self, "allowed_domains", tuple(sorted(_coerce_string_tuple(
71-
self.allowed_domains,
72-
"allowed_domains",
73-
))))
70+
object.__setattr__(self, "allowed_domains",
71+
tuple(sorted(_coerce_string_tuple(
72+
self.allowed_domains,
73+
"allowed_domains",
74+
))))
7475
blocked_paths = self.blocked_paths if self.blocked_paths is not None else _DEFAULT_BLOCKED_PATHS
7576
object.__setattr__(self, "blocked_paths", _coerce_blocked_paths(blocked_paths))
76-
object.__setattr__(self, "allowed_commands", tuple(sorted(_coerce_string_tuple(
77-
self.allowed_commands,
78-
"allowed_commands",
79-
))))
77+
object.__setattr__(self, "allowed_commands",
78+
tuple(sorted(_coerce_string_tuple(
79+
self.allowed_commands,
80+
"allowed_commands",
81+
))))
8082
object.__setattr__(self, "max_timeout", _coerce_positive_int(self.max_timeout, "max_timeout"))
8183
object.__setattr__(self, "max_output_size", _coerce_positive_int(
8284
self.max_output_size,

trpc_agent_sdk/tools/_base_tool.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,7 @@ async def run_async(self, *, tool_context: InvocationContext, args: dict[str, An
177177
after_tool_callback = getattr(tool_context.agent, "after_tool_callback", None)
178178
# Import here to avoid circular import
179179
from trpc_agent_sdk.agents import ToolCallbackFilter
180-
from trpc_agent_sdk.tools.safety.filter import ToolSafetyFilter
181-
extra_filters = [
182-
ToolSafetyFilter(),
183-
ToolCallbackFilter(before_tool_callback, after_tool_callback),
184-
]
180+
extra_filters = [ToolCallbackFilter(before_tool_callback, after_tool_callback)]
185181

186182
token = set_tool_var(self)
187183
handler = partial(self._run_async_impl, tool_context=tool_context, args=args)

0 commit comments

Comments
 (0)