feat(composio,agent): async sync + gated-tools surface + UI-only scope elevation + force-delegate capability questions#2348
Conversation
…-delegate capability questions + spawn-and-return composio_sync
composio_sync now spawns and returns: the RPC handler validates synchronously
(connection_id / toolkit / provider lookup), then tokio::spawn(provider.sync)
and returns SyncOutcome{status="started"} immediately. UI is no longer pinned
for the 30s frontend RPC cap while the provider walks an entire prod inbox in
band. Per-source progress is already exposed via openhuman.memory_sync_status_list
(poll-driven; reads mem_tree_chunks so it reflects the spawned task in real time).
ConnectedIntegration gains gated_tools: Vec<GatedIntegrationTool>. The new
struct intentionally has NO parameters field so the LLM cannot construct a
call envelope for one — it can only describe their existence and point at
the unlock path. fetch_connected_integrations_uncached now partitions a
toolkit's per-action set into the existing tools (visible/callable) and
gated_tools (curated but blocked by the user's per-toolkit scope pref).
New composio::providers::curated_scope_for(slug) helper backs the partition.
integrations_agent prompt renders an 'Additional capabilities behind a
permission toggle' section so the agent can honestly answer 'do you support X?'
with 'yes, but it needs admin scope — want me to enable it?' instead of
confabulating 'no I can't'.
New ComposioEnableScopeTool meta-tool (agent-callable, wired into
all_composio_agent_tools): takes (toolkit, scope?), flips the per-toolkit
composio-user-scopes KV row. Strong 'ALWAYS ask the user for explicit
consent first' wording in the description; relies on the prompt contract
today and is the right shape to plug into the wider tool-policy /
approval-gate layer (tinyhumansai#2149/tinyhumansai#2166 area) when it matures.
Orchestrator delegation-guide gains a 'Capability questions about connected
toolkits' subsection that forbids answering 'can you do X with {toolkit}?'
from training-data priors (which were systematically a subset of the real
catalog — confidently denied Gmail bulk-delete when the toolkit ships
GMAIL_BATCH_DELETE_MESSAGES). The only honest 'no' comes back from a
delegation that found the action neither in visible tools nor gated_tools.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds gated tools to ConnectedIntegration and GatedIntegrationTool type; composio splits visible vs gated actions and spawns provider.sync returning a “started” outcome; prompts, memory header, logging, providers, and tests updated to surface unlock guidance while keeping scope elevation UI-only. ChangesGated Tools Implementation
🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Pure formatting. CI Rust Quality (fmt) gate flagged my added code on PR tinyhumansai#2348: - composio/ops.rs fetch_connected_integrations_uncached: the (tools, gated_tools) let-binding's tuple type annotation pushed the line past the limit; rustfmt re-indents the if-connected block by one extra level. - integrations_agent/prompt.rs render_connected_integrations: the chained iter().filter() call on the gated-tools render loop wants to break across lines instead of staying on one. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…turn composio_sync - tools_tests.rs: bump both registry-count asserts 5->6 to include the new ComposioEnableScopeTool meta-tool. - ops_test.rs::composio_sync_gmail_via_mock_archives_raw_email_and_updates_outcome: composio_sync is now spawn-and-return; the immediate envelope is a 'started' sentinel (items_ingested=0, finished_at_ms=0, summary contains 'started'). Poll list_chunks_rpc up to 50 x 100ms for the spawned ingest task to drain before asserting the persisted chunk.
Before saving a per-toolkit scope pref, the agent-callable composio_enable_scope tool now short-circuits in two cases: 1. Not applicable. If the toolkit's curated catalog has zero actions tagged with the requested scope, flipping the bit graduates nothing into the callable surface. The tool returns a clear no-op message so the agent re-evaluates the user's original ask instead of silently mutating prefs that change nothing. 2. Already enabled. If pref.<scope> is already true, the tool returns 'already on' without a redundant DB write and without a misleading 'Enabled' message that implied a state change. Adds providers::toolkit_has_scope(toolkit, scope) helper that walks both the native provider catalog and the catalog_for_toolkit fallback so the answer matches what is_action_visible_with_pref would gate against. Adds a unit test covering gmail (has all three scopes) + case-insensitive slug + unknown toolkit.
…eshness caveat
Two related fixes for a UX bug where the agent surfaced only one unlock
path ("want me to enable admin scope?") and never offered the manual
Settings → Integrations toggle, then quoted past 'I can't do X' refusals
as authoritative on later turns.
1. Data carries the consent policy, prompts don't:
- GatedIntegrationTool gains unlock_paths: Vec<String>, populated at
partition time in composio::ops with the two routes (agent meta-tool
+ manual UI toggle).
- integrations_agent/prompt.rs renders 'unlock path:' lines under each
gated action so the agent reads paths off data, not a memorized
template. Verbatim consent template removed from the prompt.
- composio_enable_scope tool description: 'Two options' template
removed; replaced with 'surface ALL unlock paths from the
integrations data'.
- scope_error_message: composio_execute blocked-by-scope error now
embeds both unlock paths so the policy travels with every blocked
execution, not just the proactive prompt-time list.
2. Cross-chat context is historical, not authoritative:
- Renamed cross-chat block header to
'[Cross-chat context — historical; capabilities may have changed since]'
in both memory_loader.rs and harness/memory_context.rs.
- Orchestrator prompt's 'Capability questions' section gains a 4th
bullet: a past 'I can/can't do X' surfaced via cross-chat may be
stale — verify against the current Connected Integrations block
and delegate before quoting.
- 5 existing test assertions loosened from contains('[Cross-chat
context]') to contains('[Cross-chat context') so the header can
evolve without churning tests.
Together these address the live failure mode where the agent both
(a) presented only the agent-side unlock option and (b) parroted a
prior chat's stale refusal even after admin scope was enabled.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
src/openhuman/agent/memory_loader.rs (1)
607-608: ⚡ Quick winUse full-header assertions here as well to protect the cross-module wording contract.
These prefix checks can pass even if the critical suffix changes. Please assert the complete header text (or a shared constant) in all three places.
Proposed test hardening
- assert!( - out.contains("[Cross-chat context"), + assert!( + out.contains( + "[Cross-chat context — historical; capabilities may have changed since]" + ), "expected cross-chat header, got:\n{out}" ); ... - assert!( - !out.contains("[Cross-chat context"), + assert!( + !out.contains( + "[Cross-chat context — historical; capabilities may have changed since]" + ), "no cross-chat hits must produce no header, got:\n{out}" ); ... - assert!( - out.contains("[Cross-chat context"), + assert!( + out.contains( + "[Cross-chat context — historical; capabilities may have changed since]" + ), "JSONL primary path must emit the cross-chat header, got:\n{out}" );Also applies to: 682-683, 754-755
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent/memory_loader.rs` around lines 607 - 608, The test uses a loose prefix check out.contains("[Cross-chat context") which can miss changes to the expected header suffix; update the assertions in memory_loader.rs (the out.contains checks at the occurrences around the current one and also at the other two spots referenced) to assert the full header string (or compare against a shared constant) instead of using contains, e.g., replace the partial prefix check with an equality or full starts_with match against the canonical header constant so the exact cross-chat header wording is enforced in all three locations.src/openhuman/agent/harness/memory_context.rs (1)
361-362: ⚡ Quick winTighten these assertions to the exact cross-chat header string.
Using
contains("[Cross-chat context")can let contract drift pass silently. Since this header is cross-referenced across components, assert the full literal (or a shared constant) in tests.Proposed test hardening
- assert!( - context.contains("[Cross-chat context"), + assert!( + context.contains( + "[Cross-chat context — historical; capabilities may have changed since]" + ), "expected cross-chat header, got:\n{context}" ); ... - assert!( - !context.contains("[Cross-chat context"), + assert!( + !context.contains( + "[Cross-chat context — historical; capabilities may have changed since]" + ), "no cross-chat hits must produce no header, got:\n{context}" );Also applies to: 427-428
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent/harness/memory_context.rs` around lines 361 - 362, The current tests use context.contains("[Cross-chat context") which is too loose; change those assertions to check for the exact header literal (or a shared constant) instead — e.g. replace the contains check on the context variable with an assertion that the exact header string is present (use a constant like CROSS_CHAT_HEADER or assert!(context.contains(EXACT_HEADER_LITERAL)), and update both occurrences (the assertion at the shown context.contains(...) and the similar one around lines 427-428) so they require the full cross-chat header string.src/openhuman/composio/tools_tests.rs (1)
215-217: ⚡ Quick winExtend metadata/category test coverage to include
composio_enable_scope.You updated registration counts to 6, but the dedicated
all_composio_tools_are_in_skill_categoryfixture still constructs only 5 tools. AddComposioEnableScopeToolthere so category/name contract checks stay aligned with the new surface.Also applies to: 239-241
🤖 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_tests.rs` around lines 215 - 217, The test fixture all_composio_tools_are_in_skill_category constructs the expected Composio tools list but still contains only five entries; update it to include ComposioEnableScopeTool so the expected tool count and category/name checks match the new registration surface (now 6 tools). Modify the fixture that builds the expected tools (and the duplicate expectation around the other occurrence at lines noted) to append or include ComposioEnableScopeTool alongside list_toolkits, list_connections, authorize, list_tools, and execute so the name/category assertions cover the new tool.src/openhuman/agent/agents/integrations_agent/prompt.rs (2)
141-187: ⚡ Quick winAdd structured debug logs for gated-section state transitions.
This branch introduces meaningful prompt-surface transitions (
has_gated, connected-gated count, emitted section) but currently has no diagnostics, which makes runtime prompt behavior harder to inspect.Suggested patch
fn render_connected_integrations(integrations: &[ConnectedIntegration]) -> String { @@ - let mut has_gated = false; + let mut has_gated = false; + let mut connected_with_gated = 0usize; for ci in integrations.iter().filter(|ci| ci.connected) { if !ci.gated_tools.is_empty() { has_gated = true; + connected_with_gated += 1; break; } } + tracing::debug!( + total_integrations = integrations.len(), + has_gated, + connected_with_gated, + "[integrations-prompt] gated tools scan complete" + ); if has_gated { @@ out.push('\n'); + tracing::debug!( + section_len = out.len(), + "[integrations-prompt] emitted gated tools section" + ); }As per coding guidelines: “Use
log/tracingatdebugortracelevel on ... state transitions, and any branch that is hard to infer from tests alone.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent/agents/integrations_agent/prompt.rs` around lines 141 - 187, Add structured debug/tracing calls around the gated-tools discovery and emission: log the number of integrations inspected and the count of connected integrations with non-empty gated_tools (compute this from integrations.iter().filter(|ci| ci.connected && !ci.gated_tools.is_empty()).count()), emit a debug when has_gated flips true and when you push the gated section to out, and log per-integration details (toolkit name and gated_tools.count() or toolkit + each gt.name) inside the loop; use the existing local names (has_gated, integrations, ci, gt, out, gt.unlock_paths) and call tracing::debug! or log::debug! at appropriate trace/debug level so these state transitions and the list of gated toolkits are visible at runtime.
233-263: ⚡ Quick winAdd a focused test for gated-tools prompt rendering.
Current fixture updates only keep old assertions compiling. Please add one case with non-empty
gated_toolsand assert the new section header plus at least oneunlock pathline so this behavior is locked down.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/agent/agents/integrations_agent/prompt.rs` around lines 233 - 263, Add a new unit test in prompt.rs that constructs a ConnectedIntegration with a non-empty gated_tools vec and uses ctx_with(...) and build(...) to render the prompt, then assert the rendered body contains the new gated-tools section header (e.g., "## Gated Tools" or the exact header your template uses) and at least one expected "unlock path" line string; place the test next to build_includes_connected_integrations_in_executor_voice and build_skips_unconnected_integrations so it exercises build(...) with a ConnectedIntegration that has gated_tools populated and verifies the new section is emitted and contains an unlock path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/agent/agents/orchestrator/prompt.rs`:
- Around line 140-143: Replace the hardcoded header "[Cross-chat context —
historical]" used in the prompt template with the canonical header that includes
the suffix (e.g. "[Cross-chat context — historical — capabilities may have
changed since]") so block matching uses the updated exact string; locate the
literal in src/openhuman/agent/agents/orchestrator/prompt.rs (the
prompt/template constant or function that builds the instructions) and update
that string everywhere it appears to the full canonical header.
In `@src/openhuman/composio/providers/mod.rs`:
- Around line 174-212: Extract the operational helpers curated_scope_for and
toolkit_has_scope into a new sibling module (e.g., scope_lookup.rs): move the
function bodies to that file, keep their signatures and required imports
(ToolScope, toolkit_from_slug, get_provider, catalog_for_toolkit, find_curated)
and make them pub so callers still work; then in mod.rs add a module declaration
(mod scope_lookup;) and re-export the functions with a pub use
scope_lookup::{curated_scope_for, toolkit_has_scope}; ensuring any relative
paths/imports are adjusted for the new module location and tests/uses compile.
In `@src/openhuman/composio/tools.rs`:
- Around line 1107-1124: The composio_enable_scope handler currently preserves
caller casing for the toolkit variable causing mismatched keys; update the code
in composio_enable_scope to canonicalize toolkit (e.g., trim() then
to_ascii_lowercase()) immediately after extracting it from args—before the empty
check and before any load/save or visibility checks—so the same canonical key is
used throughout (match the existing scope canonicalization pattern used for the
scope variable).
---
Nitpick comments:
In `@src/openhuman/agent/agents/integrations_agent/prompt.rs`:
- Around line 141-187: Add structured debug/tracing calls around the gated-tools
discovery and emission: log the number of integrations inspected and the count
of connected integrations with non-empty gated_tools (compute this from
integrations.iter().filter(|ci| ci.connected &&
!ci.gated_tools.is_empty()).count()), emit a debug when has_gated flips true and
when you push the gated section to out, and log per-integration details (toolkit
name and gated_tools.count() or toolkit + each gt.name) inside the loop; use the
existing local names (has_gated, integrations, ci, gt, out, gt.unlock_paths) and
call tracing::debug! or log::debug! at appropriate trace/debug level so these
state transitions and the list of gated toolkits are visible at runtime.
- Around line 233-263: Add a new unit test in prompt.rs that constructs a
ConnectedIntegration with a non-empty gated_tools vec and uses ctx_with(...) and
build(...) to render the prompt, then assert the rendered body contains the new
gated-tools section header (e.g., "## Gated Tools" or the exact header your
template uses) and at least one expected "unlock path" line string; place the
test next to build_includes_connected_integrations_in_executor_voice and
build_skips_unconnected_integrations so it exercises build(...) with a
ConnectedIntegration that has gated_tools populated and verifies the new section
is emitted and contains an unlock path.
In `@src/openhuman/agent/harness/memory_context.rs`:
- Around line 361-362: The current tests use context.contains("[Cross-chat
context") which is too loose; change those assertions to check for the exact
header literal (or a shared constant) instead — e.g. replace the contains check
on the context variable with an assertion that the exact header string is
present (use a constant like CROSS_CHAT_HEADER or
assert!(context.contains(EXACT_HEADER_LITERAL)), and update both occurrences
(the assertion at the shown context.contains(...) and the similar one around
lines 427-428) so they require the full cross-chat header string.
In `@src/openhuman/agent/memory_loader.rs`:
- Around line 607-608: The test uses a loose prefix check
out.contains("[Cross-chat context") which can miss changes to the expected
header suffix; update the assertions in memory_loader.rs (the out.contains
checks at the occurrences around the current one and also at the other two spots
referenced) to assert the full header string (or compare against a shared
constant) instead of using contains, e.g., replace the partial prefix check with
an equality or full starts_with match against the canonical header constant so
the exact cross-chat header wording is enforced in all three locations.
In `@src/openhuman/composio/tools_tests.rs`:
- Around line 215-217: The test fixture all_composio_tools_are_in_skill_category
constructs the expected Composio tools list but still contains only five
entries; update it to include ComposioEnableScopeTool so the expected tool count
and category/name checks match the new registration surface (now 6 tools).
Modify the fixture that builds the expected tools (and the duplicate expectation
around the other occurrence at lines noted) to append or include
ComposioEnableScopeTool alongside list_toolkits, list_connections, authorize,
list_tools, and execute so the name/category assertions cover the new tool.
🪄 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: a75edd60-3d68-4572-8212-a075a4827b9f
📒 Files selected for processing (14)
src/openhuman/agent/agents/integrations_agent/prompt.rssrc/openhuman/agent/agents/orchestrator/prompt.rssrc/openhuman/agent/agents/welcome/prompt.rssrc/openhuman/agent/harness/memory_context.rssrc/openhuman/agent/harness/subagent_runner/ops.rssrc/openhuman/agent/harness/test_support_test.rssrc/openhuman/agent/memory_loader.rssrc/openhuman/agent/prompts/types.rssrc/openhuman/composio/ops.rssrc/openhuman/composio/ops_test.rssrc/openhuman/composio/providers/mod.rssrc/openhuman/composio/tools.rssrc/openhuman/composio/tools_tests.rssrc/openhuman/tools/orchestrator_tools.rs
Scope elevation (read / write / admin) was previously exposed as the
agent-callable composio_enable_scope meta-tool. It is now removed:
the user must toggle scopes themselves in the Connections UI.
Why:
- Scope elevation is a security-sensitive, cross-session state change
that unlocks destructive actions. Putting that flip behind LLM
'user-consent' framing made the safety contract depend on model
behavior — the weakest place for it.
- The agent could (and did) route around the explicit *_TRASH_* /
*_DELETE_* gates by calling Write-scoped GMAIL_ADD_LABEL_TO_EMAIL
with the system label 'TRASH' to achieve the same destructive
effect. That escape exists regardless of how scope elevation is
presented; removing the soft prompt-mediated gate avoids the false
sense of safety.
Surfaces updated:
- composio/tools.rs: ComposioEnableScopeTool struct + impl deleted.
Registration list back to 5 tools (list_toolkits, list_connections,
authorize, list_tools, execute). Header comment block preserves the
rationale in case the design is revisited.
- composio/ops.rs: GatedIntegrationTool.unlock_paths now carries one
path only ('the user enables it themselves in Connections →
{toolkit} → {scope}'). Render in integrations_agent prompt is
unchanged — the data shape is the same, just shorter.
- composio/tools.rs::scope_error_message: blocked-by-scope execute
error now describes the UI path only and explicitly tells the LLM
'Do not claim you can flip it — you cannot'.
- agent/prompts/types.rs: GatedIntegrationTool + ConnectedIntegration
doc comments updated to reflect the UI-only flow.
- agent/agents/integrations_agent/prompt.rs: gated-tools section
comment refers to the Connections UI; no template change needed
because unlock_paths still feeds the rendered 'unlock path:' lines.
- providers/mod.rs: toolkit_has_scope doc explains it is no longer
used in production (still useful for future UI hints; kept).
- tools_tests.rs: tool-count asserts bumped 6 → 5 in both
registration tests + comments updated.
Also adds INFO-level audit logging to composio_execute:
- [composio][execute] >> dispatch (slug, connection_id, arg KEYS)
- [composio][execute] << result (slug, success, error, elapsed_ms,
cost_usd)
- [composio][execute] << dispatch error (catch-all warn)
- DEBUG-level paired logs for full args + full response (PII-safe by
default; opt-in via log level).
UI path text everywhere now reads 'Connections → {toolkit} →
{scope}' (the actual nav, per user correction — not nested under
Settings).
Three of CodeRabbit's six items applied; the other three are stale because they predate the composio_enable_scope removal in c6f3404. 1. Single source of truth for the cross-chat block header. - Add 'pub const CROSS_CHAT_HEADER' in agent/memory_loader.rs with the full literal 'Cross-chat context - historical; capabilities may have changed since\n'. - memory_loader.rs producer, harness/memory_context.rs fallback producer, and orchestrator/prompt.rs all bind to this constant. - 5 test assertions now use 'contains(CROSS_CHAT_HEADER.trim_end())' so the full header text is enforced and CodeRabbit's concern about loose prefix matches is addressed. - Orchestrator prompt switched to write!(out, ...) so the canonical header interpolates verbatim into the system-prompt section that names it (previously hardcoded the shorter '- historical' form, which was a drift hazard CodeRabbit flagged). 2. Extract scope-lookup helpers out of providers/mod.rs into a sibling 'scope_lookup.rs' module, matching the project rule from CLAUDE.md ('keep mod.rs export-focused; operational logic in sibling files'). curated_scope_for + toolkit_has_scope move wholesale, with their unit test. mod.rs re-exports both via 'pub use scope_lookup::{...}'. No API change for callers. 3. Add structured debug log in integrations_agent prompt for the gated-tools scan: emits total_integrations, has_gated, and connected_with_gated counts so runtime prompt behavior is easy to inspect without adding println-style tracing. Skipped (stale after c6f3404 removed composio_enable_scope): - 'canonicalize toolkit before scope-pref save in composio_enable_scope' - 'bump tool-count fixture from 5 to 6 to include enable_scope' Both refer to a tool that no longer exists; user must toggle scopes in the Connections UI (see the long note above the removed ComposioEnableScopeTool block).
…e elevation + force-delegate capability questions (tinyhumansai#2348) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e elevation + force-delegate capability questions (tinyhumansai#2348) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e elevation + force-delegate capability questions (tinyhumansai#2348) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
composio_syncis now fire-and-forget: the RPC validates synchronously (connection_id / toolkit / provider), thentokio::spawn(provider.sync)and returns{status: "started", ...}immediately. UI is no longer pinned for the 30 s frontend RPC cap while the provider walks an entire prod inbox in-band. Per-source progress already flows via the existingopenhuman.memory_sync_status_listpoll (readsmem_tree_chunksdirectly so the spawned task is reflected in real time).ConnectedIntegrationgainsgated_tools: Vec<GatedIntegrationTool>(noparametersfield — LLM can't construct a call envelope for one).fetch_connected_integrationsnow partitions a toolkit's actions into visible/callable vs gated-by-pref. Theintegrations_agentprompt renders an "Additional capabilities behind a permission toggle" section so the agent can answer "do you support X?" with "yes but admin scope is off — want me to enable it?" instead of confabulating "no I can't".composio_enable_scopemeta-tool: agent-callable (registered inall_composio_agent_tools), takes(toolkit, scope?), flips the per-toolkitcomposio-user-scopesKV row. Strong "ALWAYS ask the user for explicit consent first" wording in the description.integrations_agentrather than answering from training-data priors (which were systematically wrong — confidently claimed Gmail had no bulk-delete when the toolkit shipsGMAIL_BATCH_DELETE_MESSAGES).Problem
Observed in production-mode use over a long debugging session:
composio_syncconsistently shows "sync failed" in the UI ~30 s in, but the server-side sync is healthy and continues. The frontend RPC.awaitcap is too tight for a real prod inbox —provider.syncwalks all pages in-band and ingests every message synchronously inside the RPC handler. There is no good reason for the UI to block on it; per-source progress is already polled separately.GMAIL_BATCH_DELETE_MESSAGES,GMAIL_DELETE_THREAD,GMAIL_MOVE_TO_TRASH, etc. Two root causes — (a) admin-scoped tools are filtered out of the agent's tool list by default user pref ({read:true, write:true, admin:false}) AND filtered out of the prompt-sidetools:field, so the agent has zero awareness they exist; (b) for the orchestrator agent, even when admin is on, the orchestrator's training-data prior about toolkit capabilities is unreliable — it answers "can you do X?" from priors rather than delegating to the agent that actually has the tool list.Solution
composio/ops.rs:1012tokio::spawn(provider.sync(...))+ returnSyncOutcome{status:"started", finished_at_ms:0, ...}immediately. Validation (resolve client, lookup provider, lookup toolkit) stays synchronous so bad-request errors surface via the RPC envelope.agent/prompts/types.rs,composio/providers/mod.rs,composio/ops.rs,integrations_agent/prompt.rsGatedIntegrationTool(noparametersfield — descriptive only). Newcurated_scope_for(slug)helper. Producer (fetch_connected_integrations_uncached) partitions intotools+gated_tools. Integrations-agent prompt renders a "behind a permission toggle" section, including instructions on how to callcomposio_enable_scopewith user consent.composio/tools.rsComposioEnableScopeTool(struct,impl Tool, registered inall_composio_agent_tools).orchestrator/prompt.rstoolsnorgated_tools.welcome/prompt.rs,harness/subagent_runner/ops.rs,harness/test_support_test.rs,composio/ops_test.rs,tools/orchestrator_tools.rsConnectedIntegration { ... }literal sites to populate the newgated_toolsfield (defaults toVec::new()).Design choices worth flagging for reviewers:
gated_toolsfield instead of inlining: theparameters: Nonedistinction is load-bearing — without it the LLM can construct call envelopes for tools it can't actually invoke. Splitting at the type level enforces that the prompt-render path produces descriptive-only output for gated entries.ComposioEnableScopeToolis a separate meta-tool rather than auto-elevation: granting Admin scope persists across sessions. Self-elevation without explicit user consent is a security boundary regression. The tool relies on the prompt contract today ("ALWAYS ask the user for explicit consent first") and is the right shape to wire into the wider tool-policy / approval-gate layer (feat(approval): gate external-effect tool calls until user approves (#1339) #2149 / feat(agent): add tool policy session boundary #2166 area) when that lands.Submission Checklist
[composio][scopes] integrations prompt action set toolkit=googledrive visible=14 gated=6, etc.) but unit-test additions for the partition logic,composio_enable_scope.execute(), and the orchestrator prompt-render change are pending. The PR is opened as DRAFT for this reason.gated_toolssurface is an additive extension of the existingcomposio.connected_integrationscapability; no new top-level feature row required.composio_enable_scopewrites to the existing per-toolkit KV;composio_syncchange is purely scheduling.Impact
Runtime / platform: desktop, all platforms. No platform branches added or changed.
Performance: net positive.
composio_syncno longer pins the UI thread; the orchestrator delegation cost (one extra sub-agent round-trip for capability questions) is bounded and only fires on questions that today produce confidently wrong refusals.Migration / compatibility: schema-level — adding
gated_toolstoConnectedIntegrationis an additive Rust struct field; every internal constructor was updated toVec::new()so behaviour is identical when no curated entries are pref-blocked. Backward-compatible.Security:
composio_enable_scopeis the only privilege-changing surface added. Documented requirement: agent must ask for explicit user consent before invocation. No silent self-elevation. Wider tool-policy / approval-gate integration recommended as a follow-up.Related
gated_toolspartition,ComposioEnableScopeTool.execute(), and the orchestrator force-delegate prompt section.composio_enable_scopeinto the tool-policy / approval-gate layer (feat(approval): gate external-effect tool calls until user approves (#1339) #2149 / feat(agent): add tool policy session boundary #2166 area) so the user-consent contract is enforced at runtime, not only via prompt instruction.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/composio-agent-ux0e5b042eValidation Run
pnpm --filter openhuman-app format:checknot run — Windows working tree has the documented ~600-file CRLF/LF drift;format:checkwould rewrite unrelated files. Pre-push hook bypassed via--no-verifyper the same documented pattern.pnpm typechecknot run — no TypeScript surface changed in this PR (Rust core only).integrations_agent/prompt.rs,orchestrator/prompt.rs,welcome/prompt.rswere updated to populate the newgated_toolsfield onConnectedIntegrationliterals; existingcomposio/ops_test.rsandtools/orchestrator_tools.rstest helpers similarly updated.cargo checkran implicitly viapnpm dev:app:winrebuild — 0 errors.cargo fmtnot explicitly run; will run in the follow-up that adds tests.app/src-tauri/Rust changes in this PR.Validation Blocked
command:cargo fmtand unit-test additions for the new partition + meta-tool pathserror:n/a — not blocked technically; bundled with the test-coverage follow-up before un-drafting.impact:PR is opened as DRAFT; reviewer feedback welcome on the architectural direction (esp. thegated_tools/enable_scopeshape) before I invest in the test suite.Behavior Changes
composio_syncRPC returns{status:"started"}immediately instead of awaiting full provider sync.composio_enable_scopemeta-tool flips per-toolkit user scopes (with prompt-enforced user-consent contract).integrations_agentinstead of confabulating from priors.composio_enable_scopeprompt) rather than a confidently wrong "no".Parity Contract
composio_syncstill returns aSyncOutcome(now withstatus:"started"/finished_at_ms:0as sentinels for "spawned").ConnectedIntegrationconsumers still receive populatedtools; the newgated_toolsis additive and starts empty whenever no curated entries are pref-blocked.delegate_to_integrations_agent, etc.) are unchanged; the new subsection only adds behavioural guidance, no new tools.welcome,orchestrator,integrations_agentwere updated to populate the new field; they still assert the same prompt-text invariants as before (## Connected Integrations,delegate_to_integrations_agent, per-toolkit bullets).Duplicate / Superseded PR Handling
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements