Skip to content

feat: add ToolPolicy middleware for tool-loop allow/deny gate#2303

Open
Liohtml wants to merge 9 commits into
tinyhumansai:mainfrom
Liohtml:feat/tool-policy-middleware
Open

feat: add ToolPolicy middleware for tool-loop allow/deny gate#2303
Liohtml wants to merge 9 commits into
tinyhumansai:mainfrom
Liohtml:feat/tool-policy-middleware

Conversation

@Liohtml
Copy link
Copy Markdown
Contributor

@Liohtml Liohtml commented May 20, 2026

Summary

Closes #2131.

  • Adds a synchronous ToolPolicy trait in src/openhuman/tools/policy.rs with a PolicyDecision enum (Allow / Deny(String))
  • Ships a DefaultToolPolicy that returns Allow unconditionally (backward compatible)
  • Integrates the policy check into run_tool_call_loop (the bus/CLI tool execution path) -- denied tools return a ToolResult error to the model without executing, preventing any side effects
  • Emits tracing::debug! with tool name and denial reason when a call is denied (updated from warn! per code review)
  • Policy gate runs before the approval hook and external-effect gate so denied tools cannot trigger approval-side effects
  • Includes 5 unit tests: default allows all, custom deny blocks matching tool, allows non-matching, deny-all blocks every tool, unknown tool names allowed by default

This complements the existing async agent::tool_policy::ToolPolicy (session layer) by covering the lower-level run_tool_call_loop path used by the bus and CLI dispatchers.

Scope

Test plan

  • cargo check -p openhuman --lib passes
  • cargo test -p openhuman "tool" --lib -- 1402 tests pass
  • cargo test -p openhuman "policy" --lib -- 153 tests pass (includes 5 new)
  • cargo fmt --all applied
  • CI checks pass (TypeScript type-check failure is pre-existing i18n issue fixed in fix(i18n): remove duplicate German keys unblocking main's Type Check #2495, unrelated to this PR's Rust-only changes)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced a tool-execution policy system: each tool call is evaluated and may be Allowed or Denied. Denials emit clear rejection messages, are recorded as failed per-call results, and the call is skipped. A DefaultToolPolicy preserves previous allow-all behavior and is applied per turn.
  • Tests

    • Updated harness and tests to cover policy evaluation across many tool-call scenarios and edge cases.

Review Change Stack

…inyhumansai#2131)

Introduces a synchronous ToolPolicy trait in tools/policy.rs that the
run_tool_call_loop evaluates before every tool.execute() call. Denied
tools return an error ToolResult to the model without executing,
preventing any side effects. The DefaultToolPolicy allows all calls,
preserving backward compatibility.

This complements the existing async agent::tool_policy (session layer)
by covering the bus/CLI tool execution path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Liohtml Liohtml requested a review from a team May 20, 2026 07:53
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a pluggable tool-policy gate: ToolPolicy and PolicyDecision, DefaultToolPolicy returning Allow, threaded into run_tool_call_loop. Each parsed tool call is evaluated; Deny emits a failed progress event, records a denial result for the model, and skips execution. Production and test call sites are updated.

Changes

Tool execution policy gating

Layer / File(s) Summary
Policy abstraction and default implementation
src/openhuman/tools/policy.rs, src/openhuman/tools/mod.rs
PolicyDecision enum and ToolPolicy trait define the policy contract. DefaultToolPolicy always returns Allow. Module re-exports make types available at openhuman::tools. Tests validate default, custom deny-by-name, and deny-all policies.
Agent tool-loop policy evaluation
src/openhuman/agent/harness/tool_loop.rs
run_tool_call_loop gains a tool_policy: &dyn ToolPolicy parameter. The loop evaluates the policy before approval/execution; on Deny, emission of failed ToolCallCompleted and recording a denial in per-call results replaces execution. Imports and per-turn DefaultToolPolicy creation are added.
Native agent handler wiring
src/openhuman/agent/bus.rs
register_agent_handlers passes a per-turn DefaultToolPolicy into run_tool_call_loop in the native agent.run_turn handler.
Test harness call-site integration
src/openhuman/agent/harness/*, src/openhuman/agent/harness/tool_loop_tests.rs
All test call sites invoking run_tool_call_loop were updated to pass &crate::openhuman::tools::policy::DefaultToolPolicy. No test assertions or behavior were changed; only invocation signatures were updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • senamakel
  • graycyrus

"🐰 I hopped through code and found a gate,
A policy listens, patient and polite,
Default says yes, but some names must wait,
Denials are logged, the loop skips to the light,
Tools now knock before they write."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing a ToolPolicy middleware gate for tool-loop allow/deny decisions, which is the core feature across all changed files.
Linked Issues check ✅ Passed The PR fully implements issue #2131 requirements: adds ToolPolicy trait with PolicyDecision enum, integrates policy into run_tool_call_loop before execution, emits structured denials, maintains default allow behavior, includes unit tests proving denial and allow paths work correctly.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the stated objectives: policy trait definition, default implementation, tool-loop integration point, and test coverage. No unrelated refactoring, approval workflow changes, or per-tool config introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. working A PR that is being worked on by the team. labels May 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhuman/agent/harness/tool_loop.rs (1)

608-740: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Policy gate runs too late in the call pipeline.

On Line 609 and Line 687, approval paths can execute before the policy check at Line 721. A call denied by policy can still trigger approval-side effects first. Move policy evaluation before approval handling (and before approval-gate interception) so denied calls short-circuit immediately.

Suggested reordering
-            // ── Approval hook ────────────────────────────────
-            if let Some(mgr) = approval {
-                ...
-            }
-
             let tool_opt: Option<&dyn Tool> = tools_registry
                 .iter()
                 .chain(extra_tools.iter())
                 .find(|t| t.name() == call.name && is_visible(t.name()))
                 .map(|b| b.as_ref());

+            if let PolicyDecision::Deny(reason) =
+                tool_policy.evaluate(&call.name, &call.arguments)
+            {
+                let denied = format!("Tool '{}' denied by policy: {reason}", call.name);
+                emit_failed_completion(&denied).await;
+                individual_results.push(denied.clone());
+                let _ = writeln!(
+                    tool_results,
+                    "<tool_result name=\"{}\">\n{denied}\n</tool_result>",
+                    call.name
+                );
+                continue;
+            }
+
+            // ── Approval hook ────────────────────────────────
+            if let Some(mgr) = approval {
+                ...
+            }
🤖 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 `@src/openhuman/agent/harness/tool_loop.rs` around lines 608 - 740, The
PolicyDecision check currently runs after approval handling and the
external-effect ApprovalGate; move the policy evaluation to run immediately
after resolving tool_opt (i.e., right after the tool lookup/tracing block) so
denied tools short-circuit before any approval-side effects. Concretely:
relocate the block that calls tool_policy.evaluate(&call.name, &call.arguments)
/ matches PolicyDecision::Deny (and its
emit_failed_completion/individual_results/tool_results handling) to immediately
follow the `let tool_opt: Option<&dyn Tool> = ...` and tracing::debug(...)
lines; keep later checks (approval manager using ApprovalRequest,
mgr.prompt_cli, mgr.record_decision, ApprovalGate::try_global, and
tool.external_effect_with_args) after that repositioned policy block so
approvals and gate interception never run for policy-denied calls.
🤖 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 `@src/openhuman/agent/harness/tool_loop.rs`:
- Around line 724-729: The diagnostic log currently uses tracing::warn! for the
expected policy-denial branch; change that call to tracing::debug! (or
tracing::trace!) so it uses a lower verbosity level per repo logging rules.
Locate the tracing::warn! invocation that logs iteration, tool =
call.name.as_str(), and reason (the policy-denied branch in tool_loop.rs) and
replace the macro with tracing::debug! while preserving the structured fields
(iteration, tool, reason) and the message string so caller semantics and fields
remain identical.

---

Outside diff comments:
In `@src/openhuman/agent/harness/tool_loop.rs`:
- Around line 608-740: The PolicyDecision check currently runs after approval
handling and the external-effect ApprovalGate; move the policy evaluation to run
immediately after resolving tool_opt (i.e., right after the tool lookup/tracing
block) so denied tools short-circuit before any approval-side effects.
Concretely: relocate the block that calls tool_policy.evaluate(&call.name,
&call.arguments) / matches PolicyDecision::Deny (and its
emit_failed_completion/individual_results/tool_results handling) to immediately
follow the `let tool_opt: Option<&dyn Tool> = ...` and tracing::debug(...)
lines; keep later checks (approval manager using ApprovalRequest,
mgr.prompt_cli, mgr.record_decision, ApprovalGate::try_global, and
tool.external_effect_with_args) after that repositioned policy block so
approvals and gate interception never run for policy-denied calls.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a525d67-79cf-4de6-bb36-3a4f42e9ed20

📥 Commits

Reviewing files that changed from the base of the PR and between 65d92bf and c737377.

📒 Files selected for processing (9)
  • src/openhuman/agent/bus.rs
  • src/openhuman/agent/harness/bughunt_tests.rs
  • src/openhuman/agent/harness/harness_gap_tests.rs
  • src/openhuman/agent/harness/test_support_test.rs
  • src/openhuman/agent/harness/tests.rs
  • src/openhuman/agent/harness/tool_loop.rs
  • src/openhuman/agent/harness/tool_loop_tests.rs
  • src/openhuman/tools/mod.rs
  • src/openhuman/tools/policy.rs

Comment thread src/openhuman/agent/harness/tool_loop.rs Outdated
Liohtml and others added 2 commits May 20, 2026 10:11
Policy evaluation now runs before both the approval hook and the
external-effect approval gate, so a denied call short-circuits
immediately without triggering approval side-effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Policy denial is an expected control-flow branch, not a warning condition.
Downgrade tracing::warn! to tracing::debug! per CodeRabbit review feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 20, 2026
@Liohtml
Copy link
Copy Markdown
Contributor Author

Liohtml commented May 20, 2026

Credit to @vaddisrinivas for the well-specified issue (#2131) that made this implementation straightforward. The tool-policy middleware design and acceptance criteria in the issue guided the trait shape and test coverage directly.

@senamakel
Copy link
Copy Markdown
Member

hey @Liohtml can u enable maintainers to make edits in this PR? and also patch the typecheck and e2e tests?

@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label May 23, 2026
@Liohtml
Copy link
Copy Markdown
Contributor Author

Liohtml commented May 23, 2026

@senamakel Done — maintainer edits enabled on both PRs (#2303 and #2306). Both branches just merged latest upstream/main (v0.54.9) to pick up the i18n fixes. Fresh CI is running now.

@Liohtml
Copy link
Copy Markdown
Contributor Author

Liohtml commented May 23, 2026

@senamakel Update: maintainer edits are now enabled on both PRs.

The TypeScript and E2E failures are from duplicate property keys in the German i18n file (de-5.ts lines 529-532) on main — affects all open PRs, not specific to our changes. The Rust test/coverage failures appear to be CI infra timeouts.

src/lib/i18n/chunks/de-5.ts(529,3): error TS1117: An object literal cannot have multiple properties with the same name.

Once the German i18n dupes are removed from main, a merge + CI rerun should clear these. Happy to fix the dupes ourselves if you'd prefer — just let us know.

The upstream merge introduced duplicate property keys in the German
translation file de-5.ts, causing TS1117 errors in CI. Restored to
the clean upstream/main version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Liohtml
Copy link
Copy Markdown
Contributor Author

Liohtml commented May 23, 2026

@senamakel Correction: the i18n duplicates in de-5.ts were introduced by our upstream merges, not on main itself. Fixed in ca9dc0c6 — restored the clean upstream version. Fresh CI should now pass the TypeScript check. Sorry for the confusion in my earlier comment.

@senamakel
Copy link
Copy Markdown
Member

@Liohtml that's super awesome. thanks for this. just the last few tests that need to pass. if you can have a look at that all good to merge. thanks for your contrivbution!

Liohtml and others added 3 commits May 23, 2026 09:58
The upstream merge added a new test in tool_loop_tests.rs that calls
run_tool_call_loop with the pre-policy 16-arg signature. Updated to
pass &DefaultToolPolicy as the 17th argument.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upstream added settings.mascot.customGif{Error,Heading,Label} to the
English locale; the German translation file was missing them, causing
i18n coverage tests to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add generic tool-policy middleware before agent tool execution

2 participants