diff --git a/crates/tinymemory-api/src/host/composio.rs b/crates/tinymemory-api/src/host/composio.rs index f3789012..b3267633 100644 --- a/crates/tinymemory-api/src/host/composio.rs +++ b/crates/tinymemory-api/src/host/composio.rs @@ -488,6 +488,53 @@ pub struct ComposioTriggerHistoryResult { pub entries: Vec, } +/// Static overview of the Composio integrations this build supports. +/// +/// Deliberately does not consult the live Composio backend or a direct tenant: +/// it is an observability surface over what the code knows how to do, not over +/// what the signed-in user has authorized. Callers wanting the latter want +/// `composio.list_toolkits` / `composio.list_connections`. +/// +/// # Why this lives here and not in the engine crate +/// +/// It reads only [`ComposioCapability`] and the contract's curated catalogs +/// (OpenHuman#5560). The version it replaces sat in +/// `tinymemory_core::sync::composio::providers`, so a host rendering its own +/// capability matrix had to link the engine to spell a table of `&'static str`. +/// +/// The two provider-shaped facts it needs — which toolkits have a native +/// provider, and how often each syncs — are +/// [`catalogs::NATIVE_PROVIDERS`][crate::composio::catalogs::NATIVE_PROVIDERS], +/// a const table that carries the same defaults the provider impls pass to +/// their own interval resolver. +#[must_use] +pub fn capability_matrix() -> Vec { + use crate::composio::catalogs; + catalogs::CAPABILITY_TOOLKITS + .iter() + .map(|toolkit| { + let native_provider = catalogs::has_native_provider(toolkit); + let catalog = catalogs::catalog_for_toolkit(toolkit); + let sync_interval_secs = catalogs::native_provider_sync_interval_secs(toolkit); + ComposioCapability { + toolkit: (*toolkit).to_string(), + description: catalogs::toolkit_description(toolkit).to_string(), + native_provider, + curated_tools: catalog.is_some(), + curated_tool_count: catalog + .map_or(0, <[crate::composio::scopes::CuratedTool]>::len), + tool_execution: catalog.is_some(), + user_profile: native_provider, + initial_sync: native_provider, + periodic_sync: sync_interval_secs.is_some(), + sync_interval_secs, + trigger_webhooks: native_provider, + memory_ingest: native_provider, + } + }) + .collect() +} + #[cfg(test)] #[path = "composio_tests.rs"] mod tests; diff --git a/crates/tinymemory-bus/src/composio/catalogs/README.md b/crates/tinymemory-bus/src/composio/catalogs/README.md new file mode 100644 index 00000000..b4bb7974 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/README.md @@ -0,0 +1,89 @@ +# composio::catalogs + +The curated Composio tool catalogs, and the lookups over them. Composio +publishes 60+ actions per toolkit; most are noise for an agent's planning +loop. Each toolkit that has one gets a hand-curated `&'static [CuratedTool]` +slice that pares the surface down to a useful subset and tags every action +with a [`ToolScope`](../scopes.rs), so a user's scope preference can gate +execution per action. + +Moved here from the engine crate (`tinymemory-core`) by OpenHuman#5560: the +catalogs are `&'static str` action slugs with no dependency of any kind, and +the *host* is their heaviest reader (it filters the agent's visible tool +list, renders unlock hints, and decides which connected toolkits get the +"agent-ready" badge). While they lived in the engine crate, every one of +those host reads was a compile-time link to `tinymemory-core`; now a host can +read them by depending on `tinymemory-bus` (or `tinymemory-api`) alone. + +## Responsibilities + +- Hold one curated `&'static [CuratedTool]` table per catalogued toolkit, + grouped by category (see Key files). +- Resolve a toolkit slug — including its known aliases and casing — to its + catalog via [`catalog_for_toolkit`]. +- Answer "should this action slug be visible to the agent, given a loaded + user scope preference?" via [`is_action_visible_with_pref`], falling back + to [`classify_unknown`] for toolkits with no curated catalog. +- Answer "what scope does this slug require?" via [`curated_scope_for`]. +- Provide a short human-readable description of a toolkit for the UI via + [`toolkit_description`] (`descriptions.rs`). +- Track which catalogued toolkits have a native `ComposioProvider` in the + engine crate ([`NATIVE_PROVIDERS`]) and how often each one syncs + ([`native_provider_sync_interval_secs`]), without depending on the engine + crate's provider trait or registry. + +## Key files + +| File | Role | +| --- | --- | +| `mod.rs` | Module docs, category re-exports, `CAPABILITY_TOOLKITS`, `catalog_for_toolkit`, `is_action_visible_with_pref`, `curated_scope_for`, `toolkit_has_scope`, `NATIVE_PROVIDERS`, sync-interval helpers. | +| `descriptions.rs` | `toolkit_description` — one short sentence per toolkit slug (including aliases), generic fallback for anything uncatalogued. | +| `business.rs`, `google.rs`, `messaging.rs`, `microsoft.rs`, `productivity.rs`, `social_media.rs` | The category-grouped `CuratedTool` tables (Shopify/Stripe/HubSpot/…, Google apps, Slack/Discord/…, OneDrive/Excel, Outlook/Linear/Jira/…, Twitter/Spotify/YouTube). | +| `github.rs`, `gmail.rs`, `notion.rs`, `linear.rs`, `clickup.rs` | Provider-colocated catalogs for the five toolkits with a native `ComposioProvider` in the engine crate. | +| `mod_tests.rs`, `microsoft_tests.rs`, `productivity_tests.rs` | Module-local unit tests, wired from the bottom of `mod.rs` / the relevant category file with `#[cfg(test)] #[path = "…_tests.rs"] mod tests;`. | + +## Public surface + +Re-exported from `mod.rs` (and re-exported again from `tinymemory-api::composio::catalogs`, +and from `tinymemory-core::sync::composio::providers::catalogs` for the historical flat +path — plus `tinymemory-core`'s `providers::catalogs_compat` module, which restores the +six per-category module names — `catalogs_business`, `catalogs_google`, … — that predate +this move): + +- `catalog_for_toolkit`, `is_action_visible_with_pref`, `curated_scope_for`, `toolkit_has_scope`, `has_native_provider` +- `CAPABILITY_TOOLKITS`, `NATIVE_PROVIDERS` +- `toolkit_description` +- `sync_interval_env_var`, `parse_sync_interval_override`, `native_provider_sync_interval_secs` +- every category module (`business`, `google`, `messaging`, `microsoft`, `productivity`, + `social_media`) and every provider-colocated module (`gmail`, `notion`, `github`, + `linear`, `clickup`), each exporting its `&'static [CuratedTool]` constants. + +## Dependencies + +None beyond `serde`/`std` (via [`CuratedTool`]/[`ToolScope`] in `../scopes.rs`). This +module must stay dependency-light — see the guard command in +`tinymemory-bus/Cargo.toml`'s doc comment (`cargo tree -p tinymemory-bus -e normal,build +--prefix none | grep -Ei 'rusqlite|libsqlite|git2|reqwest|regex|tokio|tinybus'`, expect no +match) before adding anything here. + +## Used by + +- `tinymemory-api::host::composio::capability_matrix` — the static integrations-overview + RPC surface. +- `tinymemory-core::sync::composio::providers` — re-exports every symbol above at its + historical path so in-engine callers (trigger dispatch, periodic sync, the six native + `ComposioProvider` impls) keep resolving unchanged. +- The OpenHuman host — filters the agent's visible tool list and renders "agent-ready" / + unlock-hint UI without linking `tinymemory-core`. + +## Notes / gotchas + +- `get_provider(..).curated_tools()` (the engine's provider-registry hop) is deliberately + **not** consulted here. Every native provider's `curated_tools()` was verified to return + exactly the slice `catalog_for_toolkit` returns for the same toolkit, so the hop was pure + indirection — see the "`get_provider(..).curated_tools()` is not a separate source" + section in `mod.rs`'s module docs. +- `resolve_sync_interval_secs` (engine-side) logs via `tracing::warn!` on a malformed + interval override; `native_provider_sync_interval_secs` here applies the identical rule + (`parse_sync_interval_override`) silently, because an observability read should not emit + warnings. Do not add `tracing` to this crate to "fix" that — it is intentional. diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_business.rs b/crates/tinymemory-bus/src/composio/catalogs/business.rs similarity index 96% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_business.rs rename to crates/tinymemory-bus/src/composio/catalogs/business.rs index 0033aec8..6333fe1b 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs_business.rs +++ b/crates/tinymemory-bus/src/composio/catalogs/business.rs @@ -1,9 +1,10 @@ -//! Curated catalogs — business toolkits: Shopify, Stripe, HubSpot, +//! Curated catalogs — business toolkits: Shopify, Stripe, `HubSpot`, //! Salesforce, Airtable, Figma. -use super::tool_scope::{CuratedTool, ToolScope}; +use crate::composio::scopes::{CuratedTool, ToolScope}; // ── shopify ───────────────────────────────────────────────────────── +/// The curated action catalog for the `shopify` toolkit. pub const SHOPIFY_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "SHOPIFY_BULK_QUERY_OPERATION", @@ -88,6 +89,7 @@ pub const SHOPIFY_CURATED: &[CuratedTool] = &[ ]; // ── stripe ────────────────────────────────────────────────────────── +/// The curated action catalog for the `stripe` toolkit. pub const STRIPE_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "STRIPE_GET_PAYMENT_INTENT", @@ -168,6 +170,7 @@ pub const STRIPE_CURATED: &[CuratedTool] = &[ ]; // ── hubspot ───────────────────────────────────────────────────────── +/// The curated action catalog for the `hubspot` toolkit. pub const HUBSPOT_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "HUBSPOT_GET_CONTACTS", @@ -272,6 +275,7 @@ pub const HUBSPOT_CURATED: &[CuratedTool] = &[ ]; // ── salesforce ────────────────────────────────────────────────────── +/// The curated action catalog for the `salesforce` toolkit. pub const SALESFORCE_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "SALESFORCE_RUN_SOQL_QUERY", @@ -388,6 +392,7 @@ pub const SALESFORCE_CURATED: &[CuratedTool] = &[ ]; // ── airtable ──────────────────────────────────────────────────────── +/// The curated action catalog for the `airtable` toolkit. pub const AIRTABLE_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "AIRTABLE_LIST_RECORDS", @@ -464,6 +469,7 @@ pub const AIRTABLE_CURATED: &[CuratedTool] = &[ ]; // ── figma ─────────────────────────────────────────────────────────── +/// The curated action catalog for the `figma` toolkit. pub const FIGMA_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "FIGMA_GET_FILE_JSON", diff --git a/crates/tinymemory-bus/src/composio/catalogs/clickup.rs b/crates/tinymemory-bus/src/composio/catalogs/clickup.rs new file mode 100644 index 00000000..2caa0382 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/clickup.rs @@ -0,0 +1,125 @@ +//! Curated catalog of ClickUp Composio actions exposed to the agent. +//! +//! Slugs match Composio's naming convention (`_`) for +//! the ClickUp REST surface. See +//! for the canonical action list; the entries here are the read-oriented +//! subset the periodic Memory Tree sync relies on, plus the most common +//! task-write surface the agent already uses through generic tool-calling. + +use crate::composio::scopes::{CuratedTool, ToolScope}; + +/// The curated action catalog for the `clickup` toolkit. +pub const CLICKUP_CURATED: &[CuratedTool] = &[ + // ── Read: identity ───────────────────────────────────────────── + CuratedTool { + slug: "CLICKUP_GET_AUTHORIZED_USER", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES", + scope: ToolScope::Read, + }, + // ── Read: structure (workspace → space → folder → list) ────── + CuratedTool { + slug: "CLICKUP_GET_SPACES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_FOLDERS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_LISTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_FOLDERLESS_LISTS", + scope: ToolScope::Read, + }, + // ── Read: tasks (the main memory ingest surface) ────────────── + CuratedTool { + slug: "CLICKUP_GET_FILTERED_TEAM_TASKS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_TASKS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_TASK", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_TASK_COMMENTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_LIST_COMMENTS", + scope: ToolScope::Read, + }, + // ── Read: docs / views / time tracking ──────────────────────── + CuratedTool { + slug: "CLICKUP_SEARCH_DOCS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_DOC_PAGES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_VIEW_TASKS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_TIME_ENTRIES_WITHIN_A_DATE_RANGE", + scope: ToolScope::Read, + }, + // ── Read: members ───────────────────────────────────────────── + CuratedTool { + slug: "CLICKUP_GET_WORKSPACE_MEMBERS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "CLICKUP_GET_TASK_MEMBERS", + scope: ToolScope::Read, + }, + // ── Write: create / update tasks ────────────────────────────── + CuratedTool { + slug: "CLICKUP_CREATE_TASK", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "CLICKUP_UPDATE_TASK", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "CLICKUP_CREATE_TASK_COMMENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "CLICKUP_UPDATE_COMMENT", + scope: ToolScope::Write, + }, + // ── Write: structure ────────────────────────────────────────── + CuratedTool { + slug: "CLICKUP_CREATE_LIST", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "CLICKUP_UPDATE_LIST", + scope: ToolScope::Write, + }, + // ── Admin: destructive ──────────────────────────────────────── + CuratedTool { + slug: "CLICKUP_DELETE_TASK", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "CLICKUP_DELETE_COMMENT", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "CLICKUP_DELETE_LIST", + scope: ToolScope::Admin, + }, +]; diff --git a/crates/tinymemory-bus/src/composio/catalogs/descriptions.rs b/crates/tinymemory-bus/src/composio/catalogs/descriptions.rs new file mode 100644 index 00000000..5f5fd856 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/descriptions.rs @@ -0,0 +1,65 @@ +//! Human-readable capability summaries for Composio toolkit slugs. + +/// Human-readable capability summary for a Composio toolkit slug. +/// +/// Used by the prompt renderer to tell the orchestrator what each connected +/// integration can do. Covers the most common toolkits; unknown slugs get +/// a generic fallback so newly connected services still appear. +pub fn toolkit_description(slug: &str) -> &'static str { + match slug { + "gmail" => { + "Send, read, draft, reply, forward, and search emails; manage labels and threads" + } + "notion" => "Create, read, update, and search notion pages and notion databases", + "github" => { + "Manage repositories, issues, and pull requests on GitHub; sync \ + assigned issues into Memory Tree" + } + "slack" => "Send messages, read channels, manage threads, and post updates in Slack", + "discord" => "Send messages, manage channels, and interact with Discord servers", + "google_calendar" | "googlecalendar" => { + "Create, update, and query calendar events; check availability" + } + "google_drive" | "googledrive" => { + "Upload, download, search, and share files in Google Drive" + } + "google_docs" | "googledocs" => "Create, read, and edit Google Docs documents", + "google_sheets" | "googlesheets" => "Read, write, and manage Google Sheets spreadsheets", + "outlook" => "Send, read, and manage emails in Microsoft Outlook", + "microsoft" | "microsoft_teams" => "Send messages and manage channels in Microsoft Teams", + "larksuite" => { + "Connect Lark / Feishu workspace chat, docs, wiki, and meetings via Composio" + } + "linear" => { + "Create, read, and manage issues, projects, and cycles in Linear; sync \ + assigned issues into Memory Tree" + } + "jira" => "Create and manage issues, projects, and sprints in Jira", + "trello" => "Create and manage cards, lists, and boards in Trello", + "asana" => "Create and manage tasks, projects, and sections in Asana", + "clickup" => { + "Create, read, and manage tasks, lists, and docs in ClickUp; sync \ + assigned tasks into Memory Tree" + } + "dropbox" => "Upload, download, and share files in Dropbox", + "twitter" => "Post tweets, read timelines, and manage Twitter interactions", + "spotify" => "Control playback, search music, and manage playlists on Spotify", + "telegram" => "Send and receive messages via Telegram", + "whatsapp" => "Send and receive messages via WhatsApp", + "twilio" => "Send SMS, make calls, and manage communications via Twilio", + "shopify" => "Manage products, orders, and customers in Shopify", + "stripe" => "Manage payments, subscriptions, and customers in Stripe", + "hubspot" => "Manage contacts, deals, and marketing in HubSpot", + "salesforce" => "Manage contacts, leads, and opportunities in Salesforce", + "airtable" => "Read and write records in Airtable bases", + "figma" => "Access and manage Figma design files and components", + "youtube" => "Search videos, manage playlists, and interact with YouTube", + "calendar" => "Create, update, and query calendar events", + "one_drive" | "onedrive" | "one" => { + "Upload, download, search, and share files in Microsoft OneDrive" + } + "excel" => "Read, write, and manage workbooks, worksheets, and tables in Microsoft Excel", + "todoist" => "Create and manage tasks, projects, sections, and labels in Todoist", + _ => "Interact with this connected service via its available actions", + } +} diff --git a/crates/tinymemory-bus/src/composio/catalogs/github.rs b/crates/tinymemory-bus/src/composio/catalogs/github.rs new file mode 100644 index 00000000..b506e8d8 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/github.rs @@ -0,0 +1,178 @@ +//! Curated catalog of GitHub Composio actions exposed to the agent. +//! +//! Composio publishes hundreds of GitHub actions; this hand-tuned slice +//! covers the day-to-day operations an AI assistant actually performs +//! (browsing repos, reading/writing issues + PRs, code search, basic +//! workflow control) and hides the long tail of admin endpoints. + +use crate::composio::scopes::{CuratedTool, ToolScope}; + +/// The curated action catalog for the `github` toolkit. +pub const GITHUB_CURATED: &[CuratedTool] = &[ + // ── Read: user / repos ────────────────────────────────────────── + CuratedTool { + slug: "GITHUB_GET_THE_AUTHENTICATED_USER", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_GET_A_REPOSITORY", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_LIST_REPOSITORY_COLLABORATORS", + scope: ToolScope::Read, + }, + // ── Read: search ──────────────────────────────────────────────── + CuratedTool { + slug: "GITHUB_SEARCH_REPOSITORIES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_SEARCH_CODE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_SEARCH_ISSUES_AND_PULL_REQUESTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_SEARCH_USERS", + scope: ToolScope::Read, + }, + // ── Read: issues ──────────────────────────────────────────────── + CuratedTool { + slug: "GITHUB_LIST_REPOSITORY_ISSUES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_GET_AN_ISSUE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_LIST_ISSUE_COMMENTS", + scope: ToolScope::Read, + }, + // ── Read: pull requests ───────────────────────────────────────── + CuratedTool { + slug: "GITHUB_LIST_PULL_REQUESTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_GET_A_PULL_REQUEST", + scope: ToolScope::Read, + }, + // ── Read: branches / commits ──────────────────────────────────── + CuratedTool { + slug: "GITHUB_LIST_BRANCHES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_GET_A_BRANCH", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_LIST_COMMITS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GITHUB_GET_A_COMMIT", + scope: ToolScope::Read, + }, + // ── Write: repos / contents ───────────────────────────────────── + CuratedTool { + slug: "GITHUB_CREATE_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_CREATE_OR_UPDATE_FILE_CONTENTS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_CREATE_A_COMMIT", + scope: ToolScope::Write, + }, + // GITHUB_COMMIT_MULTIPLE_FILES removed from Composio catalog + CuratedTool { + slug: "GITHUB_CREATE_A_COMMIT_COMMENT", + scope: ToolScope::Write, + }, + // ── Write: issues ─────────────────────────────────────────────── + CuratedTool { + slug: "GITHUB_CREATE_AN_ISSUE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_UPDATE_AN_ISSUE", + scope: ToolScope::Write, + }, + // GITHUB_CLOSE_AN_ISSUE — removed: no dedicated Composio slug. + // Use GITHUB_UPDATE_AN_ISSUE with state:"closed" instead. + CuratedTool { + slug: "GITHUB_CREATE_AN_ISSUE_COMMENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_ADD_LABELS_TO_AN_ISSUE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_ADD_ASSIGNEES_TO_AN_ISSUE", + scope: ToolScope::Write, + }, + // ── Write: pull requests ──────────────────────────────────────── + CuratedTool { + slug: "GITHUB_CREATE_A_PULL_REQUEST", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_UPDATE_A_PULL_REQUEST", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_MERGE_A_PULL_REQUEST", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_CREATE_A_REVIEW_FOR_A_PULL_REQUEST", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_CREATE_A_REVIEW_COMMENT_FOR_A_PULL_REQUEST", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GITHUB_CREATE_A_GIST", + scope: ToolScope::Write, + }, + // ── Admin: destructive / permission-changing ──────────────────── + CuratedTool { + slug: "GITHUB_DELETE_A_REPOSITORY", + scope: ToolScope::Admin, + }, + // DELETE_A_REFERENCE maps to DELETE /repos/{owner}/{repo}/git/refs/{ref}. + // The ref must be a full path (e.g. `refs/heads/branch-name` or + // `refs/tags/v1.0`) — passing a bare branch name deletes nothing (404). + // This replaces the old GITHUB_DELETE_A_BRANCH slug (Composio v3 rename); + // it is broader — it can delete tags too — so agents should always specify + // a `refs/heads/` prefix when the intent is branch deletion. + CuratedTool { + slug: "GITHUB_DELETE_A_REFERENCE", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GITHUB_DELETE_A_FILE", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GITHUB_ADD_A_REPOSITORY_COLLABORATOR", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GITHUB_CANCEL_A_WORKFLOW_RUN", + scope: ToolScope::Admin, + }, +]; diff --git a/crates/tinymemory-bus/src/composio/catalogs/gmail.rs b/crates/tinymemory-bus/src/composio/catalogs/gmail.rs new file mode 100644 index 00000000..f0877102 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/gmail.rs @@ -0,0 +1,135 @@ +//! Curated catalog of Gmail Composio actions exposed to the agent. +//! +//! Composio publishes 60+ Gmail actions; this hand-tuned slice covers +//! the cases the agent actually plans for (read, compose, manage) and +//! hides the long tail of edge-case admin endpoints. + +use crate::composio::scopes::{CuratedTool, ToolScope}; + +/// The curated action catalog for the `gmail` toolkit. +pub const GMAIL_CURATED: &[CuratedTool] = &[ + // ── Read: messages & threads ──────────────────────────────────── + CuratedTool { + slug: "GMAIL_FETCH_EMAILS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_LIST_MESSAGES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_FETCH_MESSAGE_BY_THREAD_ID", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_LIST_THREADS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_GET_ATTACHMENT", + scope: ToolScope::Read, + }, + // ── Read: profile & settings ──────────────────────────────────── + CuratedTool { + slug: "GMAIL_GET_PROFILE", + scope: ToolScope::Read, + }, + // ── Read: contacts & people ───────────────────────────────────── + CuratedTool { + slug: "GMAIL_GET_CONTACTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_GET_PEOPLE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_SEARCH_PEOPLE", + scope: ToolScope::Read, + }, + // ── Read: drafts & labels ─────────────────────────────────────── + CuratedTool { + slug: "GMAIL_LIST_DRAFTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_GET_DRAFT", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_LIST_LABELS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "GMAIL_GET_LABEL", + scope: ToolScope::Read, + }, + // ── Write: send & compose ─────────────────────────────────────── + CuratedTool { + slug: "GMAIL_SEND_EMAIL", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GMAIL_REPLY_TO_THREAD", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GMAIL_FORWARD_MESSAGE", + scope: ToolScope::Write, + }, + // ── Write: drafts ─────────────────────────────────────────────── + CuratedTool { + slug: "GMAIL_CREATE_EMAIL_DRAFT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GMAIL_UPDATE_DRAFT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "GMAIL_SEND_DRAFT", + scope: ToolScope::Write, + }, + // ── Write: labels (create/update on user labels) ──────────────── + CuratedTool { + slug: "GMAIL_ADD_LABEL_TO_EMAIL", + scope: ToolScope::Write, + }, + // ── Admin: destructive & permission-changing ──────────────────── + CuratedTool { + slug: "GMAIL_DELETE_MESSAGE", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_BATCH_DELETE_MESSAGES", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_MOVE_TO_TRASH", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_DELETE_THREAD", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_MOVE_THREAD_TO_TRASH", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_UNTRASH_THREAD", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_DELETE_DRAFT", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "GMAIL_DELETE_LABEL", + scope: ToolScope::Admin, + }, +]; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_google.rs b/crates/tinymemory-bus/src/composio/catalogs/google.rs similarity index 95% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_google.rs rename to crates/tinymemory-bus/src/composio/catalogs/google.rs index be25c83d..2308d98e 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs_google.rs +++ b/crates/tinymemory-bus/src/composio/catalogs/google.rs @@ -1,9 +1,10 @@ -//! Curated catalogs — Google toolkits: GoogleCalendar, GoogleDrive, -//! GoogleDocs, GoogleSheets. +//! Curated catalogs — Google toolkits: `GoogleCalendar`, `GoogleDrive`, +//! `GoogleDocs`, `GoogleSheets`. -use super::tool_scope::{CuratedTool, ToolScope}; +use crate::composio::scopes::{CuratedTool, ToolScope}; // ── googlecalendar ────────────────────────────────────────────────── +/// The curated action catalog for the `googlecalendar` toolkit. pub const GOOGLECALENDAR_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "GOOGLECALENDAR_EVENTS_LIST", @@ -92,6 +93,7 @@ pub const GOOGLECALENDAR_CURATED: &[CuratedTool] = &[ ]; // ── googledrive ───────────────────────────────────────────────────── +/// The curated action catalog for the `googledrive` toolkit. pub const GOOGLEDRIVE_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "GOOGLEDRIVE_FIND_FILE", @@ -180,6 +182,7 @@ pub const GOOGLEDRIVE_CURATED: &[CuratedTool] = &[ ]; // ── googledocs ────────────────────────────────────────────────────── +/// The curated action catalog for the `googledocs` toolkit. pub const GOOGLEDOCS_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "GOOGLEDOCS_GET_DOCUMENT_BY_ID", @@ -268,6 +271,7 @@ pub const GOOGLEDOCS_CURATED: &[CuratedTool] = &[ ]; // ── googlesheets ──────────────────────────────────────────────────── +/// The curated action catalog for the `googlesheets` toolkit. pub const GOOGLESHEETS_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "GOOGLESHEETS_BATCH_GET", diff --git a/crates/tinymemory-bus/src/composio/catalogs/linear.rs b/crates/tinymemory-bus/src/composio/catalogs/linear.rs new file mode 100644 index 00000000..50bb2b41 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/linear.rs @@ -0,0 +1,91 @@ +//! Curated catalog of Linear Composio actions. + +use crate::composio::scopes::{CuratedTool, ToolScope}; + +/// The curated action catalog for the `linear` toolkit. +pub const LINEAR_CURATED: &[CuratedTool] = &[ + CuratedTool { + slug: "LINEAR_LIST_LINEAR_USERS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_LIST_LINEAR_ISSUES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_GET_LINEAR_ISSUE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_SEARCH_ISSUES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_LIST_LINEAR_TEAMS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_LIST_LINEAR_PROJECTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_GET_LINEAR_PROJECT", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_LIST_LINEAR_STATES", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_GET_CYCLES_BY_TEAM_ID", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_LIST_LINEAR_LABELS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "LINEAR_CREATE_LINEAR_ISSUE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_UPDATE_ISSUE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_CREATE_LINEAR_COMMENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_UPDATE_LINEAR_COMMENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_CREATE_ATTACHMENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_CREATE_ISSUE_RELATION", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_CREATE_LINEAR_PROJECT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_UPDATE_LINEAR_PROJECT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_CREATE_LINEAR_LABEL", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "LINEAR_DELETE_LINEAR_ISSUE", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "LINEAR_REMOVE_ISSUE_LABEL", + scope: ToolScope::Admin, + }, +]; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_messaging.rs b/crates/tinymemory-bus/src/composio/catalogs/messaging.rs similarity index 96% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_messaging.rs rename to crates/tinymemory-bus/src/composio/catalogs/messaging.rs index 33d2975e..2adfb3fc 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs_messaging.rs +++ b/crates/tinymemory-bus/src/composio/catalogs/messaging.rs @@ -1,9 +1,10 @@ //! Curated catalogs — messaging toolkits: Slack, Discord, Telegram, //! WhatsApp, Microsoft Teams. -use super::tool_scope::{CuratedTool, ToolScope}; +use crate::composio::scopes::{CuratedTool, ToolScope}; // ── slack ─────────────────────────────────────────────────────────── +/// The curated action catalog for the `slack` toolkit. pub const SLACK_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "SLACK_FIND_CHANNELS", @@ -116,6 +117,7 @@ pub const SLACK_CURATED: &[CuratedTool] = &[ ]; // ── discord ───────────────────────────────────────────────────────── +/// The curated action catalog for the `discord` toolkit. pub const DISCORD_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "DISCORD_GET_MY_USER", @@ -162,6 +164,7 @@ pub const DISCORD_CURATED: &[CuratedTool] = &[ ]; // ── telegram ──────────────────────────────────────────────────────── +/// The curated action catalog for the `telegram` toolkit. pub const TELEGRAM_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "TELEGRAM_GET_UPDATES", @@ -238,6 +241,7 @@ pub const TELEGRAM_CURATED: &[CuratedTool] = &[ ]; // ── whatsapp ──────────────────────────────────────────────────────── +/// The curated action catalog for the `whatsapp` toolkit. pub const WHATSAPP_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "WHATSAPP_GET_PHONE_NUMBERS", @@ -302,6 +306,7 @@ pub const WHATSAPP_CURATED: &[CuratedTool] = &[ ]; // ── microsoft_teams ───────────────────────────────────────────────── +/// The curated action catalog for the `microsoft_teams` toolkit. pub const MICROSOFT_TEAMS_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "MICROSOFT_TEAMS_GET_CHAT", diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft.rs b/crates/tinymemory-bus/src/composio/catalogs/microsoft.rs similarity index 93% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft.rs rename to crates/tinymemory-bus/src/composio/catalogs/microsoft.rs index f0e5e62f..e609e5bd 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft.rs +++ b/crates/tinymemory-bus/src/composio/catalogs/microsoft.rs @@ -1,8 +1,8 @@ //! Curated catalogs — Microsoft personal-productivity toolkits: -//! OneDrive (files) and Excel (spreadsheets). +//! `OneDrive` (files) and Excel (spreadsheets). //! //! These toolkits are catalog-only: they don't ship a native -//! [`super::ComposioProvider`] implementation, so they have no +//! `ComposioProvider` implementation (in `tinymemory-core`), so they have no //! user-profile fetch, no initial/periodic sync, no trigger webhooks, //! and no memory ingestion. Connecting them via the UI lets the agent //! invoke the listed actions through Composio's API, but their data @@ -13,9 +13,10 @@ //! exist on the backend simply never appear in `composio_list_tools`, //! so over-shooting is harmless. -use super::tool_scope::{CuratedTool, ToolScope}; +use crate::composio::scopes::{CuratedTool, ToolScope}; // ── onedrive ──────────────────────────────────────────────────────── +/// The curated action catalog for the `one_drive` toolkit. pub const ONE_DRIVE_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "ONE_DRIVE_GET_FILE", @@ -84,6 +85,7 @@ pub const ONE_DRIVE_CURATED: &[CuratedTool] = &[ ]; // ── excel ─────────────────────────────────────────────────────────── +/// The curated action catalog for the `excel` toolkit. pub const EXCEL_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "EXCEL_GET_WORKBOOK", @@ -160,5 +162,5 @@ pub const EXCEL_CURATED: &[CuratedTool] = &[ ]; #[cfg(test)] -#[path = "catalogs_microsoft_tests.rs"] +#[path = "microsoft_tests.rs"] mod tests; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft_tests.rs b/crates/tinymemory-bus/src/composio/catalogs/microsoft_tests.rs similarity index 100% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_microsoft_tests.rs rename to crates/tinymemory-bus/src/composio/catalogs/microsoft_tests.rs diff --git a/crates/tinymemory-bus/src/composio/catalogs/mod.rs b/crates/tinymemory-bus/src/composio/catalogs/mod.rs new file mode 100644 index 00000000..2010317b --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/mod.rs @@ -0,0 +1,257 @@ +//! The curated Composio tool catalogs, and the lookups over them. +//! +//! Composio publishes 60+ actions per toolkit; most are noise for an agent's +//! planning loop. Each toolkit gets a hand-curated `&'static [CuratedTool]` +//! slice that pares the surface down to a useful subset and tags every action +//! with a [`ToolScope`], so per-user scope preferences can gate execution. +//! +//! # Why the catalogs are here and not in the engine crate +//! +//! [`super`]'s module docs used to end with "the curated catalogs and the +//! provider registry ... are the engine's", and the catalogs half of that is no +//! longer true. The registry still is — it is a process-global map of trait +//! objects that reach `reqwest` and the chunk store, and none of that may enter +//! this crate. +//! +//! The catalogs are the opposite: several thousand `&'static str` action slugs +//! and a `match` over them, with no dependency of any kind. And the *host* is +//! their heaviest reader — it filters the agent's visible tool list, renders +//! the `gated_tools` unlock hints, and decides which connected toolkits get the +//! "agent-ready" badge. While they lived in the engine crate, every one of +//! those host reads was a compile-time link to `tinymemory-core`, which is the +//! link OpenHuman#5560 removes. Same argument [`super::scopes`] already makes +//! for the verdict functions: the *same answer* has to be reachable on both +//! sides of the module boundary, so the data has to be nameable from the +//! contract. +//! +//! # `get_provider(..).curated_tools()` is not a separate source +//! +//! The lookups here consult [`catalog_for_toolkit`] alone. The engine's +//! versions walked the registered provider's `curated_tools()` first and fell +//! back to the static map, which read as two sources of truth; it was one. +//! Every native provider's `curated_tools()` returns exactly the slice +//! `catalog_for_toolkit` returns for the same toolkit — verified against all +//! six (`gmail`, `notion`, `github`, `linear`, `clickup`, `slack`) — so the +//! provider hop was pure indirection, and dropping it is what lets a host +//! answer "may this action run" without a provider registry at all. + +pub mod business; +pub mod clickup; +pub mod descriptions; +pub mod github; +pub mod gmail; +pub mod google; +pub mod linear; +pub mod messaging; +pub mod microsoft; +pub mod notion; +pub mod productivity; +pub mod social_media; + +use super::scopes::{ + classify_unknown, find_curated, toolkit_from_slug, CuratedTool, ToolScope, UserScopePref, +}; + +pub use descriptions::toolkit_description; + +/// Every toolkit the capability surface reports on, in display order. +pub const CAPABILITY_TOOLKITS: &[&str] = &[ + "gmail", + "notion", + "slack", + "clickup", + "github", + "discord", + "googlecalendar", + "googledrive", + "googledocs", + "googlesheets", + "outlook", + "microsoft_teams", + "linear", + "jira", + "trello", + "asana", + "dropbox", + "twitter", + "spotify", + "telegram", + "whatsapp", + "shopify", + "stripe", + "hubspot", + "salesforce", + "airtable", + "figma", + "youtube", + "one_drive", + "excel", + "todoist", +]; + +/// Toolkits with a native `ComposioProvider` in the engine, and the +/// compile-time default periodic-sync interval each one ships. +/// +/// The provider impls are the engine's, but *which* toolkits have one and how +/// often they run are facts a host reports in its capability surface, so the +/// table is contract data. Each provider still calls its own +/// `resolve_sync_interval_secs` with the same default, and +/// `native_provider_sync_interval_secs` below resolves the identical env +/// override — so the two cannot disagree without this table being edited. +pub const NATIVE_PROVIDERS: &[(&str, u64)] = &[ + ("gmail", 15 * 60), + ("notion", 30 * 60), + ("slack", 15 * 60), + ("clickup", 30 * 60), + ("github", 30 * 60), + ("linear", 30 * 60), +]; + +/// Does `toolkit` have a native provider implementation? +#[must_use] +pub fn has_native_provider(toolkit: &str) -> bool { + NATIVE_PROVIDERS.iter().any(|(slug, _)| *slug == toolkit) +} + +/// The env var read to override a toolkit's periodic sync interval. +/// +/// Exposed so tests and `.env.example` stay in lockstep with the runtime +/// lookup without re-implementing the casing. +#[must_use] +pub fn sync_interval_env_var(toolkit: &str) -> String { + format!( + "OPENHUMAN_COMPOSIO_{}_SYNC_INTERVAL_SECS", + toolkit.to_ascii_uppercase() + ) +} + +/// Apply a raw env-var override to a default interval. +/// +/// Split out from the env read so both sides can share the rule and differ on +/// what they do about a bad value: a provider warns once, an observability read +/// stays silent. `0` is never honoured — it would burn the scheduler in a tight +/// loop — so a non-positive or unparseable value yields `None` and the caller +/// keeps its default. +#[must_use] +pub fn parse_sync_interval_override(raw: &str) -> Option { + raw.trim().parse::().ok().filter(|n| *n >= 1) +} + +/// The effective periodic sync interval for a native provider, honouring the +/// `OPENHUMAN_COMPOSIO__SYNC_INTERVAL_SECS` override. +/// +/// `None` for a toolkit with no native provider. +#[must_use] +pub fn native_provider_sync_interval_secs(toolkit: &str) -> Option { + let default_secs = NATIVE_PROVIDERS + .iter() + .find(|(slug, _)| *slug == toolkit) + .map(|(_, secs)| *secs)?; + let resolved = std::env::var(sync_interval_env_var(toolkit)) + .ok() + .and_then(|raw| parse_sync_interval_override(&raw)) + .unwrap_or(default_secs); + Some(resolved) +} + +/// Static toolkit → curated catalog map. +/// +/// The lookup key is the lowercased prefix [`toolkit_from_slug`] returns for an +/// action slug — `GOOGLECALENDAR_CREATE_EVENT` → `"googlecalendar"`. +/// Multi-segment prefixes like `MICROSOFT_TEAMS_*` return their known toolkit +/// slug. +#[must_use] +pub fn catalog_for_toolkit(toolkit: &str) -> Option<&'static [CuratedTool]> { + match toolkit.trim().to_ascii_lowercase().as_str() { + // Toolkits with a native provider. Each provider's `curated_tools()` + // returns this same slice. + "gmail" => Some(gmail::GMAIL_CURATED), + "notion" => Some(notion::NOTION_CURATED), + "github" => Some(github::GITHUB_CURATED), + "linear" => Some(linear::LINEAR_CURATED), + "clickup" => Some(clickup::CLICKUP_CURATED), + "slack" => Some(messaging::SLACK_CURATED), + // Catalog-only toolkits. + "discord" => Some(messaging::DISCORD_CURATED), + "googlecalendar" | "google_calendar" => Some(google::GOOGLECALENDAR_CURATED), + "googledrive" | "google_drive" => Some(google::GOOGLEDRIVE_CURATED), + "googledocs" | "google_docs" => Some(google::GOOGLEDOCS_CURATED), + "googlesheets" | "google_sheets" => Some(google::GOOGLESHEETS_CURATED), + "outlook" => Some(productivity::OUTLOOK_CURATED), + // The legacy "microsoft" alias stays while `toolkit_from_slug` returns + // the precise "microsoft_teams" slug for Teams actions. + "microsoft" | "microsoft_teams" => Some(messaging::MICROSOFT_TEAMS_CURATED), + "jira" => Some(productivity::JIRA_CURATED), + "trello" => Some(productivity::TRELLO_CURATED), + "asana" => Some(productivity::ASANA_CURATED), + "dropbox" => Some(productivity::DROPBOX_CURATED), + "twitter" => Some(social_media::TWITTER_CURATED), + "spotify" => Some(social_media::SPOTIFY_CURATED), + "telegram" => Some(messaging::TELEGRAM_CURATED), + "whatsapp" => Some(messaging::WHATSAPP_CURATED), + "shopify" => Some(business::SHOPIFY_CURATED), + "stripe" => Some(business::STRIPE_CURATED), + "hubspot" => Some(business::HUBSPOT_CURATED), + "salesforce" => Some(business::SALESFORCE_CURATED), + "airtable" => Some(business::AIRTABLE_CURATED), + "figma" => Some(business::FIGMA_CURATED), + "youtube" => Some(social_media::YOUTUBE_CURATED), + // `ONE_DRIVE_*` slugs extract to "one" via `toolkit_from_slug`; alias + // both the prefix and the canonical UI/backend slugs. + "one" | "one_drive" | "onedrive" => Some(microsoft::ONE_DRIVE_CURATED), + "excel" => Some(microsoft::EXCEL_CURATED), + "todoist" => Some(productivity::TODOIST_CURATED), + _ => None, + } +} + +/// Should this action slug appear in the agent's tool surface, given an +/// already-loaded user scope preference? +/// +/// `true` when the action is in its toolkit's curated whitelist (or the toolkit +/// has no curation) **and** the preference allows its classification. Falls +/// back to [`classify_unknown`] for uncurated toolkits. +/// +/// Takes a pre-loaded preference because the typical caller loops over +/// toolkits, where awaiting once per toolkit is cheaper than once per action. +#[must_use] +pub fn is_action_visible_with_pref(slug: &str, pref: &UserScopePref) -> bool { + let Some(toolkit) = toolkit_from_slug(slug) else { + return true; + }; + match catalog_for_toolkit(&toolkit) { + Some(catalog) => match find_curated(catalog, slug) { + Some(curated) => pref.allows(curated.scope), + None => false, + }, + None => pref.allows(classify_unknown(slug)), + } +} + +/// The curated scope `slug` requires, if it appears in any catalog. +/// +/// `None` for a genuinely uncurated slug — a caller wanting a defensible +/// heuristic for those should reach for [`classify_unknown`] explicitly. +/// +/// Sibling of [`is_action_visible_with_pref`]: that one answers "visible?", +/// this one answers "what scope is required?", so a caller can render an unlock +/// hint without redoing the catalog walk. +#[must_use] +pub fn curated_scope_for(slug: &str) -> Option { + let toolkit = toolkit_from_slug(slug)?; + let catalog = catalog_for_toolkit(&toolkit)?; + find_curated(catalog, slug).map(|c| c.scope) +} + +/// Does any curated action for `toolkit` require `scope`? +/// +/// Useful whenever the question is "would flipping the {scope} bit unlock +/// anything here?" — a UI hint that greys out a toggle with no effect. +#[must_use] +pub fn toolkit_has_scope(toolkit: &str, scope: ToolScope) -> bool { + catalog_for_toolkit(toolkit).is_some_and(|cat| cat.iter().any(|t| t.scope == scope)) +} + +#[cfg(test)] +#[path = "mod_tests.rs"] +mod tests; diff --git a/crates/tinymemory-bus/src/composio/catalogs/mod_tests.rs b/crates/tinymemory-bus/src/composio/catalogs/mod_tests.rs new file mode 100644 index 00000000..619c6e47 --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/mod_tests.rs @@ -0,0 +1,206 @@ +//! Tests for the surrounding module. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + +use super::*; + +#[test] +fn catalog_for_toolkit_resolves_every_capability_toolkit() { + // Every toolkit the capability surface reports on must have a catalog — + // that is what `curated_tools: true` in the matrix claims about it. + for toolkit in CAPABILITY_TOOLKITS { + assert!( + catalog_for_toolkit(toolkit).is_some(), + "no curated catalog for advertised toolkit {toolkit}" + ); + } +} + +#[test] +fn catalog_for_toolkit_honours_slug_aliases() { + // `toolkit_from_slug` extracts "one" from `ONE_DRIVE_*`, while the UI and + // the backend both spell it "one_drive" / "onedrive". + for alias in ["one", "one_drive", "onedrive", "OneDrive"] { + assert!( + catalog_for_toolkit(alias).is_some(), + "OneDrive alias {alias} did not resolve" + ); + } + // The legacy "microsoft" alias still reaches the Teams catalog. + assert_eq!( + catalog_for_toolkit("microsoft").map(<[CuratedTool]>::len), + catalog_for_toolkit("microsoft_teams").map(<[CuratedTool]>::len) + ); + for alias in ["google_calendar", "googlecalendar", "GOOGLECALENDAR"] { + assert!( + catalog_for_toolkit(alias).is_some(), + "{alias} did not resolve" + ); + } + assert!( + catalog_for_toolkit(" gmail ").is_some(), + "slug is not trimmed" + ); + assert!(catalog_for_toolkit("nonexistent-toolkit").is_none()); +} + +#[test] +fn every_native_provider_has_a_catalog_and_a_positive_default_interval() { + for (slug, default_secs) in NATIVE_PROVIDERS { + assert!( + catalog_for_toolkit(slug).is_some(), + "native provider {slug} has no curated catalog" + ); + assert!(has_native_provider(slug)); + assert!( + *default_secs >= 1, + "{slug} default interval must be positive" + ); + assert!( + CAPABILITY_TOOLKITS.contains(slug), + "native provider {slug} is missing from the capability surface" + ); + } + assert!(!has_native_provider("jira")); + assert!(!has_native_provider("nonexistent-toolkit")); +} + +#[test] +fn sync_interval_env_var_upper_cases_the_toolkit() { + assert_eq!( + sync_interval_env_var("gmail"), + "OPENHUMAN_COMPOSIO_GMAIL_SYNC_INTERVAL_SECS" + ); + assert_eq!( + sync_interval_env_var("microsoft_teams"), + "OPENHUMAN_COMPOSIO_MICROSOFT_TEAMS_SYNC_INTERVAL_SECS" + ); +} + +#[test] +fn parse_sync_interval_override_rejects_zero_and_junk() { + // `0` would burn the scheduler in a tight loop, so it is never honoured. + assert_eq!(parse_sync_interval_override("0"), None); + assert_eq!(parse_sync_interval_override("-5"), None); + assert_eq!(parse_sync_interval_override("soon"), None); + assert_eq!(parse_sync_interval_override(""), None); + assert_eq!(parse_sync_interval_override(" 900 "), Some(900)); + assert_eq!(parse_sync_interval_override("1"), Some(1)); +} + +#[test] +fn native_provider_sync_interval_is_none_for_catalog_only_toolkits() { + // Reads no env var, so it is safe beside the process-global env tests. + assert_eq!(native_provider_sync_interval_secs("jira"), None); + assert_eq!( + native_provider_sync_interval_secs("nonexistent-toolkit"), + None + ); + assert!(native_provider_sync_interval_secs("gmail").is_some()); +} + +#[test] +fn toolkit_has_scope_distinguishes_gated_from_ungated_scopes() { + // The gmail catalog includes destructive verbs (delete / trash / + // batch_delete), so admin-gating actually unlocks something. + assert!(toolkit_has_scope("gmail", ToolScope::Admin)); + assert!(toolkit_has_scope("gmail", ToolScope::Read)); + assert!(toolkit_has_scope("gmail", ToolScope::Write)); + // Case-insensitive toolkit slug → still routes to the catalog. + assert!(toolkit_has_scope("GMAIL", ToolScope::Admin)); + // Unknown toolkit → no catalog → no scope is "gating" anything. + assert!(!toolkit_has_scope("nonexistent-toolkit", ToolScope::Admin)); +} + +#[test] +fn curated_scope_for_reads_the_catalogs_entry_not_the_heuristic() { + // Pick a real curated read action and assert the catalog's own verdict. + let catalog = catalog_for_toolkit("gmail").expect("gmail catalog"); + let read_action = catalog + .iter() + .find(|t| t.scope == ToolScope::Read) + .expect("gmail has a curated read action"); + assert_eq!(curated_scope_for(read_action.slug), Some(ToolScope::Read)); + + // An uncurated slug on a curated toolkit is `None` — deliberately not the + // `classify_unknown` heuristic, which callers opt into explicitly. + assert_eq!(curated_scope_for("GMAIL_NO_SUCH_ACTION_EXISTS"), None); + // A slug with no toolkit prefix at all. + assert_eq!(curated_scope_for("nonsense"), None); +} + +#[test] +fn is_action_visible_gates_on_the_curated_scope() { + let catalog = catalog_for_toolkit("gmail").expect("gmail catalog"); + let read_action = catalog + .iter() + .find(|t| t.scope == ToolScope::Read) + .expect("gmail has a curated read action"); + let admin_action = catalog + .iter() + .find(|t| t.scope == ToolScope::Admin) + .expect("gmail has a curated admin action"); + + let read_only = UserScopePref { + read: true, + write: false, + admin: false, + }; + assert!(is_action_visible_with_pref(read_action.slug, &read_only)); + assert!(!is_action_visible_with_pref(admin_action.slug, &read_only)); + + let all = UserScopePref { + read: true, + write: true, + admin: true, + }; + assert!(is_action_visible_with_pref(admin_action.slug, &all)); + + // Uncurated action on a curated toolkit is hidden regardless of pref — + // curation is a whitelist, so absence means "not surfaced", never + // "fall back to the heuristic". + assert!(!is_action_visible_with_pref("GMAIL_NO_SUCH_ACTION", &all)); + + // A slug with no toolkit prefix is not ours to gate. + assert!(is_action_visible_with_pref("nonsense", &read_only)); +} + +#[test] +fn toolkit_description_is_populated_for_every_capability_toolkit() { + let generic = toolkit_description("definitely-not-a-real-toolkit"); + for toolkit in CAPABILITY_TOOLKITS { + let d = toolkit_description(toolkit); + assert!(!d.trim().is_empty(), "{toolkit} has an empty description"); + assert_ne!( + d, generic, + "{toolkit} falls through to the generic description" + ); + } +} + +#[test] +fn toolkit_description_recognizes_the_legacy_microsoft_alias() { + // `catalog_for_toolkit` resolves both "microsoft" and "microsoft_teams" to + // the same Teams catalog; the description must not diverge for the alias. + assert_eq!( + toolkit_description("microsoft"), + toolkit_description("microsoft_teams") + ); + assert_ne!( + toolkit_description("microsoft"), + toolkit_description("definitely-not-a-real-toolkit") + ); +} + +#[test] +fn curated_catalogs_carry_no_duplicate_slugs() { + // `find_curated` returns the first match, so a duplicate with a different + // scope would make the gate's answer depend on table order. + for toolkit in CAPABILITY_TOOLKITS { + let catalog = catalog_for_toolkit(toolkit).expect("catalog"); + let mut seen: Vec<&str> = catalog.iter().map(|t| t.slug).collect(); + seen.sort_unstable(); + let before = seen.len(); + seen.dedup(); + assert_eq!(before, seen.len(), "{toolkit} catalog has duplicate slugs"); + } +} diff --git a/crates/tinymemory-bus/src/composio/catalogs/notion.rs b/crates/tinymemory-bus/src/composio/catalogs/notion.rs new file mode 100644 index 00000000..d2a64f0d --- /dev/null +++ b/crates/tinymemory-bus/src/composio/catalogs/notion.rs @@ -0,0 +1,197 @@ +//! Curated catalog of Notion Composio actions exposed to the agent. + +use crate::composio::scopes::{CuratedTool, ToolScope}; + +/// The curated action catalog for the `notion` toolkit. +pub const NOTION_CURATED: &[CuratedTool] = &[ + // ── Read: search & fetch ──────────────────────────────────────── + CuratedTool { + slug: "NOTION_SEARCH_NOTION_PAGE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_DATA", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_DATABASE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_ROW", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_BLOCK_METADATA", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_BLOCK_CONTENTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_ALL_BLOCK_CONTENTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_FETCH_COMMENTS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_GET_PAGE_MARKDOWN", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_GET_PAGE_PROPERTY_ACTION", + scope: ToolScope::Read, + }, + // ── Read: query & retrieve ────────────────────────────────────── + CuratedTool { + slug: "NOTION_QUERY_DATABASE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_QUERY_DATABASE_WITH_FILTER", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_QUERY_DATA_SOURCE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_RETRIEVE_PAGE", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_RETRIEVE_COMMENT", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_RETRIEVE_DATABASE_PROPERTY", + scope: ToolScope::Read, + }, + // ── Read: profile / users / files ─────────────────────────────── + CuratedTool { + slug: "NOTION_LIST_USERS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_GET_ABOUT_USER", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_GET_ABOUT_ME", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_LIST_FILE_UPLOADS", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_RETRIEVE_FILE_UPLOAD", + scope: ToolScope::Read, + }, + CuratedTool { + slug: "NOTION_LIST_DATA_SOURCE_TEMPLATES", + scope: ToolScope::Read, + }, + // ── Write: create ─────────────────────────────────────────────── + CuratedTool { + slug: "NOTION_CREATE_NOTION_PAGE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_CREATE_DATABASE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_CREATE_COMMENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_CREATE_FILE_UPLOAD", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_SEND_FILE_UPLOAD", + scope: ToolScope::Write, + }, + // ── Write: update / append ────────────────────────────────────── + CuratedTool { + slug: "NOTION_UPDATE_PAGE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_UPDATE_BLOCK", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_UPDATE_ROW_DATABASE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_INSERT_ROW_DATABASE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_INSERT_ROW_FROM_NL", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_REPLACE_PAGE_CONTENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_ADD_PAGE_CONTENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_ADD_MULTIPLE_PAGE_CONTENT", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_BLOCK_CHILDREN", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_TEXT_BLOCKS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_TASK_BLOCKS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_CODE_BLOCKS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_MEDIA_BLOCKS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_LAYOUT_BLOCKS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_APPEND_TABLE_BLOCKS", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_DUPLICATE_PAGE", + scope: ToolScope::Write, + }, + CuratedTool { + slug: "NOTION_MOVE_PAGE", + scope: ToolScope::Write, + }, + // ── Admin: destructive ────────────────────────────────────────── + CuratedTool { + slug: "NOTION_DELETE_BLOCK", + scope: ToolScope::Admin, + }, + CuratedTool { + slug: "NOTION_ARCHIVE_NOTION_PAGE", + scope: ToolScope::Admin, + }, +]; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity.rs b/crates/tinymemory-bus/src/composio/catalogs/productivity.rs similarity index 96% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity.rs rename to crates/tinymemory-bus/src/composio/catalogs/productivity.rs index 9e819b9e..9e4df67a 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity.rs +++ b/crates/tinymemory-bus/src/composio/catalogs/productivity.rs @@ -2,15 +2,16 @@ //! Trello, Asana, Dropbox, Todoist. //! //! Catalog-only toolkits (Linear, Jira, Trello, Asana, Dropbox, -//! Todoist) don't ship a native [`super::ComposioProvider`] — they +//! Todoist) don't ship a native `ComposioProvider` (in `tinymemory-core`) — they //! have no user-profile fetch, no initial/periodic sync, no trigger //! webhooks, and no memory ingestion. The agent invokes their actions //! through Composio's API, but their data is not pre-ingested into //! OpenHuman's memory tree. -use super::tool_scope::{CuratedTool, ToolScope}; +use crate::composio::scopes::{CuratedTool, ToolScope}; // ── outlook ───────────────────────────────────────────────────────── +/// The curated action catalog for the `outlook` toolkit. pub const OUTLOOK_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "OUTLOOK_GET_MESSAGE", @@ -115,6 +116,7 @@ pub const OUTLOOK_CURATED: &[CuratedTool] = &[ // matches how `gmail` / `notion` / `clickup` are wired. // ── jira ──────────────────────────────────────────────────────────── +/// The curated action catalog for the `jira` toolkit. pub const JIRA_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "JIRA_GET_ISSUE", @@ -215,6 +217,7 @@ pub const JIRA_CURATED: &[CuratedTool] = &[ ]; // ── trello ────────────────────────────────────────────────────────── +/// The curated action catalog for the `trello` toolkit. pub const TRELLO_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "TRELLO_GET_BOARDS_BY_ID_BOARD", @@ -307,6 +310,7 @@ pub const TRELLO_CURATED: &[CuratedTool] = &[ ]; // ── asana ─────────────────────────────────────────────────────────── +/// The curated action catalog for the `asana` toolkit. pub const ASANA_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "ASANA_GET_A_TASK", @@ -411,6 +415,7 @@ pub const ASANA_CURATED: &[CuratedTool] = &[ ]; // ── dropbox ───────────────────────────────────────────────────────── +/// The curated action catalog for the `dropbox` toolkit. pub const DROPBOX_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "DROPBOX_GET_METADATA", @@ -479,6 +484,7 @@ pub const DROPBOX_CURATED: &[CuratedTool] = &[ ]; // ── todoist ───────────────────────────────────────────────────────── +/// The curated action catalog for the `todoist` toolkit. pub const TODOIST_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "TODOIST_GET_TASK", @@ -576,5 +582,5 @@ pub const TODOIST_CURATED: &[CuratedTool] = &[ ]; #[cfg(test)] -#[path = "catalogs_productivity_tests.rs"] +#[path = "productivity_tests.rs"] mod tests; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity_tests.rs b/crates/tinymemory-bus/src/composio/catalogs/productivity_tests.rs similarity index 100% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_productivity_tests.rs rename to crates/tinymemory-bus/src/composio/catalogs/productivity_tests.rs diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_social_media.rs b/crates/tinymemory-bus/src/composio/catalogs/social_media.rs similarity index 96% rename from crates/tinymemory-core/src/sync/composio/providers/catalogs_social_media.rs rename to crates/tinymemory-bus/src/composio/catalogs/social_media.rs index a05c2b40..98d41658 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs_social_media.rs +++ b/crates/tinymemory-bus/src/composio/catalogs/social_media.rs @@ -1,9 +1,10 @@ //! Curated catalogs — social media / entertainment toolkits: Twitter, -//! Spotify, YouTube. +//! Spotify, `YouTube`. -use super::tool_scope::{CuratedTool, ToolScope}; +use crate::composio::scopes::{CuratedTool, ToolScope}; // ── twitter ───────────────────────────────────────────────────────── +/// The curated action catalog for the `twitter` toolkit. pub const TWITTER_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "TWITTER_RECENT_SEARCH", @@ -92,6 +93,7 @@ pub const TWITTER_CURATED: &[CuratedTool] = &[ ]; // ── spotify ───────────────────────────────────────────────────────── +/// The curated action catalog for the `spotify` toolkit. pub const SPOTIFY_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "SPOTIFY_GET_CURRENT_USER_S_PROFILE", @@ -168,6 +170,7 @@ pub const SPOTIFY_CURATED: &[CuratedTool] = &[ ]; // ── youtube ───────────────────────────────────────────────────────── +/// The curated action catalog for the `youtube` toolkit. pub const YOUTUBE_CURATED: &[CuratedTool] = &[ CuratedTool { slug: "YOUTUBE_SEARCH_YOU_TUBE", diff --git a/crates/tinymemory-bus/src/composio/mod.rs b/crates/tinymemory-bus/src/composio/mod.rs index 760c8824..84602f92 100644 --- a/crates/tinymemory-bus/src/composio/mod.rs +++ b/crates/tinymemory-bus/src/composio/mod.rs @@ -44,11 +44,14 @@ //! - **`profile_md`** — rewrites managed blocks in the host's `PROFILE.md`. //! Filesystem mutation against a host-owned file; it is host policy that //! happens to be written in the memory stack, not a wire type. -//! - **the curated catalogs and the provider registry** — several thousand -//! `&'static str` action slugs and a process-global `HashMap` of trait -//! objects. The [`scopes::CuratedTool`] *shape* is here so a catalog can be -//! typed; the catalogs are the engine's. +//! - **the provider registry** — a process-global `HashMap` of trait objects +//! that reach `reqwest` and the chunk store. +//! +//! The curated catalogs were on that list and are **not** any more: they are +//! several thousand `&'static str` action slugs with no dependency at all, and +//! the host is their heaviest reader. See [`catalogs`] for why they moved. +pub mod catalogs; pub mod profile; pub mod runs; pub mod scopes; @@ -69,3 +72,9 @@ pub use state::{ STATE_NAMESPACE, }; pub use tasks::{GithubFetchMode, NormalizedTask, TaskContainer, TaskFetchFilter, TaskKind}; + +pub use catalogs::{ + catalog_for_toolkit, curated_scope_for, has_native_provider, is_action_visible_with_pref, + native_provider_sync_interval_secs, parse_sync_interval_override, sync_interval_env_var, + toolkit_description, toolkit_has_scope, CAPABILITY_TOOLKITS, NATIVE_PROVIDERS, +}; diff --git a/crates/tinymemory-bus/src/composio/scopes.rs b/crates/tinymemory-bus/src/composio/scopes.rs index 09714e9d..1a8a7e85 100644 --- a/crates/tinymemory-bus/src/composio/scopes.rs +++ b/crates/tinymemory-bus/src/composio/scopes.rs @@ -7,7 +7,7 @@ //! toolkit: reads and writes on by default, destructive and permission-changing //! actions off until explicitly opted into. //! -//! # Why the classification is here and the catalogs are not +//! # Why the classification (and now the catalogs) live here //! //! Two different consumers ask the same question from opposite sides of the //! module boundary. The host asks it when it renders the integrations panel and @@ -18,9 +18,13 @@ //! side's private policy. //! //! The catalogs themselves — thousands of `&'static str` action slugs across -//! thirty toolkits — stay in the engine crate. They are provider data, they -//! change whenever a provider does, and nothing about them has to cross a -//! frame: what crosses is the verdict. +//! thirty toolkits — live alongside this module, in [`super::catalogs`] +//! (moved here from the engine crate by OpenHuman#5560, for the same reason +//! the classification lives here: both sides of the module boundary need the +//! same answer, and a contract-crate lookup is the only way to get it without +//! either side linking the other). They are provider data, they change +//! whenever a provider does, and nothing about them has to cross a frame: what +//! crosses is the verdict. //! //! Reading and writing a preference is likewise the engine crate's; this module //! defines what a preference *is*, not where it is stored. diff --git a/crates/tinymemory-bus/src/provider/retrieval.rs b/crates/tinymemory-bus/src/provider/retrieval.rs index 31b86b00..950c1dd1 100644 --- a/crates/tinymemory-bus/src/provider/retrieval.rs +++ b/crates/tinymemory-bus/src/provider/retrieval.rs @@ -128,6 +128,11 @@ pub struct RetrievalResponse { } /// Options for `MemoryRetrieval::fast_retrieve`. +/// +/// [`Default`] carries the engine's own defaults — a limit of 10 and 2 graph +/// hops, the values `tinycortex`'s `FastRetrieveOptions::default` has always +/// used. It is here so a caller migrating off that type does not have to +/// re-spell them, which is how the two would drift (OpenHuman#5560). #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct FastRetrieveQuery { /// Maximum hits to return. @@ -139,6 +144,16 @@ pub struct FastRetrieveQuery { pub time_window_days: Option, } +impl Default for FastRetrieveQuery { + fn default() -> Self { + Self { + limit: 10, + max_hops: 2, + time_window_days: None, + } + } +} + /// A time window to cover. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct CoverWindowQuery { diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs.rs index dfd0d817..a8461ad8 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/catalogs.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/catalogs.rs @@ -1,38 +1,29 @@ -//! Curated catalogs for Composio toolkits that don't (yet) have a -//! native [`super::ComposioProvider`] implementation. +//! The curated catalogs, re-exported at their historical path. //! -//! These slices are consulted by [`super::catalog_for_toolkit`] alongside -//! provider-supplied catalogs (gmail, notion, github), so the meta-tool -//! layer applies the same whitelist + scope filtering. +//! The tables themselves moved to [`tinymemory_api::composio::catalogs`] +//! (OpenHuman#5560): they are `&'static str` slugs with no dependency, and the +//! *host* is their heaviest reader — it filters the agent's visible tool list +//! and renders the unlock hints. While they lived here, every one of those +//! reads was a compile-time link to this crate. //! -//! Slugs sourced from `https://docs.composio.dev/toolkits/.md` — -//! best-effort. Slugs that don't exist on the backend simply never -//! appear in `composio_list_tools`, so extras are harmless. -//! -//! Data is split into category submodules: -//! - `catalogs_messaging` — Slack, Discord, Telegram, WhatsApp, MS Teams -//! - `catalogs_google` — GoogleCalendar, GoogleDrive, GoogleDocs, GoogleSheets -//! - `catalogs_microsoft` — OneDrive, Excel -//! - `catalogs_productivity` — Outlook, Linear, Jira, Trello, Asana, Dropbox, Todoist -//! - `catalogs_social_media` — Twitter, Spotify, YouTube -//! - `catalogs_business` — Shopify, Stripe, HubSpot, Salesforce, Airtable, Figma +//! Nothing about the data changed. This module keeps +//! `providers::catalogs::SLACK_CURATED` and its siblings resolving for the +//! provider impls beside it. -pub use super::catalogs_business::{ +pub use tinymemory_api::composio::catalogs::business::{ AIRTABLE_CURATED, FIGMA_CURATED, HUBSPOT_CURATED, SALESFORCE_CURATED, SHOPIFY_CURATED, STRIPE_CURATED, }; -pub use super::catalogs_google::{ +pub use tinymemory_api::composio::catalogs::google::{ GOOGLECALENDAR_CURATED, GOOGLEDOCS_CURATED, GOOGLEDRIVE_CURATED, GOOGLESHEETS_CURATED, }; -pub use super::catalogs_messaging::{ +pub use tinymemory_api::composio::catalogs::messaging::{ DISCORD_CURATED, MICROSOFT_TEAMS_CURATED, SLACK_CURATED, TELEGRAM_CURATED, WHATSAPP_CURATED, }; -pub use super::catalogs_microsoft::{EXCEL_CURATED, ONE_DRIVE_CURATED}; -pub use super::catalogs_productivity::{ +pub use tinymemory_api::composio::catalogs::microsoft::{EXCEL_CURATED, ONE_DRIVE_CURATED}; +pub use tinymemory_api::composio::catalogs::productivity::{ ASANA_CURATED, DROPBOX_CURATED, JIRA_CURATED, OUTLOOK_CURATED, TODOIST_CURATED, TRELLO_CURATED, }; -// `LINEAR_CURATED` moved into `super::linear::LINEAR_CURATED` alongside -// the native LinearProvider impl. `catalog_for_toolkit("linear")` now -// routes there directly. Removing the re-export keeps a single source -// of truth and matches how `gmail` / `notion` / `clickup` are wired. -pub use super::catalogs_social_media::{SPOTIFY_CURATED, TWITTER_CURATED, YOUTUBE_CURATED}; +pub use tinymemory_api::composio::catalogs::social_media::{ + SPOTIFY_CURATED, TWITTER_CURATED, YOUTUBE_CURATED, +}; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_compat.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_compat.rs new file mode 100644 index 00000000..ca3db4ec --- /dev/null +++ b/crates/tinymemory-core/src/sync/composio/providers/catalogs_compat.rs @@ -0,0 +1,59 @@ +//! Historical per-category catalog module paths. +//! +//! Before OpenHuman#5560 moved the curated catalogs into the contract crate, +//! each category lived in its own `pub mod catalogs_` here (e.g. +//! `providers::catalogs_business::SHOPIFY_CURATED`). The move consolidated +//! them into [`super::catalogs`], which flattens every constant to one level +//! (`providers::catalogs::SHOPIFY_CURATED`) rather than nesting them by +//! category — so the six original module names stopped resolving even though +//! [`super::catalogs`] kept every constant reachable under a different path. +//! +//! `AGENTS.md`'s SemVer policy treats a removed public path as a breaking +//! change unless the crate takes a major (pre-1.0: minor) bump for it. Rather +//! than force that bump for a rename, these six modules re-export the same +//! constants under their historical names — pure re-exports, no behavior, no +//! new dependency. +//! +//! # Deletion +//! +//! This module is a deprecation shim, not a permanent home. It may be deleted +//! in the next minor version bump that is *already* taking other breaking +//! changes (so the cost is paid once), or once nothing in this workspace or a +//! known downstream consumer (the OpenHuman host) still names a +//! `catalogs_` path — check with +//! `grep -rn 'catalogs_business\|catalogs_google\|catalogs_messaging\|catalogs_microsoft\|catalogs_productivity\|catalogs_social_media'` +//! across both repositories before removing it. + +pub mod catalogs_business { + //! Historical compat shim — see the module docs above. + pub use tinymemory_api::composio::catalogs::business::*; +} + +pub mod catalogs_google { + //! Historical compat shim — see the module docs above. + pub use tinymemory_api::composio::catalogs::google::*; +} + +pub mod catalogs_messaging { + //! Historical compat shim — see the module docs above. + pub use tinymemory_api::composio::catalogs::messaging::*; +} + +pub mod catalogs_microsoft { + //! Historical compat shim — see the module docs above. + pub use tinymemory_api::composio::catalogs::microsoft::*; +} + +pub mod catalogs_productivity { + //! Historical compat shim — see the module docs above. + pub use tinymemory_api::composio::catalogs::productivity::*; +} + +pub mod catalogs_social_media { + //! Historical compat shim — see the module docs above. + pub use tinymemory_api::composio::catalogs::social_media::*; +} + +#[cfg(test)] +#[path = "catalogs_compat_tests.rs"] +mod tests; diff --git a/crates/tinymemory-core/src/sync/composio/providers/catalogs_compat_tests.rs b/crates/tinymemory-core/src/sync/composio/providers/catalogs_compat_tests.rs new file mode 100644 index 00000000..9091002d --- /dev/null +++ b/crates/tinymemory-core/src/sync/composio/providers/catalogs_compat_tests.rs @@ -0,0 +1,31 @@ +//! Tests for the surrounding module. + +use super::*; + +#[test] +fn historical_module_paths_resolve_to_the_same_constants_as_the_new_path() { + assert_eq!( + catalogs_business::SHOPIFY_CURATED.len(), + crate::sync::composio::providers::catalogs::SHOPIFY_CURATED.len() + ); + assert_eq!( + catalogs_google::GOOGLEDRIVE_CURATED.len(), + crate::sync::composio::providers::catalogs::GOOGLEDRIVE_CURATED.len() + ); + assert_eq!( + catalogs_messaging::SLACK_CURATED.len(), + crate::sync::composio::providers::catalogs::SLACK_CURATED.len() + ); + assert_eq!( + catalogs_microsoft::EXCEL_CURATED.len(), + crate::sync::composio::providers::catalogs::EXCEL_CURATED.len() + ); + assert_eq!( + catalogs_productivity::JIRA_CURATED.len(), + crate::sync::composio::providers::catalogs::JIRA_CURATED.len() + ); + assert_eq!( + catalogs_social_media::TWITTER_CURATED.len(), + crate::sync::composio::providers::catalogs::TWITTER_CURATED.len() + ); +} diff --git a/crates/tinymemory-core/src/sync/composio/providers/clickup/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/clickup/tools.rs index d0c89fe4..b52c6dd7 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/clickup/tools.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/clickup/tools.rs @@ -1,124 +1,6 @@ -//! Curated catalog of ClickUp Composio actions exposed to the agent. +//! The curated `clickup` catalog, re-exported at its historical path. //! -//! Slugs match Composio's naming convention (`_`) for -//! the ClickUp REST surface. See -//! for the canonical action list; the entries here are the read-oriented -//! subset the periodic Memory Tree sync relies on, plus the most common -//! task-write surface the agent already uses through generic tool-calling. +//! The table moved to [`tinymemory_api::composio::catalogs::clickup`] with every +//! other catalog — see [`super::super::catalogs`] for why. -use crate::sync::composio::providers::tool_scope::{CuratedTool, ToolScope}; - -pub const CLICKUP_CURATED: &[CuratedTool] = &[ - // ── Read: identity ───────────────────────────────────────────── - CuratedTool { - slug: "CLICKUP_GET_AUTHORIZED_USER", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_AUTHORIZED_TEAMS_WORKSPACES", - scope: ToolScope::Read, - }, - // ── Read: structure (workspace → space → folder → list) ────── - CuratedTool { - slug: "CLICKUP_GET_SPACES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_FOLDERS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_LISTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_FOLDERLESS_LISTS", - scope: ToolScope::Read, - }, - // ── Read: tasks (the main memory ingest surface) ────────────── - CuratedTool { - slug: "CLICKUP_GET_FILTERED_TEAM_TASKS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_TASKS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_TASK", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_TASK_COMMENTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_LIST_COMMENTS", - scope: ToolScope::Read, - }, - // ── Read: docs / views / time tracking ──────────────────────── - CuratedTool { - slug: "CLICKUP_SEARCH_DOCS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_DOC_PAGES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_VIEW_TASKS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_TIME_ENTRIES_WITHIN_A_DATE_RANGE", - scope: ToolScope::Read, - }, - // ── Read: members ───────────────────────────────────────────── - CuratedTool { - slug: "CLICKUP_GET_WORKSPACE_MEMBERS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "CLICKUP_GET_TASK_MEMBERS", - scope: ToolScope::Read, - }, - // ── Write: create / update tasks ────────────────────────────── - CuratedTool { - slug: "CLICKUP_CREATE_TASK", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "CLICKUP_UPDATE_TASK", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "CLICKUP_CREATE_TASK_COMMENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "CLICKUP_UPDATE_COMMENT", - scope: ToolScope::Write, - }, - // ── Write: structure ────────────────────────────────────────── - CuratedTool { - slug: "CLICKUP_CREATE_LIST", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "CLICKUP_UPDATE_LIST", - scope: ToolScope::Write, - }, - // ── Admin: destructive ──────────────────────────────────────── - CuratedTool { - slug: "CLICKUP_DELETE_TASK", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "CLICKUP_DELETE_COMMENT", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "CLICKUP_DELETE_LIST", - scope: ToolScope::Admin, - }, -]; +pub use tinymemory_api::composio::catalogs::clickup::CLICKUP_CURATED; diff --git a/crates/tinymemory-core/src/sync/composio/providers/descriptions.rs b/crates/tinymemory-core/src/sync/composio/providers/descriptions.rs index 09468447..b617a833 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/descriptions.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/descriptions.rs @@ -1,61 +1,6 @@ -//! Human-readable capability summaries for Composio toolkit slugs. +//! Human-readable capability summaries, re-exported at their historical path. +//! +//! Moved to [`tinymemory_api::composio::catalogs::descriptions`] with the +//! catalogs — see [`super::catalogs`] for why. -/// Human-readable capability summary for a Composio toolkit slug. -/// -/// Used by the prompt renderer to tell the orchestrator what each connected -/// integration can do. Covers the most common toolkits; unknown slugs get -/// a generic fallback so newly connected services still appear. -pub fn toolkit_description(slug: &str) -> &'static str { - match slug { - "gmail" => { - "Send, read, draft, reply, forward, and search emails; manage labels and threads" - } - "notion" => "Create, read, update, and search notion pages and notion databases", - "github" => { - "Manage repositories, issues, and pull requests on GitHub; sync \ - assigned issues into Memory Tree" - } - "slack" => "Send messages, read channels, manage threads, and post updates in Slack", - "discord" => "Send messages, manage channels, and interact with Discord servers", - "google_calendar" => "Create, update, and query calendar events; check availability", - "google_drive" => "Upload, download, search, and share files in Google Drive", - "google_docs" => "Create, read, and edit Google Docs documents", - "google_sheets" => "Read, write, and manage Google Sheets spreadsheets", - "outlook" => "Send, read, and manage emails in Microsoft Outlook", - "microsoft_teams" => "Send messages and manage channels in Microsoft Teams", - "larksuite" => { - "Connect Lark / Feishu workspace chat, docs, wiki, and meetings via Composio" - } - "linear" => { - "Create, read, and manage issues, projects, and cycles in Linear; sync \ - assigned issues into Memory Tree" - } - "jira" => "Create and manage issues, projects, and sprints in Jira", - "trello" => "Create and manage cards, lists, and boards in Trello", - "asana" => "Create and manage tasks, projects, and sections in Asana", - "clickup" => { - "Create, read, and manage tasks, lists, and docs in ClickUp; sync \ - assigned tasks into Memory Tree" - } - "dropbox" => "Upload, download, and share files in Dropbox", - "twitter" => "Post tweets, read timelines, and manage Twitter interactions", - "spotify" => "Control playback, search music, and manage playlists on Spotify", - "telegram" => "Send and receive messages via Telegram", - "whatsapp" => "Send and receive messages via WhatsApp", - "twilio" => "Send SMS, make calls, and manage communications via Twilio", - "shopify" => "Manage products, orders, and customers in Shopify", - "stripe" => "Manage payments, subscriptions, and customers in Stripe", - "hubspot" => "Manage contacts, deals, and marketing in HubSpot", - "salesforce" => "Manage contacts, leads, and opportunities in Salesforce", - "airtable" => "Read and write records in Airtable bases", - "figma" => "Access and manage Figma design files and components", - "youtube" => "Search videos, manage playlists, and interact with YouTube", - "calendar" => "Create, update, and query calendar events", - "one_drive" | "onedrive" => { - "Upload, download, search, and share files in Microsoft OneDrive" - } - "excel" => "Read, write, and manage workbooks, worksheets, and tables in Microsoft Excel", - "todoist" => "Create and manage tasks, projects, sections, and labels in Todoist", - _ => "Interact with this connected service via its available actions", - } -} +pub use tinymemory_api::composio::catalogs::descriptions::toolkit_description; diff --git a/crates/tinymemory-core/src/sync/composio/providers/github/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/github/tools.rs index 40be9137..cada1872 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/github/tools.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/github/tools.rs @@ -1,189 +1,6 @@ -//! Curated catalog of GitHub Composio actions exposed to the agent. +//! The curated `github` catalog, re-exported at its historical path. //! -//! Composio publishes hundreds of GitHub actions; this hand-tuned slice -//! covers the day-to-day operations an AI assistant actually performs -//! (browsing repos, reading/writing issues + PRs, code search, basic -//! workflow control) and hides the long tail of admin endpoints. +//! The table moved to [`tinymemory_api::composio::catalogs::github`] with every +//! other catalog — see [`super::super::catalogs`] for why. -use crate::sync::composio::providers::tool_scope::{CuratedTool, ToolScope}; - -pub const GITHUB_CURATED: &[CuratedTool] = &[ - // ── Read: user / repos ────────────────────────────────────────── - CuratedTool { - slug: "GITHUB_GET_THE_AUTHENTICATED_USER", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_LIST_REPOSITORIES_FOR_THE_AUTHENTICATED_USER", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_GET_A_REPOSITORY", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_LIST_REPOSITORY_COLLABORATORS", - scope: ToolScope::Read, - }, - // ── Read: search ──────────────────────────────────────────────── - CuratedTool { - slug: "GITHUB_SEARCH_REPOSITORIES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_SEARCH_CODE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_SEARCH_ISSUES_AND_PULL_REQUESTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_SEARCH_USERS", - scope: ToolScope::Read, - }, - // ── Read: issues ──────────────────────────────────────────────── - CuratedTool { - slug: "GITHUB_LIST_REPOSITORY_ISSUES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_GET_AN_ISSUE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_LIST_ISSUE_COMMENTS", - scope: ToolScope::Read, - }, - // ── Read: pull requests ───────────────────────────────────────── - CuratedTool { - slug: "GITHUB_LIST_PULL_REQUESTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_GET_A_PULL_REQUEST", - scope: ToolScope::Read, - }, - // CuratedTool { slug: "GITHUB_CHECK_IF_PULL_REQUEST_HAS_BEEN_MERGED", scope: ToolScope::Read }, - // ── Read: branches / commits ──────────────────────────────────── - CuratedTool { - slug: "GITHUB_LIST_BRANCHES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_GET_A_BRANCH", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_LIST_COMMITS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GITHUB_GET_A_COMMIT", - scope: ToolScope::Read, - }, - // CuratedTool { slug: "GITHUB_COMPARE_TWO_COMMITS", scope: ToolScope::Read }, - // // ── Read: contents / releases / gists ─────────────────────────── - // CuratedTool { slug: "GITHUB_GET_REPOSITORY_CONTENTS", scope: ToolScope::Read }, - // CuratedTool { slug: "GITHUB_LIST_RELEASES", scope: ToolScope::Read }, - // CuratedTool { slug: "GITHUB_LIST_GISTS", scope: ToolScope::Read }, - // // ── Read: workflows ───────────────────────────────────────────── - // CuratedTool { slug: "GITHUB_LIST_WORKFLOWS", scope: ToolScope::Read }, - // CuratedTool { slug: "GITHUB_LIST_WORKFLOW_RUNS", scope: ToolScope::Read }, - // ── Write: repos / contents ───────────────────────────────────── - CuratedTool { - slug: "GITHUB_CREATE_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_CREATE_OR_UPDATE_FILE_CONTENTS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_CREATE_A_COMMIT", - scope: ToolScope::Write, - }, - // GITHUB_COMMIT_MULTIPLE_FILES removed from Composio catalog - CuratedTool { - slug: "GITHUB_CREATE_A_COMMIT_COMMENT", - scope: ToolScope::Write, - }, - // ── Write: issues ─────────────────────────────────────────────── - CuratedTool { - slug: "GITHUB_CREATE_AN_ISSUE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_UPDATE_AN_ISSUE", - scope: ToolScope::Write, - }, - // GITHUB_CLOSE_AN_ISSUE — removed: no dedicated Composio slug. - // Use GITHUB_UPDATE_AN_ISSUE with state:"closed" instead. - CuratedTool { - slug: "GITHUB_CREATE_AN_ISSUE_COMMENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_ADD_LABELS_TO_AN_ISSUE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_ADD_ASSIGNEES_TO_AN_ISSUE", - scope: ToolScope::Write, - }, - // ── Write: pull requests ──────────────────────────────────────── - CuratedTool { - slug: "GITHUB_CREATE_A_PULL_REQUEST", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_UPDATE_A_PULL_REQUEST", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_MERGE_A_PULL_REQUEST", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_CREATE_A_REVIEW_FOR_A_PULL_REQUEST", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GITHUB_CREATE_A_REVIEW_COMMENT_FOR_A_PULL_REQUEST", - scope: ToolScope::Write, - }, - // // ── Write: releases / gists / workflows ───────────────────────── - // CuratedTool { slug: "GITHUB_CREATE_A_RELEASE", scope: ToolScope::Write }, - CuratedTool { - slug: "GITHUB_CREATE_A_GIST", - scope: ToolScope::Write, - }, - // CuratedTool { slug: "GITHUB_CREATE_WORKFLOW_DISPATCH", scope: ToolScope::Write }, - // ── Admin: destructive / permission-changing ──────────────────── - CuratedTool { - slug: "GITHUB_DELETE_A_REPOSITORY", - scope: ToolScope::Admin, - }, - // DELETE_A_REFERENCE maps to DELETE /repos/{owner}/{repo}/git/refs/{ref}. - // The ref must be a full path (e.g. `refs/heads/branch-name` or - // `refs/tags/v1.0`) — passing a bare branch name deletes nothing (404). - // This replaces the old GITHUB_DELETE_A_BRANCH slug (Composio v3 rename); - // it is broader — it can delete tags too — so agents should always specify - // a `refs/heads/` prefix when the intent is branch deletion. - CuratedTool { - slug: "GITHUB_DELETE_A_REFERENCE", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GITHUB_DELETE_A_FILE", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GITHUB_ADD_A_REPOSITORY_COLLABORATOR", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GITHUB_CANCEL_A_WORKFLOW_RUN", - scope: ToolScope::Admin, - }, -]; +pub use tinymemory_api::composio::catalogs::github::GITHUB_CURATED; diff --git a/crates/tinymemory-core/src/sync/composio/providers/gmail/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/gmail/tools.rs index ac14e106..477161ba 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/gmail/tools.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/gmail/tools.rs @@ -1,145 +1,6 @@ -//! Curated catalog of Gmail Composio actions exposed to the agent. +//! The curated `gmail` catalog, re-exported at its historical path. //! -//! Composio publishes 60+ Gmail actions; this hand-tuned slice covers -//! the cases the agent actually plans for (read, compose, manage) and -//! hides the long tail of edge-case admin endpoints. +//! The table moved to [`tinymemory_api::composio::catalogs::gmail`] with every +//! other catalog — see [`super::super::catalogs`] for why. -use crate::sync::composio::providers::tool_scope::{CuratedTool, ToolScope}; - -pub const GMAIL_CURATED: &[CuratedTool] = &[ - // ── Read: messages & threads ──────────────────────────────────── - CuratedTool { - slug: "GMAIL_FETCH_EMAILS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_LIST_MESSAGES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_FETCH_MESSAGE_BY_THREAD_ID", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_LIST_THREADS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_GET_ATTACHMENT", - scope: ToolScope::Read, - }, - // ── Read: profile & settings ──────────────────────────────────── - CuratedTool { - slug: "GMAIL_GET_PROFILE", - scope: ToolScope::Read, - }, - // CuratedTool { slug: "GMAIL_GET_LANGUAGE_SETTINGS", scope: ToolScope::Read }, - // CuratedTool { slug: "GMAIL_GET_VACATION_SETTINGS", scope: ToolScope::Read }, - // CuratedTool { slug: "GMAIL_GET_AUTO_FORWARDING", scope: ToolScope::Read }, - // ── Read: contacts & people ───────────────────────────────────── - CuratedTool { - slug: "GMAIL_GET_CONTACTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_GET_PEOPLE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_SEARCH_PEOPLE", - scope: ToolScope::Read, - }, - // ── Read: drafts & labels ─────────────────────────────────────── - CuratedTool { - slug: "GMAIL_LIST_DRAFTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_GET_DRAFT", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_LIST_LABELS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "GMAIL_GET_LABEL", - scope: ToolScope::Read, - }, - // ── Write: send & compose ─────────────────────────────────────── - CuratedTool { - slug: "GMAIL_SEND_EMAIL", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GMAIL_REPLY_TO_THREAD", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GMAIL_FORWARD_MESSAGE", - scope: ToolScope::Write, - }, - // ── Write: drafts ─────────────────────────────────────────────── - CuratedTool { - slug: "GMAIL_CREATE_EMAIL_DRAFT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GMAIL_UPDATE_DRAFT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "GMAIL_SEND_DRAFT", - scope: ToolScope::Write, - }, - // ── Write: labels (create/update on user labels) ──────────────── - // CuratedTool { slug: "GMAIL_CREATE_LABEL", scope: ToolScope::Write }, - // CuratedTool { slug: "GMAIL_UPDATE_LABEL", scope: ToolScope::Write }, - // CuratedTool { slug: "GMAIL_PATCH_LABEL", scope: ToolScope::Write }, - CuratedTool { - slug: "GMAIL_ADD_LABEL_TO_EMAIL", - scope: ToolScope::Write, - }, - // ── Admin: destructive & permission-changing ──────────────────── - CuratedTool { - slug: "GMAIL_DELETE_MESSAGE", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GMAIL_BATCH_DELETE_MESSAGES", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GMAIL_MOVE_TO_TRASH", - scope: ToolScope::Admin, - }, - // CuratedTool { slug: "GMAIL_UNTRASH_MESSAGE", scope: ToolScope::Admin }, - CuratedTool { - slug: "GMAIL_DELETE_THREAD", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GMAIL_MOVE_THREAD_TO_TRASH", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GMAIL_UNTRASH_THREAD", - scope: ToolScope::Admin, - }, - // CuratedTool { slug: "GMAIL_MODIFY_THREAD_LABELS", scope: ToolScope::Admin }, - // CuratedTool { slug: "GMAIL_BATCH_MODIFY_MESSAGES", scope: ToolScope::Admin }, - CuratedTool { - slug: "GMAIL_DELETE_DRAFT", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "GMAIL_DELETE_LABEL", - scope: ToolScope::Admin, - }, - // CuratedTool { slug: "GMAIL_PATCH_SEND_AS", scope: ToolScope::Admin }, - // CuratedTool { slug: "GMAIL_UPDATE_IMAP_SETTINGS", scope: ToolScope::Admin }, -]; +pub use tinymemory_api::composio::catalogs::gmail::GMAIL_CURATED; diff --git a/crates/tinymemory-core/src/sync/composio/providers/linear/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/linear/tools.rs index 9eb61b5f..610bac00 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/linear/tools.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/linear/tools.rs @@ -1,90 +1,6 @@ -//! Curated catalog of Linear Composio actions. +//! The curated `linear` catalog, re-exported at its historical path. +//! +//! The table moved to [`tinymemory_api::composio::catalogs::linear`] with every +//! other catalog — see [`super::super::catalogs`] for why. -use crate::sync::composio::providers::tool_scope::{CuratedTool, ToolScope}; - -pub const LINEAR_CURATED: &[CuratedTool] = &[ - CuratedTool { - slug: "LINEAR_LIST_LINEAR_USERS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_LIST_LINEAR_ISSUES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_GET_LINEAR_ISSUE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_SEARCH_ISSUES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_LIST_LINEAR_TEAMS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_LIST_LINEAR_PROJECTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_GET_LINEAR_PROJECT", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_LIST_LINEAR_STATES", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_GET_CYCLES_BY_TEAM_ID", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_LIST_LINEAR_LABELS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "LINEAR_CREATE_LINEAR_ISSUE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_UPDATE_ISSUE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_CREATE_LINEAR_COMMENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_UPDATE_LINEAR_COMMENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_CREATE_ATTACHMENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_CREATE_ISSUE_RELATION", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_CREATE_LINEAR_PROJECT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_UPDATE_LINEAR_PROJECT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_CREATE_LINEAR_LABEL", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "LINEAR_DELETE_LINEAR_ISSUE", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "LINEAR_REMOVE_ISSUE_LABEL", - scope: ToolScope::Admin, - }, -]; +pub use tinymemory_api::composio::catalogs::linear::LINEAR_CURATED; diff --git a/crates/tinymemory-core/src/sync/composio/providers/mod.rs b/crates/tinymemory-core/src/sync/composio/providers/mod.rs index a46b9523..2876da57 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/mod.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/mod.rs @@ -41,12 +41,7 @@ mod types; pub mod user_scopes; pub mod catalogs; -pub mod catalogs_business; -pub mod catalogs_google; -pub mod catalogs_messaging; -pub mod catalogs_microsoft; -pub mod catalogs_productivity; -pub mod catalogs_social_media; +mod catalogs_compat; pub mod clickup; pub mod github; pub mod gmail; @@ -58,174 +53,11 @@ pub mod registry; pub mod slack; pub mod sync_state; -use crate::composio_host::ComposioCapability; - -const CAPABILITY_TOOLKITS: &[&str] = &[ - "gmail", - "notion", - "slack", - "clickup", - "github", - "discord", - "googlecalendar", - "googledrive", - "googledocs", - "googlesheets", - "outlook", - "microsoft_teams", - "linear", - "jira", - "trello", - "asana", - "dropbox", - "twitter", - "spotify", - "telegram", - "whatsapp", - "shopify", - "stripe", - "hubspot", - "salesforce", - "airtable", - "figma", - "youtube", - "one_drive", - "excel", - "todoist", -]; - -fn native_provider_sync_interval(toolkit: &str) -> Option { - match toolkit { - "gmail" => Some(gmail::GmailProvider::new().sync_interval_secs()), - "notion" => Some(notion::NotionProvider::new().sync_interval_secs()), - "slack" => Some(slack::SlackProvider::new().sync_interval_secs()), - "clickup" => Some(clickup::ClickUpProvider::new().sync_interval_secs()), - "github" => Some(github::GitHubProvider::new().sync_interval_secs()), - "linear" => Some(linear::LinearProvider::new().sync_interval_secs()), - _ => None, - } - .flatten() -} - -fn has_native_provider(toolkit: &str) -> bool { - matches!( - toolkit, - "gmail" | "notion" | "slack" | "clickup" | "github" | "linear" - ) -} - -/// Static overview of the Composio integrations supported by this core build. -/// -/// This deliberately does not consult the live Composio backend/direct tenant: -/// it is an observability surface for OpenHuman's own capability tiers. Use -/// `composio_list_toolkits` / `composio_list_connections` when callers need -/// the currently signed-in user's allowlist or OAuth state. -pub fn capability_matrix() -> Vec { - CAPABILITY_TOOLKITS - .iter() - .map(|toolkit| { - let native_provider = has_native_provider(toolkit); - let catalog = catalog_for_toolkit(toolkit); - let sync_interval_secs = native_provider_sync_interval(toolkit); - ComposioCapability { - toolkit: (*toolkit).to_string(), - description: toolkit_description(toolkit).to_string(), - native_provider, - curated_tools: catalog.is_some(), - curated_tool_count: catalog.map_or(0, <[CuratedTool]>::len), - tool_execution: catalog.is_some(), - user_profile: native_provider, - initial_sync: native_provider, - periodic_sync: sync_interval_secs.is_some(), - sync_interval_secs, - trigger_webhooks: native_provider, - memory_ingest: native_provider, - } - }) - .collect() -} - -/// Static toolkit → curated catalog map. -/// -/// This is consulted by the meta-tool layer alongside any registered -/// provider's [`ComposioProvider::curated_tools`]. It lets toolkits -/// without a full native provider still benefit from curated -/// whitelisting. -/// -/// Lookup key is the lowercased prefix returned by -/// [`toolkit_from_slug`] applied to the action slug — e.g. -/// `GOOGLECALENDAR_CREATE_EVENT` → `"googlecalendar"`. Multi-segment -/// prefixes like `MICROSOFT_TEAMS_*` return their known toolkit slug. -/// Synchronous visibility check for a Composio action slug given a -/// pre-loaded user scope preference. -/// -/// Returns `true` if the action should appear in the agent's tool -/// surface — i.e. it's in the toolkit's curated whitelist (or the -/// toolkit has no curation) **and** the user's scope pref allows its -/// classification. Falls back to [`classify_unknown`] for un-curated -/// toolkits. -/// -/// Use this when the user pref has already been loaded for the -/// toolkit (typical inside a `for slug in toolkits {...}` loop where -/// awaiting once per toolkit is cheaper than once per action). -pub fn is_action_visible_with_pref(slug: &str, pref: &UserScopePref) -> bool { - let Some(toolkit) = toolkit_from_slug(slug) else { - return true; - }; - let catalog = get_provider(&toolkit) - .and_then(|p| p.curated_tools()) - .or_else(|| catalog_for_toolkit(&toolkit)); - match catalog { - Some(catalog) => match find_curated(catalog, slug) { - Some(curated) => pref.allows(curated.scope), - None => false, - }, - None => pref.allows(classify_unknown(slug)), - } -} - -pub fn catalog_for_toolkit(toolkit: &str) -> Option<&'static [CuratedTool]> { - match toolkit.trim().to_ascii_lowercase().as_str() { - // Native providers - "gmail" => Some(gmail::GMAIL_CURATED), - "notion" => Some(notion::NOTION_CURATED), - "github" => Some(github::GITHUB_CURATED), - "linear" => Some(linear::LINEAR_CURATED), - // Catalog-only toolkits - "slack" => Some(catalogs::SLACK_CURATED), - "discord" => Some(catalogs::DISCORD_CURATED), - "googlecalendar" | "google_calendar" => Some(catalogs::GOOGLECALENDAR_CURATED), - "googledrive" | "google_drive" => Some(catalogs::GOOGLEDRIVE_CURATED), - "googledocs" | "google_docs" => Some(catalogs::GOOGLEDOCS_CURATED), - "googlesheets" | "google_sheets" => Some(catalogs::GOOGLESHEETS_CURATED), - "outlook" => Some(catalogs::OUTLOOK_CURATED), - // Keep the legacy "microsoft" alias while toolkit_from_slug now - // returns the precise "microsoft_teams" slug for Teams actions. - "microsoft" | "microsoft_teams" => Some(catalogs::MICROSOFT_TEAMS_CURATED), - "jira" => Some(catalogs::JIRA_CURATED), - "trello" => Some(catalogs::TRELLO_CURATED), - "asana" => Some(catalogs::ASANA_CURATED), - "clickup" => Some(clickup::CLICKUP_CURATED), - "dropbox" => Some(catalogs::DROPBOX_CURATED), - "twitter" => Some(catalogs::TWITTER_CURATED), - "spotify" => Some(catalogs::SPOTIFY_CURATED), - "telegram" => Some(catalogs::TELEGRAM_CURATED), - "whatsapp" => Some(catalogs::WHATSAPP_CURATED), - "shopify" => Some(catalogs::SHOPIFY_CURATED), - "stripe" => Some(catalogs::STRIPE_CURATED), - "hubspot" => Some(catalogs::HUBSPOT_CURATED), - "salesforce" => Some(catalogs::SALESFORCE_CURATED), - "airtable" => Some(catalogs::AIRTABLE_CURATED), - "figma" => Some(catalogs::FIGMA_CURATED), - "youtube" => Some(catalogs::YOUTUBE_CURATED), - // ONE_DRIVE_* slugs extract to "one" via toolkit_from_slug; - // alias both the prefix and the canonical UI/backend slugs. - "one" | "one_drive" | "onedrive" => Some(catalogs::ONE_DRIVE_CURATED), - "excel" => Some(catalogs::EXCEL_CURATED), - "todoist" => Some(catalogs::TODOIST_CURATED), - _ => None, - } -} +// The capability matrix, the curated-catalog lookup and the visibility gate +// all moved to the contract crate (OpenHuman#5560) — see [`catalogs`] and +// [`tinymemory_api::host::composio::capability_matrix`]. They are re-exported +// at the bottom of this file, so every historical `providers::…` path keeps +// resolving and the wire surface is unchanged. /// All toolkit slugs that have a curated agent-ready catalog. /// @@ -239,8 +71,18 @@ pub fn catalog_for_toolkit(toolkit: &str) -> Option<&'static [CuratedTool]> { /// `providers::agent_ready_toolkits()` call keeps resolving. pub use tinymemory_api::composio::scopes::agent_ready_toolkits; +// Historical per-category module paths (`providers::catalogs_business::…`, +// pre-#5560). See [`catalogs_compat`] for why these stay as thin re-exports +// rather than a semver bump. +pub use catalogs_compat::{ + catalogs_business, catalogs_google, catalogs_messaging, catalogs_microsoft, + catalogs_productivity, catalogs_social_media, +}; + pub use descriptions::toolkit_description; pub(crate) use helpers::{first_array_str, merge_extra}; +pub use tinymemory_api::composio::catalogs::{catalog_for_toolkit, is_action_visible_with_pref}; +pub use tinymemory_api::host::composio::capability_matrix; // `pick_str` is a provider payload normaliser and lives in tinycortex; it is // re-exported here so the ~40 in-tree call sites keep resolving unchanged. // Note this is deliberately NOT `providers::common::pick_str`, which coerces diff --git a/crates/tinymemory-core/src/sync/composio/providers/notion/tools.rs b/crates/tinymemory-core/src/sync/composio/providers/notion/tools.rs index 89f4efc5..dd0840ec 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/notion/tools.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/notion/tools.rs @@ -1,196 +1,6 @@ -//! Curated catalog of Notion Composio actions exposed to the agent. +//! The curated `notion` catalog, re-exported at its historical path. +//! +//! The table moved to [`tinymemory_api::composio::catalogs::notion`] with every +//! other catalog — see [`super::super::catalogs`] for why. -use crate::sync::composio::providers::tool_scope::{CuratedTool, ToolScope}; - -pub const NOTION_CURATED: &[CuratedTool] = &[ - // ── Read: search & fetch ──────────────────────────────────────── - CuratedTool { - slug: "NOTION_SEARCH_NOTION_PAGE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_DATA", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_DATABASE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_ROW", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_BLOCK_METADATA", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_BLOCK_CONTENTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_ALL_BLOCK_CONTENTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_FETCH_COMMENTS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_GET_PAGE_MARKDOWN", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_GET_PAGE_PROPERTY_ACTION", - scope: ToolScope::Read, - }, - // ── Read: query & retrieve ────────────────────────────────────── - CuratedTool { - slug: "NOTION_QUERY_DATABASE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_QUERY_DATABASE_WITH_FILTER", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_QUERY_DATA_SOURCE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_RETRIEVE_PAGE", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_RETRIEVE_COMMENT", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_RETRIEVE_DATABASE_PROPERTY", - scope: ToolScope::Read, - }, - // ── Read: profile / users / files ─────────────────────────────── - CuratedTool { - slug: "NOTION_LIST_USERS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_GET_ABOUT_USER", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_GET_ABOUT_ME", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_LIST_FILE_UPLOADS", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_RETRIEVE_FILE_UPLOAD", - scope: ToolScope::Read, - }, - CuratedTool { - slug: "NOTION_LIST_DATA_SOURCE_TEMPLATES", - scope: ToolScope::Read, - }, - // ── Write: create ─────────────────────────────────────────────── - CuratedTool { - slug: "NOTION_CREATE_NOTION_PAGE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_CREATE_DATABASE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_CREATE_COMMENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_CREATE_FILE_UPLOAD", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_SEND_FILE_UPLOAD", - scope: ToolScope::Write, - }, - // ── Write: update / append ────────────────────────────────────── - CuratedTool { - slug: "NOTION_UPDATE_PAGE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_UPDATE_BLOCK", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_UPDATE_ROW_DATABASE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_INSERT_ROW_DATABASE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_INSERT_ROW_FROM_NL", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_REPLACE_PAGE_CONTENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_ADD_PAGE_CONTENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_ADD_MULTIPLE_PAGE_CONTENT", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_BLOCK_CHILDREN", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_TEXT_BLOCKS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_TASK_BLOCKS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_CODE_BLOCKS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_MEDIA_BLOCKS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_LAYOUT_BLOCKS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_APPEND_TABLE_BLOCKS", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_DUPLICATE_PAGE", - scope: ToolScope::Write, - }, - CuratedTool { - slug: "NOTION_MOVE_PAGE", - scope: ToolScope::Write, - }, - // ── Admin: destructive ────────────────────────────────────────── - CuratedTool { - slug: "NOTION_DELETE_BLOCK", - scope: ToolScope::Admin, - }, - CuratedTool { - slug: "NOTION_ARCHIVE_NOTION_PAGE", - scope: ToolScope::Admin, - }, -]; +pub use tinymemory_api::composio::catalogs::notion::NOTION_CURATED; diff --git a/crates/tinymemory-core/src/sync/composio/providers/scope_lookup.rs b/crates/tinymemory-core/src/sync/composio/providers/scope_lookup.rs index c1f6e5c1..922c2e88 100644 --- a/crates/tinymemory-core/src/sync/composio/providers/scope_lookup.rs +++ b/crates/tinymemory-core/src/sync/composio/providers/scope_lookup.rs @@ -1,64 +1,13 @@ -//! Scope-lookup operational helpers for the curated tool catalogs. +//! Scope-lookup helpers, re-exported at their historical path. //! -//! Lives in a sibling module (extracted from the formerly-thick -//! `providers/mod.rs`) to keep the module entrypoint export-focused — -//! matches the project rule "keep mod.rs light; operational logic in -//! ops.rs / store.rs / types.rs" from CLAUDE.md. +//! Moved to [`tinymemory_api::composio::catalogs`] with the catalogs they walk. //! -//! - [`curated_scope_for`] answers "what scope does this action slug -//! require?" — used by `composio::ops` to render `gated_tools` -//! unlock hints. -//! - [`toolkit_has_scope`] answers "does this toolkit have any -//! actions at the given scope?" — currently used by tests; intended -//! for future UI hints (grey-out a toggle that unlocks nothing). -//! -//! Both walk the native provider catalog first, then fall back to the -//! static `catalog_for_toolkit` map — so the answers match what -//! [`super::is_action_visible_with_pref`] would gate against. - -use super::tool_scope::{find_curated, toolkit_from_slug, ToolScope}; -use super::{catalog_for_toolkit, get_provider}; - -/// Look up the curated scope for `slug` if it appears in any registered -/// catalog (native provider's `curated_tools()` first, then the fallback -/// catalog from [`super::catalog_for_toolkit`]). Returns `None` for -/// genuinely uncurated slugs — callers that want a defensible heuristic -/// for those should fall back to [`super::classify_unknown`] explicitly. -/// -/// Sibling of [`super::is_action_visible_with_pref`]: that one decides -/// "visible?", this one returns "what scope is required?" so callers -/// (e.g. the `gated_tools` partition in -/// `composio::ops::fetch_connected_integrations`) can render a useful -/// unlock hint to the agent without re-doing the catalog walk. -pub fn curated_scope_for(slug: &str) -> Option { - let toolkit = toolkit_from_slug(slug)?; - let catalog = get_provider(&toolkit) - .and_then(|p| p.curated_tools()) - .or_else(|| catalog_for_toolkit(&toolkit))?; - find_curated(catalog, slug).map(|c| c.scope) -} - -/// Does any curated action for `toolkit` require `scope`? -/// -/// Currently used by this module's tests only (added when the -/// now-removed `composio_enable_scope` meta-tool needed a no-op -/// short-circuit). Kept because the same probe is useful any time we -/// ask "would flipping the {scope} bit unlock anything in this -/// toolkit?" — e.g. a UI hint that greys out a toggle with no effect. -/// -/// Walks both the native provider catalog and the fallback -/// [`super::catalog_for_toolkit`] so the answer matches what -/// [`super::is_action_visible_with_pref`] would gate against. -pub fn toolkit_has_scope(toolkit: &str, scope: ToolScope) -> bool { - let catalog = get_provider(toolkit) - .and_then(|p| p.curated_tools()) - .or_else(|| catalog_for_toolkit(toolkit)); - match catalog { - Some(cat) => cat.iter().any(|t| t.scope == scope), - None => false, - } -} - -#[cfg(test)] -#[path = "scope_lookup_tests.rs"] -mod tests; +//! One behaviour note, because it looks like a change and is not: the versions +//! here consulted `get_provider(..).curated_tools()` before falling back to +//! `catalog_for_toolkit`. Every native provider's `curated_tools()` returns +//! exactly the slice `catalog_for_toolkit` returns for the same toolkit — true +//! of all six — so the provider hop was pure indirection and the contract +//! versions drop it. That is what lets a host answer "may this action run" +//! without a provider registry. + +pub use tinymemory_api::composio::catalogs::{curated_scope_for, toolkit_has_scope}; diff --git a/crates/tinymemory-core/src/sync/composio/providers/scope_lookup_tests.rs b/crates/tinymemory-core/src/sync/composio/providers/scope_lookup_tests.rs deleted file mode 100644 index f1db92b0..00000000 --- a/crates/tinymemory-core/src/sync/composio/providers/scope_lookup_tests.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Tests for the surrounding module. - -use super::*; - -#[test] -fn toolkit_has_scope_distinguishes_gated_from_ungated_scopes() { - // gmail catalog includes destructive verbs (delete / trash / - // batch_delete), so admin-gating actually unlocks something. - assert!(toolkit_has_scope("gmail", ToolScope::Admin)); - assert!(toolkit_has_scope("gmail", ToolScope::Read)); - assert!(toolkit_has_scope("gmail", ToolScope::Write)); - // Case-insensitive toolkit slug → still routes to the catalog. - assert!(toolkit_has_scope("GMAIL", ToolScope::Admin)); - // Unknown toolkit → no catalog → no scope is "gating" anything. - assert!(!toolkit_has_scope("nonexistent-toolkit", ToolScope::Admin)); -} diff --git a/crates/tinymemory-core/src/tree/retrieval/fast_tests.rs b/crates/tinymemory-core/src/tree/retrieval/fast_tests.rs index ccbc12eb..6860a113 100644 --- a/crates/tinymemory-core/src/tree/retrieval/fast_tests.rs +++ b/crates/tinymemory-core/src/tree/retrieval/fast_tests.rs @@ -49,3 +49,26 @@ async fn ambient_empty_source_scope_remains_fail_closed() { .unwrap(); assert!(response.hits.is_empty()); } + +/// The contract's `FastRetrieveQuery::default()` must stay the engine's +/// `FastRetrieveOptions::default()`. +/// +/// The contract grew a `Default` so a host migrating off the engine type does +/// not have to re-spell `limit: 10, max_hops: 2` at every call site +/// (OpenHuman#5560) — which is exactly how two defaults drift apart. Neither +/// crate can see the other's constant, so this is the only place the two can +/// be compared. A change to either side without the other lands here. +#[test] +fn the_contract_default_matches_the_engine_default() { + let engine = FastRetrieveOptions::default(); + let contract = tinymemory_api::provider::retrieval::FastRetrieveQuery::default(); + assert_eq!(engine.limit, contract.limit, "default limit drifted"); + assert_eq!( + engine.max_hops, contract.max_hops, + "default max_hops drifted" + ); + assert_eq!( + engine.time_window_days, contract.time_window_days, + "default time_window_days drifted" + ); +}