Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/integration-composio

Adapter/Tooling

The Composio IntegrationProvider backend, the first catalog-owning connector backend: 1000+ apps' tools and triggers come from Composio, which also runs the tool and holds the credentials.

What it does

@nightowlsdev/integration-composio implements the `IntegrationProvider` seam from `@nightowlsdev/connectors` against Composio, and it is the first backend that declares `ownsCatalog: true`: the tools and triggers are Composio's typed catalog rather than your own `defineConnector` definitions, and Composio executes the call and stores the user's credentials in its cloud. That makes it an optional, managed convenience, never the OSS default, the open defaults stay `@nightowlsdev/integration-native` (self-run OAuth, credentials on your infrastructure) or self-hosted Nango. The Composio SDK is INJECTED as a narrow `ComposioClient` seam, so the package carries no vendor dependency, stays engine-wall clean, and is hermetically unit-testable (the host wires the real `@composio/core`). `listTools` scopes the catalog to the connection's own app by default so a call never dumps thousands of tools into model context, then applies your optional `allow` predicate. `executeTool` maps failures onto the connector error model, a dead credential becomes a `ConnectorConnectionError` (re-auth), a transient or 429 becomes a `ConnectorRetryableError` (back off), anything else becomes a `ToolResult.error`, and success data is fenced before it reaches model context, because output from tools you did not author needs fencing more, not less. `startConnect` / `resolveConnection` / `revokeConnection` / `refreshStatus` map onto Composio's connection lifecycle; `handleInboundWebhook` appears only when you inject `parseTriggerEvent` (Composio is a ROUTED backend, so the host verifies Composio's own webhook signature at the HTTP layer first, then this parses the envelope for `dispatchTriggerEvent`), and `subscribeTrigger` appears only when the injected client can `enableTrigger`. To offer Composio for some apps and the open backends for others, compose them with `compositeProvider` rather than merging by hand.

Install

pnpm add @nightowlsdev/integration-composio

Key exports

  • composioProvider
  • types: ComposioProviderOpts, ComposioClient, ComposioToolSpec, ComposioListToolsInput, ComposioExecuteInput, ComposioExecuteResult, ComposioConnection

Usage

integration-composio.ts
import { composioProvider, type ComposioClient } from "@nightowlsdev/integration-composio";
import { compositeProvider, materializeIntegration } from "@nightowlsdev/connectors";
import { Composio } from "@composio/core"; // the SDK is INJECTED — this package has no vendor dep

// Wire the SDK to the narrow ComposioClient seam (listTools/executeTool/connections) — the
// package never imports the vendor, so the adapter is yours and the SDK can move under you.
const sdk = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });
const client: ComposioClient = {
  listTools: async (q) => toToolPage(await sdk.tools.get(q.entityId, { apps: q.apps, cursor: q.cursor })),
  // connectionId PINS execution to the resolved connected account — forward it, never execute against "any".
  executeTool: (q) => sdk.tools.execute(q.toolName, { userId: q.entityId, connectedAccountId: q.connectionId, arguments: q.args }),
  initiateConnection: (q) => sdk.connectedAccounts.initiate({ userId: q.entityId, appName: q.app }),
  getConnection: (q) => findActive(sdk.connectedAccounts, q),
  disconnect: (q) => revoke(sdk.connectedAccounts, q),
};

// Composio owns the catalog (ownsCatalog: true): its 1000+ apps' tools, its execution, its credentials.
const composio = composioProvider({
  composio: client,
  entityIdFor: (ref) => `${ref.tenantId}:${ref.userId ?? "org"}`,
  allow: (t) => !t.name.endsWith("_DELETE"),        // optional, on top of the per-connection scoping
  parseTriggerEvent,                                 // optional: enables handleInboundWebhook
});

// Managed convenience, not the OSS default — route the sensitive apps to an open backend instead.
const provider = compositeProvider({
  backends: { native: nativeProvider(/* … */), composio },
  routeNew: (p) => (SENSITIVE.has(p) ? "native" : "composio"),
  lookupConnectionBackend,
});

const swarm = defineSwarm({ agents, connectorTools: materializeIntegration(provider, ctxToRefs) });