Connections and integrations

Last updated October 5, 2026

On this page

Agents act on the outside world through tools: send an email, refund an invoice, reply to a ticket, comment on a pull request. Each tool belongs to a tool server, the outside service that carries the tool out. A connection tells OrchKernel where that server is and which credentials reach it. This page covers the ways to reach an outside system, how a module's tools are routed to a connection, the rules every connection keeps, and what happens to a call that has nowhere to go.

Every call still goes through the gate first, whichever way it travels: policy, delegation, budgets, kill switches and approvals work the same (see How the gate decides). A module is one packaged part of the company's work, such as Support operations (see Modules). What is not possible yet is listed at the end.

Three ways to reach a system#

MCP (Model Context Protocol) is a common way for a service to offer tools to AI agents. A service that speaks it is an MCP server.

Way What it is Where you set it up Use it when
Remote MCP connection An MCP server reached over HTTP, with at most one sealed secret Modules, Connections tab, or ok connection add --url The vendor or your team runs an MCP server for the system
Adapter connection (kind http) The vendor's own REST API, which OrchKernel calls through an adapter: a built-in description of that API Modules, Connections tab, choosing an adapter, or ok connection add --http There is no MCP server, but there is a built-in adapter (Zendesk, Salesforce, Google, GitHub, Stripe, Slack, Gmail, Google Drive) or a pack brings one
mcp.yaml server A server listed in a file the operator writes, over HTTP or as a local program (stdio) mcp.yaml next to the server, read at start The server is a local program, or you want the setup kept in a file. Shown in the app read-only

Only an admin who is a person can see, add, change or remove connections. Anyone else who calls the API gets "connections are admin-only"; from the command line a change is refused with "connections are changed by human admins only".

The Connections tab#

Sign in as ada, open Modules and choose the Connections tab. The demo has twelve connections, demo ads to demo stripe, each pointing at a local fake on http://127.0.0.1:<port>/mcp with no secret. Rows are sorted by name.

Column Shows
Name The name, its id (conn:demo-billing), a from mcp.yaml badge for file servers, and the adapter (for example Zendesk) for an adapter connection
Endpoint The URL, and for an adapter connection its auth scheme (basic)
Secret None or Set for an MCP connection; the names held (token, username) or None for an adapter connection; Locked or Needs new credentials when it cannot be used (below)
Last test never; Ok · 2 tools for an MCP server; Ok · 200 · 4 ms for an adapter; or Failed with the time and the reason
Tools Every installed tool routed to this connection

Each row has Test, Edit and Remove; rows from mcp.yaml have none.

Try it:

  1. On the demo billing row, press Test. A note says "demo billing answered with 2 tools" and Last test shows Ok · 2 tools with the time.
  2. On the same row, press Remove. A dialog asks "Remove demo billing? Modules that bind it must be changed first." Press Remove in it.
  3. The removal is refused, because modules use the connection. A note says "connection conn:demo-billing is in use by modules support, support-ops (tools billing.refund, support-ops.billing.account, support-ops.billing.refund)". The dialog stays open; press Cancel.

Remove works only for a connection no module uses.

On a phone the table scrolls sideways inside its frame; Test, Edit and Remove are at the far right.

Adding a remote MCP connection#

  1. On Modules, Connections, choose Add a connection. The sheet opens with Adapter set to None: a remote MCP server.

  2. Fill in:

    Field What it does
    Name Shown everywhere. The id is made from it: "Acme Google" becomes conn:acme-google.
    Endpoint (Streamable HTTP) The server's URL, for example https://mcp.example.com/mcp.
    Header Where the secret is sent. Empty means Authorization.
    Scheme Put before the secret in that header. Empty with Authorization sends Bearer <secret>; empty with another header sends the secret as it is.
    Secret Write-only. Sealed when you save and never shown again. Leave it empty in the demo (see below).
  3. Choose Add. A note says "Added Acme Google" and the row appears with Secret None and Last test never.

  4. Press Test. OrchKernel connects, runs the MCP handshake and lists the server's tools, and the row shows Ok with the number of tools. A failure shows Failed and the reason. In the demo, mcp.example.com does not exist, so you see Failed and "MCP server conn:acme-google: cannot reach https://mcp.example.com/mcp: ... Dns Failed ...". Use a real server's URL to see a passing test.

A secret can be stored only when the server has a secrets key (see The secrets key). Without one, Add shows "a secret cannot be stored: OK_SECRETS_KEY is not set" in the sheet and nothing is saved. The demo starts without a key, so in the demo you can add connections without secrets only, unless you start it with one.

When you edit a connection, changing its endpoint, header or scheme clears the stored secret, because the secret is sealed to that endpoint. The form warns "A new endpoint, header or scheme clears the secret: enter it again here." Leaving Secret empty otherwise keeps the one stored.

Adding an adapter connection#

An adapter lets OrchKernel call a vendor's REST API directly. Calls through it are governed, approved and logged like any other tool call.

  1. Add a connection, then pick the adapter in Adapter, for example zendesk · helpdesk. Each entry shows what the adapter serves; the ones used for context capture (gdrive, gmail, slack) show source.
  2. A card describes the adapter: Built-in or From a pack, a Webhooks badge if it takes webhooks, and a line such as "Serves helpdesk. Requests go only to the URL's origin, which must match *.zendesk.com. Keeps records for tickets (tickets)."
  3. Fill in Name and Base URL, any settings the adapter declares (each checked against its pattern as you type), and Authentication.
  4. Under Secrets, fill in the named secrets the adapter uses. Each shows Not set or Set. In the demo, leave them empty: without a secrets key they cannot be stored.
  5. Choose Add, then Test on the row. The test runs the adapter's read-only check (for Zendesk, the current user) and keeps the status and time, for example Ok · 200 · 4 ms. A vendor error shows as "the vendor answered status 404". In the demo, with no secret, the test fails with "the connection has no secret token".

The base URL is where the adapter's paths start. For most built-ins it is just the vendor origin, because the adapter's paths already carry the API version. The adapter, what it serves, its host pattern, auth schemes, secrets and webhooks below are what the adapter library reports; the base URLs have not been tried against a live account (see How far the built-ins are tested).

Adapter Serves Base URL Auth schemes Secrets Webhooks
zendesk helpdesk tools; can keep tickets https://<you>.zendesk.com basic, bearer username (as email/token), token, webhook_secret yes
salesforce crm; can keep leads and contacts https://<you>.my.salesforce.com OAuth refresh token; setting api_version (default v61.0) client_id, client_secret, refresh_token no
google google tools: send email, create event, search mail, list events https://www.googleapis.com OAuth refresh token, service account client_id, client_secret, refresh_token, service_account no
github github tools https://api.github.com bearer token, webhook_secret yes
stripe stripe and billing tools https://api.stripe.com bearer token, webhook_secret yes
slack context capture: channels https://slack.com/api bearer token no
gmail context capture: a mailbox https://gmail.googleapis.com/gmail/v1 service account, OAuth refresh token as google no
gdrive context capture: folders https://www.googleapis.com/drive/v3 service account, OAuth refresh token as google no

A built-in can answer some tools with unsupported when the vendor has no matching endpoint. GitHub's dependencies is one: the form says "Always answers unsupported, since no github endpoint matches: github.dependencies", and a module's Connections tab says "Unsupported by this connection's adapter, always fails" beside the tool.

What is checked when you save, with the message you see:

Mistake Refused with
URL host does not match the adapter "url host acme.example.com does not match adapter zendesk's host pattern *.zendesk.com"
An auth scheme the adapter does not take (API or CLI; the form lists only the ones it takes) "adapter zendesk does not accept auth scheme oauth2_refresh; it accepts basic, bearer"
A secret name the adapter does not use (API or CLI) "secret apikey is not one adapter zendesk uses (username, token, webhook_secret)"
Changing the adapter later (API or CLI; the edit form does not offer it) "adapter cannot change after a connection is created (connection conn:acme-zendesk has zendesk)". Add another connection instead.

Changing an adapter connection's URL or auth clears every secret it holds. The form warns before you save ("A new URL or authentication clears every secret this connection holds (token, username): enter them again here.") and the note after saving names them ("cleared token, username: enter them again").

Keep capture and sending on separate connections: the connection a context source reads mail from should not be the one a module sends mail with, so each can be revoked alone (see Context capture).

Auth schemes#

The names are as they appear in the Authentication list.

Authentication Secrets What is sent
Bearer token token Authorization: Bearer <token>
API key in a header token; a Header name The key in the header you name (X-Api-Key)
Basic (user and token) username (or email), and password (or token) HTTP basic auth
OAuth refresh token client_id, client_secret, refresh_token; a Token URL Access tokens obtained with the refresh token
OAuth client credentials client_id, client_secret; Token URL and Scope Access tokens for the client itself
Service account (JWT bearer) service_account; Token URL, Scope, optional Shared mailbox Signed requests for a person OrchKernel chooses (below)

Access tokens live in memory only. When the vendor rejects the credentials themselves (invalid_grant, invalid_client), the connection shows Needs new credentials and its tools are refused before anyone is asked to approve a call, until you enter new secrets.

Whose data a call reads#

With Google Workspace and a service account connection, one connection can read anyone's mailbox. OrchKernel, never the adapter or the request, decides whose:

  • A tool call reads as the person the run acts for, by their directory email. When alice's Sales Assistant searches mail, it searches alice's. A run that acts for nobody is refused.
  • Context capture reads only the mailbox of a person who has opted in to that source, checked again on every call.
  • A shared source reads as the connection's Shared mailbox, which may not be any person's address.
  • Test acts for nobody, so a service account connection without a shared mailbox says it cannot be tested without a subject and sends nothing.

If two active people share one directory email, calls for either are refused.

How far the built-ins are tested#

Every built-in adapter is tested against sample requests and answers copied from the vendor's published API reference. None has been checked against a live vendor account. Field quirks, rate limits, paging and error bodies of a real account may differ. Treat your first real connection as a test: run Test, then one read-only tool, before you turn on anything that writes.

Routing a module's tools to a connection#

A connection does nothing until a module uses it. Each module lists the tool servers its tools call, and you choose a connection for each.

  1. As ada, open Modules, Installed, and choose Support operations. On its page, choose the Connections tab.
  2. The table has the columns Server, Tools, Connection and Last test. You see billing (support-ops.billing.account, support-ops.billing.refund) on demo billing, and helpdesk (support-ops.helpdesk.tickets, support-ops.helpdesk.reply) on demo helpdesk. A server with no connection shows Needs a connection.
  3. Choose Change connections. A sheet titled Connections has one list per server: Not connected, then every connection by name, with mcp.yaml servers marked (from mcp.yaml).
  4. Pick one and Save. The table updates, and Test on a row tests that connection.

Add a connection on this tab opens the same sheet as on the Modules page.

Two modules can use one connection: in the demo, Support and Support operations both send billing calls to demo billing. A module that keeps records in the other system (Salesforce leads, Zendesk tickets) lists them under the table in Records kept in the other system, with the server and object each comes from.

Two known problems: on a phone the module's Connections table is wider than the screen and does not scroll, so Test is out of reach; and the module's header still says Ok while a server needs a connection.

A call with nowhere to go is refused before approval#

A tool whose server has no connection is refused at once, before the gate, so nobody is asked to approve a call that could not run. Try it in the demo; the stub planner (the scripted stand-in the demo uses when no model key is set) is enough:

  1. As ada, open Support operations, Connections, Change connections, set billing to Not connected and Save. The billing row shows Needs a connection.
  2. Choose Sign out, and sign in as maya with her token. Open Threads and, under Talk to an agent, choose Churn-risk watcher.
  3. Type Northwind wants a refund for the double charge and press Ask agent.
  4. The run finishes at once. Open Details on the agent's answer: step 3 reads Refused: tool support-ops.billing.account: tool support-ops.billing.account needs a connection: its module binds none for server billing, and step 6 says the same for support-ops.billing.refund.
  5. Sign out and sign in as ada: no approval waits in the Inbox, and Events shows no tool_called.
  6. Bind billing to demo billing again. As maya, ask the same thing in a new conversation. This time the answer is an approval card, Issue a refund or credit, Pending, "Waiting for an admin (Ada Park)": the rule support-ops-refunds-need-admin parks it, as in the Quickstart.

In step 4 the stub planner's answer still says "Refunded after an admin approved it", $250.00, Done, because its answer is scripted. No refund was made and nothing was approved; trust the steps under Details. This is a known demo bug. A real model writes its answer from the steps; that was not checked here.

The reasons a tool is refused before approval:

You see Why Fix
"tool X needs a connection: its module binds none for server S" The module has no connection for that server Bind one on the module's Connections tab
"MCP server S is not configured; add it to mcp.yaml" The tool's server is in neither a module binding nor mcp.yaml Bind a connection, or list the server in mcp.yaml
"tool X is routed to connection C, which no longer exists" The connection was removed Bind another
"connection C is locked: its secret was sealed under a key this server does not have; enter the secret again with an update" Its secret does not open with the current key Enter the secret again
"connection C needs new credentials" The vendor rejected the OAuth credentials Enter new secrets

When ok serve starts it prints a note for each problem of this kind: "note: no MCP configuration for S; their tools are refused until they are in mcp.yaml", and "note: N routed tool(s) cannot run yet:" followed by each tool and its reason. The demo has none, so it prints neither.

Vendor webhooks#

A webhook is a request the vendor sends to OrchKernel when something happens on its side. An adapter that takes webhooks (Zendesk, GitHub, Stripe, or a pack's) lets the vendor start work in OrchKernel: a new Zendesk ticket can start the Support module's new-ticket trigger.

  1. Add the connection with its webhook_secret, the signing secret the vendor gives you. This needs a secrets key, so it cannot be done in the demo as it starts.
  2. Open Edit on it. The Vendor webhooks box shows the URL to give the vendor: "The vendor posts to https://<your server>/api/webhooks/connections/conn:acme-zendesk, signed with webhook_secret", then Signing secret set, or No signing secret yet: deliveries are refused.
  3. Bind the module's server to this connection (Support's helpdesk).
  4. Turn on the module's webhook trigger on its Automation tab.

What happens to a delivery:

Delivery Answer
Bad or missing signature, stale timestamp, unknown connection 401 "bad webhook signature", nothing recorded
Signed, but no module binds a trigger to this connection, or an event type nothing maps 202, no task
Signed, the routed trigger is off 409 trigger_disabled; not remembered, so the vendor's retry works once it is on
Signed and routed 202 with the new task id
The same delivery again (same delivery id, or the same signed bytes) 202 with the first delivery's tasks, replayed: true; nothing new

Once a module's trigger is served through a connection, the older /api/webhooks/<source> route no longer takes it. If two enabled modules bind the same connection and each has a trigger on it, a delivery fires neither; give each module its own connection.

mcp.yaml servers#

The operator can list servers in mcp.yaml (copy mcp.example.yaml), or in the file OK_MCP_CONFIG names. It is read once at start; restart after editing it.

yaml
servers:
  - name: helpdesk               # the server name the pack's tools use
    transport: http
    url: https://mcp.helpdesk.example.com/mcp
    bearer_env: HELPDESK_MCP_TOKEN   # sent as Authorization: Bearer <value>
    timeout_ms: 15000
  - name: google
    transport: stdio             # a local program OrchKernel starts on first use
    command: npx
    args: ["-y", "your-google-workspace-mcp-server"]
    env_env: { GOOGLE_OAUTH_TOKEN: GOOGLE_OAUTH_TOKEN }
  • Keys ending in _env name an environment variable, so the file holds no secrets. A variable that is not set stops every ok command: "server helpdesk: environment variable HELPDESK_MCP_TOKEN (bearer_env) is not set".
  • A misspelled key is an error too ("unknown field bearer_envv"), so a typo never silently drops a header.
  • A stdio program inherits only a short list of variables (HOME, PATH, LANG, proxy and CA settings); your provider keys are not passed on. Pass anything else with env or env_env. The stock container image has no Node or Python, so prefer HTTP servers there.
  • Each call has a deadline (timeout_ms, default 30 seconds). A timed-out call fails its step and is never resent.
  • A tool with no module binding goes to the mcp.yaml server of the same name; a module can also bind a server to an mcp.yaml entry explicitly.

mcp.yaml servers appear on the Connections tab read-only, with from mcp.yaml and a dash as the endpoint, even for an HTTP server with a URL. The demo has no mcp.yaml, so this section was not tried in it.

Federated collections#

A collection can live in the other system instead of in OrchKernel's brain (its own store of records, see The brain). Reads list or fetch records there; writes go there and OrchKernel keeps a versioned copy with who changed what. There are two ways:

  • An adapter object: Salesforce's Lead and Contact back leads and contacts, Zendesk's tickets back tickets. The module says which collections it keeps in the other system, and you bind the server to an adapter connection.
  • An MCP connector in mcp.yaml: four tools on one server (list, get, upsert and an optional field list), named by a collection declared with storage: federated.

Reads and writes go through the gate as ordinary brain reads and writes. An adapter object list stops at 5,000 rows and says the read is partial. None of the demo's modules keeps a collection in another system, so this was not tried in the demo.

Egress rules#

Egress is traffic from OrchKernel out to other systems. Every connection URL is checked when it is saved and every request is checked again before it is sent:

  • https only, no user:password@, no query string. Refused with "the URL must not carry userinfo" or "the URL must not carry a query string".
  • Every address the host resolves to must be public. Loopback, private, link-local (including the cloud metadata address 169.254.169.254), CGNAT, multicast and unique-local addresses are refused: "address 169.254.169.254 is link-local, not global unicast", "address 10.0.0.5 is private, not global unicast".
  • Redirects are never followed. Response bodies are capped (4 MB for an adapter call).
  • An adapter connection only ever reaches its URL's origin, and its token URL for OAuth.
  • OK_CONNECTION_HOSTS (for example *.zendesk.com,api.stripe.com) narrows the hosts any connection may name. Unset, any public host is allowed.
  • OK_CONNECTION_PROXY sends connection traffic through a proxy; connections never read HTTPS_PROXY.

Plain http is allowed only to a loopback address such as 127.0.0.1, and only in development mode (--dev-auth on the server, OK_DEV_AUTH=1 for CLI commands). Anything else is refused with "the URL scheme must be https (plain http is allowed only to a loopback IP literal in dev mode)". The demo runs in development mode, which is why its connections point at http://127.0.0.1:...; it says so at start: "Egress development mode: connections may reach loopback over plain http, so an admin can point one at any local service."

The secrets key#

Connection secrets are sealed (encrypted) with the key in OK_SECRETS_KEY (base64 of 32 random bytes) and bound to the connection's endpoint, so a secret opens only for the place it was entered for. The plain secret appears nowhere afterwards: not in the API, events, approvals, logs, the state file or backups.

sh
export OK_SECRETS_KEY=$(openssl rand -base64 32)   # once; keep it in your secret store
Situation What happens
No key set Connections without secrets work. Saving a secret is refused (503 secrets_key_missing). Connections that already hold one are Locked, and every command warns "OK_SECRETS_KEY is not set; 1 connection(s) are locked: conn:acme-zendesk".
The key changed or is wrong Every connection whose secret does not open is Locked: its tools are refused before approval and Test answers 423. Enter the secret again to unlock it.
Rotating the key Stop the server, run ok secrets rotate, then start it with the new key (below).
sh
export OK_SECRETS_KEY_NEW=$(openssl rand -base64 32)
ok --state /data/state.db secrets rotate --new-key-env OK_SECRETS_KEY_NEW
# rotated 1 connection(s): conn:acme-zendesk
# 16 connection(s) have no secret: ...
# now set OK_SECRETS_KEY to the new key before starting the server
export OK_SECRETS_KEY=$OK_SECRETS_KEY_NEW

Keep the key apart from your backups, and keep an old key as long as you keep backups made under it. See Self-hosting.

To try a secret in the demo, stop it, export a key, and run ok demo serve again (tokens change on every start). ok demo serve only re-points connections that still point at loopback, so one you edited keeps your URL.

The development echo#

--dev-echo-tools (or OK_DEV_ECHO_TOOLS=1) answers every tool call with {"tool": ..., "echo": <arguments>, "ok": true} and ignores mcp.yaml, so nothing reaches a real system. Every command prints:

warning: --dev-echo-tools answers every tool call with an echo; nothing reaches a real system

Under the echo no connection client is installed, so Test answers 503 no_connection_client and tools routed to connections are refused. Never use it on a server real people use: an approved email would report success without being sent. The demo does not use it; it runs local fakes behind real connections instead.

Not possible yet#

  • Checking a built-in adapter against a live vendor account: none has been.
  • Signing in to a vendor with an OAuth button. You paste the client id, secret and refresh token yourself.
  • Adding a stdio MCP server from the app: stdio servers live in mcp.yaml.
  • Editing mcp.yaml servers from the app, or reloading the file without a restart.
  • Changing a connection's kind or adapter.
  • Seeing a secret after it is saved.
  • One webhook delivery firing triggers in two modules bound to the same connection.
  • MCP resources, prompts and sampling; tool names are not checked against the server's list at start, so a misspelled tool fails on its first call.

For developers#

API#

All under /api, with Authorization: Bearer <token>, admin only.

Method and path Body Notes
GET /connections An array of connections and mcp.yaml servers (source: "mcp.yaml", read_only: true). Secrets as "set"/"none" or, for http, a list of names in secrets. Paged with limit; the next page's cursor is in the x-next-cursor header.
POST /connections MCP: { name, url, auth_header?, auth_scheme?, secret? }. Adapter: { name, url, kind: "http", adapter, auth: {scheme, ...}, settings?, secrets?: {name: value} } 422 invalid with problems; 503 secrets_key_missing.
PUT /connections/:id As POST kind and adapter cannot change. For http, secrets sets a name or removes it with null; the response's cleared lists names cleared by a URL or auth change.
DELETE /connections/:id 409 connection_in_use with tools, modules, users.
POST /connections/:id/test { ok, at, status, latency_ms, tools, error }; a failed test is still 200 with ok: false. 423 locked, 503 no_connection_client.
GET /adapters The adapter library: id, serves, auth, settings, secrets, host_pattern, objects, webhook, unsupported, required_for, source.
PUT /modules/:pack { record, version } A module's record.connections maps server to connection id (or an mcp.yaml server name).
POST /webhooks/connections/:id The vendor's body Signed per the adapter. 202 { delivery, event, replayed, tasks, trigger }, 401, 409 trigger_disabled.

Zendesk signs {timestamp}{body} with HMAC-SHA256 in base64, in X-Zendesk-Webhook-Signature and X-Zendesk-Webhook-Signature-Timestamp, within 300 seconds; the delivery id is X-Zendesk-Webhook-Invocation-Id. GitHub uses X-Hub-Signature-256 (hex), Stripe its t=,v1= header.

CLI#

ok connection works on the state file; a secret is never taken on the command line, only from an environment variable (--secret-env) or standard input (--secret-stdin, set-secret).

sh
GOOGLE_MCP_TOKEN=... ok --as ada connection add "Acme Google" \
    --url https://mcp.acme.example/mcp --secret-env GOOGLE_MCP_TOKEN
ZENDESK_TOKEN=... ZENDESK_USER=agent@acme.example/token \
ok --as ada connection add "Acme Zendesk" --http https://acme.zendesk.com \
    --adapter zendesk --auth basic \
    --secret-env token=ZENDESK_TOKEN --secret-env username=ZENDESK_USER
ok --as ada connection test conn:acme-zendesk
ok --as ada connection list                          # JSON, mcp.yaml servers included
printf %s "$NEW" | ok --as ada connection set-secret conn:acme-zendesk token
ok --as ada connection update conn:acme-zendesk --remove-secret webhook_secret
ok --as ada connection remove conn:acme-zendesk      # refused while a module uses it
ok secrets rotate --new-key-env OK_SECRETS_KEY_NEW   # server stopped

Other flags: --auth-header, --auth-scheme (MCP); --token-url, --scope, --shared-subject, --setting name=value (adapters). ok connection list run as someone who is not an admin prints [] rather than an error. See CLI and API reference.

Events#

Each connection change records connection_changed (never a secret). Each adapter tool call records adapter_request: connection, adapter, operation, request count, final status, duration, bytes and retries; never a filled-in path, a header, a query value or a body.

Writing an adapter#

A pack can declare its own adapters for a system with no MCP server and no built-in: operations per tool, objects that back collections, auth, a test request and a webhook. They appear in the library as <pack>.<name>. See Writing packs and skills.