Skip to content

Commit d2c0af1

Browse files
committed
Align tool safety quickstart structure
1 parent f71c80f commit d2c0af1

10 files changed

Lines changed: 255 additions & 180 deletions

File tree

examples/tool_safety_guard/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ This example shows how to scan Python and Bash tool scripts before execution. It
77
For a project-shaped walkthrough similar to the optimization quickstart examples, start here:
88

99
```bash
10-
python examples/tool_safety_guard/quickstart/run_guard.py
10+
python examples/tool_safety_guard/quickstart/run_agent.py
1111
```
1212

13-
That directory contains an entry script, a dry-run tool service, a policy file, sample tool scripts, a design note, and generated report/audit outputs. It demonstrates the same guard through direct scan, Tool Filter, and CodeExecutor wrapper paths without executing untrusted script bodies.
13+
That directory follows the same shape as other quickstart examples: `run_agent.py` is the entry point, `agent/` contains the application logic and dry-run tool service, `policy.yaml` holds the safety knobs, and `scripts/` contains sample tool payloads. It demonstrates the same guard through direct scan, Tool Filter, and CodeExecutor wrapper paths without executing untrusted script bodies.
1414

1515
## Run The Samples
1616

examples/tool_safety_guard/quickstart/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
# Tool Safety Guard Quickstart
22

3-
This quickstart is a small project-shaped example for the tool script safety guard. It mirrors the structure used by optimization quickstart examples: one entry script, one service module, one policy file, and a few representative inputs.
3+
This quickstart is a small project-shaped example for the tool script safety guard. It mirrors the structure used by other quickstart examples: `run_agent.py` is the entry point, `agent/` contains the reusable application modules, `policy.yaml` contains policy knobs, and `scripts/` contains representative tool payloads.
44

55
## Directory
66

77
```text
88
examples/tool_safety_guard/quickstart/
9-
|-- run_guard.py
10-
|-- tool_service.py
9+
|-- run_agent.py
1110
|-- policy.yaml
1211
|-- design.md
12+
|-- agent/
13+
| |-- __init__.py
14+
| |-- agent.py
15+
| |-- config.py
16+
| `-- tools.py
1317
`-- scripts/
1418
|-- safe_report.py
1519
|-- external_upload.py
@@ -25,13 +29,13 @@ examples/tool_safety_guard/quickstart/
2529
From the repository root:
2630

2731
```bash
28-
python examples/tool_safety_guard/quickstart/run_guard.py
32+
python examples/tool_safety_guard/quickstart/run_agent.py
2933
```
3034

3135
Or with the Windows launcher used in this workspace:
3236

3337
```bash
34-
py -3.14 examples/tool_safety_guard/quickstart/run_guard.py
38+
py -3.14 examples/tool_safety_guard/quickstart/run_agent.py
3539
```
3640

3741
The run writes:
@@ -54,7 +58,7 @@ Expected decisions:
5458

5559
## What This Demonstrates
5660

57-
The same `ToolSafetyGuard` is used in three places:
61+
The same `ToolSafetyGuard` is used in three places from `agent/agent.py`:
5862

5963
- Direct scan: `guard.scan(ToolSafetyScanRequest(...))` returns a structured report.
6064
- Tool Filter: `ToolSafetyFilter` blocks script-like tool arguments before the tool body runs.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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 safety guard quickstart agent package."""
7+
8+
from .agent import run_quickstart
9+
10+
__all__ = ["run_quickstart"]
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 safety guard quickstart application logic.
7+
8+
This module is shaped like other quickstart ``agent/agent.py`` files, but it is
9+
model-free on purpose: the issue being demonstrated is pre-execution script
10+
safety for tools and code executors, so the sample can run without API keys.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import json
16+
from collections.abc import Iterable
17+
from pathlib import Path
18+
from typing import Any
19+
20+
from trpc_agent_sdk.code_executors import CodeBlock
21+
from trpc_agent_sdk.code_executors import CodeExecutionInput
22+
from trpc_agent_sdk.context import create_agent_context
23+
from trpc_agent_sdk.filter import FilterResult
24+
from trpc_agent_sdk.tools.safety import SafetyGuardedCodeExecutor
25+
from trpc_agent_sdk.tools.safety import ToolSafetyFilter
26+
from trpc_agent_sdk.tools.safety import ToolSafetyGuard
27+
from trpc_agent_sdk.tools.safety import ToolSafetyPolicy
28+
from trpc_agent_sdk.tools.safety import ToolSafetyScanRequest
29+
30+
from .config import DEFAULT_CASES
31+
from .config import DEFAULT_OUT
32+
from .config import DEFAULT_POLICY
33+
from .config import QUICKSTART_DIR
34+
from .config import ScriptSafetyCase
35+
from .tools import DryRunScriptExecutor
36+
from .tools import dry_run_tool
37+
from .tools import read_script
38+
39+
40+
def _enum_value(value: Any) -> str:
41+
return value.value if hasattr(value, "value") else str(value)
42+
43+
44+
def _write_json(path: Path, payload: Any) -> None:
45+
path.parent.mkdir(parents=True, exist_ok=True)
46+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
47+
48+
49+
async def _run_filter_case(guard: ToolSafetyGuard, script: str, language: str) -> dict[str, Any]:
50+
filter_ = ToolSafetyFilter(guard=guard)
51+
52+
async def handle() -> FilterResult:
53+
return FilterResult(rsp=await dry_run_tool(script, language=language))
54+
55+
result = await filter_.run(
56+
create_agent_context(),
57+
{
58+
"script": script,
59+
"language": language,
60+
"cwd": str(QUICKSTART_DIR),
61+
},
62+
handle,
63+
)
64+
return {
65+
"continued": bool(result.is_continue),
66+
"response": result.rsp,
67+
}
68+
69+
70+
async def _run_executor_case(guard: ToolSafetyGuard, script: str, language: str) -> dict[str, Any]:
71+
executor = SafetyGuardedCodeExecutor(
72+
delegate=DryRunScriptExecutor(work_dir=str(QUICKSTART_DIR)),
73+
guard=guard,
74+
)
75+
result = await executor.execute_code(
76+
invocation_context=None, # type: ignore[arg-type]
77+
code_execution_input=CodeExecutionInput(code_blocks=[CodeBlock(language=language, code=script)]),
78+
)
79+
return {
80+
"outcome": _enum_value(result.outcome),
81+
"output": result.output,
82+
}
83+
84+
85+
async def run_quickstart(
86+
*,
87+
policy_path: Path = DEFAULT_POLICY,
88+
output_dir: Path = DEFAULT_OUT,
89+
cases: Iterable[ScriptSafetyCase] = DEFAULT_CASES,
90+
) -> dict[str, Any]:
91+
"""Run the model-free tool safety quickstart and write a report."""
92+
93+
policy = ToolSafetyPolicy.load(policy_path)
94+
audit_path = output_dir / "audit.jsonl"
95+
report_path = output_dir / "quickstart_report.json"
96+
guard = ToolSafetyGuard(policy=policy, audit_log_path=audit_path)
97+
case_results = []
98+
99+
for case in cases:
100+
script = read_script(case.script_path)
101+
report = guard.scan(
102+
ToolSafetyScanRequest(
103+
script=script,
104+
language=case.language,
105+
cwd=str(QUICKSTART_DIR),
106+
tool_metadata={
107+
"name": case.name,
108+
"script_path": str(case.script_path),
109+
},
110+
)
111+
)
112+
filter_result = await _run_filter_case(guard, script, case.language)
113+
executor_result = await _run_executor_case(guard, script, case.language)
114+
case_results.append(
115+
{
116+
"name": case.name,
117+
"script": str(case.script_path.relative_to(QUICKSTART_DIR)),
118+
"language": case.language,
119+
"decision": _enum_value(report.decision),
120+
"risk_level": _enum_value(report.risk_level),
121+
"blocked": report.blocked,
122+
"rule_ids": [finding.rule_id for finding in report.findings],
123+
"summary": report.summary,
124+
"filter": filter_result,
125+
"code_executor": executor_result,
126+
}
127+
)
128+
129+
summary = {
130+
"policy": {
131+
"name": policy.name,
132+
"version": policy.version,
133+
"path": str(policy_path),
134+
},
135+
"case_count": len(case_results),
136+
"decision_counts": {
137+
decision: sum(1 for case in case_results if case["decision"] == decision)
138+
for decision in sorted({case["decision"] for case in case_results})
139+
},
140+
"cases": case_results,
141+
"report": str(report_path),
142+
"audit_log": str(audit_path),
143+
}
144+
_write_json(report_path, summary)
145+
return summary
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
"""Configuration for the tool safety guard quickstart."""
7+
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass
11+
from pathlib import Path
12+
13+
14+
QUICKSTART_DIR = Path(__file__).resolve().parents[1]
15+
DEFAULT_POLICY = QUICKSTART_DIR / "policy.yaml"
16+
DEFAULT_OUT = QUICKSTART_DIR / "out"
17+
SCRIPTS_DIR = QUICKSTART_DIR / "scripts"
18+
19+
20+
@dataclass(frozen=True)
21+
class ScriptSafetyCase:
22+
"""A script-like payload that the quickstart feeds into the guard."""
23+
24+
name: str
25+
script_path: Path
26+
language: str
27+
28+
29+
DEFAULT_CASES = (
30+
ScriptSafetyCase("safe_report", SCRIPTS_DIR / "safe_report.py", "python"),
31+
ScriptSafetyCase("external_upload", SCRIPTS_DIR / "external_upload.py", "python"),
32+
ScriptSafetyCase("read_secret", SCRIPTS_DIR / "read_secret.py", "python"),
33+
ScriptSafetyCase("review_subprocess", SCRIPTS_DIR / "review_subprocess.py", "python"),
34+
ScriptSafetyCase("dangerous_cleanup", SCRIPTS_DIR / "dangerous_cleanup.sh", "bash"),
35+
)

examples/tool_safety_guard/quickstart/tool_service.py renamed to examples/tool_safety_guard/quickstart/agent/tools.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
# Copyright (C) 2026 Tencent. All rights reserved.
44
#
55
# tRPC-Agent-Python is licensed under Apache-2.0.
6-
"""Tiny script execution service used by the safety quickstart example.
6+
"""Dry-run tools used by the tool safety guard quickstart.
77
88
The service intentionally does not execute untrusted scripts. Its methods are
9-
small enough to show exactly where the safety guard is inserted in front of a
10-
tool-like function and a CodeExecutor-like runtime.
9+
small enough to show where the safety guard is inserted in front of a tool-like
10+
function and a CodeExecutor-like runtime.
1111
"""
1212

1313
from __future__ import annotations

examples/tool_safety_guard/quickstart/design.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ Agents increasingly call tools that accept script-like payloads: a Bash command,
66

77
## Architecture
88

9+
The quickstart keeps the same directory convention as `examples/quickstart`: `run_agent.py` is a thin executable entry point, while the reusable logic lives under `agent/`. This keeps the sample easy to copy into an application without mixing CLI parsing, policy paths, dry-run tools, and guard orchestration in one file.
10+
911
The quickstart has three layers:
1012

1113
1. `policy.yaml` defines environment-specific rules: allowed network domains, allowed commands, denied paths, protected system roots, resource thresholds, and the risk levels that map to `allow`, `needs_human_review`, or `deny`.
12-
2. `ToolSafetyGuard` owns scanning, decision calculation, telemetry attributes, and audit emission. It receives a `ToolSafetyScanRequest` and returns a `ToolSafetyReport`.
13-
3. Integration adapters place the guard in front of execution. `ToolSafetyFilter` protects ordinary tools by scanning common argument names such as `command`, `script`, `code`, and `code_blocks`. `SafetyGuardedCodeExecutor` protects CodeExecutor delegates by scanning every code block before delegation.
14+
2. `agent/agent.py` owns the orchestration around `ToolSafetyGuard`: scanning, decision collection, report writing, and audit emission. It sends a `ToolSafetyScanRequest` for each payload and records the resulting `ToolSafetyReport`.
15+
3. `agent/tools.py` provides dry-run execution delegates. Integration adapters place the guard in front of them: `ToolSafetyFilter` protects ordinary tools by scanning common argument names such as `command`, `script`, `code`, and `code_blocks`; `SafetyGuardedCodeExecutor` protects CodeExecutor delegates by scanning every code block before delegation.
1416

1517
The quickstart runner exercises all three layers with the same sample scripts. Direct scan shows the raw report. The filter path proves that a blocked script prevents the tool handler from running. The CodeExecutor path proves that generated code is stopped before the executor delegate sees it.
1618

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
"""Run the tool safety guard quickstart project."""
7+
8+
from __future__ import annotations
9+
10+
import argparse
11+
import asyncio
12+
import sys
13+
from pathlib import Path
14+
15+
16+
HERE = Path(__file__).resolve().parent
17+
REPO_ROOT = HERE.parents[2]
18+
if str(REPO_ROOT) not in sys.path:
19+
sys.path.insert(0, str(REPO_ROOT))
20+
21+
from examples.tool_safety_guard.quickstart.agent.agent import run_quickstart # noqa: E402
22+
from examples.tool_safety_guard.quickstart.agent.config import DEFAULT_OUT # noqa: E402
23+
from examples.tool_safety_guard.quickstart.agent.config import DEFAULT_POLICY # noqa: E402
24+
25+
26+
def _parse_args() -> argparse.Namespace:
27+
parser = argparse.ArgumentParser(description="Run the tool safety guard quickstart.")
28+
parser.add_argument("--policy", type=Path, default=DEFAULT_POLICY)
29+
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUT)
30+
return parser.parse_args()
31+
32+
33+
def main() -> None:
34+
args = _parse_args()
35+
summary = asyncio.run(run_quickstart(policy_path=args.policy.resolve(), output_dir=args.output_dir.resolve()))
36+
print("Tool safety quickstart decisions:")
37+
for case in summary["cases"]:
38+
rule_ids = ", ".join(case["rule_ids"]) or "no findings"
39+
print(f"- {case['name']}: {case['decision']} ({rule_ids})")
40+
print(f"Report written to: {summary['report']}")
41+
print(f"Audit log written to: {summary['audit_log']}")
42+
43+
44+
if __name__ == "__main__":
45+
main()

0 commit comments

Comments
 (0)