Skip to content
Pilotoo
v0.1

Connections

An agent that triages issues, reads a mailbox or files a ticket needs a credential for someone else’s service. Handing the model a token puts the token in the prompt, the logs and the checkpoint. Connections keep it in a vault on the server instead.

A connection is one account at one external provider, such as GitHub or Gmail, linked with OAuth 2 (PKCE) or an API key. The credential is stored encrypted and refreshed before it expires. Callers use it through two tools, connections_search and connections_execute; they never receive the credential.

A user may connect several accounts of one provider. An administrator may also connect an organisation account that members use but cannot connect or remove.

You want Use Read
Connections inside the agent server you already run connections option of PilotooModule.forRoot Server integration
Connections in your own NestJS app, without the agent server @pilotoo/connections-core and @pilotoo/connections-server Using the library
One shared service that many agent servers, desktops and MCP clients call Pilotoo Connect, or your own host with the public API Pilotoo Connect, Tools API and MCP, Remote sources

A user links their GitHub account, then asks an agent about their issues.

apps/api/src/app.module.ts
import { githubProvider, PilotooModule } from '@pilotoo/server';
PilotooModule.forRoot({
connections: {
providers: [githubProvider()],
tools: true,
},
});
.env
CONNECTIONS_ENCRYPTION_KEYS=v1:<32 random bytes, base64>
CONNECTIONS_CALLBACK_URL=https://api.example.com/connections/oauth/callback
CONNECTIONS_RETURN_ORIGINS=https://app.example.com
CONNECTIONS_ROLES=member,owner
CONNECTIONS_ADMIN_ROLES=owner

Generate the key with node -e "console.log('v1:' + require('crypto').randomBytes(32).toString('base64'))".

Register an OAuth app at GitHub with the callback URL above. Then, as a user with an admin role, enable the provider with the app’s client id and secret:

Terminal window
curl -X PUT https://api.example.com/admin/connections/providers/github \
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{ "enabled": true, "clientId": "<client id>", "clientSecret": "<client secret>" }'

The settings UI does the same on an app’s page in ConnectionsAdminSection; see Settings UI. The Setup step also shows a “How to set up” guide for each app that defines one, with the exact callback URL to register.

In your settings screen, the user selects Connect on GitHub. A popup opens GitHub’s consent page and the callback page closes it.

src/settings.tsx
<ConnectionsSection
client={connections}
onConnect={(provider) => connectProvider({ client: connections, provider, opener: browserOpener, allowedOrigin })}
/>

List the two tools in an agent definition:

tools: [connections_search, connections_execute]

The user asks “what issues are open in acme/api?”. The agent searches, then executes:

{ "tool": "connections_search", "arguments": { "query": "list issues" } }
{ "tool": "connections_execute", "arguments": { "name": "github__list_issues", "arguments": { "owner": "acme", "repo": "api" } } }

The server opens the user’s sealed token, calls GitHub and returns { "ok": true, "result": [...] }. The model never sees the token.

Role Does Through
Host developer Defines which providers exist and registers their tools in code. connections option, or connectionProviders
Tenant admin Enables a provider, supplies its OAuth client, narrows scopes and tools, connects organisation accounts, can revoke any connection. /admin/connections, ConnectionsAdminSection
User Connects their own accounts, picks the default, disconnects. /connections, ConnectionsSection
Caller Finds and calls provider tools as the user: an agent run, an MCP client or a script. connections_search, connections_execute

Access is role-gated twice: CONNECTIONS_ROLES decides who may connect and use tools, CONNECTIONS_ADMIN_ROLES who administers. Both are empty by default, which means nobody. See Authentication for roles.

  • A credential is never returned by any route or tool, and never written to state, the checkpoint or the stream.
  • Every refusal comes back as data, { ok: false, error: { code, message } }, never as a thrown error.
  • A connection belongs to one tenant. No read or write crosses tenants.
Situation Result
No CONNECTIONS_ENCRYPTION_KEYS Nothing can be connected; routes answer unavailable (501).
No database and no connections.storage Same: the routes answer 501.
Provider not enabled for the tenant provider_disabled; the tool is hidden from search.
Refresh token refused by the provider The connection becomes needs_reauth; the user reconnects.
Caller holds no connection role forbidden.