Source: https://docs.any.org/auth/devices.html # Devices Every install of `any` that authorizes the same account is a **device**, identified by its peer id. The account keeps one synced registry of its devices, and a deterministic reader-side election answers "which of my devices runs the agent" with no coordinator. ## The registry The `devices` dataset lives in the account's tech space — the same place the space list lives — with one row per device, row id = peer id. All fields are synced, so the registry replicates to every device of the account and is invisible to other accounts. A raw row, as `POST /v1/devices/query` returns it: ```json { "id": "12D3KooW…", "name": "workstation", "os": "linux", "version": "0.9.1", "apps": { "bao": { "version": "1.2" } }, "activeClaims": { "bao": { "seq": 3, "at": 1755450000 } } } ``` | Field | Written by | Meaning | | --- | --- | --- | | `name` | client (`PUT /v1/devices/me`); seeded from the hostname on first registration | display name — a user-set name is never clobbered | | `os`, `version` | the server, on every boot | `runtime.GOOS` vocabulary and the `any` build | | `apps` | client | open slug set; presence means "installed here"; the value is a scalar bag, conventionally `{version}` | | `activeClaims.` | `POST /v1/devices/activate` | this device's claim to be the slug's active instance: `seq` (max of all visible seqs + 1) and `at` (unix seconds) | Online status is deliberately not a field — a liveness bit would churn CRDT history on every heartbeat. ## Election Two devices can claim the same slug concurrently; the design makes that survivable by resolving it at read time: > Candidates for a slug are the live rows that carry the slug under `apps` **and** hold a claim for it. The winner is the candidate with the highest `seq`; ties fall to the highest `at`, then to the lexicographically largest peer id. Consequences: - **Deterministic on converged data.** Every reader computes the same winner. The claim is writer-supplied data, not a CRDT version id — version ids are allocated per peer and would split-brain an election. - **Dangling claims never win.** Uninstalling an app (or pruning a row) forfeits its claim; there is no un-claim write. - **Nobody deactivates another device.** The role moves only through a newer claim or the winner's row losing the app. The rule is implemented once, inside the SDK, and returned pre-resolved as the `active` map on `GET /v1/devices`. Read that map; do not reimplement the rule. > **Note.** A claim minted on a replica that has not synced the newest claims can lose once heads converge. After sync, observe `active.` and re-claim if the role did not land. ## HTTP surface Account-scoped, behind the `/v1` auth guard. | Method | Path | Purpose | | --- | --- | --- | | GET | `/v1/devices` | mapped rows (the row id as `peerId`) + `active` (per-slug winner) + `self` (this server's peer id) | | POST | `/v1/devices/query` | raw windowed snapshot, standard [query body](https://docs.any.org/database/reading-data.md.txt) | | POST | `/v1/devices/query/subscribe` | raw live view over SSE, standard [frames](https://docs.any.org/realtime/subscribe.md.txt) | | PUT | `/v1/devices/me` | self row only: `{name?, apps?}`; `"apps": {"slug": null}` uninstalls → `204` | | POST | `/v1/devices/activate` | `{app}` — claim the slug on this device; also self-heals `apps.` → `204` | | DELETE | `/v1/devices/:peerId` | prune a row → `204` | ```bash curl -s -X PUT http://127.0.0.1:7001/v1/devices/me \ -d '{"name": "laptop", "apps": {"bao": {"version": "1.2"}}}' curl -s -X POST http://127.0.0.1:7001/v1/devices/activate -d '{"app": "bao"}' curl -s http://127.0.0.1:7001/v1/devices ``` ```json { "devices": [ { "peerId": "12D3KooW…", "name": "laptop", "apps": { "bao": { "version": "1.2" } }, "…": "…" } ], "active": { "bao": "12D3KooW…" }, "self": "12D3KooW…" } ``` ```bash any devices list any devices register --name laptop --app bao=1.2 any devices register --remove-app bao any devices activate bao any devices remove --yes any devices subscribe ``` The self-row restriction is structural: the SDK resolves its own peer id for every write, so `PUT /me` and `activate` cannot touch another device's row. ## A runtime's loop For an agent runtime keyed on slug `S`: | Moment | Action | | --- | --- | | Boot; no other row carries `apps.S` | claim — the majority case, valid even if a dangling claim points elsewhere | | Boot; `active.S` is another live device | nothing — a second install stays passive | | Active device pruned or app uninstalled | any survivor observing the change claims | | Manual switch | the user activates **on the target device**; the newest claim wins by `seq` | | Concurrent claims | every reader applies the same tiebreak; the loser sees it on subscribe and stands down | Upsert self (`apps.S`) → read `active.S` and `self` → claim or stand by → watch `POST /v1/devices/query/subscribe` and react when the winner moves to or away from `self`. A UI works off the same endpoint and the same `active` map. The [scheduling section](https://docs.any.org/scheduling/device-pins.md.txt) builds on this election. ## Pruning is permanent Record tombstones are sticky: a pruned peer id can never re-register. A device that comes back stays unlisted until it runs with a fresh device key — a new `any init` on a standalone server, a removed `device.key` on a managed one. Prune dead devices, not resting ones — and prune from another device: the SDK refuses to delete its own row. | Status | Code | When | | --- | --- | --- | | 404 | `device.not_found` | unknown peer id on DELETE | | 400 | `device.self_delete` | DELETE of this server's own row | | 409 | `device.pruned` | a self-row write after the row was pruned | | 400 | `request.invalid_field` | bad slug, or a non-scalar app value | | 400 | `request.missing_field` | empty update, or `activate` without `app` |