Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions plugins/composio-direct/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ Use `composio-direct` when a user asks Teleton to work with an external app supp

## Required Setup

The plugin requires the Teleton secret `composio_api_key`, using either a Composio project API key or user API key. It can also be supplied through `COMPOSIO_DIRECT_COMPOSIO_API_KEY`, with `COMPOSIO_API_KEY` kept as a legacy fallback. If the key is missing, stop and ask the operator to configure it before attempting Composio calls.
The plugin requires the Teleton secret `composio_api_key`, using a Composio project API key, user API key, or organization API key. It can also be supplied through `COMPOSIO_DIRECT_COMPOSIO_API_KEY`, with `COMPOSIO_API_KEY` kept as a legacy fallback. If the key is missing, stop and ask the operator to configure it before attempting Composio calls.

Default runtime settings:

| Setting | Default | Use |
|---|---:|---|
| `base_url` | `https://backend.composio.dev/api/v3.1` | Composio API endpoint |
| `api_key_auth_scheme` | `auto` | API key header mode: `auto`, `project`, or `user` |
| `api_key_auth_scheme` | `auto` | API key header mode: `auto`, `project`, `user`, or `org` |
| `timeout_ms` | `30000` | Default request timeout |
| `max_parallel_executions` | `10` | Batch execution concurrency |
| `tool_version` | `latest` | Tool execution/schema version |
Expand All @@ -29,6 +29,18 @@ Default runtime settings:

This order matters because Composio tool inputs vary by toolkit and version. Do not guess parameter names when `composio_get_tool_schemas` can confirm them.

## Custom Provider And Sessions Alignment

Composio's TypeScript custom provider docs define a provider as the adapter layer that transforms tool definitions, executes tool calls, and exposes platform helpers. Teleton already provides the agent tool registry, so `composio-direct` acts as the Teleton-specific direct provider over Composio REST instead of importing `@composio/core` provider classes at runtime.

The mapping is:

- Transform step: `composio_search_tools` and `composio_get_tool_schemas` convert Composio tool objects into Teleton-visible results with `tool_slug`, schemas, and `execute_with` guidance.
- Execution step: `composio_execute_tool` and `composio_multi_execute` call the current Composio execute API with the sender-scoped `user_id`, tool `arguments`, optional `connected_account_id`, and selected tool version.
- Provider helpers: auth, connection, toolkit, file, trigger, webhook, and remote meta-tool wrappers expose the supporting Composio flows an agent needs around execution.

Composio sessions are the recommended SDK path for new agent integrations. `composio-direct` stays on the direct REST path because Teleton registers static plugin tools, this package must remain dependency-free, and trigger/webhook/file workflows still rely on direct API surfaces. If a future Teleton integration uses Tool Router sessions or MCP, keep it separate from the direct plugin so the current execution contract remains stable.

## Tool Discovery

Call `composio_search_tools` when the user describes an action but not a specific Composio slug.
Expand Down Expand Up @@ -186,7 +198,7 @@ The plugin returns structured results:

For `auth_required`, do not retry blindly. Generate or surface a connection link, wait for user confirmation, then retry. For validation errors, fetch the schema again and correct the parameters. For transient network or 5xx failures, the plugin already retries three times with exponential backoff.

HTTP 401/403 from Composio indicates API key authentication or permission failure, not a missing external app connection. Do not call `composio_auth_link` for those errors; verify the `composio_api_key` key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist.
HTTP 401/403 from Composio indicates API key authentication or permission failure, not a missing external app connection. Do not call `composio_auth_link` for those errors; verify the `composio_api_key` key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist. In `auto` mode, the plugin tries `x-api-key`, then `x-user-api-key`, then `x-org-api-key` on endpoints that are not project-only.

## Security Rules

Expand Down
20 changes: 16 additions & 4 deletions plugins/composio-direct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@ Direct integration with **1000+ Composio automation tools** — no MCP transport
- **Parallel batch execution** — configurable concurrency limit
- **Zero sensitive data in logs** — API keys and OAuth tokens are never logged

## Composio Provider Model

Composio's TypeScript custom provider guide describes a provider as the layer that transforms tools, executes tool calls, and exposes helper methods for a target agent framework. Teleton plugins are registered as static Teleton tools, so `composio-direct` implements that provider role directly over Composio REST instead of installing a Composio SDK provider class at runtime.

- Discovery and schema tools are the transform step: they return `tool_slug`, JSON schemas, and `execute_with` instructions.
- `composio_execute_tool` and `composio_multi_execute` are the execution step: they send sender-scoped `user_id`, tool `arguments`, optional top-level `connected_account_id`, and version data to Composio.
- Auth, connection, toolkit, file, trigger, webhook, and remote meta-tool wrappers are the provider helpers Teleton needs around execution.

Composio sessions are still the preferred SDK path for new agent integrations. This plugin intentionally remains the direct REST integration because it has no npm runtime dependency, Teleton registers fixed plugin tools, and several supported flows still use direct API surfaces.

## Setup

1. Get your Composio project or user API key at <https://app.composio.dev/settings>
1. Get your Composio project, user, or organization API key at <https://app.composio.dev/settings>
2. Set the `composio_api_key` secret in Teleton:

```text
Expand All @@ -27,14 +37,14 @@ Direct integration with **1000+ Composio automation tools** — no MCP transport

For container and CI deployments, Teleton also resolves the secret from `COMPOSIO_DIRECT_COMPOSIO_API_KEY`. The plugin keeps `COMPOSIO_API_KEY` as a direct fallback for older deployments.

By default `api_key_auth_scheme` is `auto`: the plugin sends the key as a project key (`x-api-key`) first and, for endpoints that accept user API keys, retries as `x-user-api-key` on Composio 401/403 responses. Set it to `project` or `user` only when you want to force a specific header.
By default `api_key_auth_scheme` is `auto`: the plugin sends the key as a project key (`x-api-key`) first and, for endpoints that accept non-project API keys, retries as `x-user-api-key` and then `x-org-api-key` on Composio 401/403 responses. Set it to `project`, `user`, or `org` only when you want to force a specific header. Project-only endpoints, such as Files and Webhooks, still use `x-api-key`.

```yaml
# config.yaml example
plugins:
composio_direct:
base_url: "https://backend.composio.dev/api/v3.1" # optional
api_key_auth_scheme: "auto" # optional (auto/project/user)
api_key_auth_scheme: "auto" # optional (auto/project/user/org)
timeout_ms: 30000 # optional (default: 30s)
max_parallel_executions: 10 # optional (default: 10)
tool_version: "latest" # optional
Expand Down Expand Up @@ -473,4 +483,6 @@ node --test plugins/composio-direct/test/unit/composio-direct.test.js \
- Added Triggers API coverage through trigger type discovery, active trigger listing, trigger upsert, enable/disable, and delete endpoints.
- Added Webhooks API coverage through event type discovery and webhook subscription CRUD/secret rotation endpoints.
- Meta-tool alignment: `composio_search_tools`, `composio_get_tool_schemas`, `composio_multi_execute`, connection/auth tools, `composio_manage_connections`, `composio_remote_bash`, and `composio_remote_workbench` cover the practical `search_tools`, `get_tool_schemas`, `multi_execute_tool`, `manage_connections`, `remote_bash_tool`, and `remote_workbench` flows for Teleton.
- HTTP 401/403 responses are reported as Composio API key access failures, not as `auth_required` service authorization. Check the project/user key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist before retrying.
- Custom provider alignment: Teleton consumes the static plugin tools exported here, so the plugin implements the Composio provider transform/execute/helper contract over REST instead of importing `@composio/core` provider classes.
- Sessions note: Composio recommends sessions for SDK agents, while this direct plugin keeps the explicit `/tools`, `/tools/execute`, auth, connection, trigger, webhook, and file routes available to Teleton.
- HTTP 401/403 responses are reported as Composio API key access failures, not as `auth_required` service authorization. Check the project/user/org key type, `api_key_auth_scheme`, endpoint permissions, and any Composio IP allowlist before retrying.
84 changes: 47 additions & 37 deletions plugins/composio-direct/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
*
* Authentication:
* - Requires a Composio API key stored in sdk.secrets as "composio_api_key"
* - Supports project API keys (x-api-key) and user API keys (x-user-api-key)
* - Supports project API keys (x-api-key), user API keys (x-user-api-key),
* and organization API keys (x-org-api-key)
* - Set COMPOSIO_DIRECT_COMPOSIO_API_KEY, COMPOSIO_API_KEY, or use the secrets store
*
* Transport:
Expand Down Expand Up @@ -66,7 +67,7 @@ const COMPOSIO_EXECUTION_GUIDANCE = {

export const manifest = {
name: "composio-direct",
version: "1.9.3",
version: "1.9.4",
sdkVersion: ">=1.0.0",
description:
"Direct access to 1000+ Composio automation tools plus v3.1 toolkits, files, triggers, webhooks, connection reuse, and meta-tools without MCP transport",
Expand All @@ -75,7 +76,7 @@ export const manifest = {
required: true,
env: "COMPOSIO_DIRECT_COMPOSIO_API_KEY",
description:
"Composio project or user API key (create at https://app.composio.dev/settings)",
"Composio project, user, or organization API key (create at https://app.composio.dev/settings)",
},
},
defaultConfig: {
Expand Down Expand Up @@ -105,12 +106,11 @@ function sleep(ms) {
/**
* Build common headers for Composio API requests.
* @param {string} apiKey
* @param {"project" | "user"} [apiKeyAuthScheme]
* @param {"project" | "user" | "org"} [apiKeyAuthScheme]
* @returns {Record<string, string>}
*/
function buildHeaders(apiKey, apiKeyAuthScheme = "project") {
const apiKeyHeader =
apiKeyAuthScheme === "user" ? "x-user-api-key" : "x-api-key";
const apiKeyHeader = getApiKeyHeaderName(apiKeyAuthScheme);
return {
"Content-Type": "application/json",
[apiKeyHeader]: apiKey,
Expand All @@ -120,43 +120,50 @@ function buildHeaders(apiKey, apiKeyAuthScheme = "project") {
/**
* Normalize API-key auth scheme config.
* @param {unknown} value
* @returns {"auto" | "project" | "user"}
* @returns {"auto" | "project" | "user" | "org"}
*/
function normalizeApiKeyAuthScheme(value) {
const scheme = String(value ?? DEFAULT_API_KEY_AUTH_SCHEME)
.trim()
.toLowerCase()
.replace(/_/g, "-");

if (scheme === "auto" || scheme === "project" || scheme === "user") {
if (scheme === "auto" || scheme === "project" || scheme === "user" || scheme === "org") {
return scheme;
}
if (scheme === "organization") return "org";
if (scheme === "x-api-key") return "project";
if (scheme === "x-user-api-key") return "user";
if (scheme === "x-org-api-key") return "org";
return DEFAULT_API_KEY_AUTH_SCHEME;
}

/**
* @param {"project" | "user"} apiKeyAuthScheme
* @returns {"x-api-key" | "x-user-api-key"}
* @param {"project" | "user" | "org"} apiKeyAuthScheme
* @returns {"x-api-key" | "x-user-api-key" | "x-org-api-key"}
*/
function getApiKeyHeaderName(apiKeyAuthScheme) {
return apiKeyAuthScheme === "user" ? "x-user-api-key" : "x-api-key";
if (apiKeyAuthScheme === "user") return "x-user-api-key";
if (apiKeyAuthScheme === "org") return "x-org-api-key";
return "x-api-key";
}

/**
* Composio v3.1 accepts project keys on x-api-key and user keys on
* x-user-api-key for most user-facing endpoints. In auto mode, try the
* project header first to preserve existing behavior, then retry with the
* user header only when the endpoint supports it and Composio rejected access.
* @param {"auto" | "project" | "user"} apiKeyAuthScheme
* @param {boolean} supportsUserApiKey
* @returns {Array<"project" | "user">}
* x-user-api-key for most user-facing endpoints. Composio also exposes
* organization keys on x-org-api-key for org-scoped access. In auto mode, try
* the project header first to preserve existing behavior, then retry with
* user and organization headers only when the endpoint is not project-only and
* Composio rejected access.
* @param {"auto" | "project" | "user" | "org"} apiKeyAuthScheme
* @param {boolean} supportsNonProjectApiKeys
* @returns {Array<"project" | "user" | "org">}
*/
function getApiKeyAuthAttempts(apiKeyAuthScheme, supportsUserApiKey) {
function getApiKeyAuthAttempts(apiKeyAuthScheme, supportsNonProjectApiKeys) {
if (apiKeyAuthScheme === "project") return ["project"];
if (apiKeyAuthScheme === "user") return ["user"];
return supportsUserApiKey ? ["project", "user"] : ["project"];
if (apiKeyAuthScheme === "org") return ["org"];
return supportsNonProjectApiKeys ? ["project", "user", "org"] : ["project"];
}

/**
Expand Down Expand Up @@ -291,15 +298,15 @@ function formatComposioApiAccessError(response) {
if (response.status === 403) {
return (
"Composio API key permission denied. Check that composio_api_key is a " +
"project or user API key with permissions for this endpoint, that " +
"project, user, or organization API key with permissions for this endpoint, that " +
"api_key_auth_scheme matches the key type when set explicitly, and that " +
`any Composio IP allowlist includes this runtime. Composio response: ${message}`
);
}

return (
"Composio API key was rejected. Check that composio_api_key is a valid " +
`project or user API key. Composio response: ${message}`
`project, user, or organization API key. Composio response: ${message}`
);
}

Expand Down Expand Up @@ -905,7 +912,7 @@ export const tools = (sdk) => {
return {
success: false,
error:
"Composio API key is not configured. Please set the composio_api_key secret or COMPOSIO_DIRECT_COMPOSIO_API_KEY with your project or user key from https://app.composio.dev/settings",
"Composio API key is not configured. Please set the composio_api_key secret or COMPOSIO_DIRECT_COMPOSIO_API_KEY with your project, user, or organization key from https://app.composio.dev/settings",
};
}

Expand All @@ -917,7 +924,7 @@ export const tools = (sdk) => {
* @param {string} opts.method
* @param {unknown} [opts.body]
* @param {number} opts.timeoutMs
* @param {boolean} [opts.supportsUserApiKey]
* @param {boolean} [opts.supportsNonProjectApiKeys]
* @returns {Promise<{ status: number; data: unknown }>}
*/
async function requestComposio({
Expand All @@ -926,10 +933,13 @@ export const tools = (sdk) => {
method,
body,
timeoutMs,
supportsUserApiKey = true,
supportsNonProjectApiKeys = true,
}) {
const { apiKeyAuthScheme } = getConfig();
const authAttempts = getApiKeyAuthAttempts(apiKeyAuthScheme, supportsUserApiKey);
const authAttempts = getApiKeyAuthAttempts(
apiKeyAuthScheme,
supportsNonProjectApiKeys
);

let response = null;
let apiAccessErrorResponse = null;
Expand Down Expand Up @@ -975,7 +985,7 @@ export const tools = (sdk) => {
* @param {URLSearchParams} [opts.query]
* @param {unknown} [opts.body]
* @param {number} [opts.timeoutMs]
* @param {boolean} [opts.supportsUserApiKey]
* @param {boolean} [opts.supportsNonProjectApiKeys]
* @returns {Promise<{ status: number; data: unknown }>}
*/
function callComposio({
Expand All @@ -985,7 +995,7 @@ export const tools = (sdk) => {
query,
body,
timeoutMs,
supportsUserApiKey = true,
supportsNonProjectApiKeys = true,
}) {
const cfg = getConfig();
return requestComposio({
Expand All @@ -994,7 +1004,7 @@ export const tools = (sdk) => {
method,
body,
timeoutMs: timeoutMs ?? cfg.timeoutMs,
supportsUserApiKey,
supportsNonProjectApiKeys,
});
}

Expand Down Expand Up @@ -2301,7 +2311,7 @@ export const tools = (sdk) => {
apiKey,
path: "/files/list",
query: qs,
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (response.status !== 200) {
return failedResponse("Could not list files", response);
Expand Down Expand Up @@ -2383,7 +2393,7 @@ export const tools = (sdk) => {
path: "/files/upload/request",
method: "POST",
body,
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (!isOkStatus(response.status, [200, 201]) || !isRecord(response.data)) {
return failedResponse("Could not request file upload", response);
Expand Down Expand Up @@ -2897,7 +2907,7 @@ export const tools = (sdk) => {
const response = await callComposio({
apiKey,
path: "/webhook_subscriptions/event_types",
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (response.status !== 200) {
return failedResponse("Could not list webhook event types", response);
Expand Down Expand Up @@ -2960,7 +2970,7 @@ export const tools = (sdk) => {
apiKey,
path: "/webhook_subscriptions",
query: qs,
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (response.status !== 200) {
return failedResponse("Could not list webhook subscriptions", response);
Expand Down Expand Up @@ -3019,7 +3029,7 @@ export const tools = (sdk) => {
const response = await callComposio({
apiKey,
path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}`,
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (response.status !== 200 || !isRecord(response.data)) {
return failedResponse(`Could not get webhook ${params.webhook_id}`, response);
Expand Down Expand Up @@ -3096,7 +3106,7 @@ export const tools = (sdk) => {
path: "/webhook_subscriptions",
method: "POST",
body,
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (!isOkStatus(response.status, [200, 201]) || !isRecord(response.data)) {
return failedResponse("Could not create webhook subscription", response);
Expand Down Expand Up @@ -3179,7 +3189,7 @@ export const tools = (sdk) => {
path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}`,
method: "PATCH",
body,
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (!isOkStatus(response.status, [200, 204]) || (response.status !== 204 && !isRecord(response.data))) {
return failedResponse(`Could not update webhook ${params.webhook_id}`, response);
Expand Down Expand Up @@ -3238,7 +3248,7 @@ export const tools = (sdk) => {
apiKey,
path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}/rotate_secret`,
method: "POST",
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (!isOkStatus(response.status, [200, 201]) || !isRecord(response.data)) {
return failedResponse(`Could not rotate webhook ${params.webhook_id} secret`, response);
Expand Down Expand Up @@ -3289,7 +3299,7 @@ export const tools = (sdk) => {
apiKey,
path: `/webhook_subscriptions/${encodeURIComponent(params.webhook_id)}`,
method: "DELETE",
supportsUserApiKey: false,
supportsNonProjectApiKeys: false,
});
if (!isOkStatus(response.status, [200, 204])) {
return failedResponse(`Could not delete webhook ${params.webhook_id}`, response);
Expand Down
Loading
Loading