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
2 changes: 2 additions & 0 deletions plugins/composio-direct/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ 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` project key, its endpoint permissions, and any Composio IP allowlist.

## Security Rules

- Never ask the user to paste OAuth tokens or Composio API keys into chat.
Expand Down
1 change: 1 addition & 0 deletions plugins/composio-direct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,4 @@ 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 API key permissions and any Composio IP allowlist before retrying.
60 changes: 52 additions & 8 deletions plugins/composio-direct/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const COMPOSIO_EXECUTION_GUIDANCE = {

export const manifest = {
name: "composio-direct",
version: "1.9.1",
version: "1.9.2",
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 Down Expand Up @@ -187,12 +187,18 @@ async function fetchWithRetry({ url, method, headers, body, timeoutMs, log }) {
* @returns {boolean}
*/
function isAuthError(response) {
if (response.status === 401 || response.status === 403) return true;
if (isAuthRequiredPayload(response.data)) return true;

// Composio reserves HTTP 401/403 for API-key authentication and permission
// failures. Toolkit OAuth/account authorization is returned through explicit
// auth_required payloads or connected-account messages instead.
if (response.status === 401 || response.status === 403) return false;

const msg = getComposioMessage(response.data).toLowerCase();
if (msg) {
return (
msg.includes("auth") ||
msg.includes("auth_required") ||
msg.includes("authorization required") ||
msg.includes("connect") ||
msg.includes("connection") ||
msg.includes("not connected") ||
Expand All @@ -203,6 +209,37 @@ function isAuthError(response) {
return false;
}

/**
* Detect Composio API-key authentication/permission errors.
* @param {{ status: number; data: unknown }} response
* @returns {boolean}
*/
function isComposioApiAccessError(response) {
return response.status === 401 || response.status === 403;
}

/**
* Format Composio API-key authentication/permission failures with actionable
* guidance and without exposing the key.
* @param {{ status: number; data: unknown }} response
* @returns {string}
*/
function formatComposioApiAccessError(response) {
const message = getComposioMessage(response.data) || `HTTP ${response.status}`;
if (response.status === 403) {
return (
"Composio API key permission denied. Check that composio_api_key is a " +
"project API key with permissions for this endpoint 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 API key. Composio response: ${message}`
);
}

/**
* Detect if a Composio response payload contains an auth_required flag,
* even when the HTTP status is 200 and successful may be true.
Expand Down Expand Up @@ -840,7 +877,9 @@ export const tools = (sdk) => {
function failedResponse(action, response) {
return {
success: false,
error: getComposioMessage(response.data) || `${action}: HTTP ${response.status}`,
error: isComposioApiAccessError(response)
? formatComposioApiAccessError(response)
: (getComposioMessage(response.data) || `${action}: HTTP ${response.status}`),
data: {
status: response.status,
},
Expand Down Expand Up @@ -1023,7 +1062,9 @@ export const tools = (sdk) => {
sdk.log.debug(`composio_search_tools: HTTP ${response.status}`);
return {
success: false,
error: `Composio API returned HTTP ${response.status}. Please check your API key and try again.`,
error: isComposioApiAccessError(response)
? formatComposioApiAccessError(response)
: `Composio API returned HTTP ${response.status}. Please check your API key and try again.`,
};
}

Expand Down Expand Up @@ -1310,8 +1351,9 @@ export const tools = (sdk) => {

if (response.status !== 200) {
const errMsg =
getComposioMessage(response.data) ||
`HTTP ${response.status}`;
isComposioApiAccessError(response)
? formatComposioApiAccessError(response)
: (getComposioMessage(response.data) || `HTTP ${response.status}`);
sdk.log.debug(`composio_execute_tool: error response ${response.status}`);
return { success: false, error: `Tool execution failed: ${errMsg}` };
}
Expand Down Expand Up @@ -1509,7 +1551,9 @@ export const tools = (sdk) => {
}

if (response.status !== 200) {
const errMsg = getComposioMessage(response.data) || `HTTP ${response.status}`;
const errMsg = isComposioApiAccessError(response)
? formatComposioApiAccessError(response)
: (getComposioMessage(response.data) || `HTTP ${response.status}`);
results[globalIdx] = {
tool_slug: normalizedSlug,
success: false,
Expand Down
2 changes: 1 addition & 1 deletion plugins/composio-direct/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "composio-direct",
"name": "Composio Direct",
"version": "1.9.1",
"version": "1.9.2",
"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",
Expand Down
2 changes: 1 addition & 1 deletion plugins/composio-direct/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "teleton-plugin-composio-direct",
"type": "module",
"version": "1.9.1",
"version": "1.9.2",
"private": true,
"description": "Teleton plugin for direct Composio API access"
}
8 changes: 4 additions & 4 deletions plugins/composio-direct/test/integration/composio-api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ describe("rate limit handling", () => {
// ---------------------------------------------------------------------------

describe("auth error flow", () => {
it("returns structured auth error with connect_url on 401", async () => {
it("returns structured auth error with connect_url when Composio reports a missing connected account", async () => {
const restore = mockFetchFactory(async () => ({
status: 401,
data: { message: "Not authenticated. Connect your GitHub account." },
status: 400,
data: { message: "No connected account found. Connect your GitHub account." },
}));

try {
Expand Down Expand Up @@ -207,7 +207,7 @@ describe("multi-execute batching", () => {
});

it("stops after first failure with fail_fast=true", async () => {
// Return 401 (auth error) which is NOT retried, so the first tool definitely fails
// Return 401 (API-key error) which is NOT retried, so the first tool definitely fails
let callCount = 0;
const restore = mockFetchFactory(async () => {
callCount++;
Expand Down
11 changes: 8 additions & 3 deletions plugins/composio-direct/test/unit/composio-direct.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.1");
assert.equal(manifest.version, "1.9.2");
assert.equal(manifest.defaultConfig?.base_url, "https://backend.composio.dev/api/v3.1");
});
});
Expand Down Expand Up @@ -296,8 +296,13 @@ describe("composio_execute_tool", () => {
}
});

it("returns structured auth error when 401", async () => {
const restore = mockFetch([{ status: 401, data: { message: "Unauthorized" } }]);
it("returns structured auth error when Composio reports a missing connected account", async () => {
const restore = mockFetch([
{
status: 400,
data: { message: "No connected account found. Connect your GitHub account." },
},
]);

try {
const sdk = makeSdk();
Expand Down
33 changes: 32 additions & 1 deletion plugins/composio-direct/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("composio-direct Teleton integration", () => {
const sdk = makeSdk();
const toolList = toolsFactory(sdk);

assert.equal(manifest.version, "1.9.1");
assert.equal(manifest.version, "1.9.2");
assert.equal(manifest.defaultConfig.base_url, "https://backend.composio.dev/api/v3.1");
assert.deepEqual(
toolList.map((tool) => tool.name).sort(),
Expand Down Expand Up @@ -430,6 +430,37 @@ describe("composio-direct Teleton integration", () => {
}
});

it("does not treat Composio API key permission failures as toolkit auth_required", async () => {
const { restore } = mockFetch(() => ({
status: 403,
data: {
error: {
message: "The API key doesn't have permissions to perform the request.",
slug: "HTTP_Forbidden",
status: 403,
request_id: "req_permission_denied",
suggested_fix: "Check the API key permissions or IP allowlist.",
},
},
}));

try {
const executeTool = toolsFactory(makeSdk()).find((tool) => tool.name === "composio_execute_tool");
const result = await executeTool.execute(
{ tool_slug: "github_list_repos", parameters: {} },
makeContext()
);

assert.equal(result.success, false);
assert.notEqual(result.error, "auth_required");
assert.equal(result.auth, undefined);
assert.match(result.error, /API key/i);
assert.match(result.error, /permissions/i);
} finally {
restore();
}
});

it("passes connected_account_id in multi_execute HTTP body", async () => {
const { calls, restore } = mockFetch(() => ({
status: 200,
Expand Down
Loading