# Agents API reference

**URL:** <https://forum.hal0.dev/t/agents-api-reference/118>\
**Category:** Reference\
**Created:** [October 23, 2026, 5:37am UTC](https://forum.hal0.dev/t/agents-api-reference/118 "2026-10-23T05:37:06Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![system](https://forum.hal0.dev/user_avatar/forum.hal0.dev/system/32/37_2.png) [@system](https://forum.hal0.dev/u/system)\
**Post date:** [October 23, 2026, 5:37am UTC](https://forum.hal0.dev/t/agents-api-reference/118/1 "2026-10-23T05:37:06Z")

</div>

The `/api/agents` routes that back the dashboard’s Hermes and Pi cards: the overview  
payload, the agent and gateway restarts, and the Hermes repair job. The rest of the  
management surface, and the auth tiers, are in the  
[REST API index](https://forum.hal0.dev/t/rest-api-index-hal0-docs/47).

## Agent overview (`/api/agents/{name}/overview`)

`GET /api/agents/{name}/overview[?refresh=1]` is the one payload the dashboard’s Hermes  
and Pi cards render. ADMIN-gated with the rest of `/api/agents`. `name` is `hermes` or  
`pi`; any other name is `404 agent.unknown`. The route builds the answer from live  
probes and never fails for a normal box state: a missing systemd, a stopped gateway or  
an unreadable file shows as `unknown`, empty or `null` in the matching key.

The probes are cached for 30 s per agent, counted from when the build finished, and  
built once per miss: requests that miss while a build is running wait for it.  
`?refresh=1` starts a new build and refills the cache. A Hermes build or repair starting  
or ending is a miss too, so the state it leaves behind is never served from before it.  
`generated_at` (an ISO-8601 UTC time, the start of the build behind the answer) tells a  
client how old the probes are. `status`, `unit_active`, `units.agent`, `approvals.pending`,  
`job` and `phase` are recomputed on every request.

The cache is also dropped right after an action that changes what the card shows, so the  
next request is built after it: `POST /api/agents/hermes/restart` and  
`POST /api/agents/hermes/gateway/restart` once the restart went through,  
`PUT /api/agents/hermes/command-approvals` once the mode is written (a request that  
changed nothing leaves it alone), and `POST /api/agents/hermes/repair` when it registers  
its job. A refused or failed action does not drop it.

A build is capped at 8 s. One that runs past it (a wedged `sudo` or `systemctl`, say) is  
replaced by a neutral build, answered with `200` and `"partial": true` and cached like  
any build, so the next poll inside the 30 s window does not stall again; the requests  
waiting on that build get the same body, and `?refresh=1` builds again. For Hermes the  
probed keys then read: `version.live`, `version.previous` and `version.component`  
`null`, `version.pin.ref` `"unknown"`, `update_available` `false`; `units.gateway`  
`"unknown"` and `gateway_enabled` `null`; `api_server` `endpoint` and `healthy` `null`  
and `profiles` `[]`; `platforms`, `doctor` and `degraded` `[]`; `provision.present`  
`false`; `approvals` `mode`, `effective` and `in_sync` `null`; `memory.available`  
`false`; `links.desktop` `null`. The live keys above are still current, and `phase` is  
`unknown` unless it is `working` (a running job), `broken` or `down`, because `ready` and  
`degraded` need the probes that did not run. For Pi, `version` is `{installed: null, pin: null, update_available: false}`, `profile.checks` is `[]` and the `memory` flags are  
`null` or `false`; `commands` and `phase` are as usual. A normal build has no `partial`  
key.

`platforms` comes from a vault read through `sudo`, so it is cached on its own for 300 s  
per Hermes home and vault: a platform added or removed can take up to five minutes to  
show, unless the cache is dropped by one of the actions above or by `?refresh=1`, both of  
which also clear it. A read that failed answers `[]` and is not cached.

A bundled agent that is not installed answers with only `name`, `kind`,  
`installed: false`, `phase: "not_installed"`, `commands.install`, and the version it  
would get: `pin` (`{ref, describe}`) for Hermes, `version` (`{installed: null, pin, update_available: false}`) for Pi.

Hermes (`kind: "daemon"`):

| Key | Value |
| --- | --- |
| `name`, `kind`, `installed`, `status` | Identity and install state (`status` is `installed` or `broken`). |
| `unit_active` | `true`, `false`, or `null` when systemd cannot answer. |
| `version` | `live` and `previous` release (`id`, `ref`, `describe`, and for `live` also `built_at`, `python_minor`, `extras`, `platform_extras`, `hal0_version`), the `pin` (`{ref, describe}`), the `component` row (`installed`, `pinned`, `status`), and `update_available`. |
| `units` | `agent` is `active`, `inactive` or `unknown` (systemd could not answer). `gateway` is `active`, `inactive`, `failed`, `unknown`, or `absent` when the unit does not exist; its `activating`, `reloading`, `refreshing` and `maintenance` states read as `active` and `deactivating` as `inactive`, so a restart is not reported as down. The unit restarts forever (`Restart=always`) and never reaches `failed`, so `activating` with the `auto-restart` or `auto-restart-queued` sub-state is a crash loop and reads `failed`. `gateway_enabled` is `false` when the unit file is `disabled`, `masked` or `masked-runtime`, `true` for `enabled`, `enabled-runtime`, `static`, `linked`, `generated` and `transient`, and `null` otherwise. |
| `api_server` | `endpoint` (`host:port`, from config), `healthy`, and the `profiles` the gateway serves. The gateway and every profile are probed at once, with a 1 s timeout each. |
| `platforms` | Names of the configured gateway platforms. |
| `provision` | The last provision run: `present`, versions, `completed_at`, `reported_at` (the newest phase time), `phases`, `ok`/`warn`/`fail` counts, and the configured models, MCP servers, memory provider and approvals mode. `live_built_at` is when the live release was built, and `stale` is `true` when the report is older than that build: `hal0 update` rebuilds Hermes without rewriting `provision.json`, so a stale report no longer lists `provision_failed` or `provision_warn` in `degraded`. |
| `doctor` | Rows `{id, status, detail}` for `interpreter`, `plugins`, `ownership` and `gateway_unit`; `status` is `ok`, `warn`, `fail`, `absent` or `unreadable`. |
| `approvals` | `mode`, `effective`, `in_sync` and the `pending` approval count. |
| `memory` | `available`, `writes` and `last_write` of the agent’s private namespace. |
| `links` | `desktop`: the `HAL0_HERMES_PUBLIC_URL` value without a trailing slash, or `null`. |
| `job` | The newest queued or running Hermes build, `{id, kind, state}` with `kind` `converge` or `repair`, or `null`. |
| `degraded` | Why a running Hermes is degraded: `{reason, detail, fix, command}`, where `fix` is one of `restart_gateway`, `repair`, `resync_approvals`, `cli` (a root command, in `command`) or `none`. A gateway the operator turned off (`gateway_enabled: false`) and a missing gateway unit raise no `gateway_down` (the `gateway_unit` doctor row names the install command), the API not answering is not raised while the gateway is starting, reloading or in maintenance, and `doctor_drift` is one reason per distinct fix (a `gateway_unit` row that is `unreadable`, because the check could not read Hermes’ home, has fix `none`). |
| `phase` | `working`, `broken`, `unknown`, `down`, `degraded` or `ready`. The dashboard shows `ready` as `serving` when the agent slot is up. |
| `generated_at` | When the build behind the cached probes started. |
| `partial` | `true` only on a build that hit the 8 s cap; absent otherwise. |

Pi (`kind: "cli"`):

| Key | Value |
| --- | --- |
| `name`, `kind`, `installed`, `status` | Identity and install state. |
| `version` | `installed` and `pin` as `{pi, adapter}` versions, and `update_available` (`true` when either is behind the installer’s pin). |
| `profile` | The marker’s `home` and `profiled_at`, `home_readable`, and `checks`: `{id, ok, detail}` rows where `ok` is `null` when the daemon cannot read the profiled home. |
| `memory` | `mcp_entry` and `hindsight_config` (from `profile.checks`), and `hindsight_api_reachable`. |
| `commands` | `install`, `upgrade` and `uninstall` commands to run as root. |
| `phase` | `installed` or `broken`. |
| `generated_at`, `partial` | As for Hermes. |

## Agent restart and repair

ADMIN-gated with the rest of `/api/agents`, no request body, `hermes` only (any other agent  
is `404 agent.unknown`). Each action logs to `hal0.agents.audit` with `actor` (the  
`X-hal0-Agent` header, `hal0-dashboard` when absent).

| Method | Path | Status | Notes |
| --- | --- | --- | --- |
| POST | `/api/agents/hermes/restart` | 200 | Restarts `hal0-agent@hermes.service` through the `hal0-systemctl` sudo wrapper, as the Services page does: hal0-api runs as the unprivileged `hal0` user, and a bare `systemctl restart` is denied by polkit. The command-approvals `PUT` restart takes the same path. Returns `{agent_id, unit, status: "restarted", detail}` once the verb exits 0. A failed verb is `500 agent.restart_failed` (the failure line, cut to 200 characters, in `message`); one still running after 90 s is `500 agent.restart_timeout`, outcome unknown. Events: `agent.restart.invoked`, then `.ok`, `.failed` or `.timeout`. |
| POST | `/api/agents/hermes/gateway/restart` | 200 | The same for `hermes-gateway.service`, with the same body (`unit` is the gateway’s), errors and budget; `404 agent.not_installed` when Hermes has no install record. Events: `agent.gateway_restart.invoked`, then `.finished` (`status` is `restarted` or `error`). |
| POST | `/api/agents/hermes/repair` | 202 | Queues a repair job (below). |

A restart that went through drops the agent’s cached overview.

### Repair job

`POST /api/agents/hermes/repair` runs `hal0 agent reprovision hermes --repair --verbose` as the  
`hal0` user, as a child of hal0-api, and returns the  
queued job: `id`, `state: "queued"`, `phase: "repair:hermes"`, `kind: "repair"`, `agent`,  
`created_at`, `updated_at`, `error: null`, `steps: []`, `log_path`, `api_pid` (the hal0-api  
process that owns it) and `actor` (the `X-hal0-Agent` header, `hal0-dashboard` when absent).  
The job is in the updater’s registry, so `GET /api/updates/status/{id}` serves it, and a repair  
and a converge refuse each other.

- **What it does not cover.** This is the provisioning pipeline that  
`sudo hal0 agent reprovision hermes --repair` hands to the `hal0` user, without the root-only  
steps that command runs first: the stale `hal0-agent@` drop-in sweep, handing a root-owned  
venv or uv Python tree back to `hal0`, the `/usr/local/bin/hermes` wrapper, recording the  
terminal-tool decision, mirroring the vaulted default API key, and the ownership reconcile.  
A repair can end `applied` on a box that needs one of those. For ownership, vault-key or  
wrapper problems, and for the one-time root repair an older box asks for, run  
`sudo hal0 agent reprovision hermes --repair` on the box.
- `state` goes `queued`, `running`, then `applied` (exit 0) or `failed`. A running job has `pid`  
(the child) and `steps`, one entry per `[run] <step>` line the provisioner prints. A job  
whose child was reaped has `exit_code`, including one that timed out or whose runner failed.  
`audit_finished` appears once the runner has logged the job’s `agent.repair.finished` event;  
a job failed later as `api_restarted` logs that event without it.
- A `failed` job’s `error` is one of: `bootstrap exited <code>`; `bootstrap timed out after 2700s` (a repair is bounded to 45 minutes); `could not start bootstrap: <os error>` (no `pid`,  
no `exit_code`); `repair runner failed: <error>` (the runner itself broke, for instance  
reading the output); `repair cancelled` (hal0-api was shutting down before the child  
started); or the `api_restarted` text below. A line of child output over 1 MiB is skipped, with a  
marker in the transcript; it does not fail the build.
- The output goes to `log_path`,  
`/var/lib/hal0/state/agents/hermes/provision-logs/job-<id>.log` (`null` if it could not be  
opened; never pruned). Events: `agent.repair.invoked`, `agent.repair.finished`.
- Refused, with no job: `404 agent.not_installed` without a Hermes install record;  
`409 agent.busy` when an update, converge or repair job is `queued` or `running`  
(`details.job_id`) or another process holds the Hermes builder lock (`details.holder`,  
advisory: it can name a dead holder); `500 agent.builder_lock_unreadable` when the lock  
file cannot be created or opened (`details.error`), a fault of the box that waiting will  
not clear, so not a 409. The lock check is a snapshot taken when the request arrives: a  
`hal0 update component hermes --target <sha>` that is still fetching its Python has not  
taken the lock yet and reads as idle. A repair started then waits for that build and  
rebuilds at the pin afterwards, so the `--target` release does not stay live.
- **`api_restarted`.** The child shares hal0-api’s cgroup, so restarting hal0-api mid-repair (a  
`hal0 update`, `systemctl restart hal0-api`) kills the build. The live release is unchanged,  
because the builder swaps `current` only at the very end. The next status read of the job  
records it `failed` with `error_code: "api_restarted"` and `error: "hal0-api restarted during the repair; the live release is unchanged"`; run the repair again. `hal0 update`  
does not check for a running repair, so an update started from the CLI or the Updates  
page during one ends it this way. Any update,  
converge or repair job still `queued` or `running` when it is read back after a hal0-api  
restart reads `failed` / `api_restarted` too, with the text `/api/updates` gives in the  
[REST API index](https://forum.hal0.dev/t/rest-api-index-hal0-docs/47).
