254 lines
6.8 KiB
Markdown
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.
|