apps/kelvin-gateway exposes a WebSocket control plane over Kelvin SDK runtime composition.
Protocol version constant: 1.0.0.
- connect-first handshake is required
- optional token auth on connect (
KELVIN_GATEWAY_TOKENor--token) - non-loopback binds fail closed unless TLS is configured or an explicit insecure override is set
- public binds require an auth token
- direct HTTP ingress is disabled unless
KELVIN_GATEWAY_INGRESS_BINDor--ingress-bindis set - typed request validation (
deny_unknown_fields) - fail-closed unknown method handling
- idempotency cache for side-effecting
agentrequests via requiredrequest_id - bounded connection count, websocket message/frame sizes, handshake timeout, and per-connection outbound queue
- auth failure backoff is applied per client IP
- channel adapters are disabled unless explicitly enabled by environment config
- Telegram channel defaults to pairing-required and host allowlist checks
- Slack/Discord channels are available behind explicit env enable flags
- optional per-channel WASM policy plugin (
kelvin_channel_host_v1) can deny/shape ingress before routing
- loopback/dev default:
ws://127.0.0.1:34617 - TLS mode: configure
--tls-cert <path>and--tls-key <path>for built-inwss - insecure public plaintext requires explicit override:
--allow-insecure-public-bind true
Optional direct channel ingress runs on a separate HTTP listener:
- configure
KELVIN_GATEWAY_INGRESS_BINDor--ingress-bind <host:port> - optional route prefix:
KELVIN_GATEWAY_INGRESS_BASE_PATHor--ingress-base-path <path> - optional body limit:
KELVIN_GATEWAY_INGRESS_MAX_BODY_BYTESor--ingress-max-body-bytes <n> - default base path:
/ingress - operator console path:
/operator/ - routes:
POST /ingress/telegramPOST /ingress/slackPOST /ingress/discord
Direct ingress listener state is exposed in health.payload.ingress.
Client request:
{
"type": "req",
"id": "req-1",
"method": "connect",
"params": {}
}Server response:
{
"type": "res",
"id": "req-1",
"ok": true,
"payload": {}
}Server event:
{
"type": "event",
"event": "agent",
"payload": {}
}First frame must be connect.
connect params:
auth.token(required when gateway token is configured)client_id(optional)
Successful connect responses include:
protocol_versionsupported_methods
healthagent(alias:run.submit)- params:
request_id,prompt, optionalsession_id,workspace_dir,timeout_ms,system_prompt,memory_query,run_id
- params:
agent.wait(alias:run.wait)- params:
run_id, optionaltimeout_ms
- params:
agent.state(alias:run.state)- params:
run_id
- params:
agent.outcome(alias:run.outcome)- params:
run_id, optionaltimeout_ms
- params:
channel.telegram.ingest- params:
delivery_id,chat_id,text, optionaltimeout_ms,auth_token,session_id,workspace_dir
- params:
channel.telegram.pair.approve- params:
code
- params:
channel.telegram.status- params: none
channel.slack.ingest- params:
delivery_id,channel_id,user_id,text, optionaltimeout_ms,auth_token,session_id,workspace_dir
- params:
channel.slack.status- params: none
channel.discord.ingest- params:
delivery_id,channel_id,user_id,text, optionaltimeout_ms,auth_token,session_id,workspace_dir
- params:
channel.discord.status- params: none
channel.route.inspect- params:
channel,account_id, optionalsender_tier,session_id,workspace_dir
- params:
operator.runs.list- params: optional
limit
- params: optional
operator.sessions.list- params: optional
limit
- params: optional
operator.session.get- params:
session_id, optionallimit
- params:
operator.plugins.inspect- params: none
schedule.list- params: none
schedule.history- params: optional
schedule_id
- params: optional
Telegram channel is configured only via environment variables and remains disabled unless
KELVIN_TELEGRAM_ENABLED=true.
KELVIN_TELEGRAM_API_BASE_URLmust behttps://api.telegram.orgby default- custom Telegram base URL requires
KELVIN_TELEGRAM_ALLOW_CUSTOM_BASE_URL=true - pairing is enabled by default (
KELVIN_TELEGRAM_PAIRING_ENABLED=true) - allowlist is optional (
KELVIN_TELEGRAM_ALLOW_CHAT_IDS=...) - direct webhook verification uses
KELVIN_TELEGRAM_WEBHOOK_SECRET_TOKEN - inbound dedupe, per-chat rate limits, and bounded retries are always applied
Slack and Discord channels are disabled unless explicitly enabled:
KELVIN_SLACK_ENABLED=trueKELVIN_DISCORD_ENABLED=true
Common policy controls per channel include:
- ingress auth token enforcement (
*_INGRESS_TOKEN) - account/sender allowlists and trust tiers (
*_ALLOW_ACCOUNT_IDS,*_ALLOW_SENDER_IDS,*_TRUSTED_SENDER_IDS,*_PROBATION_SENDER_IDS,*_BLOCKED_SENDER_IDS) - per-tier quotas (
*_MAX_MESSAGES_PER_MINUTE,*_MAX_MESSAGES_PER_MINUTE_TRUSTED,*_MAX_MESSAGES_PER_MINUTE_PROBATION) - probation cooldown (
*_COOLDOWN_MS_PROBATION) - bounded inbox + delivery-id dedupe (
*_MAX_QUEUE_DEPTH,*_MAX_SEEN_DELIVERY_IDS) - bounded outbound retries (
*_OUTBOUND_MAX_RETRIES,*_OUTBOUND_RETRY_BACKOFF_MS) - direct webhook verification:
- Slack:
KELVIN_SLACK_SIGNING_SECRET - Slack replay window:
KELVIN_SLACK_WEBHOOK_REPLAY_WINDOW_SECS - Discord interactions:
KELVIN_DISCORD_INTERACTIONS_PUBLIC_KEY
- Slack:
Default base URL host allowlist is enforced unless explicitly relaxed:
- Slack:
slack.com - Discord:
discord.com
Custom base URLs require *_ALLOW_CUSTOM_BASE_URL=true.
Channel routing is loaded from KELVIN_CHANNEL_ROUTING_RULES_JSON (JSON array).
Each rule supports deterministic matching by:
priority(higher first)- tie-breaker: rule
id(lexicographic)
Match fields:
channel(telegram,slack,discord, or*)- optional
account_id - optional
sender_tier - optional
session_id - optional
workspace_dir
Route output fields:
route_session_idroute_workspace_dirroute_system_prompt
Gateway includes route metadata in channel ingest responses.
Per-channel status (channel.<platform>.status) includes:
- ingress verification state (
method,configured, last success/failure timestamps, last verification error) - ingress connectivity state (last request time, last accepted time, last HTTP status)
- retry and deny counters for direct webhook traffic alongside existing ingest/dedupe/rate/outbound counters
The operator console uses health, schedule.list, schedule.history, and the operator.*
methods to render gateway, channel, scheduler, run, session, plugin, registry, and trust-policy views.
Per-channel WASM policy plugin (optional):
- env:
KELVIN_<CHANNEL>_WASM_POLICY_PATH - ABI module:
kelvin_channel_host_v1 - export:
handle_ingest - imports:
log,clock_now_ms - host has no network/system call imports
Reference: docs/channel-plugin-abi.md
agent requires request_id.
- first submission stores acceptance metadata in the cache
- repeated submission with the same
request_idreturns the cached acceptance anddeduped: true
protocol_versionis the compatibility anchor for gateway clients.- method names in
supported_methodsare treated as a frozen v1 surface. - new methods are additive; existing method names and behavior are preserved for v1 clients.
Response envelope uses:
ok: falseerror.codeerror.message
Typical codes:
handshake_requiredunauthorizedinvalid_inputnot_foundtimeoutbackend_errorio_errormethod_not_found