Skip to content

Commit 8f4696b

Browse files
authored
feat(composio): per-toolkit tool curation, user scopes, and Gmail HTML→markdown (#643)
* feat(composio): add user scope management for toolkits - Introduced `get_user_scopes` and `set_user_scopes` functions to manage per-toolkit user scope preferences, allowing for read, write, and admin classifications. - Updated `all_controller_schemas` and `all_registered_controllers` to include new schemas for user scope management. - Implemented `evaluate_tool_visibility` to determine tool visibility based on user-defined scopes, enhancing security and control over tool actions. - Added `UserScopePref` struct to store user preferences and integrated it with memory storage for persistence. - Enhanced existing tools to respect user scope preferences during execution, ensuring actions align with user-defined permissions. These changes improve the flexibility and security of toolkit interactions, allowing users to customize their access levels for different actions. * feat(gmail): expand GMAIL_CURATED tools for enhanced email management - Updated the GMAIL_CURATED constant to include additional tools for reading and writing emails, such as GMAIL_LIST_MESSAGES, GMAIL_LIST_THREADS, GMAIL_GET_ATTACHMENT, and GMAIL_FORWARD_MESSAGE. - Improved organization of tools by categorizing them under distinct sections for reading messages, managing drafts, and handling labels. - Enhanced admin tools with new functionalities like GMAIL_BATCH_DELETE_MESSAGES and GMAIL_UNTRASH_THREAD, improving overall email management capabilities. These changes provide a more comprehensive toolkit for interacting with Gmail, enhancing user experience and functionality. * refactor(tool_scope, gmail): improve code formatting and organization - Reformatted the `ADMIN` and `GMAIL_CURATED` constants for better readability by aligning the entries vertically. - Enhanced the clarity of the `classify_unknown` function by improving the structure of the constants, making it easier to maintain and understand. - Updated test assertions in `toolkit_from_slug` for consistency in formatting, ensuring clearer test outputs. These changes improve the overall readability and maintainability of the codebase, aligning with ongoing refactoring efforts. * feat(github): add GitHub toolkit and curated tools for enhanced integration - Introduced a new GitHub provider module, including a curated catalog of GitHub actions tailored for common tasks such as repository management, issue tracking, and pull request handling. - Implemented the `catalog_for_toolkit` function to allow fallback to a static curated list for toolkits without a native provider, ensuring consistent tool visibility and access. - Updated the `evaluate_tool_visibility` function to prioritize curated tools from registered providers, enhancing the overall user experience and security by enforcing whitelist checks. These changes expand the capabilities of the Composio toolkit, providing users with a comprehensive set of tools for interacting with GitHub, while maintaining a focus on user-defined permissions and visibility. * refactor(tools): improve formatting and organization of curated tools - Reformatted the `GITHUB_CURATED`, `NOTION_CURATED`, and other tool constants for better readability by aligning entries vertically. - Enhanced the clarity of the code structure, making it easier to maintain and understand. - Updated related documentation to reflect the changes in formatting and organization. These changes improve the overall readability and maintainability of the codebase, aligning with ongoing refactoring efforts. * feat(catalogs): introduce curated catalogs for various toolkits - Added a new module `catalogs.rs` containing curated tool lists for Slack, Discord, Google Calendar, Google Drive, Google Docs, and more, enhancing the Composio toolkit's integration capabilities. - Updated the `mod.rs` file to include the new `catalogs` module and modified the `catalog_for_toolkit` function to support these curated lists, allowing for better organization and access to toolkit actions. - This addition improves the overall functionality and user experience by providing a comprehensive set of tools for interacting with popular platforms. * feat(post-process): implement HTML to markdown conversion for Gmail responses - Introduced a new `post_process` module to handle per-toolkit response modifications, specifically for converting HTML content to markdown format. - Enhanced the `ComposioExecuteTool` to apply post-processing on successful responses, improving the clarity and usability of data returned from Gmail. - Added tests to ensure the correct functionality of HTML detection and conversion, validating the integrity of the post-processing logic. These changes enhance the user experience by streamlining the handling of HTML content in responses, making it more suitable for further processing and display. * refactor(post_process): reorganize HTML detection constants for improved readability - Moved HTML detection markers in the `looks_like_html` function to a more structured format, enhancing clarity and maintainability. - This change aligns with ongoing efforts to improve code organization and readability within the post-processing module. * refactor(catalogs): enhance formatting and organization of curated tool constants - Reformatted the `SLACK_CURATED`, `DISCORD_CURATED`, and `GOOGLECALENDAR_CURATED` constants for improved readability by aligning entries vertically. - This change enhances the clarity and maintainability of the code, aligning with ongoing refactoring efforts to improve code organization. * feat(connect-modal): wire read/write/admin scope toggles to composio prefs Adds the three scope toggles to the connected-state of the ComposioConnectModal so users can gate which Composio actions the agent may invoke per integration. Loads the stored pref via `composio_get_user_scopes` once the modal lands in the connected phase and persists changes through `composio_set_user_scopes` with optimistic updates and rollback on error. - Adds `getUserScopes` / `setUserScopes` to composioApi. - Adds `ComposioUserScopePref` type mirror. - Renders accessible role="switch" toggles with hint text per row. * refactor(ComposioConnectModal): simplify SCOPE_ROWS definition - Streamlined the definition of SCOPE_ROWS in ComposioConnectModal by removing unnecessary line breaks, enhancing code readability and maintainability. - Updated the agent.toml configuration to include "composio_execute" in the tools list, expanding the capabilities of the integrations agent. * fix(composio): apply curated whitelist + scope pref to integrations_agent prompt The agent prompt for integrations_agent was rendering every action returned by the backend's `composio_list_tools` for each connected toolkit, bypassing the curation/scope filter that the meta-tool layer applies. Concretely the GitHub integrations_agent prompt was showing ~500 actions including non-curated entries like GITHUB_ADD_OR_UPDATE_TEAM_REPOSITORY_PERMISSIONS. Adds `is_action_visible_with_pref(slug, pref)` — a sync helper that mirrors the meta-tool layer's decision logic — and applies it in: - `fetch_connected_integrations_uncached` (bulk session-cached path) - `fetch_toolkit_actions` (per-toolkit spawn-time path) One pref load per toolkit (not per action) keeps the cost minimal. * refactor(fetch_connected_integrations): streamline action visibility filter Simplified the action visibility filter in `fetch_connected_integrations_uncached` by consolidating the filter logic into a single line. This change enhances code readability while maintaining the existing functionality of applying user preferences to the displayed actions. * fix(prompt): drop duplicate `### Available Tools` listing in text-mode preamble Text-mode subagent prompts were rendering the tool catalog twice: once in the prompt template's `## Tools` section (with the richer `Call as: NAME[arg|arg]` signatures from `prompts::ToolsSection::build`) and once in `### Available Tools` under `## Tool Use Protocol` (`Parameters: name:type, ...` format). For an integrations_agent toolkit spawn (~50 actions) this doubled the tool listing bytes for no informational gain. Keep only the protocol preamble (essential for text mode); the catalog stays in `## Tools`. Removes `summarise_parameters` and `first_line_truncated` which were the sole consumers, plus the now-unused `std::fmt::Write` import. * review: address PR feedback (UTF-8 boundary, structured logs, doc, debug) Real fixes: - post_process: walk back to UTF-8 char boundary before truncating to 4096 bytes; previous `&s[..4096]` could panic mid-codepoint. Adds regression test that places a 3-byte char straddling the cutoff. - composioApi: move misplaced `execute` docstring back above `execute` (it had drifted above the new `getUserScopes`). - schemas: include `get_user_scopes` / `set_user_scopes` in the `every_known_schema_key_resolves` test. Diagnosability: - schemas: structured `[composio:scopes]` debug/error logs at entry, exit, and every early-return in `handle_get_user_scopes` / `handle_set_user_scopes` (method + toolkit + pref fields). The memory-not-ready branch now logs an error before returning. - composioApi + ConnectModal: grep-friendly `[composio][scopes]` debug logs around getUserScopes / setUserScopes RPC round-trips and the toggle handler (old → new state, persisted result, errors). - user_scopes: load_or_default now logs the same normalized `key` that `load()` does, so traces correlate across both code paths. Cleanups: - tools: replace external `idx` + `Vec::retain` in `filter_list_tools_response` with a drain + zip + filter_map pattern. - tool_scope: document the no-underscore-in-toolkit-name assumption of `toolkit_from_slug`, and call out the `microsoft_teams` alias. - Cargo.toml: comment on the html2md vs htmd choice. Pushback (no change): - Reviewer asked to split the type-only import in ConnectModal into a separate `import type` line; ESLint's `no-duplicate-imports` rejects that, so the inline `type` form (functionally identical) stays.
1 parent 2150944 commit 8f4696b

24 files changed

Lines changed: 3968 additions & 110 deletions

File tree

Cargo.lock

Lines changed: 85 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ crate-type = ["rlib"]
1616
[dependencies]
1717
serde = { version = "1", features = ["derive"] }
1818
serde_json = "1"
19+
# Used by composio post-processing to convert Gmail HTML bodies to
20+
# markdown. `html2md` is the canonical pure-Rust crate (no system deps);
21+
# `htmd` (>0.5) is more actively maintained but our usage is one
22+
# function call (`parse_html`) and the migration cost outweighs the
23+
# upside today. Re-evaluate if html2md goes unmaintained or we need
24+
# GFM/table fidelity beyond what html2md emits.
25+
html2md = "0.2"
1926
reqwest = { version = "0.12", default-features = false, features = ["json", "blocking", "rustls-tls", "native-tls", "stream", "http2", "multipart", "socks"] }
2027
tokio = { version = "1", features = ["full", "sync"] }
2128
once_cell = "1.19"

app/src/components/composio/ComposioConnectModal.tsx

Lines changed: 177 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,18 @@
1616
import { useCallback, useEffect, useRef, useState } from 'react';
1717
import { createPortal } from 'react-dom';
1818

19-
import { authorize, deleteConnection, listConnections } from '../../lib/composio/composioApi';
20-
import { type ComposioConnection, deriveComposioState } from '../../lib/composio/types';
19+
import {
20+
authorize,
21+
deleteConnection,
22+
getUserScopes,
23+
listConnections,
24+
setUserScopes,
25+
} from '../../lib/composio/composioApi';
26+
import {
27+
type ComposioConnection,
28+
type ComposioUserScopePref,
29+
deriveComposioState,
30+
} from '../../lib/composio/types';
2131
import { openUrl } from '../../utils/openUrl';
2232
import type { ComposioToolkitMeta } from './toolkitMeta';
2333

@@ -58,6 +68,16 @@ export default function ComposioConnectModal({
5868
connection
5969
);
6070

71+
// ── Scope preferences (read/write/admin) ────────────────────────
72+
// The pref gates which curated Composio actions the agent may call.
73+
// We load it lazily once the toolkit is connected, so the toggles in
74+
// the success view always reflect what the core actually has stored.
75+
const [scopes, setScopes] = useState<ComposioUserScopePref | null>(null);
76+
const [scopeError, setScopeError] = useState<string | null>(null);
77+
// Per-key in-flight flag so spamming a single toggle disables only
78+
// that row while the RPC round-trips.
79+
const [savingScope, setSavingScope] = useState<keyof ComposioUserScopePref | null>(null);
80+
6181
// Escape to close
6282
useEffect(() => {
6383
const handleEscape = (e: KeyboardEvent) => {
@@ -175,6 +195,78 @@ export default function ComposioConnectModal({
175195
}
176196
}, [startPolling, toolkit.slug]);
177197

198+
// Fetch the stored scope pref whenever the modal lands in the
199+
// 'connected' phase. Re-fetching each time we transition (rather
200+
// than once on mount) keeps the toggles correct after a fresh OAuth
201+
// handoff completes inside this modal.
202+
useEffect(() => {
203+
if (phase !== 'connected') return;
204+
let cancelled = false;
205+
void (async () => {
206+
try {
207+
const pref = await getUserScopes(toolkit.slug);
208+
if (!cancelled) setScopes(pref);
209+
} catch (err) {
210+
if (!cancelled) {
211+
const msg = err instanceof Error ? err.message : String(err);
212+
setScopeError(`Couldn't load scope preferences: ${msg}`);
213+
}
214+
}
215+
})();
216+
return () => {
217+
cancelled = true;
218+
};
219+
}, [phase, toolkit.slug]);
220+
221+
const handleToggleScope = useCallback(
222+
async (key: keyof ComposioUserScopePref) => {
223+
if (!scopes || savingScope) {
224+
console.debug(
225+
'[composio][scopes] toggle ignored toolkit=%s key=%s reason=%s',
226+
toolkit.slug,
227+
key,
228+
!scopes ? 'pref-not-loaded' : 'another-save-in-flight'
229+
);
230+
return;
231+
}
232+
const optimistic: ComposioUserScopePref = { ...scopes, [key]: !scopes[key] };
233+
console.debug(
234+
'[composio][scopes] toggle toolkit=%s key=%s old=%s new=%s',
235+
toolkit.slug,
236+
key,
237+
scopes[key],
238+
optimistic[key]
239+
);
240+
setScopes(optimistic);
241+
setSavingScope(key);
242+
setScopeError(null);
243+
try {
244+
const persisted = await setUserScopes(toolkit.slug, optimistic);
245+
console.debug(
246+
'[composio][scopes] toggle persisted toolkit=%s key=%s pref=%o',
247+
toolkit.slug,
248+
key,
249+
persisted
250+
);
251+
setScopes(persisted);
252+
} catch (err) {
253+
// Roll back on failure so the toggle reflects reality.
254+
const msg = err instanceof Error ? err.message : String(err);
255+
console.error(
256+
'[composio][scopes] toggle failed toolkit=%s key=%s error=%o',
257+
toolkit.slug,
258+
key,
259+
err
260+
);
261+
setScopes(scopes);
262+
setScopeError(`Couldn't save ${key} scope: ${msg}`);
263+
} finally {
264+
setSavingScope(null);
265+
}
266+
},
267+
[savingScope, scopes, toolkit.slug]
268+
);
269+
178270
const handleDisconnect = useCallback(async () => {
179271
if (!activeConnection) return;
180272
setPhase('disconnecting');
@@ -299,6 +391,12 @@ export default function ComposioConnectModal({
299391
id: {activeConnection.id}
300392
</p>
301393
)}
394+
<ScopeToggles
395+
scopes={scopes}
396+
savingScope={savingScope}
397+
onToggle={handleToggleScope}
398+
error={scopeError}
399+
/>
302400
<button
303401
type="button"
304402
onClick={() => void handleDisconnect()}
@@ -333,3 +431,80 @@ export default function ComposioConnectModal({
333431

334432
return createPortal(modalContent, document.body);
335433
}
434+
435+
// ── Scope toggles ───────────────────────────────────────────────────
436+
437+
const SCOPE_ROWS: Array<{ key: keyof ComposioUserScopePref; label: string; hint: string }> = [
438+
{
439+
key: 'read',
440+
label: 'Read',
441+
hint: 'Allow listing, fetching, searching (e.g. read emails / pages).',
442+
},
443+
{
444+
key: 'write',
445+
label: 'Write',
446+
hint: 'Allow sending, creating, updating (e.g. send emails, create pages).',
447+
},
448+
{
449+
key: 'admin',
450+
label: 'Admin',
451+
hint: 'Allow destructive or permission-changing actions (delete, share, etc.).',
452+
},
453+
];
454+
455+
interface ScopeTogglesProps {
456+
scopes: ComposioUserScopePref | null;
457+
savingScope: keyof ComposioUserScopePref | null;
458+
onToggle: (key: keyof ComposioUserScopePref) => void;
459+
error: string | null;
460+
}
461+
462+
function ScopeToggles({ scopes, savingScope, onToggle, error }: ScopeTogglesProps) {
463+
// Render skeleton placeholders while we wait on the initial load so
464+
// the modal layout doesn't jump when the pref arrives.
465+
const loading = scopes === null;
466+
467+
return (
468+
<div className="border-t border-stone-100 pt-3 mt-1 space-y-2">
469+
<div className="flex items-baseline justify-between">
470+
<h3 className="text-xs font-semibold text-stone-700 uppercase tracking-wide">
471+
Agent permissions
472+
</h3>
473+
<p className="text-[10px] text-stone-400">Read+Write enabled by default</p>
474+
</div>
475+
<ul className="space-y-1.5">
476+
{SCOPE_ROWS.map(row => {
477+
const enabled = scopes?.[row.key] ?? false;
478+
const isSaving = savingScope === row.key;
479+
return (
480+
<li
481+
key={row.key}
482+
className="flex items-start justify-between gap-3 rounded-lg px-2 py-1.5 hover:bg-stone-50">
483+
<div className="min-w-0 flex-1">
484+
<span className="text-sm font-medium text-stone-900">{row.label}</span>
485+
<p className="text-[11px] text-stone-400 leading-snug">{row.hint}</p>
486+
</div>
487+
<button
488+
type="button"
489+
role="switch"
490+
aria-checked={enabled}
491+
aria-label={`${enabled ? 'Disable' : 'Enable'} ${row.label} scope`}
492+
disabled={loading || savingScope !== null}
493+
onClick={() => onToggle(row.key)}
494+
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 ${
495+
enabled ? 'bg-primary-500' : 'bg-stone-300'
496+
}`}>
497+
<span
498+
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
499+
enabled ? 'translate-x-5' : 'translate-x-0.5'
500+
} ${isSaving ? 'animate-pulse' : ''}`}
501+
/>
502+
</button>
503+
</li>
504+
);
505+
})}
506+
</ul>
507+
{error && <p className="text-[11px] text-coral-600">{error}</p>}
508+
</div>
509+
);
510+
}

0 commit comments

Comments
 (0)