Gateways
A gateway is a credential-isolated connection your employee holds. This page covers the two kinds plus the one direct category, why a gateway is modeled as data so a new one needs no core rewrite, the available channels, and how to add your own.
A gateway is how your employee reaches the outside world: a credential-isolated connection it holds. Conversational gateways are how you talk to your employee. Integration gateways are how your employee acts in your tools. The employee never holds your tokens, and every gateway is modeled as data, so adding one touches no core code.
The two kinds, plus one direct category#
Every connection into a cell is one of three things. Two of them are gateways in the strict sense: the employee sits in the middle. The third is the deliberate exception.
| Category | What it is | Examples | Mediation |
|---|---|---|---|
| Conversational | How you talk to your employee. A message goes in, the employee works the folders, a reply comes back. | Web, the jetsyn agent terminal, Telegram, Slack, Discord, email | Mediated, async |
| Integration | How your employee acts in your tools and data, holding none of your credentials. | ClickUp, a CRM, Google Sheets | Mediated |
| Pro direct | You work the folders yourself, live, with no employee in the loop. A separate category, not a channel. | The /pro console, the direct CLI | Unmediated, synchronous |
The axis that separates them is mediation. A conversational or integration gateway is employee-mediated: you direct the work and it reports back. Pro is direct: you are in the same folders the employee works, editing them raw. The direct category is covered in The CLI and Surfaces & tiers; the rest of this page is about the two mediated kinds.
A gateway is data, not code#
Adding a channel does not mean editing the agent. A channel is a ChannelSpec: a small frozen record whose fields point at the adapter's own functions and carry its metadata. Registering one more spec at import time is the entire integration. The registry, the inbound pipeline, and the webhook route all read your spec as data and gain zero new branches. Logic stays in the adapter; the spec inlines none of it.
The channel spec
| Field | What it holds |
|---|---|
name | The channel key ("slack"), the registry lookup key. It must be unique: a duplicate name fails loud rather than silently shadowing another tenant's traffic onto the wrong adapter. |
label | A human label for logs and dashboards. |
parse_event | The pure parser: a raw payload to a normalized InboundEvent, or None when the payload is not a routable message. Network-free, so it unit-tests offline. |
verify | The security boundary: (raw_body, signature, secret) -> bool. Each channel keeps its own; the registry never defines or weakens it. |
intent_tool_names | The channel-agnostic intent vocabulary this channel surfaces (see the integration recipe below). |
connection_key | Optional hook that reads which account or workspace a delivery targets: the key the multi-tenant resolver uses to find the right cell. |
Every channel normalizes its raw payload into one shape, InboundEvent. Three of its fields are the facts the router acts on, computed once at the adapter boundary so nothing downstream re-parses raw bytes:
was_agent_mentionedis the selective-answering signal.is_selfis the recursion guard: the employee's own output echoed back, dropped before any work is dispatched.is_directis the DM-versus-group discriminator that drives the mention gate.
The credential invariant
The employee never holds your tokens. For an integration gateway the credential lives gateway-side and the employee only emits intents for a privileged parent to execute. For a conversational gateway the bot secret is vaulted gateway-side. This is what makes a gateway a first-class connection and not a capability bolted into the agent.
The available channels#
| Channel | Kind | How you reach it |
|---|---|---|
| Web | Conversational | The workspace chat surface, the default door on every tier. |
| `jetsyn agent` terminal | Conversational | The employee over your shell; the CLI is only the pipe. See The CLI. |
| Telegram / Slack / Discord | Conversational | A provider webhook delivered to POST /gateway/{channel}/webhook. |
| Conversational | Inbound mail addressed to your employee. | |
| ClickUp | Integration | An inbound webhook plus an intent executor for outbound actions. The complete reference integration. |
Availability
Available now: the web and `jetsyn agent` terminal conversational gateways, and the ClickUp integration gateway (running in production). Coming soon: Telegram, Slack, and Discord as hosted webhook channels (the channel spec, verify, and parser are in place; live per-provider webhook wiring is the remaining step), and email.
How an inbound message is routed#
A provider delivers a webhook to POST /gateway/{channel}/webhook. From there a single pipeline, route_inbound, handles every registered channel, driven entirely by the channel's spec. The order is load-bearing:
- Resolve the channel spec from the registry. An unknown channel is a 404, never routed.
- Raw body first. Signature and timestamp come from headers; the body is never parsed before the boundary passes.
- Verify and resolve the tenant. The resolver runs the channel's own
verifyagainst each active workspace's stored secret for this channel. The secret that verifies is the tenant. Nothing verifies means 401, fail closed, never a default tenant. - Parse the verified body into an
InboundEvent. A non-routable payload (a Slackurl_verification, a Discord ping) parses toNoneand returns a benign 200 no-op. - Recursion guard. Drop on
is_selfbefore any dispatch. - Mention gate. A DM always responds; a group responds only when the employee was mentioned.
- Dispatch the event to the tenant's isolated cell, where the employee runs.
Tenant by secret
The per-tenant secret lives on the workspace config at config['gateways'][<channel>]['secret'], so adding a channel needs no schema change. Because the secret that verifies is the tenant, cross-tenant traffic cannot leak: an unverifiable delivery is rejected, never assigned to a fallback tenant.
Recipe: add a conversational channel#
A new conversational channel is a folder under jetsyn/gateways/<name>/. Model it on the existing slack/ or discord/ packages.
- 1
Write the adapter
In
<name>/client.pyadd the pure functions:parse_event(payload) -> InboundEvent | None,verify_signature(raw_body, signature, secret) -> bool, and a transport client for outbound replies. Keep parse and verify network-free so they unit-test offline. - 2
Declare the spec
In
<name>/channel.py, point aChannelSpecat those functions and register it at import (see the snippet below). - 3
Register at import
Add one import line to the bottom of
jetsyn/gateways/__init__.pyso importing the package registers your channel alongside the built-ins. - 4
Map the headers
In
jetsyn/gateways_inbound.py, add your channel's signature header toSIGNATURE_HEADERS(and its timestamp header toTIMESTAMP_HEADERSif the signature covers a timestamp). - 5
Wire the tenant
Store each tenant's secret at
config['gateways']['<name>']['secret']and point the provider webhook atPOST /gateway/<name>/webhook.
from ... import config
from .. import ChannelSpec, InboundEvent, register
from . import client as _client
NAME = "<name>"
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
# The security boundary. Fail closed; constant-time compare.
return _client.verify_signature(raw_body, signature, secret)
def parse_event(payload: dict, *, bot_user_id: str | None = None) -> InboundEvent | None:
# Pure: raw payload -> normalized event, or None to no-op.
return _client.parse_event(payload, bot_user_id=bot_user_id)
def connection_key(payload) -> str | None:
# Which account/workspace this delivery targets (the tenant resolver's key).
return _client.connection_key(payload)
SPEC = register(
ChannelSpec(
name=NAME,
label="<Label>",
parse_event=parse_event,
verify=verify,
intent_tool_names=config.INTENT_TOOL_NAMES, # shared intent vocabulary
connection_key=connection_key,
)
)That is the whole integration. route_inbound and the webhook route gain no new branches; the registry, the tenant resolver, and the mention gate all read your spec as data.
Recipe: add an integration gateway#
An integration gateway lets your employee act in a tool without ever holding the credential. The employee proposes credential-free intents; a privileged parent executes them with the token. The intent vocabulary is channel-agnostic, so you never fork the intent server. The shared intents (config.INTENT_TOOL_NAMES) are comment, status, react, field, and attach; a channel declares which it surfaces. A comment is a chat message on Slack and a task comment on ClickUp.
- 1
Keep the intent server as is
Inside the cell the employee calls a credential-free stdio MCP server (
intent_server.py) that writes JSON intents into the run'sintents/folder. It holds no token, so you do not modify it. - 2
Add an executor
Create
<name>/executor.pywithexecute_intentanddrain_intentsthat map each intent onto your provider's API. Copyclickup/ortelegram/executor.pyas the template. - 3
Hold the token in the parent
The privileged parent drains the intents and calls the API with the credential. The employee proposed the action; the parent performed it. The credential never crosses into the agent's environment.
Model on the reference
ClickUp is the complete, in-production integration gateway: an inbound webhook, an intent executor for outbound actions, and a deterministic status guard. Read it before building your own, and reuse the shared intent vocabulary rather than inventing new tools.
Where to go next#
What Jetsyn is
How the gateway fits: folders are the data, gateways are how you reach in, the employee works the folders, and every surface works over the same files.
Read →The CLI
The Pro direct gateway and the `jetsyn agent` conversational terminal. Install it, sign in once, and reach your cloud cell.
Read →The agent runtime
What happens after dispatch: the spawn-per-event employee, credential isolation, and durable resume.
Read →Build an AI employee
Author the employee that acts through your gateways, as plain Claude Code files with no compile step.
Read →