Skip to content

fix(composio): gate write actions through approval + reshape agent-path results - #5259

Open
yh928 wants to merge 1 commit into
tinyhumansai:mainfrom
yh928:fix/composio-approval-reshape
Open

fix(composio): gate write actions through approval + reshape agent-path results#5259
yh928 wants to merge 1 commit into
tinyhumansai:mainfrom
yh928:fix/composio-approval-reshape

Conversation

@yh928

@yh928 yh928 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Two gaps on the agent's Composio execution surface, both diagnosed against a live instance.

Approval gate (the reported bug)

The human-in-the-loop approval card is raised only for a tool whose external_effect_with_args is true, but neither composio_execute nor the per-action ComposioActionTool declared it — so a Composio mail send (GMAIL_SEND_EMAIL) via the integrations_agent fired with no approval prompt at all, even when the user had configured "ask before sending". Neither the contract gate (which only ensures the action schema is in context) nor permission_level = Write (channel caps: allow/block, not a prompt) raises that card.

Both surfaces now report external-effect for a write/admin-scoped action and stay false for a pure read, so a write routes through the ApprovalGate (parking for the card under a WebChat turn, which the inline sub-agent inherits) while a fetch/list flows through unprompted — matching the external_effect contract. Scope is classified synchronously: resolve_action_scope's body has no await, so it is reused via a resolve_action_scope_sync core.

Agent-path envelope reshape (#2585)

When the agent calls a Composio action directly, a verbose provider envelope — Gmail's full MIME tree under payload.parts[], dozens of Received: headers — landed in context on the raw-JSON fallback body. The response reshape that slims it into one clean record per message was wired only into the sync path; it now runs inline on the agent composio_execute and per-action paths, so resp.data is slimmed before it can become the tool body. A backend-rendered markdown_formatted body is already clean and unaffected.

Tests

cargo test --lib composio — new coverage: execute_tool_gates_writes_but_not_reads_via_external_effect and per_action_tool_gates_writes_but_not_reads assert the gate/no-gate split for send/delete vs fetch. (The composio::ops delete-connection tests are flaky only under the parallel chunk-DB cold-open race, fixed separately in tinycortex; they pass --test-threads=1.)

Summary by CodeRabbit

  • New Features

    • Added clearer safeguards for actions that send, modify, or delete external data.
    • Read-only actions can proceed without unnecessary confirmation prompts.
    • Action results are now better formatted based on the connected service and action inputs.
  • Bug Fixes

    • Improved handling of missing or incomplete action details by applying conservative safeguards.
  • Tests

    • Added coverage confirming correct behavior for read, write, and administrative actions.

…ults

Two gaps on the agent's Composio execution surface, both diagnosed live.

**Approval (P1).** The human-in-the-loop approval card is raised only for tools
whose `external_effect_with_args` is true, but neither `composio_execute` nor
the per-action `ComposioActionTool` declared it — so a Composio mail send
(`GMAIL_SEND_EMAIL`) fired with no approval prompt at all, even when the user
had "ask before sending" configured. The contract gate (schema-presence) and
`permission_level = Write` (channel caps) do not raise that card. Both surfaces
now report external-effect for a write/admin-scoped action and stay false for a
pure read, so a write routes through the `ApprovalGate` while a fetch/list flows
through unprompted. Scope is classified synchronously (`resolve_action_scope`'s
body has no `await`, so it is reused via `resolve_action_scope_sync`).

**Reshape (P4, tinyhumansai#2585).** When the agent calls a Composio action directly, a
verbose provider envelope — Gmail's full MIME tree under `payload.parts[]` —
landed in context on the raw-JSON fallback body. The provider response reshape
that slims it (the same one the sync path runs) was only wired into sync; it now
runs inline on the agent execute + per-action paths, so `resp.data` is slimmed
before it can become the tool body. A backend-rendered `markdown_formatted` body
is already clean and unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRSNnqQsokuGmkbpLoLCGy
@yh928
yh928 requested a review from a team July 29, 2026 06:27
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Composio tools now classify external effects for gating and apply toolkit-provider response post-processing before serializing action results. Tests cover write, admin, read, and missing-tool cases.

Changes

Composio action flow

Layer / File(s) Summary
External-effect classification and gating
src/openhuman/composio/tools.rs, src/openhuman/composio/action_tool.rs, src/openhuman/composio/tools_tests.rs
Action scopes can be resolved synchronously; write/admin actions and missing tool slugs trigger gating, while read actions do not.
Provider response post-processing
src/openhuman/composio/tools.rs, src/openhuman/composio/action_tool.rs
Original arguments are retained and passed to toolkit providers that reshape dispatched response data before tool-result serialization.

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

Sequence Diagram(s)

sequenceDiagram
  participant ToolCaller
  participant ComposioExecuteTool
  participant ActionScopeResolver
  participant Dispatcher
  participant ToolkitProvider
  ToolCaller->>ComposioExecuteTool: provide tool slug and arguments
  ComposioExecuteTool->>ActionScopeResolver: resolve action scope
  ActionScopeResolver-->>ComposioExecuteTool: effect classification
  ComposioExecuteTool->>Dispatcher: dispatch action
  Dispatcher-->>ComposioExecuteTool: response data
  ComposioExecuteTool->>ToolkitProvider: post-process response data
  ToolkitProvider-->>ComposioExecuteTool: reshaped data
  ComposioExecuteTool-->>ToolCaller: serialized tool result
Loading

Possibly related PRs

Suggested labels: bug, rust-core

Suggested reviewers: m3ga-mind, sanil-23

Poem

I’m a rabbit guarding the gate,
Write actions wait, reads pass straight.
Provider data gets a tidy trim,
Arguments guide each shaping whim.
Hop, hop—the tool results bloom!

🚥 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 summarizes the two main changes: approval gating for write actions and agent-path response reshaping.
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.

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.

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 29, 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.

Caution

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

⚠️ Outside diff range comments (1)
src/openhuman/composio/tools.rs (1)

1477-1549: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add regression tests for response reshaping on both execution paths.

The new behavior is untested: verify shaped resp.data in the raw-JSON fallback, preservation of backend markdown_formatted, and forwarding of raw_html arguments.

  • src/openhuman/composio/tools.rs#L1477-L1549: add deterministic dispatcher-path coverage in src/openhuman/composio/tools_tests.rs.
  • src/openhuman/composio/action_tool.rs#L304-L341: add equivalent per-action coverage in this module’s tests.

As per coding guidelines, “Untested code is incomplete; add tests for new or changed behavior.”

🤖 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/composio/tools.rs` around lines 1477 - 1549, Add regression
tests for response reshaping in src/openhuman/composio/tools.rs lines 1477-1549
by adding deterministic dispatcher-path coverage in
src/openhuman/composio/tools_tests.rs, verifying shaped resp.data for the
raw-JSON fallback, preservation of backend markdown_formatted, and forwarding of
raw_html arguments. Add equivalent per-action coverage for
src/openhuman/composio/action_tool.rs lines 304-341 in that module’s tests,
covering the same behaviors.

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.

Outside diff comments:
In `@src/openhuman/composio/tools.rs`:
- Around line 1477-1549: Add regression tests for response reshaping in
src/openhuman/composio/tools.rs lines 1477-1549 by adding deterministic
dispatcher-path coverage in src/openhuman/composio/tools_tests.rs, verifying
shaped resp.data for the raw-JSON fallback, preservation of backend
markdown_formatted, and forwarding of raw_html arguments. Add equivalent
per-action coverage for src/openhuman/composio/action_tool.rs lines 304-341 in
that module’s tests, covering the same behaviors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82754407-63cc-4cb4-8159-8cdceb516340

📥 Commits

Reviewing files that changed from the base of the PR and between 8072f08 and 4f13e2b.

📒 Files selected for processing (3)
  • src/openhuman/composio/action_tool.rs
  • src/openhuman/composio/tools.rs
  • src/openhuman/composio/tools_tests.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f13e2bdad

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +152 to +158
fn external_effect_with_args(&self, _args: &Value) -> bool {
// The per-action surface must gate on the approval card exactly like the
// `composio_execute` dispatcher: a write/admin action (this tool's own
// slug) routes through the `ApprovalGate` before it runs; a pure read
// flows through unprompted. Without this the model could send mail /
// create records via a per-action tool with no approval prompt at all.
super::tools::action_mutates_external_state(&self.action_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid approval prompts for contract-only probes

When an integrations_agent uses a per-action write slug for the first time in a turn (for example GMAIL_SEND_EMAIL), this hook makes ApprovalSecurityMiddleware prompt before execute() runs, but execute() can still hit GateDecision::Surface and return only the contract/schema error before dispatching the action. That means the user approves a no-op schema probe and then gets prompted again on the retry that actually sends; if they deny the first prompt, the model never receives the contract it needs to form the safer retry.

Useful? React with 👍 / 👎.

Comment on lines 318 to +341
@@ -313,6 +325,20 @@ impl Tool for ComposioActionTool {
elapsed_ms,
},
);
// Slim the provider envelope before it can become the tool body
// (#2585) — the same reshape `ComposioExecuteTool` runs, so a
// per-action Gmail fetch doesn't drop a full MIME tree into the
// agent's context. Only the raw-JSON fallback body serializes
// `resp.data`; a backend-rendered markdown body is unaffected.
if let Some(provider) = super::providers::toolkit_from_slug(&self.action_name)
.and_then(|tk| super::providers::get_provider(&tk))
{
provider.post_process_action_result(
&self.action_name,
reshape_args.as_ref(),
&mut resp.data,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Event-bus publish / reshape ordering differs from the tools.rs path

In action_tool.rs the DomainEvent::ComposioActionExecuted event is published before post_process_action_result runs, whereas in ComposioExecuteTool::execute (tools.rs) the reshape runs first and the event fires afterwards. The event payload only captures resp.successful, resp.error, resp.cost_usd, and elapsed_ms — none of which are touched by the reshape — so there is no functional difference today. But the asymmetry means a future change that adds resp.data (or a derived field) to the event would silently produce inconsistent snapshots on the two surfaces: the dispatcher event would see slimmed data, and the per-action event would see the raw envelope. Moving the publish_global call to after the reshape block (matching tools.rs) removes the latent divergence at zero cost.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes two gaps on the Composio execution surface. First, it gates write/admin Composio actions through the human-in-the-loop ApprovalGate by implementing external_effect_with_args on both ComposioExecuteTool (dispatcher) and ComposioActionTool (per-action); previously neither declared this, so write actions such as GMAIL_SEND_EMAIL fired with no approval prompt. Second, it applies the provider envelope reshape (post_process_action_result) inline on both agent paths, so verbose payloads (Gmail MIME trees, dozens of Received: headers) are slimmed before becoming tool-body content.

  • Adds resolve_action_scope_sync as the synchronous core of scope classification, allowing the synchronous external_effect_with_args hook to classify slugs without async; resolve_action_scope is kept as a thin wrapper for backward compatibility.
  • Both ComposioExecuteTool and ComposioActionTool now call action_mutates_external_state (write/admin → true, read → false), with absent/empty slugs defaulting to true (fail-safe gating).
  • New tests cover the write-vs-read gate split for both surfaces and a regression fixture for the absent-slug case.

Confidence Score: 4/5

Safe to merge. The approval-gate fix is correctly scoped (write/admin → gate, read → pass-through) on both tool surfaces, and the envelope reshape is applied consistently to the agent path. One ordering inconsistency between the two code paths is cosmetic.

Both the gate and the reshape are implemented correctly; the sync/async split for scope classification is clean; tests cover the key regression. The only finding is a style inconsistency in the order of event-bus publish vs. reshape between the dispatcher and per-action paths — this has no current functional effect since the event payload does not include resp.data.

Files Needing Attention: The event-publish ordering in action_tool.rs (publish before reshape) vs. tools.rs (reshape before publish) is worth aligning in a follow-up, but does not require blocking this PR.

Important Files Changed

Filename Overview
src/openhuman/composio/tools.rs Adds resolve_action_scope_sync, action_mutates_external_state, and external_effect_with_args on ComposioExecuteTool; inlines the envelope reshape after task-window filtering; introduces reshape_args clone before dispatch to preserve args for post-processing.
src/openhuman/composio/action_tool.rs Adds external_effect_with_args delegating to action_mutates_external_state, clones args as reshape_args before dispatch, and applies the provider envelope reshape after the event-bus publish (order reversed relative to the tools.rs path — minor style inconsistency, not a functional bug).
src/openhuman/composio/tools_tests.rs Adds execute_tool_gates_writes_but_not_reads_via_external_effect covering send/delete (gated), fetch (not gated), and absent slug (gated). Well-structured regression test for the reported bug.

Sequence Diagram

sequenceDiagram
    participant Engine as Agent Engine
    participant Gate as ApprovalGate
    participant Tool as ComposioExecuteTool / ComposioActionTool
    participant Dispatch as execute_dispatch
    participant Provider as ProviderReshaper

    Engine->>Tool: external_effect_with_args(args)
    Tool->>Tool: action_mutates_external_state(slug) via resolve_action_scope_sync
    alt write / admin slug (or absent slug)
        Tool-->>Engine: true
        Engine->>Gate: intercept_audited(...)
        Gate-->>Engine: Allow / Deny
    else read slug
        Tool-->>Engine: false
        note over Engine: no approval prompt
    end

    Engine->>Tool: execute(args)
    Tool->>Tool: sandbox gate (ReadOnly check)
    Tool->>Dispatch: execute_composio_action_kind(...)
    Dispatch-->>Tool: ComposioExecuteResponse

    alt provider found for toolkit
        Tool->>Provider: post_process_action_result(slug, reshape_args, resp.data)
        Provider-->>Tool: data slimmed in-place
    end

    alt resp.markdown_formatted present and success
        Tool-->>Engine: ToolResult::success(markdown)
    else
        Tool-->>Engine: ToolResult::success(serde_json(resp))
    end
Loading

Reviews (1): Last reviewed commit: "fix(composio): gate write actions throug..." | Re-trigger Greptile

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

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant