> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Connected accounts

> Let each signed-in user connect their own Gmail, Slack, or GitHub through Composio so every tool call runs as that user, isolated per principal.

External connector tools (Gmail, Slack, GitHub, and anything else exposed
through Composio) execute as the signed-in user, not as one shared bot
account. Each user connects their own account once, and every later tool call
rides that connection.

Composio is the sole broker. Vendo builds no OAuth flows of its own. A
connection is a Composio connected account whose `entityId` is the Vendo
principal subject, so isolation is structural: every list, initiate, and
disconnect operation is scoped to exactly one subject. No token ever enters
Vendo — the broker holds the credentials, and initiate hands back only the
hosted redirect URL.

## Setup

You have two options: bring your own Composio key, or ride the Vendo Cloud
broker.

**Bring your own key.** Pass `composioConnector` to `createVendo`:

```ts theme={null}
import { createVendo } from "@vendoai/vendo/server";
import { composioConnector } from "@vendoai/actions";

const vendo = createVendo({
  model,
  principal,
  connectors: [
    composioConnector({
      apiKey: process.env.COMPOSIO_API_KEY!,
      apps: ["gmail", "slack"],
    }),
  ],
});
```

**Vendo Cloud broker.** Set `VENDO_API_KEY` and ship no Composio key of your
own. Connection endpoints then ride the Vendo Cloud broker using Vendo's
Composio credentials, so cloud users bring zero keys. A BYO
`composioConnector` always wins when both are configured, because connections
must live where the connector's tools execute.

`GET /api/vendo/status` reports the active posture under `blocks.connections`:
`"byo"`, `"cloud"`, or `false` when neither path is wired.

## The in-flow connect card

When a tool call needs a connection the user does not have yet, the connector
returns a typed `connect-required` outcome instead of an opaque error. The
shipped `VendoThread` renders an inline connect card next to the tool call,
following the same pattern as approvals:

1. The user selects **Connect**. The card opens the broker's hosted OAuth
   page in a popup.
2. The card polls the connection until it becomes active.
3. The thread retries the original tool call through the fresh connection —
   no re-prompting required.

Automations and MCP door calls do not have a browser surface, so they surface
`connect-required` as an actionable in-band error that names the connector and
toolkit the user needs to connect.

## The settings panel

`ConnectedAccountsPanel` (exported from `@vendoai/ui/chrome`) lists a user's
connected accounts and disconnects them. Disconnect severs the account on the
broker.

`VendoPage` chrome mounts the panel automatically under a new **Accounts**
tab. If you render your own chrome, drop the panel in wherever you already
handle user settings, or read the same state through the `useConnections`
hook from `@vendoai/ui`.

## Connection endpoints

Every connection route is per-principal. The wire uses exactly the resolved
principal's subject — there is no caller-supplied subject and no cross-user
read.

| Route                                   | Method | Purpose                                                                    |
| --------------------------------------- | ------ | -------------------------------------------------------------------------- |
| `/api/vendo/connections`                | GET    | list the caller's connected accounts                                       |
| `/api/vendo/connections/initiate`       | POST   | `{ toolkit, connector?, callbackUrl? }` → `{ id, connector, redirectUrl }` |
| `/api/vendo/connections/:id?connector=` | GET    | status; poll while connecting                                              |
| `/api/vendo/connections/:id?connector=` | DELETE | disconnect                                                                 |

Anonymous (ephemeral) visitors cannot initiate a connection — an external
account would outlive the session it was minted for. Synthetic subjects
(webhook principals, reserved `webhook:` and `vendo:` prefixes) are refused
for the same reason.

## Risk labels for Composio tools

Composio tools now carry a curated risk instead of the previous blanket
`write` default. The connector resolves risk in three steps:

1. **Metadata hints.** Composio `destructiveHint` and `readOnlyHint` tags
   take precedence when present.
2. **Slug patterns.** Destructive verbs anywhere in the slug (`DELETE`,
   `REMOVE`, `DESTROY`, and similar) mark the tool destructive. Leading read
   verbs (`GET`, `LIST`, `FETCH`, and similar) mark it read.
3. **Conservative default.** Anything else falls back to `write`.

`.vendo/overrides.json` still wins over the curated map, so you can pin risk
per tool.

## MCP connector credentials

`mcpConnector({ headers })` accepts either shared static headers (the simple
default) or an async per-principal resolver. Use the resolver when the MCP
server binds authentication to the session and you need each user to get
their own MCP session:

```ts theme={null}
import { mcpConnector } from "@vendoai/actions";

mcpConnector({
  url: "https://mcp.example.com",
  headers: async ({ principal, presence, grant }) => ({
    authorization: `Bearer ${await tokenFor(principal)}`,
  }),
});
```

With a resolver, each subject gets its own cached MCP session (bounded by an
LRU of 500 subjects). Descriptor listing still resolves without a principal,
so tool discovery keeps working.

Every connector execution is audited with its account identity — connector,
toolkit, `entityId`, and whether the credential was shared or per-principal —
in the tool-call event's `detail`. The identity is stripped from the outcome
the model and UI see, so no broker identifier leaks into a message.
