Skip to content

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

Open
yh928 wants to merge 2 commits 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 2 commits 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.

Closes #5299

…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 <[email protected]>
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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's two main changes: approval gating for write actions and reshaping agent-path results.

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

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026

@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 thread src/openhuman/composio/action_tool.rs
Comment thread src/openhuman/composio/action_tool.rs
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR closes two gaps in the Composio execution surface. First, neither composio_execute nor ComposioActionTool declared external_effect_with_args, meaning write actions (e.g. GMAIL_SEND_EMAIL) bypassed the human-in-the-loop ApprovalGate entirely. Second, the provider envelope reshape that slims verbose payloads (Gmail MIME trees, dozens of Received: headers) was only wired into the sync path and not the agent/direct-call path.

  • Both ComposioExecuteTool and ComposioActionTool now implement external_effect_with_args, delegating to the new action_mutates_external_state helper (itself backed by the extracted resolve_action_scope_sync synchronous core) so a write/admin slug is gated and a read slug flows through unprompted.
  • post_process_action_result is now called inline on the agent path in both tools.rs and action_tool.rs before the raw-JSON fallback body is assembled, matching the existing sync-path behaviour.
  • New unit tests in both files assert the gate/no-gate split for send, delete, fetch, and the empty-slug conservative fallback.

Confidence Score: 5/5

Safe to merge. The change is tightly scoped to two well-understood gaps: a missing approval-gate declaration and a missing response reshape on the agent path.

Both changes are additive, conservative, and symmetrically applied across the two Composio execution surfaces. The sync extraction of resolve_action_scope_sync makes no behavioural change to the async callers. The reshape runs on the same static provider/catalog lookup path already used by the sync executor. The empty-slug fallback in ComposioExecuteTool::external_effect_with_args errs on the side of gating, not permitting. Tests cover send, delete, fetch, and the absent-slug case on both surfaces. No existing test paths are altered.

Files Needing Attention: No files require special attention.

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; inserts inline post_process_action_result reshape on the agent path. Logic is correct and conservative (empty/non-string slug defaults to gated).
src/openhuman/composio/action_tool.rs Adds external_effect_with_args on ComposioActionTool (keyed off the fixed action_name), inserts inline reshape before event publish matching tools.rs, and adds reshape_args clone before the dispatch consumes args. Event ordering is now consistent between both surfaces.
src/openhuman/composio/tools_tests.rs Adds execute_tool_gates_writes_but_not_reads_via_external_effect covering send, delete, fetch, and absent-slug cases for ComposioExecuteTool. Clean and well-scoped.

Sequence Diagram

sequenceDiagram
    participant Agent as Agent Loop
    participant Gate as ApprovalGate
    participant EWT as external_effect_with_args
    participant Scope as resolve_action_scope_sync
    participant Exec as execute()
    participant Dispatch as execute_dispatch
    participant Reshape as post_process_action_result

    Agent->>Gate: invoke composio_execute / ComposioActionTool
    Gate->>EWT: external_effect_with_args(args)
    EWT->>Scope: action_mutates_external_state(slug)
    Scope-->>EWT: Write/Admin → true, Read → false
    EWT-->>Gate: bool
    alt write/admin action
        Gate->>Agent: show approval card (blocks)
        Agent->>Gate: user approves
    end
    Gate->>Exec: execute(args)
    Exec->>Dispatch: execute_composio_action_kind[_with_connection]
    Dispatch-->>Exec: ComposioResponse
    Exec->>Reshape: post_process_action_result(slug, reshape_args, resp.data)
    Reshape-->>Exec: slimmed resp.data
    Exec->>Agent: ToolResult (markdown_formatted or slimmed JSON)
Loading

Reviews (2): Last reviewed commit: "fix(composio): publish the action event ..." | Re-trigger Greptile

…patcher does

`ComposioExecuteTool` reshapes then publishes; the per-action tool published
then reshaped. The payload names no reshaped field today, so the order is not
observable — but two surfaces describing the same action must not disagree
about which snapshot the event saw, or the first field added to it diverges
silently between them.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SRSNnqQsokuGmkbpLoLCGy
@yh928

yh928 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 213253560 for the ordering finding; the approval one I am answering rather than changing, with reasoning.

Event-bus publish / reshape ordering (greptile) — fixed. Agreed there is no functional difference today, and that is exactly why it was worth closing: the two surfaces describing the same action must not disagree about which snapshot the event saw, or the first field added to the payload diverges silently between them. publish_global now runs after the reshape in action_tool.rs, matching ComposioExecuteTool.

Approval prompt for a contract-only probe (codex P2) — real, but the safe direction, and the fix belongs upstream of this PR.

The behaviour you describe is right: on a first-time write slug the card is raised, execute() can then return only the contract, and the retry prompts again.

The alternative is worse. To skip the prompt this PR would have to predict that execute() will surface rather than dispatch, and the gate's decision depends on state a &self accessor cannot read without consulting it (which mutates the gate's seen-set). Any approximation — for example "the contract is not in the transcript yet, so it will surface" — is wrong precisely when the model's arguments happen to satisfy the contract: the gate proceeds and the action sends unapproved. A duplicate prompt is an annoyance; a skipped one is the bug this PR exists to fix.

On the denial case: a denied GMAIL_SEND_EMAIL should not hand the model the contract to compose a better send. Denial means do not send.

The ordering fix you are pointing at is real and is being done where it belongs — moving the gate to tool admission (before_tool, ahead of schema validation) in the contract-gate work on #4861. Once the gate answers before the approval onion runs, a first-time gated call never reaches the approval middleware at all, and the double prompt disappears without weakening the gate. That change is validated live (the gate delivers once, the next call executes) and will land there rather than here, since it needs the gate's own plumbing.

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.

Composio write actions run without an approval prompt, and the agent path stores the raw provider envelope

1 participant