Files
nexus-sync/docs/api.md
T
2026-06-25 02:39:25 +03:00

254 lines
6.8 KiB
Markdown

# API contract
## Transport
- Protocol: HTTPS.
- Request and response body format: JSON.
- API prefix: `/api/v1`.
- Time format: RFC 3339 / ISO 8601, e.g. `2026-05-24T13:20:30Z`.
- Client authorization: bearer token in the `Authorization` header.
Example headers:
```text
Content-Type: application/json
Authorization: Bearer <client-token>
```
## Client heartbeat
### `POST /api/v1/client/heartbeat`
The client calls this endpoint on every polling tick. The same endpoint serves
three scenarios:
- an "I'm alive" message;
- a regular state update;
- reporting the result of the last finished command.
### 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
}
```
Fields:
- `client_id` - a stable identifier set during client setup. It must not change
on every restart.
- `observed_at` - the moment the client prepared the payload.
- `client.hostname` - the machine's current hostname.
- `client.platform` - the client platform. Prefer names close to
Python/platform: `linux`, `darwin`, `windows`.
- `client.version` - the nexus-sync client version.
- `state` - an intentionally small object. IP, disk usage, memory, battery
status and other metrics may be added here later.
- `last_command_result` - `null` if the client has nothing new to report about
command execution.
### 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<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384\n",
"stderr": ""
}
}
```
Result fields:
- `command_id` must match the id of the command the server previously returned
to the client.
- `status` takes one of: `succeeded`, `failed`, `timed_out`, `rejected`.
- `return_code` - the process exit code, if the process was started. For
`timed_out` or `rejected` the field may be `null` when there is no final exit
code.
- `stdout` and `stderr` must be size-limited by the client before sending. This
contract does not yet fix a specific byte limit.
### 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
}
}
```
Command fields:
- `id` is generated by the server and must be unique.
- `kind` describes the executor type. Only `exec` is defined so far.
- `name` - the command preset name.
- `args` holds arguments specific to a particular preset.
- `timeout_seconds` - the maximum execution time the server allows for this
command.
The client must run only commands it knows and locally allows. Unknown or
forbidden commands must be returned as `rejected`.
## Server-side API
These endpoints are for the server/admin side, to see clients and queue commands
for them. Clients directly use only the heartbeat.
### `GET /api/v1/server/clients`
Returns the list of known clients with their latest heartbeat data and
`available_commands`.
### `GET /api/v1/server/clients/{client_id}`
Returns a single client, or `404` if the server has not seen this `client_id`
yet.
### `POST /api/v1/server/clients/{client_id}/commands`
Creates a pending command for the client. The client receives it on its next
heartbeat. This endpoint accepts the command name directly.
Request:
```json
{
"name": "hostname",
"args": {},
"timeout_seconds": 30
}
```
Response:
```json
{
"id": "cmd_...",
"client_id": "macbook-pro-01",
"kind": "exec",
"name": "hostname",
"args": {},
"status": "pending",
"timeout_seconds": 30,
"attempts": 0,
"max_attempts": 1,
"created_at": "2026-05-24T13:20:30Z",
"delivered_at": null,
"finished_at": null
}
```
### `GET /api/v1/server/commands/{command_id}`
Returns the command and, if the client has already reported, the `result` field.
## Command lifecycle
Command lifecycle:
1. `pending`: the command was created on the server and not yet delivered to the
client.
2. `delivered`: the command was returned to the client in a heartbeat response.
3. `succeeded`: the client reported successful execution.
4. `failed`: the client reported an execution error.
5. `timed_out`: the client reported a timeout.
6. `rejected`: the client refused to run the command.
Terminal statuses: `succeeded`, `failed`, `timed_out`, `rejected`.
## HTTP statuses
The heartbeat endpoint should use these HTTP statuses:
- `200 OK`: heartbeat accepted; the response body matches the contract above.
- `400 Bad Request`: malformed JSON or invalid field values.
- `401 Unauthorized`: the bearer token is missing or invalid.
- `403 Forbidden`: the token is valid but not allowed to act as this
`client_id`.
- `409 Conflict`: the result references an unknown, already terminal, or
incompatible command.
- `429 Too Many Requests`: the client polls the server too often.
- `500 Internal Server Error`: an unexpected server error.
Command execution state is expressed via JSON fields, not HTTP errors. For
example, a command with exit code `1` is still delivered over a successful
heartbeat request.
## Polling rules
The server response contains `next_poll_after_seconds`.
Behavior:
- normal polling interval without a command: 60 seconds;
- polling interval after receiving a command: 10 seconds;
- client minimum interval: 5 seconds;
- client maximum interval: 300 seconds.
The client should treat the server value as a recommendation and clamp it to its
local min/max bounds.
## Security constraints
The following constraints must be respected:
- do not run arbitrary shell strings received from the server;
- run only locally known command presets;
- require a bearer token for client endpoints;
- include `command_id` in every result to avoid ambiguous matching;
- store command output as logs, not as trusted control data;
- limit the size of command output before sending it to the server;
- run every command with a timeout.
These constraints are intentionally part of the contract because the project
deals with remote machine management. It is cheaper to build the first
implementation around them than to add them after the fact.