Skip to content

Commit 6fec1b3

Browse files
lll-peanutclaude
andcommitted
fix: address code review issues in Tool Safety Guard
1. Fix RESET NameError in _print_summary when --no-color (tool_safety_check.py) - RESET was only defined in the color branch, causing crash with --no-color. 2. Fix allow_patterns overriding blocklist DENY (_scanner.py) - allow_patterns now only upgrades NEEDS_HUMAN_REVIEW → ALLOW, never overrides DENY from risk-level or blocklist decisions. 3. Fix ||/&& false positives in _check_operators (_bash_scanner.py) - shlex splits || into two | tokens causing false pipe detection. - Similarly && was falsely detected as background operator. - Now skips adjacent |/| and &/& to correctly identify shell operators. 4. Update tests to reflect new allow_patterns behavior (test_tool_safety.py) 5. Update DESIGN.md and README.md to document fixed behaviors - Decision pipeline: allow_patterns only upgrades NEEDS_HUMAN_REVIEW - Oversized scripts: always DENY with blocklist pre-check Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b7f0d3c commit 6fec1b3

6 files changed

Lines changed: 208 additions & 118 deletions

File tree

examples/tool_safety/DESIGN.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,17 @@ Safety Guard 是**执行前治理层**。它对脚本、命令、参数、工作
128128
└──────────┬───────────┘
129129
130130
┌──────────────────────┐
131-
│ 允许模式检查 │ ← 如果 allow_pattern 匹配 → ALLOW(覆盖上述)
131+
│ 允许模式检查 │ ← 如果 allow_pattern 匹配且当前
132+
│ │ 为 NEEDS_HUMAN_REVIEW → ALLOW
133+
│ │ 不会覆盖 DENY(黑名单始终优先)
132134
└──────────┬───────────┘
133135
134136
最终决策
135137
```
136138

137139
**关键约束**:黑名单模式始终升级为 DENY,即使基于风险级别的决策是 ALLOW。
138-
允许模式可以将非 DENY 决策降回 ALLOW,但不能覆盖来自黑名单的 DENY。
140+
允许模式仅将 NEEDS_HUMAN_REVIEW 升级为 ALLOW,不会覆盖任何 DENY 决策
141+
(无论是来自风险级别判定还是黑名单命中)。
139142

140143
---
141144

scripts/tool_safety_check.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,35 +61,41 @@ def main() -> int:
6161
""",
6262
)
6363
parser.add_argument(
64-
"--file", "-f",
64+
"--file",
65+
"-f",
6566
type=str,
6667
help="Path to a script file to scan.",
6768
)
6869
parser.add_argument(
69-
"--type", "-t",
70+
"--type",
71+
"-t",
7072
type=str,
7173
choices=["python", "bash", "auto"],
7274
default="auto",
7375
help="Script language hint (default: auto-detect).",
7476
)
7577
parser.add_argument(
76-
"--tool-name", "-n",
78+
"--tool-name",
79+
"-n",
7780
type=str,
7881
default="cli_tool",
7982
help="Name of the tool being scanned (for audit / report).",
8083
)
8184
parser.add_argument(
82-
"--policy", "-p",
85+
"--policy",
86+
"-p",
8387
type=str,
8488
help="Path to a custom safety policy YAML file.",
8589
)
8690
parser.add_argument(
87-
"--output", "-o",
91+
"--output",
92+
"-o",
8893
type=str,
8994
help="Write the JSON report to this file (default: stdout).",
9095
)
9196
parser.add_argument(
92-
"--audit", "-a",
97+
"--audit",
98+
"-a",
9399
type=str,
94100
help="Append an audit event to this JSONL file.",
95101
)
@@ -173,7 +179,7 @@ def main() -> int:
173179
def _print_summary(report, no_color: bool) -> None:
174180
"""Print a colourised summary to stderr."""
175181
if no_color:
176-
R, G, Y, W, B = "", "", "", "", ""
182+
R, G, Y, W, B, RESET = "", "", "", "", "", ""
177183
else:
178184
R, G, Y, W, B = "\033[91m", "\033[92m", "\033[93m", "\033[97m", "\033[94m"
179185
RESET = "\033[0m"
@@ -200,7 +206,11 @@ def _print_summary(report, no_color: bool) -> None:
200206
print(f"\n{B} Findings:{RESET}", file=sys.stderr)
201207
for f in report.findings:
202208
colour = {
203-
"critical": R, "high": R, "medium": Y, "low": W, "info": W,
209+
"critical": R,
210+
"high": R,
211+
"medium": Y,
212+
"low": W,
213+
"info": W,
204214
}.get(f.risk_level.value, W)
205215
print(f" [{colour}{f.rule_id}{RESET}] {f.message}", file=sys.stderr)
206216
if f.evidence:

tests/test_tool_safety.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,8 +1298,8 @@ def test_scanner_allow_patterns_override():
12981298
assert report.decision == Decision.ALLOW
12991299

13001300

1301-
def test_scanner_allow_patterns_override_with_dangerous():
1302-
"""allow_patterns must override DENY even for dangerous scripts (when pattern matches)."""
1301+
def test_scanner_allow_patterns_never_overrides_deny():
1302+
"""allow_patterns must NOT override DENY — blocklist always wins (security fix)."""
13031303
from trpc_agent_sdk.tools.safety._policy import SafetyPolicy
13041304
policy = SafetyPolicy(
13051305
allow_patterns=[r"rm\s+-rf\s+/tmp/safedir"],
@@ -1312,8 +1312,9 @@ def test_scanner_allow_patterns_override_with_dangerous():
13121312
script_type=ScriptType.BASH,
13131313
tool_name="allow_dangerous_test",
13141314
))
1315-
# allow_patterns should take effect
1316-
assert report.decision == Decision.ALLOW
1315+
# allow_patterns must NOT override DENY from CRITICAL risk (rm -rf)
1316+
assert report.decision == Decision.DENY, \
1317+
f"allow_patterns should NOT override DENY, got {report.decision}"
13171318

13181319

13191320
def test_scanner_reload_policy():
@@ -1541,9 +1542,7 @@ def test_process_medium_risk_for_pipe():
15411542
script_type=ScriptType.BASH,
15421543
tool_name="pipe_test",
15431544
))
1544-
proc_findings = [
1545-
f for f in report.findings if f.category == RiskCategory.PROCESS_AND_SYSTEM
1546-
]
1545+
proc_findings = [f for f in report.findings if f.category == RiskCategory.PROCESS_AND_SYSTEM]
15471546
assert len(proc_findings) > 0, "Pipe should still trigger a PROC finding (INFO level)"
15481547
# Verify it's INFO, not MEDIUM — whitelisted commands → safe pipe
15491548
assert all(f.risk_level == RiskLevel.INFO for f in proc_findings), \
@@ -1556,9 +1555,7 @@ def test_process_medium_risk_for_pipe():
15561555
script_type=ScriptType.BASH,
15571556
tool_name="pipe_test2",
15581557
))
1559-
med_findings = [
1560-
f for f in report2.findings if f.risk_level == RiskLevel.MEDIUM
1561-
]
1558+
med_findings = [f for f in report2.findings if f.risk_level == RiskLevel.MEDIUM]
15621559
assert len(med_findings) > 0, "Pipe with non-whitelisted commands should trigger MEDIUM"
15631560

15641561

@@ -2080,16 +2077,18 @@ def test_scanner_blocklist_override_invalid_regex():
20802077

20812078

20822079
def test_scanner_allow_override_with_findings():
2083-
"""allow_patterns must override DENY to ALLOW when script triggers rules."""
2080+
"""allow_patterns upgrades NEEDS_HUMAN_REVIEW → ALLOW (never overrides DENY)."""
20842081
import tempfile
20852082
import yaml
2086-
# Build a temp policy that blocks curl but allows a specific domain pattern
2083+
# Build a temp policy that allows a specific pattern.
2084+
# The script triggers MEDIUM risk (long sleep → NEEDS_HUMAN_REVIEW),
2085+
# and allow_patterns upgrades the decision to ALLOW.
20872086
policy_data = {
20882087
"global": {
20892088
"max_script_lines": 500
20902089
},
20912090
"whitelists": {
2092-
"domains": ["localhost"],
2091+
"domains": [],
20932092
"commands": [],
20942093
"patterns": []
20952094
},
@@ -2100,10 +2099,10 @@ def test_scanner_allow_override_with_findings():
21002099
"patterns": []
21012100
},
21022101
"rules": {
2103-
"network_egress": {
2102+
"resource_abuse": {
21042103
"enabled": True,
2105-
"risk_level": "high",
2106-
"bash_commands": ["curl", "wget"],
2104+
"risk_level": "medium",
2105+
"long_sleep_threshold_seconds": 60,
21072106
}
21082107
},
21092108
"sanitization": {
@@ -2118,16 +2117,16 @@ def test_scanner_allow_override_with_findings():
21182117
from trpc_agent_sdk.tools.safety._policy import PolicyLoader
21192118
policy = PolicyLoader(policy_path).load()
21202119
scanner = SafetyScanner(policy)
2121-
# Script triggers NET egress (curl to non-whitelisted domain) AND
2122-
# has an allow_pattern match → should be ALLOW
2120+
# Script triggers MEDIUM risk (sleep 120 > 60s threshold → NEEDS_HUMAN_REVIEW)
2121+
# AND has an allow_pattern match → should be ALLOW
21232122
report = scanner.scan(
21242123
SafetyScanInput(
2125-
script_content="echo safe_override_test; curl https://evil.com",
2124+
script_content="echo safe_override_test; sleep 120",
21262125
script_type=ScriptType.BASH,
21272126
tool_name="allow_override2",
21282127
))
21292128
assert report.decision == Decision.ALLOW, \
2130-
f"allow_patterns should override, got {report.decision}"
2129+
f"allow_patterns should upgrade NEEDS_HUMAN_REVIEW to ALLOW, got {report.decision}"
21312130
finally:
21322131
os.unlink(policy_path)
21332132

trpc_agent_sdk/tools/safety/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ python scripts/tool_safety_check.py -p custom_policy.yaml -f script.sh
312312
```yaml
313313
# ---- 全局设置 ----
314314
global:
315-
max_script_lines: 500 # 超过此行数触发 needs_human_review
316-
max_script_bytes: 524288 # 超过此字节数触发 needs_human_review
315+
max_script_lines: 500 # 超过此行数直接 DENY(先做黑名单预检查,防止填充绕过)
316+
max_script_bytes: 524288 # 超过此字节数触发 DENY
317317
max_timeout_seconds: 300 # 建议的最长执行时间
318318
max_output_bytes: 10485760 # 最大输出大小
319319

0 commit comments

Comments
 (0)