# Transport Protocol The gateway exposes agent execution under `/api/agents/{agentId}`. All agent routes require a valid auth context unless explicitly noted. Admin bearer tokens, user JWTs, and channel tokens are all accepted by the auth resolver where appropriate. ## Auth | Flow | Endpoint / token | Use | |------|------------------|-----| | Admin bearer | `Authorization: Bearer $GATEWAY_ADMIN_TOKEN` | agent lifecycle, admin APIs, full agent route access | | User JWT | `POST /api/auth/token` | browser/web user auth (identifies a person, not an agent) | | Admin-minted user JWT | `POST /api/admin/auth/issue-token` | admin-only mint of a user JWT for an external identity (`channel`, `channelUserId`). `purpose: 'session'` (default) returns a normal 24h credential; `purpose: 'exchange'` returns a single-use short-lived token (≤600s, default 120s) for the `/connect` SSO deep link. | | Connect SSO exchange | `POST /api/auth/exchange` | anonymous: swap an `exchange`-purpose JWT for a normal session JWT. Each `jti` is recorded on success so a replay is rejected with 401. Backs the web `/connect#token=…` deep link. | | Channel bearer | generated or configured channel token | built-in/external channel adapters scoped to an agent/channel namespace | `protected` agents require `agent_token` during device-token exchange (members self-join). `private` agents reject any sender without an explicit membership row at session-open time. **JWT claims.** Every signed user JWT carries: - `sub` — `${channel}:${channelUserId}` for the resolved identity - `channel`, `channelUserId` — the same fields split out for convenience - `purpose` — `'session'` (default, all API access) or `'exchange'` (only `POST /api/auth/exchange`) - `issuer` — audit-only: `'device-key'` (from `POST /api/auth/token`) or `'admin-issued'` (from `POST /api/admin/auth/issue-token`) - `jti` — unique token id; recorded by the exchange endpoint to enforce single-use Legacy tokens minted before this claim set was added are treated as `purpose: 'session'`. ## Agent Routes | Method | Path | Description | |--------|------|-------------| | `GET` | `/health` | gateway health | | `GET` | `/api/agents` | list registered agents, admin only | | `POST` | `/api/agents` | create an agent, admin only | | `GET` | `/api/agents/{agentId}/health` | running/stopped health | | `POST` | `/api/agents/{agentId}/manage/{start|stop|restart|delete}` | lifecycle, admin only | | `POST` | `/api/agents/{agentId}/sessions` | open or resume a session | | `GET` | `/api/agents/{agentId}/sessions` | list visible sessions | | `POST` | `/api/agents/{agentId}/sessions/{sessionId}/messages` | post a message | | `GET` | `/api/agents/{agentId}/sessions/{sessionId}/messages` | read history | | `DELETE` | `/api/agents/{agentId}/sessions/{sessionId}` | delete a non-running direct session | | `POST` | `/api/agents/{agentId}/sessions/{sessionId}/approve` | answer a pending tool approval | | `POST` | `/api/agents/{agentId}/sessions/{sessionId}/checkpoint` | run session introspection checkpoint | | `GET` | `/api/agents/{agentId}/sessions/{sessionId}/events` | durable SSE event stream | | `WS` | `/api/agents/{agentId}/ws` | WebSocket RPC and event subscription | Session list filters: - `kind` - `platform` - `interactive=true|false` - `limit` - `channel` - `metadata.{key}=value` Channel tokens are namespace-scoped. A channel token with namespace `telegram` can only access session IDs prefixed with `telegram:`. ## Session Payloads Open session: ```json { "sessionId": "cli:default", "source": { "kind": "cli", "interactive": true, "platform": "cli", "type": "direct" }, "metadata": { "username": "william" } } ``` Post message: ```json { "text": "Summarize this repo", "sender": { "channel": "cli", "channelUserId": "william", "displayName": "William" }, "mentioned": true, "metadata": {} } ``` Attachments are supported as `{ "type": "...", "url": "...", "data": "..." }`. ## Message Modes ### Fire And Forget ```http POST /api/agents/main/sessions/cli%3Adefault/messages ``` Returns after the message is accepted: ```json { "sessionId": "cli:default", "messageId": "msg-...", "triggered": true } ``` ### Append/Inject ```http POST /api/agents/main/sessions/telegram%3A123/messages?append=true ``` Stores the message and publishes `user_message` without triggering the agent. `inject=true` is accepted as an alias. ### Wait ```http POST /api/agents/main/sessions/cli%3Adefault/messages?wait=true&timeout=300000 ``` Returns one JSON result when the turn ends: ```json { "sessionId": "cli:default", "messageId": "msg-...", "text": "Done.", "toolCalls": [ { "tool": "web_search", "isError": false, "text": "..." } ] } ``` If group routing decides not to trigger the model, the response includes `triggered: false`. ### Inline SSE Stream ```http POST /api/agents/main/sessions/cli%3Adefault/messages?stream=true ``` The response is an SSE stream. A `message_ack` frame may be emitted first, followed by normal outbound events until `agent_end`. ### Durable Events ```http GET /api/agents/main/sessions/cli%3Adefault/events ``` This emits backlog events first, then live events, with `ready` and periodic `ping` frames. ## Outbound Events Current event types: - `thinking_delta` - `thinking_final` - `text_delta` - `text_final` - `tool_call` - `tool_result` - `approval_requested` — new request opened (`mode: 'realtime' | 'async'`) - `approval_pending` — async-only notification routed to the owner's session - `approval_resolved` — request closed (`decision`, optional `resolution`) - `user_message` - `agent_start` - `agent_end` - `error` - `research_progress` — Deep Research operational progress (`phase`, `status`, `message`, optional `stepId` for deduplication against durable timeline rows and `counts`) - `research_plan_ready` — a research plan version is awaiting review - `research_source_update` — a research source changed state (`candidate`/`fetched`/`blocked`/`failed`/`unsupported`/`duplicate`) - `research_report_ready` — final report persisted (`terminalStatus: 'completed' | 'budget_exhausted'`) Events are persisted in `session_events` and broadcast through the in-memory `SessionEventBroker`. Research events are live-only (the broker keeps a 100-event backlog per session): on reconnect, clients reload the durable run/steps/sources over HTTP first, then subscribe — live events only patch state and signal when to refetch. ## Deep Research Routes Nested under sessions; the same auth and participant checks apply: ``` POST /api/agents/:agentId/sessions/:sessionId/research-runs # 202, idempotent via clientRequestId GET /api/agents/:agentId/sessions/:sessionId/research-runs GET /api/agents/:agentId/sessions/:sessionId/research-runs/:runId PATCH /api/agents/:agentId/sessions/:sessionId/research-runs/:runId/plan # optimistic expectedVersion; 409 on conflict POST /api/agents/:agentId/sessions/:sessionId/research-runs/:runId/actions # approve_plan | pause | resume | cancel | refine | retry | increase_budget GET /api/agents/:agentId/sessions/:sessionId/research-runs/:runId/steps GET /api/agents/:agentId/sessions/:sessionId/research-runs/:runId/sources GET /api/agents/:agentId/sessions/:sessionId/research-runs/:runId/sources/:sourceId # metadata + evidence excerpts ``` `POST /messages` returns `409 research_run_active` while the session's run is actively planning/researching/synthesizing; chat resumes while the run awaits plan approval or is paused. WebSocket parity methods: `research.start`, `research.list`, `research.get`, `research.plan.update`, `research.action`, `research.steps`, `research.sources` — streaming stays on `session.subscribe`. ## WebSocket Connect to: ```text ws://127.0.0.1:4000/api/agents/main/ws?token= ``` Supported methods: - `session.open` - `session.message` - `session.approve` - `session.checkpoint` - `session.delete` - `session.list` - `session.history` - `session.subscribe` - `session.unsubscribe` Requests are JSON-RPC-like: ```json { "id": "1", "method": "session.message", "params": { "sessionId": "web:device", "message": { "text": "Hello" } } } ``` Responses are either: ```json { "id": "1", "result": { "sessionId": "web:device" } } ``` or: ```json { "id": "1", "error": { "code": "validation_error", "message": "..." } } ``` Subscribed events are delivered as server messages containing the session ID, event ID, and event payload. ## Management APIs Owner or admin routes: - `/api/agents/{agentId}/info` - `/api/agents/{agentId}/config` - `/api/agents/{agentId}/secrets` - `/api/agents/{agentId}/skills` - `/api/agents/{agentId}/mcp-servers` - `/api/agents/{agentId}/channels` - `/api/agents/{agentId}/schedules` - `/api/agents/{agentId}/sandboxes` — list / add / remove sandbox rows. See [sandbox-model.md](sandbox-model.md). - `/api/agents/{agentId}/members` — list / add / remove memberships. See [user-model.md](user-model.md#adding-members). - `/api/agents/{agentId}/security` — read / overwrite the agent security policy (`access`, autonomy, approvals). - `/api/agents/{agentId}/instructions` — CRUD on per-agent system-prompt sections (owner or admin) Admin fan-out: - `POST /api/admin/instructions/fanout` — body `{ mode: 'set' | 'append' | 'remove', key, content? }`. Applies the same mutation to every registered agent's row at `key`. `set` replaces; `append` reads-then-appends a newline + content (creating the row if missing); `remove` deletes. Returns `{ ok, mode, key, agents: [...] }`. Admin routes: - `/api/admin/stats` - `/api/admin/logs` - `/api/admin/skills` - `/api/admin/mcp-servers` - `/api/admin/schedules` - `/api/sandbox-presets` — read-only registry of sandbox presets defined in `gateway.json` (any authenticated user) See the focused docs for payload details: - [skills.md](skills.md) - [mcp-servers.md](mcp-servers.md) - [channel-adapter.md](channel-adapter.md) - [session-model.md](session-model.md)