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 | код | что значит |
|---|---|---|
| 400 | invalid, invalid_tools | не разбираются тело или id, либо определения инструментов не прошли проверку (больше 8 штук, больше 32 КиБ, неизвестный тул) |
| 401 | unauthorized, pairing | нет токена; код пейринга неверный, истёк или попытки исчерпаны |
| 403 | forbidden | файл не принадлежит этой сессии |
| 404 | not_found | сессии, тикета, устройства или вызова нет |
| 409 | conflict | присланный run_id уже не текущий |
| 409 | session_owned, run_active, tool_expired | журнал держит другой процесс; менять инструменты на ходу; ход уже закончился |
| 410 | gone | одобрение уже снято: решено или истекло |
| 413 | too_large | вложение больше настроенного лимита |
| 500 | storage, 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/config—{"path","values"}: правимые ключи из файла, а не из снимка старта.PUT /v1/config— подмножествоvalues; неизвестный ключ или чужой тип —400, несостоявшаяся запись —500 config. Правка действует со следующего хода.GET /v1/model—{"model":…};PUT /v1/model—{"model":"<slug>"}; пустой slug и правкаmodelпри активном профиле провайдера отвергаются.GET/PUT /v1/memory— тумблерыenabled,recall,retain;recallвнеalways|session|off—400.GET /v1/search?q=…—{"hits":[…]}.
Провайдеры
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.