JJETSYNGet in touch

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.

CategoryWhat it isExamplesMediation
ConversationalHow 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, emailMediated, async
IntegrationHow your employee acts in your tools and data, holding none of your credentials.ClickUp, a CRM, Google SheetsMediated
Pro directYou work the folders yourself, live, with no employee in the loop. A separate category, not a channel.The /pro console, the direct CLIUnmediated, 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

FieldWhat it holds
nameThe 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.
labelA human label for logs and dashboards.
parse_eventThe 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.
verifyThe security boundary: (raw_body, signature, secret) -> bool. Each channel keeps its own; the registry never defines or weakens it.
intent_tool_namesThe channel-agnostic intent vocabulary this channel surfaces (see the integration recipe below).
connection_keyOptional 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_mentioned is the selective-answering signal.
  • is_self is the recursion guard: the employee's own output echoed back, dropped before any work is dispatched.
  • is_direct is 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#

ChannelKindHow you reach it
WebConversationalThe workspace chat surface, the default door on every tier.
`jetsyn agent` terminalConversationalThe employee over your shell; the CLI is only the pipe. See The CLI.
Telegram / Slack / DiscordConversationalA provider webhook delivered to POST /gateway/{channel}/webhook.
EmailConversationalInbound mail addressed to your employee.
ClickUpIntegrationAn 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:

  1. Resolve the channel spec from the registry. An unknown channel is a 404, never routed.
  2. Raw body first. Signature and timestamp come from headers; the body is never parsed before the boundary passes.
  3. Verify and resolve the tenant. The resolver runs the channel's own verify against 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.
  4. Parse the verified body into an InboundEvent. A non-routable payload (a Slack url_verification, a Discord ping) parses to None and returns a benign 200 no-op.
  5. Recursion guard. Drop on is_self before any dispatch.
  6. Mention gate. A DM always responds; a group responds only when the employee was mentioned.
  7. 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. 1

    Write the adapter

    In <name>/client.py add 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. 2

    Declare the spec

    In <name>/channel.py, point a ChannelSpec at those functions and register it at import (see the snippet below).

  3. 3

    Register at import

    Add one import line to the bottom of jetsyn/gateways/__init__.py so importing the package registers your channel alongside the built-ins.

  4. 4

    Map the headers

    In jetsyn/gateways_inbound.py, add your channel's signature header to SIGNATURE_HEADERS (and its timestamp header to TIMESTAMP_HEADERS if the signature covers a timestamp).

  5. 5

    Wire the tenant

    Store each tenant's secret at config['gateways']['<name>']['secret'] and point the provider webhook at POST /gateway/<name>/webhook.

jetsyn/gateways/<name>/channel.py
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. 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's intents/ folder. It holds no token, so you do not modify it.

  2. 2

    Add an executor

    Create <name>/executor.py with execute_intent and drain_intents that map each intent onto your provider's API. Copy clickup/ or telegram/executor.py as the template.

  3. 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#