9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
191 lines
7.4 KiB
Markdown
191 lines
7.4 KiB
Markdown
# ODS Host Agent API
|
|
|
|
The ODS Host Agent (`bin/ods-host-agent.py`) is a lightweight HTTP server that runs **on the host machine** (outside Docker). It allows the Dashboard API (running inside a container) to manage extension containers — starting, stopping, and fetching logs — without giving the container direct access to the Docker socket.
|
|
|
|
## Why It Exists
|
|
|
|
The Dashboard API runs inside a Docker container and cannot directly run `docker compose` commands on the host. The host agent bridges this gap: it listens on `ODS_AGENT_BIND:ODS_AGENT_PORT`, accepts authenticated requests from the Dashboard API, and executes Docker Compose operations on its behalf. This avoids mounting the Docker socket into the container (a significant security risk).
|
|
|
|
## How It Runs
|
|
|
|
| Platform | Mechanism |
|
|
|----------|-----------|
|
|
| Linux | systemd user service (`scripts/systemd/ods-host-agent.service`) |
|
|
| macOS | Started by the installer (`installers/macos/install-macos.sh`) |
|
|
| Windows | Started by the installer (`installers/windows/phases/07-devtools.ps1`, managed via `ods.ps1`) |
|
|
|
|
The agent is started during installation. macOS and Windows bind to `127.0.0.1` by default. Linux auto-detects the `ods-network` gateway so containers can reach the agent, falls back to the default Docker bridge gateway for partial/older installs, and then falls back to `127.0.0.1`. It does not bind to `0.0.0.0` unless `ODS_AGENT_BIND` is explicitly set.
|
|
|
|
## Configuration
|
|
|
|
The agent reads its configuration from the `.env` file in the ODS install directory.
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `ODS_AGENT_KEY` | *(none)* | API key for authenticating requests. Falls back to `DASHBOARD_API_KEY` if unset. |
|
|
| `ODS_AGENT_BIND` | Platform-specific | Bind address. macOS/Windows default to `127.0.0.1`; Linux uses the `ods-network` gateway when detected, then the Docker bridge gateway, otherwise `127.0.0.1`. |
|
|
| `ODS_AGENT_PORT` | `7710` | Port the agent listens on. |
|
|
| `GPU_BACKEND` | `nvidia` | Passed to `resolve-compose-stack.sh` when building compose flags. |
|
|
| `TIER` | `1` | Hardware tier, passed to compose stack resolution. |
|
|
| `ODS_DATA_DIR` | `~/.ods` | Data directory root. |
|
|
| `ODS_USER_EXTENSIONS_DIR` | `$ODS_DATA_DIR/user-extensions` | Where user-installed extensions live. |
|
|
|
|
The agent also loads `config/core-service-ids.json` to determine which services are protected from management operations. If this file is missing, a hardcoded fallback list is used.
|
|
|
|
## Authentication
|
|
|
|
All mutation endpoints (`/v1/extension/*`) require a Bearer token:
|
|
|
|
```
|
|
Authorization: Bearer <ODS_AGENT_KEY>
|
|
```
|
|
|
|
The agent uses constant-time comparison (`secrets.compare_digest`) to prevent timing attacks.
|
|
|
|
## Endpoints
|
|
|
|
### `GET /health`
|
|
|
|
Health check. No authentication required.
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"version": "1.0.0"
|
|
}
|
|
```
|
|
|
|
### `GET /v1/update/status`
|
|
|
|
Return the last host-agent managed update run status.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "succeeded",
|
|
"action": "update",
|
|
"returncode": 0,
|
|
"updated_at": "2026-05-18T18:00:00Z"
|
|
}
|
|
```
|
|
|
|
If no update has run, the response is `{ "status": "idle" }`.
|
|
|
|
### `POST /v1/update/check`, `POST /v1/update/backup`, `POST /v1/update/start`
|
|
|
|
Run `ods-update.sh` from the host-agent boundary. `check` and `backup` run synchronously and return script output. `start` launches the update in a background thread and writes `data/update-status.json` for polling.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Request body:** optional JSON object. `backup` accepts an optional `backup_id`; otherwise the host agent generates one.
|
|
|
|
**Error responses:**
|
|
| Code | Condition |
|
|
|------|-----------|
|
|
| 401 | Missing Authorization header |
|
|
| 403 | Invalid API key |
|
|
| 409 | Update already running |
|
|
| 501 | Update system or usable Bash runtime not available |
|
|
| 504 | Update check/backup timed out |
|
|
|
|
### `POST /v1/extension/start`
|
|
|
|
Start an extension container. Runs `docker compose up -d <service_id>` using the full compose stack (resolved via `scripts/resolve-compose-stack.sh`). Before starting, the agent pre-creates any `./data/` volume directories declared in the extension's `compose.yaml`, with correct ownership based on the `user:` field.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Request body:**
|
|
```json
|
|
{
|
|
"service_id": "my-extension"
|
|
}
|
|
```
|
|
|
|
**Validation rules:**
|
|
- `service_id` must match `^[a-z0-9][a-z0-9_-]*$`
|
|
- Core services are rejected (403)
|
|
- Extension directory must exist in `user-extensions/` with a valid manifest
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"service_id": "my-extension",
|
|
"action": "start"
|
|
}
|
|
```
|
|
|
|
**Error responses:**
|
|
| Code | Condition |
|
|
|------|-----------|
|
|
| 400 | Invalid `service_id` format or missing request body |
|
|
| 401 | Missing Authorization header |
|
|
| 403 | Invalid API key or core service |
|
|
| 404 | Extension not found (no directory or no manifest) |
|
|
| 409 | Operation already in progress for this service |
|
|
| 500 | Docker Compose operation failed |
|
|
| 503 | Docker Compose operation timed out (120s) |
|
|
|
|
### `POST /v1/extension/stop`
|
|
|
|
Stop an extension container. Runs `docker compose stop <service_id>`.
|
|
|
|
**Authentication:** Required
|
|
|
|
**Request/response format:** Same as `/v1/extension/start` with `"action": "stop"`.
|
|
|
|
### `POST /v1/extension/logs`
|
|
|
|
Fetch recent container logs. Uses `docker logs --tail N ods-<service_id>` directly (bypasses compose for speed).
|
|
|
|
**Authentication:** Required
|
|
|
|
**Request body:**
|
|
```json
|
|
{
|
|
"service_id": "my-extension",
|
|
"tail": 100
|
|
}
|
|
```
|
|
|
|
The `tail` parameter is clamped to 1-500 (defaults to 100).
|
|
|
|
**Response (200):**
|
|
```json
|
|
{
|
|
"service_id": "my-extension",
|
|
"logs": "...log output...",
|
|
"lines": 100
|
|
}
|
|
```
|
|
|
|
If the container does not exist yet (e.g. image is still pulling), a 200 response is returned with a message instead of logs.
|
|
|
|
**Error responses:**
|
|
| Code | Condition |
|
|
|------|-----------|
|
|
| 503 | Log fetch timed out (5s) |
|
|
| 500 | Failed to fetch logs |
|
|
|
|
## Security Boundaries
|
|
|
|
The host agent is a **critical security boundary** because it can start and stop Docker containers on the host.
|
|
|
|
Protections in place:
|
|
- **Scoped network binding**: macOS/Windows bind to `127.0.0.1`; Linux binds to the `ods-network` gateway when detected so containers can reach the agent, with Docker bridge as a compatibility fallback. It does not bind to `0.0.0.0` unless explicitly configured.
|
|
- **API key auth**: All mutation endpoints require Bearer token authentication
|
|
- **Core service protection**: Core services (loaded from `config/core-service-ids.json` with hardcoded fallback) cannot be managed
|
|
- **Service ID validation**: Regex-validated, must map to an actual extension directory with a manifest
|
|
- **Per-service locking**: Prevents concurrent start+stop races on the same service via `threading.Lock`
|
|
- **Request size limit**: Request bodies capped at 4 KB
|
|
- **Subprocess timeout**: Docker operations time out after 120 seconds
|
|
|
|
## How the Dashboard API Calls It
|
|
|
|
The Dashboard API (`extensions/services/dashboard-api/routers/extensions.py`) communicates with the host agent via the `AGENT_URL` environment variable (constructed from `ODS_AGENT_HOST` and `ODS_AGENT_PORT` in `config.py`). It uses `ODS_AGENT_KEY` for authentication. The connection flows through Docker's `host.docker.internal` DNS name by default, allowing the containerized API to reach the host-bound agent.
|
|
|
|
If the host agent is unreachable, mutation operations (install, enable, disable) still succeed at the file level but return `"restart_required": true` to signal that `ods restart` is needed.
|