From f864653356fb902372f8005f8270f2d0039b4f8a Mon Sep 17 00:00:00 2001 From: ars Date: Sun, 24 May 2026 16:45:29 +0300 Subject: [PATCH] update docs --- README.md | 6 ++ docs/api.md | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 docs/api.md diff --git a/README.md b/README.md index 54ff3ce..c6b7bcf 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ server can send to every client "what to do". Centralized control for all client pip install -e .[dev] ``` +## Current design + +- API contract: [docs/api.md](docs/api.md) +- Client behavior notes: [docs/client.md](docs/client.md) +- Server behavior notes: [docs/server.md](docs/server.md) + ### To build ``` diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..6ee2860 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,201 @@ +# API contract + +## Transport + +- Протокол: HTTPS. +- Формат тела запроса и ответа: JSON. +- Префикс API: `/api/v1`. +- Формат времени: RFC 3339 / ISO 8601, например `2026-05-24T13:20:30Z`. +- Авторизация клиента: bearer token в заголовке `Authorization`. + +Пример заголовков: + +```http +Content-Type: application/json +Authorization: Bearer +``` + +## Client heartbeat + +### `POST /api/v1/client/heartbeat` + +Клиент вызывает эту ручку на каждом polling tick. Одна и та же ручка +используется для трёх сценариев: + +- сообщение "я жив"; +- регулярное обновление состояния; +- отправка результата последней завершённой команды. + +### Request + +```json +{ + "client_id": "macbook-pro-01", + "observed_at": "2026-05-24T13:20:30Z", + "client": { + "hostname": "macbook-pro.local", + "platform": "darwin", + "version": "0.1.0" + }, + "state": { + "local_time": "2026-05-24T16:20:30+03:00", + "uptime_seconds": 1200 + }, + "last_command_result": null +} +``` + +Поля: + +- `client_id` - стабильный идентификатор, заданный при настройке клиента. Он не + должен меняться при каждом рестарте. +- `observed_at` - момент, когда клиент подготовил payload. +- `client.hostname` - текущий hostname машины. +- `client.platform` - платформа клиента. Желательно использовать названия, + близкие к Python/platform: `linux`, `darwin`, `windows`. +- `client.version` - версия nexus-sync client. +- `state` - намеренно маленький объект. Может быть сюда позже можно добавить IP, + disk usage, memory, battery status и другие метрики. +- `last_command_result` - `null`, если клиенту нечего нового сообщать о + выполнении команды. + +### Request with command result + +```json +{ + "client_id": "macbook-pro-01", + "observed_at": "2026-05-24T13:21:05Z", + "client": { + "hostname": "macbook-pro.local", + "platform": "darwin", + "version": "0.1.0" + }, + "state": { + "local_time": "2026-05-24T16:21:05+03:00", + "uptime_seconds": 1235 + }, + "last_command_result": { + "command_id": "cmd_01JY3H8V8W8P3FXDR3S2BM7M6B", + "status": "succeeded", + "started_at": "2026-05-24T13:20:35Z", + "finished_at": "2026-05-24T13:20:36Z", + "return_code": 0, + "stdout": "lo0: flags=8049 mtu 16384\n", + "stderr": "" + } +} +``` + +Поля результата: + +- `command_id` должен совпадать с id команды, которую сервер ранее вернул + клиенту. +- `status` принимает одно из значений: `succeeded`, `failed`, `timed_out`, + `rejected`. +- `return_code` - exit code процесса, если процесс был запущен. Для + `timed_out` или `rejected` поле может быть `null`, если финального exit code + нет. +- `stdout` и `stderr` клиент должен ограничивать по размеру перед отправкой. + Конкретный лимит байт в этом контракте пока не фиксируется. + +### Response without command + +```json +{ + "status": "ok", + "server_time": "2026-05-24T13:20:30Z", + "next_poll_after_seconds": 60, + "command": null +} +``` + +### Response with command + +```json +{ + "status": "ok", + "server_time": "2026-05-24T13:20:30Z", + "next_poll_after_seconds": 10, + "command": { + "id": "cmd_01JY3H8V8W8P3FXDR3S2BM7M6B", + "kind": "exec", + "name": "network_interfaces", + "args": {}, + "timeout_seconds": 30 + } +} +``` + +Поля команды: + +- `id` генерируется сервером и должен быть уникальным. +- `kind` описывает тип executor. Пока определён только `exec`. +- `name` - имя command preset. +- `args` содержит аргументы, специфичные для конкретного preset. +- `timeout_seconds` - максимальное время выполнения, которое сервер допускает + для этой команды. + +Клиент обязан выполнять только те команды, которые он знает и локально +разрешает. Неизвестные или запрещённые команды нужно возвращать как `rejected`. + +## Command lifecycle + +Жизненный цикл команды: + +1. `pending`: команда создана на сервере и ещё не доставлена клиенту. +2. `delivered`: команда была возвращена клиенту в heartbeat response. +3. `succeeded`: клиент сообщил об успешном выполнении. +4. `failed`: клиент сообщил об ошибке выполнения. +5. `timed_out`: клиент сообщил о timeout. +6. `rejected`: клиент отказался выполнять команду. + +Терминальные статусы: `succeeded`, `failed`, `timed_out`, `rejected`. + +## HTTP statuses + +Heartbeat endpoint должен использовать такие HTTP-статусы: + +- `200 OK`: heartbeat принят; тело ответа соответствует контракту выше. +- `400 Bad Request`: некорректный JSON или невалидные значения полей. +- `401 Unauthorized`: bearer token отсутствует или невалиден. +- `403 Forbidden`: token валиден, но не имеет права действовать как этот + `client_id`. +- `409 Conflict`: результат ссылается на неизвестную, уже терминальную или + несовместимую команду. +- `429 Too Many Requests`: клиент опрашивает сервер слишком часто. +- `500 Internal Server Error`: неожиданная ошибка сервера. + +Состояние выполнения команды выражается полями в JSON, а не HTTP-ошибками. +Например, команда с exit code `1` всё равно отправляется через успешный +heartbeat request. + +## Polling rules + +Ответ сервера содержит `next_poll_after_seconds`. + +Поведение: + +- обычный polling interval без команды: 60 секунд; +- polling interval после получения команды: 10 секунд; +- клиентский минимальный interval: 5 секунд; +- клиентский максимальный interval: 300 секунд. + +Клиент должен воспринимать значение сервера как рекомендацию и зажимать его в +локальные min/max границы. + +## Security constraints + +Необходимо соблюдать эти ограничения: + +- не выполнять произвольные shell-строки, полученные от сервера; +- выполнять только локально известные command presets; +- требовать bearer token для клиентских endpoint'ов; +- включать `command_id` в каждый результат, чтобы не было неоднозначного + сопоставления; +- хранить command output как логи, а не как доверенные управляющие данные; +- ограничивать размер command output перед отправкой на сервер; +- выполнять каждую команду с timeout. + +Эти ограничения намеренно являются частью контракта, потому что проект +занимается удалённым управлением машинами. Дешевле строить первую реализацию +вокруг них, чем добавлять их задним числом.