Direct WebSocket API server for non-Telegram clients.
Provides transport-independent access to the same orchestrator/session system used by Telegram.
- E2E encrypted WebSocket session (NaCl Box)
- authenticated file download/upload endpoints
- shared session model (
SessionKey) with optional channel isolation
api/crypto.py: E2E session (E2ESession)api/server.py: WebSocket + HTTP handlers, auth, streaming dispatch, file endpoints
| Field | Default | Description |
|---|---|---|
enabled |
false |
start API server |
host |
0.0.0.0 |
bind address |
port |
8741 |
API port |
token |
"" |
auth token (generated when enabling/first start fallback) |
chat_id |
0 |
default chat scope (0 -> fallback to first allowed_user_ids, else 1) |
allow_public |
false |
suppress tailscale warning |
Endpoint: ws://<host>:<port>/ws
Client first frame (plaintext):
{"type":"auth","token":"...","e2e_pk":"...","chat_id":123,"channel_id":77}Required:
type=auth- valid
token - valid
e2e_pk(base64 Curve25519 public key)
Optional session scope:
chat_id: positive intchannel_id: positive int (mapped toSessionKey.topic_id)
Server responds (last plaintext frame):
{"type":"auth_ok","chat_id":123,"channel_id":77,"e2e_pk":"...","providers":[...]}After auth_ok, all frames are encrypted.
API uses SessionKey("api", chat_id, topic_id).
topic_idis populated fromchannel_idin auth payload- without
channel_id, session is chat-scoped only
This allows multiple API channels to maintain isolated contexts under one chat_id.
Client message:
{"type":"message","text":"..."}Server streaming events:
text_deltatool_activitysystem_status- final
result(text,stream_fallback, optionalfiles)
Abort:
- client sends
{"type":"abort"}or text message/stop - server returns
abort_okwith kill count
Current scope nuance:
- API abort is currently chat-scoped, not channel-scoped, because the abort path kills active work by
chat_id
GET /health(no auth)GET /files?path=...(Bearer token + allowed-root checks)POST /upload(Bearer token + multipart)
Upload target:
~/.ductor/workspace/api_files/YYYY-MM-DD/...
GET /files path checks use file_access mapping:
all-> unrestrictedhome-> home-root limitedworkspace->~/.ductor/workspacelimited
MIME and file-tag parsing share helpers from ductor_bot/files/.
Auth-phase errors are plaintext (auth_timeout, auth_required, auth_failed).
Session-phase errors are encrypted (decrypt_failed, empty, unknown_type, no_handler, internal_error).
orchestrator/lifecycle.start_api_server(...) wires:
- message handler ->
Orchestrator.handle_message_streaming - abort handler ->
Orchestrator.abort - file context (allowed roots, upload dir, workspace)
- provider metadata and active state getter