snens

HTTP API

Веб-клиент и десктоп работают через один HTTP API ядра: сессии, поток событий, настройки, провайдеры, тикеты. Маршруты собраны в одном файле.

Авторизация

Токен — один на инстанс, лежит в файле <каталог-данных>/api.token (создаётся при первом snens serve, права 0600; публичный файл ядро отвергает при старте) и передаётся заголовком Authorization: Bearer <токен>.

Исключение — SSE: EventSource в браузере не умеет заголовков, поэтому для GET-потоков (/v1/events и /v1/sessions/{id}/events) токен принимается и в query (?token=…). Цена известна: query остаётся в логах прокси; других маршрутов это не касается.

Второй законный способ войти — токен спаренного устройства из реестра clients/: его отзывают поштучно, не меняя секрет остальных. Единственный маршрут без токена — POST /v1/pair/claim. Нет токена или он чужой — 401 {"error":{"code":"unauthorized"}}.

Формат ошибок

{"error": {"code": "not_found", "message": "session not found"}}
HTTPкодчто значит
400invalid, invalid_toolsне разбираются тело или id, либо определения инструментов не прошли проверку (больше 8 штук, больше 32 КиБ, неизвестный тул)
401unauthorized, pairingнет токена; код пейринга неверный, истёк или попытки исчерпаны
403forbiddenфайл не принадлежит этой сессии
404not_foundсессии, тикета, устройства или вызова нет
409conflictприсланный run_id уже не текущий
409session_owned, run_active, tool_expiredжурнал держит другой процесс; менять инструменты на ходу; ход уже закончился
410goneодобрение уже снято: решено или истекло
413too_largeвложение больше настроенного лимита
500storage, config, history_readне записался файл, не разобрался конфиг, не прочитался журнал

Сессии

маршрутчто делает
GET /v1/sessionsсписок: {"sessions":[…]}
POST /v1/sessionsсоздать: {"command_id","surface"} → та же команда дважды даёт тот же ответ
GET /v1/sessions/{id}карточка одной сессии; DELETE там же — {"deleted": id}
POST /v1/sessions/{id}/messagesотправить ход: {"command_id","text","attachment_ids"}
POST /v1/sessions/{id}/stopостановить: {"command_id","run_id"}
PUT /v1/sessions/{id}/toolsинструменты этой сессии: {"tools":[…]}
GET …/history?recall=1{"cursor","events","messages"} — вход в сессию и восстановление после обрыва
GET …/context, GET …/file?path=…расход контекста; файл из каталогов этой сессии (чужой путь — 403)
POST …/attachments, POST …/approvals/{aid}вложение; решение по привилегированному действию

command_id — id команды клиента: повтор с тем же id не выполняет ход второй раз, а возвращает прежний результат. Присланный ход отвечает 202 с {"message_id","run_id", "state"}; идущий ход забирает текст как steer. Поля карточки: state (running, idle, failed, interrupted), run_id, parent, turns, done, started_at, done_at, updated_at, title, model.

Потоки событий (SSE)

GET /v1/sessions/{id}/events?after=<seq> — транскрипт одной сессии. Кадр несёт id: <seq>, а параметр after — курсор: клиент помнит последний seq и просит продолжение, не перечитывая всё. Пропуск кадра данных не теряет: при переполнении буфера поток дочитывается из хранилища. Keep-alive — 15 секунд.

GET /v1/events — изменения списка сессий (старт и конец хода, карточки подагентов). Клиент делает upsert по id; медленный клиент теряет кадры, а не поток.

Настройки, модель, память

Провайдеры

GET /v1/providers → {"providers":[…],"active","effective","config","presets"}. Ключей в ответе нет, только key_set. Ключ живёт строкой KEY=VALUE в .env рядом с конфигом; в TOML только имя переменной.

маршрутчто делает
POST /v1/providers{"name","base_url","model","key"} или {"name","preset":"…"}; первый профиль сразу активный
PUT /v1/providers/{name}любое подмножество полей; отсутствующий key ключ не стирает
DELETE /v1/providers/{name}активный профиль удалить нельзя — 409
POST /v1/providers/{name}/activateнеполный профиль — 400

Тикеты и ревью

GET /v1/tickets — {"tickets":[…]}, свежие сверху по mtime (дописанная «История» делает старый тикет свежим); поля строки: name, status, kind, target, family, weight (строкой), hits (число), symptom (до 200 символов), mtime. GET /v1/tickets/{name} — {"name","body"}, тело файла целиком; имя с /, .. или точкой в начале — 400 invalid. GET /v1/reviews — {"reviews":[…]}: id, session, ts, status (ok/error, нет записи — running), summary (первые 300 символов финального ответа).

Пейринг и устройства

POST /v1/pair/start (нужен токен) → {"code","seconds_left","endpoints"}; GET /v1/pair/status → {"open","seconds_left"}, сам код повторно не отдаётся; POST /v1/pair/claim — {"code","name"} → {"token","client_id"}: код одноразовый, живёт 5 минут, после 5 неудачных попыток сгорает. GET /v1/clients, DELETE /v1/clients/{id} — кто подключён и отзыв (действует сразу).

Инструменты листаются не маршрутом, а командой: snens tools --json печатает определения для модели. Отдельного /v1/tools нет — набор задаётся на сессию через PUT …/tools.