Skip to content

fix(flows): close Composio tool_call tier-gate bypass (C1) - #5000

Merged
graycyrus merged 4 commits into
tinyhumansai:mainfrom
graycyrus:fix/flows-composio-tier-gate
Jul 16, 2026
Merged

graycyrus merged 4 commits into
tinyhumansai:mainfrom
graycyrus:fix/flows-composio-tier-gate

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

C1 security fix — the Composio tool_call branch in OpenHumanTools::invoke (src/openhuman/tinyflows/caps.rs) reached the approval gate (intercept_audited) without ever consulting the user's autonomy tier. Every other acting node path — native oh: tools, http_request, and code — goes through enforce_node_tier_gate + gate_call_for_tier first. The Composio path skipped straight to curation + approval, so a read-only-tier user's flow could still fire an outbound Composio call (Slack, Gmail, etc.) unattended.

This bypass compounded with a second gap: flows_update never re-applied flows_create's Rule 2 (force require_approval: true when the graph contains an outbound side-effect node). So a flow saved read-only could later be edited to add a Composio/http_request/code node while keeping require_approval: false — the update path trusted the caller's toggle unconditionally instead of re-deriving it from the new graph.

Net effect: a flow could gain an outbound Composio node with require_approval: false and fire unattended even under a readonly autonomy tier.

Fix (two parts, one shared helper each)

1. src/openhuman/tinyflows/caps.rs — tier-gate the Composio dispatch path

  • OpenHumanTools now carries security: Arc<SecurityPolicy> (previously only config).
  • Before the curation check (is_curated_flow_tool), the Composio branch now calls enforce_node_tier_gate(&self.security, CommandClass::Network, "tool_call") — same CommandClass::Network classification http_request uses, since a Composio action is a third-party API call.
  • The bare ApprovalGate::intercept_audited call is replaced with the gate_call_for_tier pattern copied from OpenHumanHttp::request, so a Supervised-tier Prompt decision correctly escalates past a flow's own require_approval: false default (closing the same Codex P1 gap http_request/code already close).
  • Cleanup: the native oh: branch's inline SecurityPolicy::from_config(...) construction is replaced with &self.security (no behavior change, removes a redundant construction).
  • Both OpenHumanTools construction sites (build_capabilities, and the tools_adapter test helper in tests.rs) updated to pass security.

2. src/openhuman/flows/ops.rs — re-apply Rule 2 on update, not just create

  • New shared helper enforce_side_effect_approval(graph, caller_require_approval) -> (effective_require_approval, was_forced), next to graph_has_outbound_side_effect.
  • flows_create refactored to use it (behavior-preserving — same logs, same outcome).
  • flows_update now calls the same helper on the effective graph (the one actually being persisted) and passes effective_require_approval (not the caller's raw new_require_approval) to store::update_flow_graph, with the same "require_approval forced to true…" log line flows_create already emits.

Toolkit-neutral throughout — SLACK_SEND_MESSAGE is used only as a sample slug in the new test; no Slack/Gmail-specific logic was added.

Failing-test-first

  • composio_tool_call_blocks_under_readonly_tier (caps.rs): constructs OpenHumanTools under a ReadOnly policy and asserts invoke("SLACK_SEND_MESSAGE", ...) returns EngineError::Capability carrying POLICY_BLOCKED_MARKER. Before the fix, OpenHumanTools didn't even have a security field — confirmed as a compile error against the pre-fix struct shape.
  • flows_update_forces_require_approval_when_adding_side_effect_nodes (ops_tests.rs): creates a trigger-only flow with require_approval: false, then updates it to a graph with a tool_call node passing Some(false), and asserts require_approval is forced true with a matching log line. Confirmed failing pre-fix (thread ... panicked ... flows_update must force require_approval ...), passing post-fix.
  • Guard test flows_update_does_not_force_require_approval_on_readonly_graph: a name-only update to a read-only graph must not force require_approval — passes both before and after, proving the fix doesn't over-gate.

Verification

  • GGML_NATIVE=OFF cargo test --lib openhuman::tinyflows:: — 133 passed, 0 failed, 1 ignored.
  • GGML_NATIVE=OFF cargo test --lib openhuman::flows:: — 397 passed, 0 failed.
  • Explicitly re-checked no regressions in: http_request_node_tier_gate_blocks_readonly_allows_higher, code_node_tier_gate_blocks_readonly_allows_full, http_adapter_blocks_under_readonly_tier, prompt_decision_*, allow_decision_never_escalates, flows_create_outbound_node_forces_require_approval, graph_has_outbound_side_effect_*, draft_promote_* — all pass.
  • GGML_NATIVE=OFF cargo check — clean.
  • cargo fmt --check — clean.

This touches the approval/security core, so the fix mirrors existing patterns exactly (enforce_node_tier_gate + gate_call_for_tier, same as http_request/code) rather than introducing a new gating mechanism, and deliberately does not add any Block-tier behavior beyond what http_request already enforces for the same CommandClass::Network.

Test plan

  • New tests fail before the fix, pass after
  • cargo test --lib openhuman::tinyflows:: green
  • cargo test --lib openhuman::flows:: green
  • cargo check clean
  • cargo fmt --check clean

Summary by CodeRabbit

  • Security
    • Outbound side-effect workflows now consistently require approval, even when updated to add external action nodes (including tool calls).
    • Read-only execution now blocks network-based tool actions earlier, before curation and dispatch.
    • Native and external tool calls follow the configured autonomy/security policy.
  • Bug Fixes
    • Updating a flow’s name alone no longer incorrectly enables approval requirements.
  • Tests
    • Added regression coverage for approval enforcement in flow updates and for read-only blocking, including JSON-RPC flow update behavior.

Two compounding gaps let a flow gain an unattended, unapproved outbound
Composio call even under a read-only autonomy tier:

1. The Composio branch of OpenHumanTools::invoke (caps.rs) called
   intercept_audited directly, skipping the enforce_node_tier_gate +
   gate_call_for_tier pair that the native `oh:`, http_request, and code
   node paths all go through. OpenHumanTools now carries a SecurityPolicy
   and gates Composio dispatch as Network-class, before curation, mirroring
   OpenHumanHttp::request exactly.

2. flows_update never re-applied flows_create's Rule 2 (force
   require_approval when the graph contains an outbound side-effect node),
   so editing a saved read-only flow to add a tool_call/http_request/code
   node could keep require_approval=false. Both flows_create and
   flows_update now go through one shared enforce_side_effect_approval
   helper in ops.rs.

Failing-test-first: composio_tool_call_blocks_under_readonly_tier (caps.rs)
and flows_update_forces_require_approval_when_adding_side_effect_nodes
(ops_tests.rs) were added and confirmed failing before the fix, plus a
flows_update_does_not_force_require_approval_on_readonly_graph guard test.
Platform-agnostic — SLACK_SEND_MESSAGE is only a sample slug.
@graycyrus
graycyrus requested a review from a team July 16, 2026 16:47
@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69807234-d1d0-4b38-925a-a9def1b6053c

📥 Commits

Reviewing files that changed from the base of the PR and between 082aa39 and 6cd111d.

📒 Files selected for processing (3)
  • src/openhuman/flows/ops.rs
  • src/openhuman/tinyflows/caps.rs
  • tests/json_rpc_e2e.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/openhuman/tinyflows/caps.rs
  • src/openhuman/flows/ops.rs

📝 Walkthrough

Walkthrough

Flow creation and updates now share outbound side-effect approval enforcement. Tool invocation receives an injected security policy, gates Composio calls earlier by autonomy tier, and uses tier-aware approval handling with regression coverage.

Changes

Approval and security enforcement

Layer / File(s) Summary
Centralized flow approval enforcement
src/openhuman/flows/ops.rs
Flow creation and updates use enforce_side_effect_approval, persist the effective approval value, and report when approval is forced for outbound side-effect graphs.
Flow approval regression coverage
src/openhuman/flows/ops_tests.rs, tests/json_rpc_e2e.rs
Tests cover forcing approval when an update adds a side-effect node, preserving false for name-only updates, and observing the persisted value through JSON-RPC.
Injected tool security and early gating
src/openhuman/tinyflows/caps.rs, src/openhuman/tinyflows/tests.rs
OpenHumanTools receives SecurityPolicy, gates Composio calls before curation and dispatch, and routes approval through tier-aware handling. Tests verify read-only calls are blocked early.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: bug, rust-core

Poem

I’m a rabbit guarding each outbound trail,
Approval now follows every tale.
Read-only paws meet an early “no,”
Before tools or dispatch can go.
Shared gates keep the burrow bright—
Hop, hop, secure tonight! 🐇

🚥 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 clearly matches the main change: fixing the Composio tool_call tier-gate bypass in flows.
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.

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

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 16, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/openhuman/flows/ops.rs (1)

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

Expose the effective approval decision in the update persistence log.

Lines 2669-2670 compute the security-sensitive effective value, but the adjacent flows_update: persisting changes event records only has_expected. Add require_approval = effective_require_approval and side_effect_forced so both branches are grep-friendly and auditable.

As per coding guidelines, new or changed flows must include verbose, grep-friendly diagnostics covering branches and state transitions.

🤖 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/flows/ops.rs` around lines 2669 - 2670, Update the adjacent
“flows_update: persisting changes” event to include both
effective_require_approval and side_effect_forced alongside has_expected. Use
the values returned by enforce_side_effect_approval, preserving the existing
branch/state-transition diagnostics and making both approval outcomes explicitly
auditable.

Source: Coding guidelines

🤖 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/flows/ops_tests.rs`:
- Around line 4340-4382: The existing direct API test in
flows_update_forces_require_approval_when_adding_side_effect_nodes lacks
JSON-RPC coverage. Add a JSON-RPC regression test that updates a flow with
require_approval set to false and a side-effect graph, then assert the RPC
response and persisted flow both report require_approval as true.

In `@src/openhuman/tinyflows/caps.rs`:
- Around line 2639-2650: In the approval-gate branch within the flow path around
gate_call_for_tier, log a flow-level diagnostic immediately before returning a
GateOutcome::Deny error. Include only the action slug and tier decision,
excluding reason and args, then preserve the existing EngineError::Capability
return behavior.

---

Nitpick comments:
In `@src/openhuman/flows/ops.rs`:
- Around line 2669-2670: Update the adjacent “flows_update: persisting changes”
event to include both effective_require_approval and side_effect_forced
alongside has_expected. Use the values returned by enforce_side_effect_approval,
preserving the existing branch/state-transition diagnostics and making both
approval outcomes explicitly auditable.
🪄 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: 34b87662-34c8-4860-a5a8-e6b37ea84ccc

📥 Commits

Reviewing files that changed from the base of the PR and between 9fd5fde and 082aa39.

📒 Files selected for processing (4)
  • src/openhuman/flows/ops.rs
  • src/openhuman/flows/ops_tests.rs
  • src/openhuman/tinyflows/caps.rs
  • src/openhuman/tinyflows/tests.rs

Comment thread src/openhuman/flows/ops_tests.rs Resolved
Comment thread src/openhuman/tinyflows/caps.rs Resolved
…te (addresses @coderabbitai on ops.rs:2669)

Nitpick from CodeRabbit review on PR tinyhumansai#5000: the flows_update persistence
log recorded has_expected but not the security-sensitive effective
approval decision, making it harder to audit Rule 2 forcing after the
fact from logs alone.
…ses @coderabbitai on caps.rs:2650)

CodeRabbit suggestion: the Deny branch on the Composio tool_call
approval gate returned without a flow-level diagnostic, unlike the
sibling native oh: tool_call Deny branch a few lines up. Logs slug +
tier decision only (no reason/args), matching the redaction convention
used elsewhere in this file.
…(addresses @coderabbitai on ops_tests.rs:4382)

CodeRabbit flagged that flows_update_forces_require_approval_when_adding_side_effect_nodes
only covers the direct ops::flows_update API, with no JSON-RPC-layer
case proving the openhuman.flows_update controller (schema + handler
wiring) forces require_approval=true when require_approval: false is
passed alongside a graph that gained an outbound side-effect node, and
that the forced value is what's actually persisted (not just echoed
back).
@graycyrus

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit nitpick on src/openhuman/flows/ops.rs:2669-2670 (no dedicated review thread was created for it — it was embedded in the review summary under "Nitpick comments" rather than posted as a standalone inline comment): the flows_update: persisting changes debug log now includes require_approval = effective_require_approval and side_effect_forced alongside has_expected, so both branches of the Rule 2 decision are grep-friendly and auditable from logs without cross-referencing the separate info! line that only fires when forcing occurs. Fixed in b96e702.

All three CodeRabbit findings addressed:

  • ops.rs:2669-2670 (nitpick, logging) — b96e702
  • caps.rs:2650 (approval-gate denial logging) — f20fb38
  • ops_tests.rs:4382 (JSON-RPC regression coverage gap) — 6cd111d

Verification: GGML_NATIVE=OFF cargo test --lib openhuman::tinyflows:: (133 passed), GGML_NATIVE=OFF cargo test --lib openhuman::flows:: (397 passed), the three named tier-gate/Rule-2 regression tests individually (composio_tool_call_blocks_under_readonly_tier, flows_update_forces_require_approval_when_adding_side_effect_nodes, flows_update_does_not_force_require_approval_on_readonly_graph) — all pass, cargo check clean, cargo fmt clean, plus the new json_rpc_flows_update_forces_require_approval_on_side_effect_graph and the full flows JSON-RPC suite (9/9).

No changes to the security posture: tier gate still runs before curation on the Composio branch, still classified CommandClass::Network, Rule 2 still enforced via the shared enforce_side_effect_approval helper on both create+update paths, and the readonly-graph guard test still passes unmodified — these three fixes are diagnostics/test-coverage only.

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

Labels

bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

1 participant