Skip to content

Add generic tool policy middleware#2137

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
vaddisrinivas:codex/tool-policy-middleware
May 20, 2026
Merged

Add generic tool policy middleware#2137
senamakel merged 3 commits into
tinyhumansai:mainfrom
vaddisrinivas:codex/tool-policy-middleware

Conversation

@vaddisrinivas
Copy link
Copy Markdown
Contributor

@vaddisrinivas vaddisrinivas commented May 18, 2026

Summary

  • Add a generic ToolPolicy middleware trait for pre-execution agent tool checks.
  • Wire session agents to call the policy before Tool::execute_with_options.
  • Keep default behavior allow-all unless a caller installs a stricter policy.

Validation

  • cargo fmt --manifest-path Cargo.toml
  • cargo test --manifest-path Cargo.toml policy

Refs #2131

Summary by CodeRabbit

  • New Features

    • Agents now support configurable tool policies that gate tool execution before tools run.
    • Custom policies can allow or deny specific tool calls; denied calls return a failure without running the tool.
    • Default behavior continues to permit all tools unless a policy is set.
  • Tests

    • Added tests verifying allow-all policy behavior and that deny decisions prevent tool execution.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR introduces a pre-execution ToolPolicy middleware for agent tool calls. A new ToolPolicy trait and ToolPolicyRequest/ToolPolicyDecision types enable custom policies to allow or deny tool execution before side effects. The builder wires policies into agents, defaulting to AllowAllToolPolicy for backward compatibility. Tool execution now checks policies and logs denials.

Changes

Tool Policy Middleware

Layer / File(s) Summary
Policy contract and default implementation
src/openhuman/agent/tool_policy.rs, src/openhuman/agent/mod.rs
Introduces ToolPolicyRequest (tool name, arguments, session/channel/agent identifiers), ToolPolicyDecision (Allow/Deny with reason), and ToolPolicy async trait (name + check). Provides AllowAllToolPolicy default and exports module publicly.
Agent data model and builder wiring
src/openhuman/agent/harness/session/types.rs, src/openhuman/agent/harness/session/builder.rs
Adds tool_policy: Arc<dyn ToolPolicy> to Agent and Option<Arc<dyn ToolPolicy>> to AgentBuilder. Builder initializes the option to None, exposes tool_policy(...) setter, and wires the provided policy (or AllowAllToolPolicy) into constructed Agent.
Tool execution policy check
src/openhuman/agent/harness/session/turn.rs
Integrates policy enforcement into execute_tool_call: builds a ToolPolicyRequest from tool metadata and context, awaits self.tool_policy.check(...), returns a denial record with success=false on Deny (and logs), otherwise proceeds with the existing tool execution and summarizer path.
Policy enforcement and denial tests
src/openhuman/agent/harness/session/turn_tests.rs
Adds CountingTool (increments atomic counter) and DenyCountingPolicy (denies counting). New tokio test constructs an agent with the denial policy, executes the counting tool call, asserts denial and expected message, and verifies the tool was not executed (counter remains zero).
sequenceDiagram
  participant Caller
  participant Agent
  participant ToolPolicy
  participant ToolImpl
  Caller->>Agent: execute_tool_call(tool_name, args)
  Agent->>ToolPolicy: check(ToolPolicyRequest)
  alt Deny
    ToolPolicy-->>Agent: Deny{reason}
    Agent-->>Caller: denial record (success=false, message)
  else Allow
    ToolPolicy-->>Agent: Allow
    Agent->>ToolImpl: execute_with_options(args)
    ToolImpl-->>Agent: execution result
    Agent-->>Caller: result record
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested labels

working

Suggested reviewers

  • graycyrus
  • senamakel

Poem

🐰 I check the tools before they roam,
I count the hops that never comb;
A policy gate, a gentle chime,
Deny or pass, I keep the time.
Hooray—no side effects in the night! 🚪🔐

🚥 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 'Add generic tool policy middleware' is concise, clear, and accurately summarizes the main change—introducing a reusable ToolPolicy middleware system for tool call gating.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ 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.

@vaddisrinivas vaddisrinivas force-pushed the codex/tool-policy-middleware branch from d55d467 to 3294b3e Compare May 18, 2026 20:07
@vaddisrinivas
Copy link
Copy Markdown
Contributor Author

vaddisrinivas commented May 18, 2026

Rebased this draft onto current main and resolved the single test import conflict. Latest head is 3294b3e. Local validation passed: cargo fmt --manifest-path Cargo.toml --all --check and cargo test --manifest-path Cargo.toml policy. CI is now rerunning.

@vaddisrinivas vaddisrinivas marked this pull request as ready for review May 18, 2026 20:10
@vaddisrinivas vaddisrinivas requested a review from a team May 18, 2026 20:10
@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 18, 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.

🧹 Nitpick comments (1)
src/openhuman/agent/harness/session/turn.rs (1)

988-993: ⚡ Quick win

Use debug/trace for policy-denial diagnostics to avoid warn-noise.

This denial branch is expected control flow for strict policies; warn here can create noisy operational signals.

♻️ Proposed change
-                tracing::warn!(
+                tracing::debug!(
                     tool = call.name.as_str(),
                     policy = self.tool_policy.name(),
                     reason = %reason,
                     "[agent_loop] tool denied by policy"
                 );

As per coding guidelines, src/**/*.rs: Use log or tracing crate at debug or trace level for Rust diagnostic logs.

🤖 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/session/turn.rs` around lines 988 - 993, The
current policy-denial log in turn.rs uses tracing::warn which is noisy for
expected control flow; change the call site that references call.name.as_str(),
self.tool_policy.name(), %reason and the "[agent_loop] tool denied by policy"
message to use tracing::debug or tracing::trace instead (pick debug for general
diagnostics, trace if very verbose) and keep the same structured fields and
message so policy-denial remains visible at a lower log level.
🤖 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.

Nitpick comments:
In `@src/openhuman/agent/harness/session/turn.rs`:
- Around line 988-993: The current policy-denial log in turn.rs uses
tracing::warn which is noisy for expected control flow; change the call site
that references call.name.as_str(), self.tool_policy.name(), %reason and the
"[agent_loop] tool denied by policy" message to use tracing::debug or
tracing::trace instead (pick debug for general diagnostics, trace if very
verbose) and keep the same structured fields and message so policy-denial
remains visible at a lower log level.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 27bcea0f-2301-4c58-aa38-4bce332e863f

📥 Commits

Reviewing files that changed from the base of the PR and between 0b053c5 and 3294b3e.

📒 Files selected for processing (6)
  • src/openhuman/agent/harness/session/builder.rs
  • src/openhuman/agent/harness/session/turn.rs
  • src/openhuman/agent/harness/session/turn_tests.rs
  • src/openhuman/agent/harness/session/types.rs
  • src/openhuman/agent/mod.rs
  • src/openhuman/agent/tool_policy.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 18, 2026
senamakel added 2 commits May 19, 2026 16:47
# Conflicts:
#	src/openhuman/agent/harness/session/builder.rs
#	src/openhuman/agent/harness/session/types.rs
…rabbitai on turn.rs)

Policy denial is expected control flow, not a warning condition. Using
warn! creates operational noise for callers that install strict policies
to gate tool calls by design. Switch to debug! so the structured
diagnostics remain visible at a lower log level without polluting the
warn stream.
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

🧹 Nitpick comments (1)
src/openhuman/agent/harness/session/turn.rs (1)

1058-1063: ⚡ Quick win

Add correlation fields to the denial log.

This is exactly the sort of branch that's hard to reconstruct from tests, but the new event only logs tool, policy, and reason. Please include call_id plus session/agent identifiers so denials can be matched back to the emitted tool lifecycle events.

Suggested change
                 tracing::debug!(
+                    call_id = call_id.as_str(),
                     tool = call.name.as_str(),
+                    session_id = %self.event_session_id(),
+                    agent_definition_id = %self.agent_definition_id,
                     policy = self.tool_policy.name(),
                     reason = %reason,
                     "[agent_loop] tool denied by policy"
                 );

As per coding guidelines, "Use log / tracing at debug or trace level on RPC entry and exit, error paths, state transitions, and any branch that is hard to infer from tests alone. Use structured, grep-friendly context with stable prefixes ... and include correlation fields such as request IDs, method names, and entity IDs when available."

🤖 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/session/turn.rs` around lines 1058 - 1063, The
denial log in the tool-policy branch (the tracing::debug! call that currently
logs tool = call.name.as_str(), policy = self.tool_policy.name(), reason =
%reason) needs correlation fields added so denials can be matched to lifecycle
events; update that tracing::debug! invocation to include the tool call's
identifier (call_id or call.id), plus session and agent identifiers available on
the current context (e.g., self.session.id, self.agent.id or similar fields) so
the log emits tool, policy, reason, call_id, session_id and agent_id together as
structured fields.
🤖 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/session/turn.rs`:
- Around line 1055-1069: The branch handling ToolPolicyDecision::Deny currently
injects the raw policy-provided reason into the tool result; instead, keep the
detailed reason only in logs/telemetry (leave the tracing::debug call with
reason intact) and return a generic denial string for the tool output generated
in the code that formats ("Tool '{}' denied by policy '{}'...") so that
self.tool_policy.check(&policy_request).await's reason is not persisted or
exposed to the model; update the formatted message produced for call.name and
self.tool_policy.name to a non-sensitive, generic message (e.g., "Tool denied by
policy") while retaining the existing debug logging that references reason and
policy name.

---

Nitpick comments:
In `@src/openhuman/agent/harness/session/turn.rs`:
- Around line 1058-1063: The denial log in the tool-policy branch (the
tracing::debug! call that currently logs tool = call.name.as_str(), policy =
self.tool_policy.name(), reason = %reason) needs correlation fields added so
denials can be matched to lifecycle events; update that tracing::debug!
invocation to include the tool call's identifier (call_id or call.id), plus
session and agent identifiers available on the current context (e.g.,
self.session.id, self.agent.id or similar fields) so the log emits tool, policy,
reason, call_id, session_id and agent_id together as structured fields.
🪄 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: 894706ea-f2fd-490f-b472-635af41ee428

📥 Commits

Reviewing files that changed from the base of the PR and between 3294b3e and 85a9875.

📒 Files selected for processing (4)
  • src/openhuman/agent/harness/session/builder.rs
  • src/openhuman/agent/harness/session/turn.rs
  • src/openhuman/agent/harness/session/turn_tests.rs
  • src/openhuman/agent/harness/session/types.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/openhuman/agent/harness/session/turn_tests.rs
  • src/openhuman/agent/harness/session/builder.rs
  • src/openhuman/agent/harness/session/types.rs

Comment on lines +1055 to +1069
if let ToolPolicyDecision::Deny { reason } =
self.tool_policy.check(&policy_request).await
{
tracing::debug!(
tool = call.name.as_str(),
policy = self.tool_policy.name(),
reason = %reason,
"[agent_loop] tool denied by policy"
);
(
format!(
"Tool '{}' denied by policy '{}': {reason}",
call.name,
self.tool_policy.name()
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep raw policy reasons out of the tool result.

reason is arbitrary policy-provided text, but this branch turns it into tool output that gets persisted and fed back to the model on the next iteration. That makes internal enforcement details or user/session-specific context prompt-visible. Return a generic denial message here and keep the detailed reason only in logs/telemetry.

Suggested change
             if let ToolPolicyDecision::Deny { reason } =
                 self.tool_policy.check(&policy_request).await
             {
+                let policy_name = self.tool_policy.name().to_string();
                 tracing::debug!(
                     tool = call.name.as_str(),
-                    policy = self.tool_policy.name(),
+                    policy = %policy_name,
                     reason = %reason,
                     "[agent_loop] tool denied by policy"
                 );
                 (
-                    format!(
-                        "Tool '{}' denied by policy '{}': {reason}",
-                        call.name,
-                        self.tool_policy.name()
-                    ),
+                    format!("Tool '{}' denied by policy '{}'", call.name, policy_name),
                     false,
                 )
             } else {
🤖 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/session/turn.rs` around lines 1055 - 1069, The
branch handling ToolPolicyDecision::Deny currently injects the raw
policy-provided reason into the tool result; instead, keep the detailed reason
only in logs/telemetry (leave the tracing::debug call with reason intact) and
return a generic denial string for the tool output generated in the code that
formats ("Tool '{}' denied by policy '{}'...") so that
self.tool_policy.check(&policy_request).await's reason is not persisted or
exposed to the model; update the formatted message produced for call.name and
self.tool_policy.name to a non-sensitive, generic message (e.g., "Tool denied by
policy") while retaining the existing debug logging that references reason and
policy name.

@senamakel senamakel merged commit ec51cff into tinyhumansai:main May 20, 2026
29 checks passed
mtkik pushed a commit to mtkik/openhuman-meet that referenced this pull request May 21, 2026
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
CodeGhost21 pushed a commit to CodeGhost21/openhuman that referenced this pull request May 22, 2026
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants