diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 48587b8..0000000 --- a/.gitkeep +++ /dev/null @@ -1,6 +0,0 @@ -# .gitkeep file auto-generated at 2026-03-19T10:37:13.073Z for PR creation at branch issue-19-f54b585823d1 for issue https://github.com/xlabtg/teleton-plugins/issues/19 -# Updated: 2026-03-27T01:04:44.761Z -# Updated: 2026-04-05T19:20:26.278Z -# Updated: 2026-04-09T18:02:32.277Z -# Updated: 2026-06-14T10:40:42.180Z -# Updated: 2026-06-14T10:41:34.176Z \ No newline at end of file diff --git a/plugins/composio-direct/GUIDE.md b/plugins/composio-direct/GUIDE.md index d3244a9..b7d8d84 100644 --- a/plugins/composio-direct/GUIDE.md +++ b/plugins/composio-direct/GUIDE.md @@ -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 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. +The plugin requires the Teleton secret `composio_api_key`, using 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. 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`, `user`, or `org` | +| `api_key_auth_scheme` | `auto` | API key header mode: `auto`, `project`, or `user` | | `timeout_ms` | `30000` | Default request timeout | | `max_parallel_executions` | `10` | Batch execution concurrency | | `tool_version` | `latest` | Tool execution/schema version | @@ -198,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. 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. +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 sends known `ak_` keys as `x-api-key` and known `uak_` keys as `x-user-api-key`; for older unknown key shapes it tries `x-api-key` then `x-user-api-key` on endpoints that support user keys. Composio organization API keys (`oak_`) are only for organization-management endpoints and cannot execute regular `composio-direct` tools. ## Security Rules diff --git a/plugins/composio-direct/README.md b/plugins/composio-direct/README.md index f22e7de..32c7946 100644 --- a/plugins/composio-direct/README.md +++ b/plugins/composio-direct/README.md @@ -28,7 +28,7 @@ Composio sessions are still the preferred SDK path for new agent integrations. T ## Setup -1. Get your Composio project, user, or organization API key at +1. Get your Composio project or user API key at 2. Set the `composio_api_key` secret in Teleton: ```text @@ -37,14 +37,16 @@ Composio sessions are still the preferred SDK path for new agent integrations. T 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 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`. +By default `api_key_auth_scheme` is `auto`: the plugin infers known Composio key prefixes (`ak_` for project keys, `uak_` for user keys) and sends the matching header first. For older or unknown key shapes, it keeps the legacy fallback from `x-api-key` to `x-user-api-key` on endpoints that support user keys. Project-only endpoints, such as Files and Webhooks, still use `x-api-key`. + +Organization API keys (`oak_`) are for Composio organization-management endpoints. They cannot execute, discover, authorize, or manage regular tools through `composio-direct`; configure a project or user API key for this plugin. ```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/org) + api_key_auth_scheme: "auto" # optional (auto/project/user) timeout_ms: 30000 # optional (default: 30s) max_parallel_executions: 10 # optional (default: 10) tool_version: "latest" # optional @@ -485,4 +487,4 @@ node --test plugins/composio-direct/test/unit/composio-direct.test.js \ - 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. - 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. +- 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. diff --git a/plugins/composio-direct/index.js b/plugins/composio-direct/index.js index d5d05cf..7c099cb 100644 --- a/plugins/composio-direct/index.js +++ b/plugins/composio-direct/index.js @@ -20,8 +20,9 @@ * * Authentication: * - Requires a Composio API key stored in sdk.secrets as "composio_api_key" - * - Supports project API keys (x-api-key), user API keys (x-user-api-key), - * and organization API keys (x-org-api-key) + * - Supports project API keys (x-api-key) and user API keys (x-user-api-key) + * for tool/auth/connection endpoints. Organization API keys are valid only + * for Composio organization-management endpoints and cannot execute tools. * - Set COMPOSIO_DIRECT_COMPOSIO_API_KEY, COMPOSIO_API_KEY, or use the secrets store * * Transport: @@ -67,7 +68,7 @@ const COMPOSIO_EXECUTION_GUIDANCE = { export const manifest = { name: "composio-direct", - version: "1.9.4", + version: "1.9.5", 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", @@ -76,7 +77,7 @@ export const manifest = { required: true, env: "COMPOSIO_DIRECT_COMPOSIO_API_KEY", description: - "Composio project, user, or organization API key (create at https://app.composio.dev/settings)", + "Composio project or user API key (create at https://app.composio.dev/settings)", }, }, defaultConfig: { @@ -150,20 +151,94 @@ function getApiKeyHeaderName(apiKeyAuthScheme) { /** * Composio v3.1 accepts project keys on x-api-key and user keys on - * 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. + * x-user-api-key for most user-facing endpoints. Organization keys on + * x-org-api-key are reserved for /org/* management endpoints in the OpenAPI + * schema and are not valid credentials for tool execution or discovery. + * In auto mode, infer known key prefixes first; for unknown/legacy key shapes, + * try project first to preserve existing behavior, then user when supported. * @param {"auto" | "project" | "user" | "org"} apiKeyAuthScheme - * @param {boolean} supportsNonProjectApiKeys + * @param {boolean} supportsUserApiKeys + * @param {boolean} supportsOrgApiKeys + * @param {string} [apiKey] * @returns {Array<"project" | "user" | "org">} */ -function getApiKeyAuthAttempts(apiKeyAuthScheme, supportsNonProjectApiKeys) { +function getApiKeyAuthAttempts( + apiKeyAuthScheme, + supportsUserApiKeys, + supportsOrgApiKeys, + apiKey +) { if (apiKeyAuthScheme === "project") return ["project"]; - if (apiKeyAuthScheme === "user") return ["user"]; - if (apiKeyAuthScheme === "org") return ["org"]; - return supportsNonProjectApiKeys ? ["project", "user", "org"] : ["project"]; + if (apiKeyAuthScheme === "user") return supportsUserApiKeys ? ["user"] : []; + if (apiKeyAuthScheme === "org") return supportsOrgApiKeys ? ["org"] : []; + + const detectedScheme = detectApiKeyAuthScheme(apiKey); + if (detectedScheme === "project") return ["project"]; + if (detectedScheme === "user") return supportsUserApiKeys ? ["user"] : []; + if (detectedScheme === "org") return supportsOrgApiKeys ? ["org"] : []; + + return supportsUserApiKeys ? ["project", "user"] : ["project"]; +} + +/** + * Infer Composio API-key kind from stable key prefixes described by Composio. + * Unknown shapes keep legacy project->user auto fallback behavior. + * @param {string | null | undefined} apiKey + * @returns {"project" | "user" | "org" | null} + */ +function detectApiKeyAuthScheme(apiKey) { + const key = String(apiKey ?? "").trim().toLowerCase(); + if (key.startsWith("uak_")) return "user"; + if (key.startsWith("oak_")) return "org"; + if (key.startsWith("ak_")) return "project"; + return null; +} + +/** + * Build a local response when a configured key/header scheme is unsupported + * for the current Composio endpoint. This avoids a misleading remote 403. + * @param {"auto" | "project" | "user" | "org"} apiKeyAuthScheme + * @param {string} apiKey + * @param {boolean} supportsUserApiKeys + * @param {boolean} supportsOrgApiKeys + * @returns {{ status: number; data: unknown }} + */ +function unsupportedApiKeyAuthResponse( + apiKeyAuthScheme, + apiKey, + supportsUserApiKeys, + supportsOrgApiKeys +) { + const detectedScheme = detectApiKeyAuthScheme(apiKey); + const scheme = apiKeyAuthScheme === "auto" ? detectedScheme : apiKeyAuthScheme; + let message = + "Configured Composio API key auth scheme is not supported by this endpoint."; + let suggestedFix = + "Use a Composio project API key (ak_...) for project-only endpoints, or a user API key (uak_...) for regular tool/auth/connection endpoints."; + + if (scheme === "org") { + message = + "Composio organization API keys (oak_...) are only valid for organization-management endpoints and cannot execute or discover tools through composio-direct."; + suggestedFix = + "Set composio_api_key to a Composio project or user API key (ak_... or uak_...) for composio-direct tools."; + } else if (scheme === "user" && !supportsUserApiKeys) { + message = + "This Composio endpoint accepts only project API keys; the configured user API key cannot be used here."; + suggestedFix = + "Set composio_api_key to a Composio project API key (ak_...) for this endpoint."; + } + + return { + status: 400, + data: { + error: { + message, + slug: "UNSUPPORTED_COMPOSIO_API_KEY_AUTH_SCHEME", + status: 400, + suggested_fix: suggestedFix, + }, + }, + }; } /** @@ -298,7 +373,7 @@ function formatComposioApiAccessError(response) { if (response.status === 403) { return ( "Composio API key permission denied. Check that composio_api_key is a " + - "project, user, or organization API key with permissions for this endpoint, that " + + "project or user 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}` ); @@ -306,7 +381,7 @@ function formatComposioApiAccessError(response) { return ( "Composio API key was rejected. Check that composio_api_key is a valid " + - `project, user, or organization API key. Composio response: ${message}` + `project or user API key. Composio response: ${message}` ); } @@ -912,7 +987,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, user, or organization 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 or user key from https://app.composio.dev/settings", }; } @@ -925,6 +1000,8 @@ export const tools = (sdk) => { * @param {unknown} [opts.body] * @param {number} opts.timeoutMs * @param {boolean} [opts.supportsNonProjectApiKeys] + * @param {boolean} [opts.supportsUserApiKeys] + * @param {boolean} [opts.supportsOrgApiKeys] * @returns {Promise<{ status: number; data: unknown }>} */ async function requestComposio({ @@ -933,14 +1010,27 @@ export const tools = (sdk) => { method, body, timeoutMs, - supportsNonProjectApiKeys = true, + supportsNonProjectApiKeys, + supportsUserApiKeys = supportsNonProjectApiKeys ?? true, + supportsOrgApiKeys = false, }) { const { apiKeyAuthScheme } = getConfig(); const authAttempts = getApiKeyAuthAttempts( apiKeyAuthScheme, - supportsNonProjectApiKeys + supportsUserApiKeys, + supportsOrgApiKeys, + apiKey ); + if (authAttempts.length === 0) { + return unsupportedApiKeyAuthResponse( + apiKeyAuthScheme, + apiKey, + supportsUserApiKeys, + supportsOrgApiKeys + ); + } + let response = null; let apiAccessErrorResponse = null; for (let index = 0; index < authAttempts.length; index++) { @@ -986,6 +1076,8 @@ export const tools = (sdk) => { * @param {unknown} [opts.body] * @param {number} [opts.timeoutMs] * @param {boolean} [opts.supportsNonProjectApiKeys] + * @param {boolean} [opts.supportsUserApiKeys] + * @param {boolean} [opts.supportsOrgApiKeys] * @returns {Promise<{ status: number; data: unknown }>} */ function callComposio({ @@ -995,7 +1087,9 @@ export const tools = (sdk) => { query, body, timeoutMs, - supportsNonProjectApiKeys = true, + supportsNonProjectApiKeys, + supportsUserApiKeys = supportsNonProjectApiKeys ?? true, + supportsOrgApiKeys = false, }) { const cfg = getConfig(); return requestComposio({ @@ -1004,7 +1098,8 @@ export const tools = (sdk) => { method, body, timeoutMs: timeoutMs ?? cfg.timeoutMs, - supportsNonProjectApiKeys, + supportsUserApiKeys, + supportsOrgApiKeys, }); } diff --git a/plugins/composio-direct/manifest.json b/plugins/composio-direct/manifest.json index 456d671..1245858 100644 --- a/plugins/composio-direct/manifest.json +++ b/plugins/composio-direct/manifest.json @@ -1,7 +1,7 @@ { "id": "composio-direct", "name": "Composio Direct", - "version": "1.9.4", + "version": "1.9.5", "description": "Direct access to 1000+ Composio automation tools plus v3.1 toolkits, files, triggers, webhooks, connection reuse, and meta-tools without MCP transport", "author": { "name": "xlabtg", @@ -15,7 +15,7 @@ "composio_api_key": { "required": true, "env": "COMPOSIO_DIRECT_COMPOSIO_API_KEY", - "description": "Composio project, user, or organization API key (create at https://app.composio.dev/settings)" + "description": "Composio project or user API key (create at https://app.composio.dev/settings)" } }, "defaultConfig": { diff --git a/plugins/composio-direct/package.json b/plugins/composio-direct/package.json index 59c0793..7d25259 100644 --- a/plugins/composio-direct/package.json +++ b/plugins/composio-direct/package.json @@ -1,7 +1,7 @@ { "name": "teleton-plugin-composio-direct", "type": "module", - "version": "1.9.4", + "version": "1.9.5", "private": true, "description": "Teleton plugin for direct Composio API access" } diff --git a/plugins/composio-direct/test/unit/composio-direct.test.js b/plugins/composio-direct/test/unit/composio-direct.test.js index c1d171a..966ab5c 100644 --- a/plugins/composio-direct/test/unit/composio-direct.test.js +++ b/plugins/composio-direct/test/unit/composio-direct.test.js @@ -149,7 +149,7 @@ describe("manifest", () => { assert.ok(manifest.name, "manifest.name is set"); assert.ok(manifest.version, "manifest.version is set"); assert.ok(manifest.secrets?.composio_api_key, "secret composio_api_key declared"); - assert.equal(manifest.version, "1.9.4"); + assert.equal(manifest.version, "1.9.5"); assert.equal(manifest.defaultConfig?.base_url, "https://backend.composio.dev/api/v3.1"); assert.equal(manifest.defaultConfig?.api_key_auth_scheme, "auto"); }); diff --git a/plugins/composio-direct/tests/index.test.js b/plugins/composio-direct/tests/index.test.js index 4a9f8e5..24ea23b 100644 --- a/plugins/composio-direct/tests/index.test.js +++ b/plugins/composio-direct/tests/index.test.js @@ -109,7 +109,7 @@ describe("composio-direct Teleton integration", () => { const sdk = makeSdk(); const toolList = toolsFactory(sdk); - assert.equal(manifest.version, "1.9.4"); + assert.equal(manifest.version, "1.9.5"); assert.equal(manifest.defaultConfig.base_url, "https://backend.composio.dev/api/v3.1"); assert.equal(manifest.defaultConfig.api_key_auth_scheme, "auto"); assert.deepEqual( @@ -572,37 +572,11 @@ describe("composio-direct Teleton integration", () => { } }); - it("retries execute with x-org-api-key when project and user headers are rejected", async () => { - const { calls, restore } = mockFetch((call, idx) => { - if (idx === 1) { - assert.equal(call.headers["x-api-key"], "test-api-key"); - assert.equal(call.headers["x-user-api-key"], undefined); - assert.equal(call.headers["x-org-api-key"], undefined); - return { - status: 403, - data: { - error: { - message: "The API key doesn't have permissions to perform the request.", - slug: "HTTP_Forbidden", - status: 403, - }, - }, - }; - } - - if (idx === 2) { - assert.equal(call.headers["x-api-key"], undefined); - assert.equal(call.headers["x-user-api-key"], "test-api-key"); - assert.equal(call.headers["x-org-api-key"], undefined); - return { - status: 401, - data: { message: "Unauthorized" }, - }; - } - + it("uses x-user-api-key first in auto mode when the key has the user prefix", async () => { + const { calls, restore } = mockFetch((call) => { assert.equal(call.headers["x-api-key"], undefined); - assert.equal(call.headers["x-user-api-key"], undefined); - assert.equal(call.headers["x-org-api-key"], "test-api-key"); + assert.equal(call.headers["x-user-api-key"], "uak_test-user-key"); + assert.equal(call.headers["x-org-api-key"], undefined); return { status: 200, data: { @@ -613,7 +587,9 @@ describe("composio-direct Teleton integration", () => { }); try { - const executeTool = toolsFactory(makeSdk()).find((tool) => tool.name === "composio_execute_tool"); + const executeTool = toolsFactory(makeSdk({ apiKey: "uak_test-user-key" })).find( + (tool) => tool.name === "composio_execute_tool" + ); const result = await executeTool.execute( { tool_slug: "github_list_repos", parameters: {} }, makeContext() @@ -621,7 +597,7 @@ describe("composio-direct Teleton integration", () => { assert.equal(result.success, true); assert.equal(result.data.ok, true); - assert.equal(calls.length, 3); + assert.equal(calls.length, 1); } finally { restore(); } @@ -657,32 +633,24 @@ describe("composio-direct Teleton integration", () => { } }); - it("uses x-org-api-key first when api_key_auth_scheme is org", async () => { - const { calls, restore } = mockFetch((call) => { - assert.equal(call.headers["x-api-key"], undefined); - assert.equal(call.headers["x-user-api-key"], undefined); - assert.equal(call.headers["x-org-api-key"], "test-api-key"); - return { - status: 200, - data: { - successful: true, - data: { ok: true }, - }, - }; + it("rejects organization API keys before calling regular tool endpoints", async () => { + const { calls, restore } = mockFetch(() => { + throw new Error("organization API keys must not be sent to tools/execute"); }); try { const executeTool = toolsFactory( - makeSdk({ pluginConfig: { api_key_auth_scheme: "organization" } }) + makeSdk({ apiKey: "oak_test-org-key" }) ).find((tool) => tool.name === "composio_execute_tool"); const result = await executeTool.execute( { tool_slug: "github_list_repos", parameters: {} }, makeContext() ); - assert.equal(result.success, true); - assert.equal(result.data.ok, true); - assert.equal(calls.length, 1); + assert.equal(result.success, false); + assert.match(result.error, /organization API key/i); + assert.match(result.error, /project or user API key/i); + assert.equal(calls.length, 0); } finally { restore(); } @@ -706,19 +674,9 @@ describe("composio-direct Teleton integration", () => { }; } - if (idx === 2) { - assert.equal(call.headers["x-api-key"], undefined); - assert.equal(call.headers["x-user-api-key"], "test-api-key"); - assert.equal(call.headers["x-org-api-key"], undefined); - return { - status: 401, - data: { message: "Unauthorized" }, - }; - } - assert.equal(call.headers["x-api-key"], undefined); - assert.equal(call.headers["x-user-api-key"], undefined); - assert.equal(call.headers["x-org-api-key"], "test-api-key"); + assert.equal(call.headers["x-user-api-key"], "test-api-key"); + assert.equal(call.headers["x-org-api-key"], undefined); return { status: 401, data: { message: "Unauthorized" }, @@ -736,7 +694,7 @@ describe("composio-direct Teleton integration", () => { assert.equal(result.auth, undefined); assert.match(result.error, /permission denied/i); assert.match(result.error, /permissions/i); - assert.equal(calls.length, 3); + assert.equal(calls.length, 2); } finally { restore(); } @@ -771,6 +729,25 @@ describe("composio-direct Teleton integration", () => { } }); + it("rejects user API keys before calling project-only files endpoints", async () => { + const { calls, restore } = mockFetch(() => { + throw new Error("user API keys must not be sent to project-only files endpoints"); + }); + + try { + const listFiles = toolsFactory(makeSdk({ apiKey: "uak_test-user-key" })).find( + (tool) => tool.name === "composio_list_files" + ); + const result = await listFiles.execute({ toolkit: "gmail" }, makeContext()); + + assert.equal(result.success, false); + assert.match(result.error, /project API key/i); + assert.equal(calls.length, 0); + } finally { + restore(); + } + }); + it("passes connected_account_id in multi_execute HTTP body", async () => { const { calls, restore } = mockFetch(() => ({ status: 200,