Skip to content

fix(composio): require approval for agent writes - #5863

Open
simplenaive wants to merge 2 commits into
tinyhumansai:mainfrom
simplenaive:fix/5299-composio-write-approval
Open

fix(composio): require approval for agent writes#5863
simplenaive wants to merge 2 commits into
tinyhumansai:mainfrom
simplenaive:fix/5299-composio-write-approval

Conversation

@simplenaive

@simplenaive simplenaive commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Gate Composio Write and Admin actions through the existing human approval middleware.
  • Cover both agent-facing execution surfaces: composio_execute and dynamically registered ComposioActionTool instances.
  • Keep curated read actions unprompted and fail closed when the dispatcher receives a missing or blank action slug.

Problem

Composio tools declared PermissionLevel::Write, but neither tool surface declared an external effect. Permission caps and interactive approval are separate contracts: ApprovalSecurityMiddleware consults external_effect_with_args, whose inherited default was false.

As a result, agent-driven external writes such as GMAIL_SEND_EMAIL could execute without showing the configured approval card.

Solution

Reuse the existing static ToolScope classifier as the single source of truth:

  • Write and Admin actions report an external effect.
  • Read actions do not.
  • The generic dispatcher inspects its tool argument and defaults to effectful for malformed input.
  • Per-action tools classify their fixed action slug.

This keeps the production change small and avoids a second action-classification table.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/ci-lite.yml. Focused branch coverage is present; the authoritative CI gate is pending.
  • Coverage matrix updated — N/A: behavior-only correction to existing approval-gate features; no feature row changed.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: core tool-policy metadata only.
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Security: Composio writes now honor the existing interactive human-approval policy.
  • Compatibility: Read-only actions remain unprompted; dispatch, schemas, provider calls, and stored responses are unchanged.
  • Platforms: Shared Rust core behavior for desktop/CLI agent execution.
  • Performance: One static catalog lookup or slug heuristic per approval check; no I/O and no new allocation path beyond existing classification.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

Commit & Branch

  • Branch: fix/5299-composio-write-approval
  • Commit SHA: c86b738aad182c6d4084aba24aae0714258473bc

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: scripts/ci-cancel-aware.sh cargo test --manifest-path Cargo.toml --lib requires_approval_for_external_writes_only -- --nocapture — 2 passed. approval_identity_scopes_composio_dispatcher_grants_to_one_action — 1 passed.
  • Rust fmt/check (if changed): cargo fmt --all -- --check; contributor and product-feature root Clippy lanes both passed with -D warnings.
  • Tauri fmt/check (if changed): N/A — no Tauri source or manifest changed; formatting passed.

Additional checks:

  • approval_external_effect_resolution_walks_the_tool_sets — passed.
  • node scripts/ci/check-feature-forwarding.mjs — passed.
  • Full Composio unit scope — 398 passed; 2 current-main failures described below.

Validation Blocked

  • cargo test --manifest-path Cargo.toml --lib 'openhuman::integrations::composio::': two existing tests fail independently of this patch (composio_list_capabilities_does_not_require_session expects a curated Google Calendar capability; composio_execute_via_mock_propagates_backend_error expects an older error string). Neither calls the changed approval classifier; the two focused regression tests pass.
  • pnpm --filter openhuman-app rust:clippy: current main points the Tauri manifest at vendor/tinyagents, whose refreshed pinned commit is now a virtual workspace manifest. Cargo exits before compiling this patch. No Tauri files changed.
  • Impact: branch-specific root Rust compilation, both root Clippy feature lanes, typecheck, formatting, and focused behavior are verified; CI remains authoritative for the repository-wide gates.

Behavior Changes

  • Intended behavior change: interactive agent calls to Composio write/admin actions produce an approval request before execution.
  • User-visible effect: sending email, deleting external data, and similar mutations pause for confirmation; fetch/list actions continue directly.

Parity Contract

  • Legacy behavior preserved: action scope classification, read behavior, sandbox enforcement, dispatch arguments, and response handling.
  • Guard/fallback/dispatch parity checks: dispatcher and per-action surfaces share one classifier; unknown/missing dispatcher slugs fail closed.

Duplicate / Superseded PR Handling

Summary by CodeRabbit

  • New Features

    • Added automatic detection of whether Composio actions can modify external data.
    • External write and administrative actions now require approval, while read-only actions can proceed without it.
    • Unrecognized or incomplete action identifiers default to requiring approval for safer execution.
  • Bug Fixes

    • “Always allow” approvals are now scoped to individual Composio actions, preventing unintended access to other actions.
  • Tests

    • Added coverage for read, write, delete, invalid, and approval-scoping scenarios.

@simplenaive
simplenaive requested a review from a team August 31, 2026 06:44
@coderabbitai

coderabbitai Bot commented Aug 31, 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: Team

Run ID: 4defb700-165c-4f16-9611-7dae6bc0916c

📥 Commits

Reviewing files that changed from the base of the PR and between fa044d3 and 90d62c9.

📒 Files selected for processing (7)
  • src/openhuman/agent/tinyagents/middleware_part_02.rs
  • src/openhuman/agent/tinyagents/middleware_tests_part_02_tests.rs
  • src/openhuman/integrations/composio/action_tool.rs
  • src/openhuman/integrations/composio/action_tool_tests.rs
  • src/openhuman/integrations/composio/tools_part_01.rs
  • src/openhuman/integrations/composio/tools_part_03.rs
  • src/openhuman/integrations/composio/tools_tests_part_01_tests.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/openhuman/agent/tinyagents/middleware_tests_part_02_tests.rs
  • src/openhuman/integrations/composio/tools_tests_part_01_tests.rs
  • src/openhuman/agent/tinyagents/middleware_part_02.rs
  • src/openhuman/integrations/composio/action_tool.rs
  • src/openhuman/integrations/composio/tools_part_01.rs
  • src/openhuman/integrations/composio/tools_part_03.rs
  • src/openhuman/integrations/composio/action_tool_tests.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Composio tools now classify external effects from action scopes. Write and Admin actions require approval, while read actions do not. Missing or invalid dispatcher slugs remain effectful. Approval grants use normalized per-action identities. Regression tests cover both tool surfaces and middleware routing.

Changes

Composio approval gating

Layer / File(s) Summary
Synchronous action scope classification
src/openhuman/integrations/composio/tools_part_01.rs, src/openhuman/integrations/composio/action_tool.rs, src/openhuman/integrations/composio/action_tool_tests.rs
The shared scope resolver supports synchronous classification. Write and Admin actions report external effects. Per-action tests cover write and read actions.
Dispatcher effect detection
src/openhuman/integrations/composio/tools_part_03.rs, src/openhuman/integrations/composio/tools_tests_part_01_tests.rs
ComposioExecuteTool reports baseline and argument-aware external effects. Missing, blank, and invalid slugs default to effectful behavior. Tests cover write, admin, read, and malformed inputs.
Per-action approval identities
src/openhuman/agent/tinyagents/middleware_part_02.rs, src/openhuman/agent/tinyagents/middleware_tests_part_02_tests.rs
The middleware maps Composio actions to normalized namespaced approval identities. Missing actions use <invalid-action>. Non-Composio tool names remain unchanged. Tests cover these mappings.

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

Merge Risk: ⚪ Minimal · up to 90d62

This localized change makes Composio write and admin actions require approval while preserving unprompted read actions; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant ApprovalSecurityMiddleware
  participant ComposioExecuteTool
  participant ActionScopeResolver
  Agent->>ApprovalSecurityMiddleware: Invoke Composio action
  ApprovalSecurityMiddleware->>ComposioExecuteTool: Check external_effect_with_args
  ComposioExecuteTool->>ActionScopeResolver: Resolve action scope
  ActionScopeResolver-->>ComposioExecuteTool: Return Read, Write, or Admin
  ComposioExecuteTool-->>ApprovalSecurityMiddleware: Return effect status
  ApprovalSecurityMiddleware->>ApprovalSecurityMiddleware: Use normalized per-action approval identity
Loading

Poem

A rabbit checks each action’s scope,
Write and Admin wait for hope.
Read actions pass without delay,
Blank slugs choose the safer way.
Each grant guards its named tool,
Approval keeps the workflow cool.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 main change: requiring approval for Composio agent writes.
Linked Issues check ✅ Passed The changes satisfy issue #5862. Both Composio execution surfaces now classify Write and Admin actions as external effects, read actions remain unprompted, malformed dispatcher inputs fail closed, syn…
Out of Scope Changes check ✅ Passed All implementation and test changes support issue #5862. The approval namespacing middleware change is required to scope persistent grants to individual Composio actions. No unrelated response-shaping…
Full details: Linked Issues check

Explanation

The changes satisfy issue #5862. Both Composio execution surfaces now classify Write and Admin actions as external effects, read actions remain unprompted, malformed dispatcher inputs fail closed, synchronous ToolScope classification is reused, and focused regression tests cover the required paths.

Full details: Out of Scope Changes check

Explanation

All implementation and test changes support issue #5862. The approval namespacing middleware change is required to scope persistent grants to individual Composio actions. No unrelated response-shaping work or other out-of-scope changes are present.

  • Fix all pre-merge checks with AI

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


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

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking. Approving.

             $0.0168 · 132,350 in / 2,344 out · 16,415 cached (12%) · openrouter/openai/text-embedding-3-small, z-ai/glm-5.2, deepseek/deepseek-v4-flash · 245 embedded
critique:    $0.0078 · 57,329 in  / 1,036 out · 7,825 cached (14%)  · z-ai/glm-5.2, deepseek/deepseek-v4-flash
security:    $0.0073 · 57,224 in  / 865 out   · 8,590 cached (15%)  · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests:       $0.0012 · 12,552 in  / 365 out   · 0 cached (0%)       · deepseek/deepseek-v4-flash
description: $0.0005 · 5,245 in   / 78 out    · 0 cached (0%)       · deepseek/deepseek-v4-flash

@tinysweeper

tinysweeper Bot commented Aug 31, 2026

Copy link
Copy Markdown

How this change flows

2 changed behaviours across 10 relationships. 5 surrounding behaviours are shown (60 graph nodes walked). 37 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["...nal_effect_resolution_walks_the_tool_sets<br/>changed"]:::changed
  n1["execute_tool_requires_tool_argument<br/>changed<br/>1 finding"]:::flagged
  n2["openhuman"]:::impacted
  n3["error_text"]:::impacted
  n4["...aces_full_contract_then_proceeds_on_retry"]:::impacted
  n5["fake_config_arc"]:::impacted
  n6["vec"]:::impacted
  n0 -->|calls| n6
  n0 -->|tests| n6
  n1 -->|calls| n5
  n1 -->|tests| n5
  n3 -->|uses| n2
  n4 -->|calls| n3
  n4 -->|tests| n3
  n4 -->|calls| n6
  n4 -->|tests| n6
  n5 -->|uses| n2
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 31, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 31, 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: aea203cfe1

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/openhuman/integrations/composio/tools_part_03.rs
@simplenaive

Copy link
Copy Markdown
Contributor Author

@tinyhumansai/maintainers @senamakel — review requested for this P1 approval-gate fix. CodeRabbit and Tinysweeper approve the latest commit, all review threads are resolved, and the focused regressions plus both root Clippy lanes pass. Required CI currently stops on two failures already present in base main at 1904382d2: module registry/submodule pin drift introduced by the pointer refresh, and untouched subagent_runner/ops/runner.rs at 1769 lines versus its 1766-line cap. I kept those unrelated repairs out of this security patch; it can rebase and rerun as soon as base main is repaired.

M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Aug 31, 2026
Adds wire-level e2e coverage for the eight RPC controllers that
`scripts/check-domain-e2e-coverage.mjs` reported as uncovered, taking
agent 7/12 -> 12/12, approval 3/5 -> 5/5 and memory 34/35 -> 35/35.

Every method is driven over the real JSON-RPC router rather than by
calling handlers directly. That matters because the checker's notion of
"covered" is the string literal "openhuman.<method>" appearing anywhere
in a tests/**/*_e2e.rs file — it never verifies the method is invoked.
Two of the eight (approval_get_gate_state, approval_preauthorize_flow)
were false negatives: they already had handler-level coverage in
tool_registry_approval_raw_coverage_e2e.rs and scored 0 only because
those tests bypass the wire method name.

Includes an #[ignore]d reproduction of openhuman#5862: ComposioExecuteTool
and ComposioActionTool declare PermissionLevel::Write but never override
external_effect*, so they inherit the trait default of false and
ApprovalSecurityMiddleware — whose only gate predicate is
external_effect_with_args — never parks them. Agent-initiated Composio
writes therefore run with no approval card. The legacy ComposioTool
classifies correctly (tools/direct_part_03.rs:66-80) but is not an agent
tool, so the classification was simply not carried over when the surface
was split. The test asserts the correct behaviour and so fails today;
it is ignored to keep the lane green and becomes the regression guard
once tinyhumansai#5863 or an equivalent fix lands.

Each test was verified by mutation: the covered behaviour was broken and
the test confirmed to fail naming its own assertion. Three drafts did not
survive that check and were rewritten rather than kept:

  - an oversized-limit assertion (events.len() <= 1000) was unfalsifiable,
    since the queried run has no events;
  - a missing-run_id assertion matched .contains("run_id"), which also
    matches the downstream error's "run_id=" debug label — it could not
    tell rejection from acceptance-then-failure. It now matches the exact
    validate_params string;
  - a preauthorize idempotency assertion compared grant counts that are
    both empty when no gate is installed. It now pins the documented
    gate-absent contract instead, and says why the grant path belongs in
    the unit tests that own the gate lifecycle.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Aug 31, 2026
Adds wire-level e2e coverage for the eight RPC controllers that
`scripts/check-domain-e2e-coverage.mjs` reported as uncovered, taking
agent 7/12 -> 12/12, approval 3/5 -> 5/5 and memory 34/35 -> 35/35.

Every method is driven over the real JSON-RPC router rather than by
calling handlers directly. That matters because the checker's notion of
"covered" is the string literal "openhuman.<method>" appearing anywhere
in a tests/**/*_e2e.rs file — it never verifies the method is invoked.
Two of the eight (approval_get_gate_state, approval_preauthorize_flow)
were false negatives: they already had handler-level coverage in
tool_registry_approval_raw_coverage_e2e.rs and scored 0 only because
those tests bypass the wire method name.

Includes an #[ignore]d reproduction of openhuman#5862: ComposioExecuteTool
and ComposioActionTool declare PermissionLevel::Write but never override
external_effect*, so they inherit the trait default of false and
ApprovalSecurityMiddleware — whose only gate predicate is
external_effect_with_args — never parks them. Agent-initiated Composio
writes therefore run with no approval card. The legacy ComposioTool
classifies correctly (tools/direct_part_03.rs:66-80) but is not an agent
tool, so the classification was simply not carried over when the surface
was split. The test asserts the correct behaviour and so fails today;
it is ignored to keep the lane green and becomes the regression guard
once tinyhumansai#5863 or an equivalent fix lands.

Each test was verified by mutation: the covered behaviour was broken and
the test confirmed to fail naming its own assertion. Three drafts did not
survive that check and were rewritten rather than kept:

  - an oversized-limit assertion (events.len() <= 1000) was unfalsifiable,
    since the queried run has no events;
  - a missing-run_id assertion matched .contains("run_id"), which also
    matches the downstream error's "run_id=" debug label — it could not
    tell rejection from acceptance-then-failure. It now matches the exact
    validate_params string;
  - a preauthorize idempotency assertion compared grant counts that are
    both empty when no gate is installed. It now pins the documented
    gate-absent contract instead, and says why the grant path belongs in
    the unit tests that own the gate lifecycle.
@simplenaive

Copy link
Copy Markdown
Contributor Author

@senamakel @tinyhumansai/maintainers — this verified P1 fix is ready to merge.

Fresh verification at c86b738aa:

  • the untouched PR base routes both Composio write surfaces around ApprovalGate because they inherit external_effect = false;
  • 4 focused regressions pass: dispatcher classification, per-action classification, middleware routing, and action-scoped persistent grants;
  • CodeRabbit approves the latest commit and the only review thread is resolved;
  • current main advanced to 61717996d without touching any of the seven changed files, and the PR remains mergeable.

Required CI is still blocked by repository-level failures: current main run 33410357365 itself fails the Module Pin Gate. No unrelated workaround belongs in this security patch. Please merge once that base-red gate is repaired or waived; no code changes are pending.

M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
Adds wire-level e2e coverage for the eight RPC controllers that
`scripts/check-domain-e2e-coverage.mjs` reported as uncovered, taking
agent 7/12 -> 12/12, approval 3/5 -> 5/5 and memory 34/35 -> 35/35.

Every method is driven over the real JSON-RPC router rather than by
calling handlers directly. That matters because the checker's notion of
"covered" is the string literal "openhuman.<method>" appearing anywhere
in a tests/**/*_e2e.rs file — it never verifies the method is invoked.
Two of the eight (approval_get_gate_state, approval_preauthorize_flow)
were false negatives: they already had handler-level coverage in
tool_registry_approval_raw_coverage_e2e.rs and scored 0 only because
those tests bypass the wire method name.

Includes an #[ignore]d reproduction of openhuman#5862: ComposioExecuteTool
and ComposioActionTool declare PermissionLevel::Write but never override
external_effect*, so they inherit the trait default of false and
ApprovalSecurityMiddleware — whose only gate predicate is
external_effect_with_args — never parks them. Agent-initiated Composio
writes therefore run with no approval card. The legacy ComposioTool
classifies correctly (tools/direct_part_03.rs:66-80) but is not an agent
tool, so the classification was simply not carried over when the surface
was split. The test asserts the correct behaviour and so fails today;
it is ignored to keep the lane green and becomes the regression guard
once tinyhumansai#5863 or an equivalent fix lands.

Each test was verified by mutation: the covered behaviour was broken and
the test confirmed to fail naming its own assertion. Three drafts did not
survive that check and were rewritten rather than kept:

  - an oversized-limit assertion (events.len() <= 1000) was unfalsifiable,
    since the queried run has no events;
  - a missing-run_id assertion matched .contains("run_id"), which also
    matches the downstream error's "run_id=" debug label — it could not
    tell rejection from acceptance-then-failure. It now matches the exact
    validate_params string;
  - a preauthorize idempotency assertion compared grant counts that are
    both empty when no gate is installed. It now pins the documented
    gate-absent contract instead, and says why the grant path belongs in
    the unit tests that own the gate lifecycle.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
Adds wire-level e2e coverage for the eight RPC controllers that
`scripts/check-domain-e2e-coverage.mjs` reported as uncovered, taking
agent 7/12 -> 12/12, approval 3/5 -> 5/5 and memory 34/35 -> 35/35.

Every method is driven over the real JSON-RPC router rather than by
calling handlers directly. That matters because the checker's notion of
"covered" is the string literal "openhuman.<method>" appearing anywhere
in a tests/**/*_e2e.rs file — it never verifies the method is invoked.
Two of the eight (approval_get_gate_state, approval_preauthorize_flow)
were false negatives: they already had handler-level coverage in
tool_registry_approval_raw_coverage_e2e.rs and scored 0 only because
those tests bypass the wire method name.

Includes an #[ignore]d reproduction of openhuman#5862: ComposioExecuteTool
and ComposioActionTool declare PermissionLevel::Write but never override
external_effect*, so they inherit the trait default of false and
ApprovalSecurityMiddleware — whose only gate predicate is
external_effect_with_args — never parks them. Agent-initiated Composio
writes therefore run with no approval card. The legacy ComposioTool
classifies correctly (tools/direct_part_03.rs:66-80) but is not an agent
tool, so the classification was simply not carried over when the surface
was split. The test asserts the correct behaviour and so fails today;
it is ignored to keep the lane green and becomes the regression guard
once tinyhumansai#5863 or an equivalent fix lands.

Each test was verified by mutation: the covered behaviour was broken and
the test confirmed to fail naming its own assertion. Three drafts did not
survive that check and were rewritten rather than kept:

  - an oversized-limit assertion (events.len() <= 1000) was unfalsifiable,
    since the queried run has no events;
  - a missing-run_id assertion matched .contains("run_id"), which also
    matches the downstream error's "run_id=" debug label — it could not
    tell rejection from acceptance-then-failure. It now matches the exact
    validate_params string;
  - a preauthorize idempotency assertion compared grant counts that are
    both empty when no gate is installed. It now pins the documented
    gate-absent contract instead, and says why the grant path belongs in
    the unit tests that own the gate lifecycle.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
Adds wire-level e2e coverage for the eight RPC controllers that
`scripts/check-domain-e2e-coverage.mjs` reported as uncovered, taking
agent 7/12 -> 12/12, approval 3/5 -> 5/5 and memory 34/35 -> 35/35.

Every method is driven over the real JSON-RPC router rather than by
calling handlers directly. That matters because the checker's notion of
"covered" is the string literal "openhuman.<method>" appearing anywhere
in a tests/**/*_e2e.rs file — it never verifies the method is invoked.
Two of the eight (approval_get_gate_state, approval_preauthorize_flow)
were false negatives: they already had handler-level coverage in
tool_registry_approval_raw_coverage_e2e.rs and scored 0 only because
those tests bypass the wire method name.

Includes an #[ignore]d reproduction of openhuman#5862: ComposioExecuteTool
and ComposioActionTool declare PermissionLevel::Write but never override
external_effect*, so they inherit the trait default of false and
ApprovalSecurityMiddleware — whose only gate predicate is
external_effect_with_args — never parks them. Agent-initiated Composio
writes therefore run with no approval card. The legacy ComposioTool
classifies correctly (tools/direct_part_03.rs:66-80) but is not an agent
tool, so the classification was simply not carried over when the surface
was split. The test asserts the correct behaviour and so fails today;
it is ignored to keep the lane green and becomes the regression guard
once tinyhumansai#5863 or an equivalent fix lands.

Each test was verified by mutation: the covered behaviour was broken and
the test confirmed to fail naming its own assertion. Three drafts did not
survive that check and were rewritten rather than kept:

  - an oversized-limit assertion (events.len() <= 1000) was unfalsifiable,
    since the queried run has no events;
  - a missing-run_id assertion matched .contains("run_id"), which also
    matches the downstream error's "run_id=" debug label — it could not
    tell rejection from acceptance-then-failure. It now matches the exact
    validate_params string;
  - a preauthorize idempotency assertion compared grant counts that are
    both empty when no gate is installed. It now pins the documented
    gate-absent contract instead, and says why the grant path belongs in
    the unit tests that own the gate lifecycle.
M3gA-Mind added a commit to M3gA-Mind/openhuman that referenced this pull request Sep 1, 2026
Adds wire-level e2e coverage for the eight RPC controllers that
`scripts/check-domain-e2e-coverage.mjs` reported as uncovered, taking
agent 7/12 -> 12/12, approval 3/5 -> 5/5 and memory 34/35 -> 35/35.

Every method is driven over the real JSON-RPC router rather than by
calling handlers directly. That matters because the checker's notion of
"covered" is the string literal "openhuman.<method>" appearing anywhere
in a tests/**/*_e2e.rs file — it never verifies the method is invoked.
Two of the eight (approval_get_gate_state, approval_preauthorize_flow)
were false negatives: they already had handler-level coverage in
tool_registry_approval_raw_coverage_e2e.rs and scored 0 only because
those tests bypass the wire method name.

Includes an #[ignore]d reproduction of openhuman#5862: ComposioExecuteTool
and ComposioActionTool declare PermissionLevel::Write but never override
external_effect*, so they inherit the trait default of false and
ApprovalSecurityMiddleware — whose only gate predicate is
external_effect_with_args — never parks them. Agent-initiated Composio
writes therefore run with no approval card. The legacy ComposioTool
classifies correctly (tools/direct_part_03.rs:66-80) but is not an agent
tool, so the classification was simply not carried over when the surface
was split. The test asserts the correct behaviour and so fails today;
it is ignored to keep the lane green and becomes the regression guard
once tinyhumansai#5863 or an equivalent fix lands.

Each test was verified by mutation: the covered behaviour was broken and
the test confirmed to fail naming its own assertion. Three drafts did not
survive that check and were rewritten rather than kept:

  - an oversized-limit assertion (events.len() <= 1000) was unfalsifiable,
    since the queried run has no events;
  - a missing-run_id assertion matched .contains("run_id"), which also
    matches the downstream error's "run_id=" debug label — it could not
    tell rejection from acceptance-then-failure. It now matches the exact
    validate_params string;
  - a preauthorize idempotency assertion compared grant counts that are
    both empty when no gate is installed. It now pins the documented
    gate-absent contract instead, and says why the grant path belongs in
    the unit tests that own the gate lifecycle.
@M3gA-Mind
M3gA-Mind force-pushed the fix/5299-composio-write-approval branch from c86b738 to 90d62c9 Compare September 1, 2026 22:46
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer housekeeping — rebased onto current main (fa044d388), no content changes. Your two commits are unchanged apart from their new base; authorship is preserved (90d62c9, e3e6954).

All five red checks were inherited from the stale base (this branch forked at 1904382d2, 338 commits back), not caused by your diff:

  • Rust Quality — failed on the layout ratchet: subagent_runner/ops/runner.rs: 1769 lines (limit 1766). Your PR does not touch runner.rs; main has since trimmed that file to 1766 and the pin is 1769, so it passes now.
  • Rust Feature-Gate Smokeerror[E0433]: cannot find modulesinopenhuman``. The string modules does not appear anywhere in this diff.
  • Module Pin Gate — submodule/registry pin drift across nine modules; this PR touches no submodules.
  • PR CI Gate — aggregate of the two above.
  • RSS Benchmark — report-only.

main is green at fa044d388, so CI should now reflect your change alone. I also ran cargo fmt --all -- --check locally: clean.

No review threads were outstanding — CodeRabbit reported no actionable comments. Not approving; a maintainer reviews and merges separately.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking. Approving.

             $0.0174 · 183,833 in / 3,078 out · 16,676 cached (9%) · openrouter/openai/text-embedding-3-small, deepseek/deepseek-v4-flash, z-ai/glm-5.2 · 381 embedded
critique:    $0.0083 · 82,457 in  / 1,898 out · 8,461 cached (10%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
security:    $0.0076 · 81,126 in  / 1,032 out · 8,215 cached (10%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests:       $0.0010 · 13,728 in  / 72 out    · 0 cached (0%)      · deepseek/deepseek-v4-flash
description: $0.0005 · 6,522 in   / 76 out    · 0 cached (0%)      · deepseek/deepseek-v4-flash

assert!(!tool.external_effect_with_args(&serde_json::json!({
"tool": "GMAIL_FETCH_EMAILS"
})));
assert!(tool.external_effect_with_args(&serde_json::json!({})));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium critique confident

Drop assertions for malformed inputs that should not reach the approval gate

The test asserts external_effect_with_args returns true for {} (no tool key) and for {"tool": " "} (whitespace-only value). Neither is a real Composio write request; both are inputs that the execute path should reject as missing/invalid before the approval gate is consulted. Asserting that they trigger an approval prompt would force a false positive approval card in a scenario that does not represent a valid user action, making the test a bad regression guard. Remove these two lines or replace them with assertions that external_effect_with_args returns false (or the tool rejects them earlier via its own validation).

[RULE] misleading-test-assertion ·

@tinysweeper tinysweeper Bot added priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. and removed priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. labels Sep 1, 2026
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Follow-up to my rebase note above — the code review I owe you, and one correction about process.

Process correction, so it is on the record: a fleet rule changed after I pushed. Maintainer-side rebases are now limited to org-member PRs, and I should have left this branch alone. I am not reverting it — reverting would put the branch back on a stale base and re-break the five checks — but the force-push was mine, not yours, and the manager has been told. Your two commits are intact and unmodified (90d62c9, e3e6954, authorship preserved); only their base moved. Sorry for the surprise commits.

On the change — it is well judged. Two things in particular:

Reusing ToolScope rather than adding a second table. Permission caps and interactive approval genuinely are separate contracts, and it would have been easy to "fix" this by duplicating the action classification. One classifier, two consumers, no drift.

Failing closed everywhere it matters. resolve_action_scope_sync returning ToolScope::Write for an unrecognised toolkit, and external_effect_with_args defaulting to true via .unwrap_or(true) on a missing or blank slug, both mean an unclassifiable action prompts rather than slips through. That is the correct direction for a gate whose failure mode is an unapproved external write.

approval_tool_name is the subtle one and I think it is the most valuable hunk here. composio_execute multiplexes every action behind one tool name, so keying a persistent "Always allow" grant by call.name would have made approving one action authorize every Composio action for that user — a privilege-escalation bug latent in the obvious implementation. Namespacing the grant as composio_execute:<slug>, with composio_execute:<invalid-action> as the sentinel so a malformed call cannot collide with a real grant, is right.

Two small notes, neither blocking:

  • resolve_action_scope is now an async fn whose body only calls the sync core. That is deliberate (it preserves the call sites) and safe here — clippy::unused_async is pedantic and this repo enables no pedantic group, so -D warnings will not fire. Flagging it only so it is a known choice rather than a surprise later.
  • The 14 one-line result_notes-style fixture touches are the ordinary cost of the change, not padding.

For the record, all five red checks were inherited from the stale base — none of them referenced your diff. Details in my earlier comment. I also ran cargo fmt --all -- --check: clean.

Not approving; a maintainer reviews and merges.

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

Labels

priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Composio agent writes bypass the approval gate

2 participants