Skip to content

examples: add tool execution safety guard#2193

Open
March-77 wants to merge 3 commits into
trpc-group:mainfrom
March-77:agent/tool-safety-guard
Open

examples: add tool execution safety guard#2193
March-77 wants to merge 3 commits into
trpc-group:mainfrom
March-77:agent/tool-safety-guard

Conversation

@March-77

Copy link
Copy Markdown

Summary

Implements #2002 with a policy-driven Tool Execution Safety Guard.

  • scans commands, argv-like text, backend, working directory, environment, timeout, output limit, PTY, and tool metadata before execution
  • returns conservative allow, deny, or ask decisions with risk, rule, evidence, and recommendation
  • detects destructive commands, credential reads, protected paths, non-allowlisted network access, shell wrappers and metacharacters, dependency changes, unbounded work, long tasks, excessive output, host PTY/background sessions, and unknown environment variables
  • loads command/domain/path/resource/environment policy from JSON without code changes
  • supplies an execution-boundary wrapper equivalent to tool.PermissionPolicy: audit first, block deny/ask, call the executor only for allow
  • redacts secret evidence and emits JSONL audit events plus OpenTelemetry-ready span attributes
  • documents workspaceexec, hostexec, codeexecutor, Filter/Permission, shellsafe, telemetry, and sandbox boundaries

Validation

  • go test ./... in examples/tool_safety_guard
  • go vet ./... in examples/tool_safety_guard
  • 12/12 public samples matched expected decisions
  • destructive deletion, credential-file reads, and non-allowlisted network access: 100% denied
  • safe go test and allowlisted GitHub API request: allowed
  • execution wrapper test verifies blocked requests never invoke the underlying executor and always write an audit event
  • secret-redaction test verifies plaintext token values do not enter reports
  • 500-command performance test completes below one second

Usage

cd examples/tool_safety_guard
go run . \
  --policy tool_safety_policy.json \
  --samples samples.json \
  --report tool_safety_report.json \
  --audit tool_safety_audit.jsonl

The samples are scanned only; dangerous commands are never executed. This guard is a policy layer and intentionally does not claim to replace container/E2B sandbox isolation, clean environments, output bounds, or process-tree cleanup.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02b5e57f-cc0a-4bca-991d-1edfae81c6bc

📥 Commits

Reviewing files that changed from the base of the PR and between 03d5db9 and 3d96c6a.

📒 Files selected for processing (3)
  • examples/tool_safety_guard/guard.go
  • examples/tool_safety_guard/guard_test.go
  • examples/tool_safety_guard/samples.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/tool_safety_guard/samples.json
  • examples/tool_safety_guard/guard_test.go
  • examples/tool_safety_guard/guard.go

📝 Walkthrough

English

  • Adds a standalone Go 1.23 example under examples/tool_safety_guard that gates tool execution with a JSON-configured safety policy (tool_safety_policy.json) and a pre-execution command-scanning guard.
  • Implements Guard.Scan to classify requests as allow, deny, or ask (with “ask” treated as blocked by the wrapper), selecting the highest-risk finding and producing risk details, redacted evidence, recommendations, and OpenTelemetry-ready span attributes (tool.safety.*).
  • Implements Guard.Wrap as the execution boundary middleware: it always calls an audit callback with an AuditEvent, blocks execution when ScanResult.Blocked is true (i.e., deny or ask), and only invokes the wrapped executor for allow.
  • Provides a CLI (main.go) that loads policy + JSON-defined samples, scans all samples without executing dangerous commands, writes a JSON report, and emits JSONL audit events.
  • Adds example/test artifacts and coverage: README.md, samples.json (12 cases with expected decisions), tool_safety_report.json.example, tool_safety_audit.jsonl.example, plus guard_test.go including sample decision matching, wrapper block/audit behavior, secret redaction (TestRedaction), forbidden working-directory enforcement (TestForbiddenWorkingDirectory), and a 500-command performance benchmark.

Compatibility / behavioral risks

  • New standalone module/example requires Go 1.23 (examples/tool_safety_guard/go.mod).
  • Integrations must handle that the wrapper blocks both deny and ask (execution only proceeds for allow), and must decide operationally how to treat “ask”-level outcomes.
  • Policy and heuristics can produce false positives/negatives; the guard is a pre-execution safety gate, not a runtime containment/isolation boundary (especially for hostexec and untrusted code).

Recommended validation steps

  • Run: cd examples/tool_safety_guard && go test ./... and go vet ./....
  • Verify sample correctness: TestAllSamples decision equality for all 12 samples and that findings include non-empty RuleID, Evidence, and Recommendation.
  • Verify enforcement boundary: TestWrapperBlocksBeforeExecutionAndAudits (wrapped executor not called when blocked; exactly one audit event with blocked=true).
  • Verify secret handling: TestRedaction ensures secrets (e.g., token=...) do not appear in marshaled scan results.
  • Verify working-directory protection: TestForbiddenWorkingDirectory ensures forbidden WorkingDir is denied with RuleID=FORBIDDEN_WORKING_DIR (including the follow-up fix to forbidden working-directory checks).
  • Review policy-driven behavior by editing tool_safety_policy.json (commands/paths/domains/env/time/output) and re-running tests/samples; then check overhead via BenchmarkPerformance500Commands.
中文
  • examples/tool_safety_guard 新增一个独立的 Go 1.23 示例:通过 JSON 配置的安全策略(tool_safety_policy.json)与预执行命令扫描 guard 来约束工具执行。
  • 实现 Guard.Scan:将请求判定为 allow / deny / ask(其中 “ask” 在 wrapper 中同样会被视为阻断),并输出风险分级、脱敏后的证据、建议,同时提供 OpenTelemetry 就绪的 span 属性(tool.safety.*)。最终决策取最高风险发现。
  • 实现 Guard.Wrap 作为执行边界中间件:先调用审计回调(生成 AuditEvent),若 ScanResult.Blocked 为真(即 denyask),则阻断执行;仅当 allow 时才会调用被包装的 executor。
  • 提供 CLI(main.go):加载策略与 JSON 样例,只做扫描不执行危险命令,生成 JSON 报告,并写出 JSONL 审计事件。
  • 补充文档与示例/测试:README.mdsamples.json(12 个带期望决策的用例)、tool_safety_report.json.exampletool_safety_audit.jsonl.example,以及 guard_test.go(样例决策匹配、wrapper 阻断与审计行为、密钥脱敏 TestRedaction、禁止 working directory 校验 TestForbiddenWorkingDirectory、500 命令性能基准)。

兼容性 / 行为风险

  • 新增独立示例模块,要求 Go 1.23(examples/tool_safety_guard/go.mod)。
  • 集成时需注意:wrapper 会同时阻断 denyask,只有 allow 才会真正执行;业务侧需要明确如何处理 “ask” 级别请求。
  • 该 guard 属于启发式 + 策略门控,可能存在误报/漏报;它是预执行安全门,而非运行时隔离/容器化防护边界(尤其是 hostexec 与不可信代码场景)。

建议验证步骤

  • 运行:cd examples/tool_safety_guard && go test ./...go vet ./...
  • 验证样例正确性:TestAllSamples 对全部 12 个样例的决策一致性,并确认每条 finding 的 RuleID / Evidence / Recommendation 均非空。
  • 验证执行边界:TestWrapperBlocksBeforeExecutionAndAudits(被阻断时 wrapped executor 不会被调用;且每次阻断只产出一条 blocked=true 的审计事件)。
  • 验证密钥处理:TestRedaction 确保如 token=... 等敏感信息不会出现在序列化后的 scan 结果中。
  • 验证 working directory 保护:TestForbiddenWorkingDirectory 确保禁止的 WorkingDir 会被拒绝且 RuleID=FORBIDDEN_WORKING_DIR(包含对禁止 working-directory 校验逻辑的后续修正)。
  • 修改 tool_safety_policy.json(命令/路径/域名/env/超时/输出上限)后重跑测试与样例,确认策略生效;并运行 BenchmarkPerformance500Commands 评估开销。

Walkthrough

Adds a standalone Go tool-safety guard example with configurable policy scanning, redaction, audit middleware, CLI-generated reports, sample requests, tests, and usage documentation.

Changes

Tool Safety Guard

Layer / File(s) Summary
Policy contracts and sample inputs
examples/tool_safety_guard/go.mod, examples/tool_safety_guard/guard.go, examples/tool_safety_guard/tool_safety_policy.json, examples/tool_safety_guard/samples.json
Defines policy, request, scan-result, finding, and audit-event structures; loads JSON policy data; and adds sample policies and requests.
Safety scanning and redaction
examples/tool_safety_guard/guard.go
Evaluates command, path, network, shell, resource, PTY, and environment risks, redacts sensitive content, aggregates decisions, and records span attributes.
Execution boundary and validation
examples/tool_safety_guard/guard.go, examples/tool_safety_guard/guard_test.go
Adds Guard.Wrap to audit scans, block deny/ask decisions before execution, and test policy decisions, blocking, redaction, and batch performance.
CLI reports and example usage
examples/tool_safety_guard/main.go, examples/tool_safety_guard/*.json.example, examples/tool_safety_guard/README.md
Adds sample loading, JSONL audit output, JSON report generation, example outputs, and documentation for configuration and integration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: sandyskies

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main addition: a tool execution safety guard example under examples.
Description check ✅ Passed The description matches the changeset and accurately summarizes the safety-guard behavior, validation, and usage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

if regexp.MustCompile(`\b(?:sh|bash|cmd|powershell)\s+(?:-c|/c|-command)\b|\beval\b`).MatchString(lower) {
add("ask", "high", "SHELL_WRAPPER", req.Command, "expand and review the wrapped command before execution")
}
if strings.ContainsAny(req.Command, "`$") || strings.Contains(req.Command, "|") || strings.ContainsAny(req.Command, ";><") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline separators bypass denied command checks, so go test ./... followed by sudo returns allow. Treat newlines as command separators before returning allow.

中文 换行分隔符会绕过禁用命令检查,因此 `go test ./...` 后接 `sudo` 会返回 `allow`。请在返回 `allow` 前将换行视为命令分隔符。

@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@codecov

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.88162%. Comparing base (d4f78f6) to head (03d5db9).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@                 Coverage Diff                 @@
##                main       #2193         +/-   ##
===================================================
+ Coverage   89.86563%   89.88162%   +0.01599%     
===================================================
  Files           1144        1144                 
  Lines         198483      198520         +37     
===================================================
+ Hits          178368      178433         +65     
+ Misses         12602       12579         -23     
+ Partials        7513        7508          -5     
Flag Coverage Δ
unittests 89.88162% <ø> (+0.01599%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
examples/tool_safety_guard/main.go (2)

45-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use keyed struct fields for instantiation.

Using unkeyed struct literals is fragile. If fields are added or reordered in the structs later, these initializations will silently break or compile with errors. It is best practice to use keyed fields, especially in example code that users may use as a reference.

中文

使用带键的结构体字段进行实例化。

使用未命名的结构体字面量是很脆弱的。如果在未来的版本中向结构体添加或重新排列字段,这些初始化将失败或引发编译错误。建议使用带键的字段,特别是在用户可能作为参考的示例代码中。

  • examples/tool_safety_guard/main.go#L45-L45: Use keyed fields for SafetyReport.
  • examples/tool_safety_guard/main.go#L53-L53: Use keyed fields for AuditEvent.
♻️ Proposed fixes

For SafetyReport:

	report := SafetyReport{
		GeneratedAt:     time.Now().UTC().Format(time.RFC3339Nano),
		Policy:          *policyPath,
		Samples:         len(samples),
		MatchedExpected: 0,
		Results:         nil,
	}

For AuditEvent:

		event := AuditEvent{
			Timestamp:      time.Now().UTC().Format(time.RFC3339Nano),
			ToolName:       sample.Request.ToolName,
			Decision:       result.Decision,
			RiskLevel:      result.RiskLevel,
			RuleID:         result.RuleID,
			Backend:        sample.Request.Backend,
			DurationMicros: result.DurationMicros,
			Redacted:       result.Redacted,
			Blocked:        result.Blocked,
		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tool_safety_guard/main.go` at line 45, Replace the unkeyed
SafetyReport literal at examples/tool_safety_guard/main.go:45 with keyed fields
matching GeneratedAt, Policy, Samples, MatchedExpected, and Results; likewise
update the AuditEvent literal at examples/tool_safety_guard/main.go:53 to use
its named fields, preserving all existing values.

54-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Handle json.Marshal errors.

While json.Marshal is unlikely to fail with the current types, it is good practice to handle its errors, especially in example code that users might copy and modify. You can leverage the existing must() helper.

中文

处理 json.Marshal 错误。

尽管 json.Marshal 在当前类型下不太可能失败,但处理其错误是一个好习惯,特别是在用户可能会复制和修改的示例代码中。你可以利用现有的 must() 辅助函数。

♻️ Proposed fix
-		line, _ := json.Marshal(event)
+		line, e := json.Marshal(event)
+		must(e)
		_, _ = writer.Write(append(line, '\n'))
	}
-	out, _ := json.MarshalIndent(report, "", "  ")
+	out, e := json.MarshalIndent(report, "", "  ")
+	must(e)
	must(os.WriteFile(*reportPath, append(out, '\n'), 0o644))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tool_safety_guard/main.go` around lines 54 - 58, Handle the error
returned by the report serialization call in the shown flow by routing
json.MarshalIndent through the existing must() helper, while preserving the
current output formatting and WriteFile behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/tool_safety_guard/guard_test.go`:
- Around line 76-84: Replace TestPerformance500Commands with a Go benchmark
using testing.B and benchmark iterations, removing the hardcoded one-second
assertion while repeatedly measuring the guard.Scan call. Update loadTest to
accept testing.TB so the benchmark and existing tests can share it, preserving
the current test setup and scan request.

In `@examples/tool_safety_guard/guard.go`:
- Line 116: In examples/tool_safety_guard/guard.go at lines 116, 124, 127, 138,
144, 147, and 150, move the static regexp.MustCompile patterns used by Scan to
package-level compiled regex variables and reuse them on each request. At line
112, compile the DeniedCommands-dependent regexes once during NewGuard
initialization and store them on the guard for Scan to reuse; remove all
per-request compilation while preserving the existing matching behavior.
- Line 112: Update the denied-command regex in the guard check to include
newline as a valid command separator alongside semicolons, ampersands, and
pipes. Preserve the existing command matching and boundary behavior so chained
commands such as “go test\nsudo” are detected.

---

Nitpick comments:
In `@examples/tool_safety_guard/main.go`:
- Line 45: Replace the unkeyed SafetyReport literal at
examples/tool_safety_guard/main.go:45 with keyed fields matching GeneratedAt,
Policy, Samples, MatchedExpected, and Results; likewise update the AuditEvent
literal at examples/tool_safety_guard/main.go:53 to use its named fields,
preserving all existing values.
- Around line 54-58: Handle the error returned by the report serialization call
in the shown flow by routing json.MarshalIndent through the existing must()
helper, while preserving the current output formatting and WriteFile behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c844a7c1-53a6-45e6-a1db-a9bcdf7c4ee0

📥 Commits

Reviewing files that changed from the base of the PR and between d4f78f6 and d6fefc5.

📒 Files selected for processing (9)
  • examples/tool_safety_guard/README.md
  • examples/tool_safety_guard/go.mod
  • examples/tool_safety_guard/guard.go
  • examples/tool_safety_guard/guard_test.go
  • examples/tool_safety_guard/main.go
  • examples/tool_safety_guard/samples.json
  • examples/tool_safety_guard/tool_safety_audit.jsonl.example
  • examples/tool_safety_guard/tool_safety_policy.json
  • examples/tool_safety_guard/tool_safety_report.json.example

Comment thread examples/tool_safety_guard/guard_test.go Outdated
Comment thread examples/tool_safety_guard/guard.go Outdated
Comment thread examples/tool_safety_guard/guard.go Outdated
@March-77

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@March-77

Copy link
Copy Markdown
Author

recheck

if destructiveCommandRE.MatchString(lower) {
add("deny", "critical", "DESTRUCTIVE_COMMAND", req.Command, "do not run destructive filesystem commands")
}
for _, path := range g.policy.ForbiddenPaths {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ForbiddenPaths only checks req.Command, so WorkingDir: "~/.ssh" with a harmless command can be allowed even though the guard says it protects working directories. Normalize and check req.WorkingDir against ForbiddenPaths as well, then add a sample/test for protected working dirs.

中文 `ForbiddenPaths` 现在只检查 `req.Command`,所以即使 `WorkingDir` 是 `~/.ssh`,只要命令本身看起来无害就可能被放行,这和 guard 宣称会保护工作目录不一致。建议把 `req.WorkingDir` 规范化后也纳入 `ForbiddenPaths` 检查,并补一个受保护工作目录的样例/测试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants