Concepts
Connections & secrets
Your agent needs to send from your client's mailbox and read your client's Stripe account. The naive version of this — put the token in the prompt, or in the sandbox environment — is how you end up explaining to a customer why their API key appeared in a log.
A connectionis the alternative: a named capability the harness can exercise on the agent's behalf. The agent knows the connection exists. It never learns the credential.
Four kinds, and one of them is a broker
ConnectionKind is a short list on purpose. It is what the kernel actually implements, not what it aspires to:
| Kind | What it does |
|---|---|
email | Send over HTTP to config.api_url with a bearer secret. Postmark, SendGrid and Resend all fit. |
webhook | POST to a host you fixed in the connection config |
custom | The same HTTP transport, for anything that is neither of the above |
composio | Brokered: Stripe, Gmail, Xero, HubSpot, calendars and a few hundred others |
There is no native Stripe executor, no SMS executor, no calendar executor. Those all run through Composio. If you were expecting a long integration catalogue in the kernel, this is the honest shape of it — two transports and a broker.
Register one
kind and name are the required fields. Everything else is optional.
curl -sX POST http://localhost:4000/v1/connections \
-H "authorization: Bearer $MYCEL_API_KEY" \
-H "content-type: application/json" \
-d '{
"kind": "email",
"name": "billing-email",
"config": {
"api_url": "https://api.postmarkapp.com/email",
"from": "billing@example.com"
},
"secret_ref": "env:POSTMARK_TOKEN"
}'The name matters more than it looks: it is what a wedge refers to in its connections array, and what blueprint provisioning matches on when deciding whether to create or reuse.
Two ways to hold the secret
secret_ref chooses which:
env:NAME— read from the kernel's own environment. Right for your static, founder-level credentials: your Postmark token, your Stripe key.vault:KEY— read from the encrypted store. Right for anything per-client or rotatable.
Or set neither and post the value; the vault key defaults to the connection id, so the connection row never has to change:
curl -sX POST http://localhost:4000/v1/connections/<id>/secret \
-H "authorization: Bearer $MYCEL_API_KEY" \
-H "content-type: application/json" \
-d '{"value": "pm_live_…"}'Values are sealed with AES-256-GCM before they reach storage — the backend only ever sees an envelope of { v, kid, iv, tag, ct }. The kid is a fingerprint of the key that sealed it, so a mismatch after a key change is reported rather than silently returning rubbish.
Nothing reads a secret back out. There is no GET. Listing a connection returns its config, its secret_ref and a has_secret boolean — presence, not value, not length. Writing one is audited as secret.written with the connection and kind, never the value.
Set MYCEL_SECRET_KEY before you store anything you care about. Without it the kernel generates an ephemeral key per process and warns loudly: every vaulted secret becomes unreadable on restart. It must decode to exactly 32 bytes — head -c 32 /dev/urandom | base64. Losing it later means every customer re-authorises from scratch, so back it up somewhere that is not the same account as the database.
Connecting a brokered provider
Composio needs COMPOSIO_API_KEY set on the kernel. Without it every Composio route returns 501.
# browse what is available
GET /v1/composio/toolkits?search=stripe
# create a connection and start the OAuth flow in one call
POST /v1/composio/toolkits/stripe/connect
{ "client_id": "<client>", "read_tools": ["STRIPE_LIST_INVOICES"] }
→ { connection_id, redirect_url, … }
# poll until it is live
GET /v1/connections/<id>/composio/status → { status: "ACTIVE", active: true }Send the customer to redirect_url. Composio owns the callback, which is why Mycel exposes no redirect route of its own. A connection counts as connected once config.connected_account_id exists; until then, actions fail with is not connected yet — authorise <toolkit> first.
For brokered connections the capability name is the Composio tool slug — STRIPE_LIST_INVOICES, GMAIL_SEND_EMAIL. Composio's user identity is derived from the connection alone (project, and client if client-owned), so nothing the agent says can make it operate as a different tenant.
Founder-owned versus client-owned
owner is { kind: "founder" | "client", id }. This is the field that decides who a run is allowed to act as.
When a task belongs to a client, it is granted founder-level connections plus that client's own — and never another client's. That check happens when the grant is built, before the sandbox starts, so entitlement is not something the agent can talk its way past.
In practice: your Postmark account is founder-owned, and each customer's Stripe or mailbox is client-owned. Their credential runs their work and nothing else.
Reads are not actions
| Reads | Actions | |
|---|---|---|
| Endpoint | /v1/internal/reads/:capability | /v1/internal/actions/:capability |
| Approval | No | Yes — human, or a matching policy rule |
| Limits | GET only, relative paths only, 256KB, 20s, 200 per task | Whatever the connection can do, after someone said yes |
| Brokered | Only tools listed in config.read_tools | Any tool on the connected toolkit |
read_tools is a real allowlist. Call a Composio tool that is not on it and you get is not a declared read for "<name>" — use the action proxy so a human approves it. The design assumption is that anything undeclared might be a write.
Channels: work that arrives
A channel binds an inbound address to a wedge and a task type. All four fields are required.
curl -sX POST http://localhost:4000/v1/channels \
-H "authorization: Bearer $MYCEL_API_KEY" \
-H "content-type: application/json" \
-d '{
"connection_id": "<email-connection-id>",
"address": "billing@example.com",
"wedge": "invoice-chaser",
"task_type": "chase_invoice"
}'Your provider posts its webhook to your app; your app verifies the signature — Mycel does not do that for you — and forwards to POST /v1/channels/:id/inbound with { from: { handle, name }, body, subject }. The kernel resolves or creates the client, appends the message to a thread, and spawns the task with the conversation history attached. You get back { task_id, thread_id, client_id }.
Failure modes
email connection missing config.api_url— you registered the connection but not where to send.configis not validated at registration time.no executor for connection kind "x"— you invented a kind. Usecomposiowith the matching toolkit.- Secrets silently unreadable after a deploy —
MYCEL_SECRET_KEYchanged or was never set. The log says which key sealed them. - Composio routes returning 501 —
COMPOSIO_API_KEYis not set on the kernel. - Agent reads returning 429 — 200 reads per task, tunable with
MYCEL_READ_MAX_PER_TASK. Usually it means the agent is looping.
What to read next
Wedges — where you declare which connections a service needs, and which of its actions must stop for a human.