Skip to content

Channels

The cockpit (webview) is the default channel and needs no external platform. A chat channel is an addition — one owner transport per agent, set with --channel, with the cockpit alongside it.

channel owner transport routing key host service needs
webview the browser cockpit --user-email webview-gateway + webview-login nothing external
telegram a Telegram bot --owner-id (or pairing) none (per-agent poller) a bot token from @BotFather
teams Microsoft Teams --owner-aad teams-gateway an Azure app + a public messaging endpoint
slack Slack DMs --owner-email slack-gateway a Slack app (Socket Mode, no public URL)

Quick start stands up webview.

Switching an agent's channel

Switching is a ledger scalar, not a rebuild:

agentctl agents set <agent> channel webview   # needs user_email set first
agentctl apply <agent>
  • --channel webview makes the cockpit the only surface, so it requires --user-email: the gateway has no other way to route to the agent.
  • --user-email on any other channel is additive — that agent gets a cockpit too.
  • Each integration refuses to wire until its secrets are set. The refusal prints the exact secret set line, with the value format (e.g. --value <xoxb-…>).

How the agent talks to its channel

Something has to hand each turn to the model and stream the answer back. Which process does that is the one thing the two runtimes do differently — everything else on this page (the channels, the routing keys, the lanes below) is identical either way.

runtime the process how a turn flows
pi (default) pi-agent@<name>.service → pi-run.sh → bridge.ts the bridge is the agent: it embeds the pi SDK and owns the channel directly. No PTY, no subprocess
claude agent@<name>.service → start-agent.sh → the channel shim the shim spawns claude and drives it over stdio (see TXT mode below)

Both speak the same WebSocket frame protocol to the gateways (runtime/channel/protocol.ts + runtime/channel/shared/), which is why a Slack or Teams gateway serves either runtime without knowing which it is talking to.

pi: the bridge is the agent

One long-lived, disk-persisted session per agent. The turn loop talks to a Channel interface, and PI_CHANNEL (rendered by apply) picks the implementation:

PI_CHANNEL implementation
telegram TelegramChannel — grammy long-poll, in-process. Lazily imported, so a non-Telegram agent never installs grammy
teams · slack · webview SocketChannel — an outbound WS dial to the shared gateway

The turn loop is identical across all four. A non-webview primary additionally gets the cockpit as an additive lane when the host runs a gateway, fanned out to both.

There is no TXT mode on pi and nothing to switch: with no claude process to drive, there is no second transport to choose between. AGENTCTL_TXT_MODE is not read anywhere in the pi runtime.

TXT mode (claude only)

On the claude runtime something must still hand each turn to claude itself. TXT mode is now the only way that happens: the shim spawns claude -p --input-format stream-json, writes turns to its stdin and streams stdout deltas back, and depends on nothing outside agentctl.

Both launchers exec the shim unconditionally. AGENTCTL_TXT_MODE has no second transport to select: unset or truthy is the working configuration, and the shim reads the flag only to tell its two roles apart — the TXT driver that owns claude's stdin, and the MCP child the telegram catalog MCP launches.

A falsy AGENTCTL_TXT_MODE leaves the agent mute

The shim stops spawning claude and hands each turn to an MCP notification nothing consumes. Inbound messages keep arriving and nothing answers them.

A subsession inherits its parent's value explicitly, because systemd units inherit nothing from each other.

Upgrading. A running agent keeps its old transport until it is restarted, and apply only starts the unit — a no-op on one already running. Restarting ends every agent's current session:

agentctl agents restart all

Lanes: how one agent carries several conversations

Every frame carries an optional session, normalized so that the empty string means the primary lane.

RegisterFrame.session the authoritative routing key; gateways key their routing table on (agent, session)
CHANNEL_SESSION (env) read by a channel process, which stamps its own lane onto every outbound frame
"" the primary lane

session is not a trust dimension. Authorization stays agent × owner-identity; a lane selects which conversation a frame belongs to, never what the caller may do.

Adding a chat channel

channel install secrets
webview webview-gateway install · webview-login install, both behind your HTTPS proxy — see the cockpit WEBVIEW_HMAC_KEY + your auth driver's config
telegram none; each agent runs its own poller the per-agent bot token, passed at add time
slack slack-gateway install (Socket Mode, no public URL) SLACK_BOT_TOKEN (xoxb-…), SLACK_APP_TOKEN (xapp-…)
teams teams-gateway install (needs a public messaging endpoint) TEAMS_APP_ID (GUID), TEAMS_APP_PASSWORD, TEAMS_APP_TENANT_ID (GUID)

Telegram, Slack and Microsoft Teams (click-through + CLI)

Telegram. - Click: DM @BotFather → /newbot → copy the token. - CLI: agentctl agents add <name> --channel telegram --bot-token <token>. --channel telegram is required — webview is the default channel, so the token alone is no longer enough. ► PAIR: message the bot once and the owner is captured, or pass --owner-id to skip. ► LOGIN: the bot DMs an auth URL → authorize → paste the code back → the agent is live.

Slack. - Click: api.slack.com/apps → Create (From an app manifest, or scratch) → Socket Mode on → App-Level Token (connections:write) = SLACK_APP_TOKEN → OAuth & Permissions bot scopes chat:write, reactions:write, users:read, users:read.email, files:read → Install to Workspace → Bot User OAuth Token = SLACK_BOT_TOKEN → Event Subscriptions → bot event message.im. - CLI: Slack has no create-CLI; the "CLI" path is the app manifest (paste it in step 1). Tokens are still minted by the Install button. Then: secret set SLACK_BOT_TOKEN · secret set SLACK_APP_TOKEN · slack-gateway install.

Microsoft Teams. - Click (Azure Portal): App registrations → New → copy Application (client) ID = TEAMS_APP_ID, Directory (tenant) ID = TEAMS_APP_TENANT_ID → Certificates & secrets → New client secret = TEAMS_APP_PASSWORD → Azure Bot resource → Messaging endpoint = https://<teams-gateway-host>/api/messages → enable the Teams channel → sideload the Teams app package. - CLI (az): az ad app create --display-name agentctl-teams --query appId -o tsv · az ad app credential reset --id <appId> --query password -o tsv · az account show --query tenantId -o tsv · az bot create … · az bot msteams create …. Then the three secret set … + teams-gateway install. ► TENANT: Teams is the one channel whose transport lives in your own Azure tenant. The full sequence, the trust boundary and the cross-host runbook are in The Teams gateway.

Capability setup (not a channel)

Google Workspace (google-workspace MCP + login google) is a capability an agent is wired into on any channel, not a channel of its own. It needs GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in the secret store and a one-time consent-screen setup in the Cloud Console, most of which has no CLI.

The scopes, the 6 APIs, and the console steps

Secrets: GOOGLE_CLIENT_ID (…apps.googleusercontent.com), GOOGLE_CLIENT_SECRET. The consent screen requests 8 scopes → you enable 6 APIs: gmail.modify · gmail.send · drive · drive.file · calendar · documents · spreadsheets · presentations ⇒ enable gmail · drive · calendar-json · docs · sheets · slides googleapis. - Click (Cloud Console): create/select a project → APIs & Services → Enable those 6 → OAuth consent screen → User type Internal (your Workspace org, e.g. ajil.ch) — Internal ⇒ no verification and no 7-day refresh-token expiry → Credentials → Create OAuth client ID → type Web application (for the broker; or Desktop for loopback) → Web only: add Authorized redirect URI https://<BROKER_URL>/oauth/callback → download JSON (has the "web" key). - CLI (gcloud): gcloud projects create agentctl-fleet · gcloud services enable gmail.googleapis.com drive.googleapis.com calendar-json.googleapis.com docs.googleapis.com sheets.googleapis.com slides.googleapis.com --project agentctl-fleet. Console-only caveat: the consent screen (Internal) and the OAuth client ID have no gcloud/API: gcloud does the project + API enablement, the rest is the clicks above. Then: secret set GOOGLE_CLIENT_ID · secret set GOOGLE_CLIENT_SECRET · mcp add google-workspace <agent> · login google <agent>|self.