openvibers/openvibe.bot AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Bot |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | The open control panel for robots: pairs a robot's machines, keeps their state and gates every |
| 6 | operator command. A device opens one outbound WebSocket to `/device`; a person drives it from |
| 7 | `/control` or the REST API through the same gate — roles, per-robot limits, cooldowns and the turn |
| 8 | budget — over a panel built from the robot's profile rather than per-robot code. Node.js/Express + |
| 9 | `ws`, PostgreSQL (`ov_bot`), CommonJS, **no build step**. |
| 10 | |
| 11 | - Service id `bot`, port `4630`, origin `https://openvibe.bot`, unit `openvibe-bot.service`, |
| 12 | release root `/opt/openvibe.bot`. |
| 13 | - Design: OpenVibe.Contracts `docs/adr/ADR-043-bot-devices-and-control.md`. |
| 14 | - Boot applies `migrations/` with the owner role (`DATABASE_DIRECT_URL`, one connection, advisory |
| 15 | lock), seeds the shipped profiles, then serves on the pooled runtime role (`DATABASE_URL`) with |
| 16 | Valkey for shared per-process state. Several processes may start together. |
| 17 | |
| 18 | ## What it owns |
| 19 | |
| 20 | - Robots, profiles, per-robot limits, `allow` lists, roles and operators; the curated kit catalogue |
| 21 | (`server/kits/*.json`, metadata only). |
| 22 | - Devices and their credentials (hashed at rest, rotated, revoked, never readable back) and pairing |
| 23 | codes (one-time, short-lived; `BOT_PAIRING_AUTHORITY=network` lets Network mint them). |
| 24 | - The `/device`, `/control` and `/watch` WebSockets, the command gate and the operator queue. |
| 25 | - Routines (`server/routines.js`), owner webhooks (`server/webhooks.js`, signed, retried), community |
| 26 | robot profiles (`server/submissions.js`), the phone-as-robot device, the in-process simulator, the |
| 27 | server-side ONVIF camera connector, and the job dispatcher for paired Link machines |
| 28 | (`server/jobs/`). |
| 29 | - The front page, `/robots`, `/models`, `/r/:id`, `/pair/:id`, `/panel/:id`, `/try`, `/docs`, |
| 30 | `/robots/submit`, legal pages, sitemap/llms/manifest, and the read-only embeddable panel. |
| 31 | - The command audit (`command_audit`, 30 days) and the `bot.*` outbox events. |
| 32 | |
| 33 | Not owned: the robot hardware or its driver (Link plugins / the device), video ingest (OpenRestream), |
| 34 | live streaming and VODs (Live/Media), identity, the money ledger, AI model execution. |
| 35 | |
| 36 | ## Who calls it, and whom it calls |
| 37 | |
| 38 | - Callers: people in the browser; the OpenVibe.Actor agent (`svc:actor`, only for the robot's owner, |
| 39 | `GET/POST /api/v1/robots/:id/steps`), Live (a channel bound to a robot embeds `/panel/:id/embed`), |
| 40 | and services using a Network service token with `bot.*` capabilities. |
| 41 | - Depends on: Network (user/service/node tokens, pairing, identity resolution, account export/ |
| 42 | deletion events), OpenRestream (WHIP ingest and the robot's stream key; `openre.*` capabilities), |
| 43 | Billing (`billing.usage.record` for job usage), Search (`search.document.write`, model documents), |
| 44 | Live (the owner's channel for the on-stream switch), and `openvibe-contracts`/`openvibe-sdk`/ |
| 45 | `openvibe-shared` pins. |
| 46 | - A device credential is a secret: `Authorization` header only (the phone uses the subprotocol |
| 47 | `ov.device.<credential>` because a browser cannot set the header), never a query string. |
| 48 | |
| 49 | ## Repository map |
| 50 | |
| 51 | - **Entry:** [server/index.js](server/index.js) — config, owner-role migration, profile seed, app, |
| 52 | Valkey, simulators, telemetry, jobs and subscriptions; graceful stop. |
| 53 | - **App:** [server/app.js](server/app.js) — CSP (a special `HOME_CSP` for the front page), |
| 54 | `/api/v1`, `/auth`, `/shared`, `/device`, `/control`, `/watch` upgrades, pages, error handling. |
| 55 | - **Config:** [server/config.js](server/config.js); database [server/db.js](server/db.js). |
| 56 | - **Domain:** [server/domain/](server/domain/), [server/profiles/](server/profiles/), |
| 57 | [server/sim/](server/sim/), [server/onvif/](server/onvif/), [server/kits/](server/kits/). |
| 58 | - **API:** [server/api/](server/api/) — `v1.js`, `auth.js`, `actor-limits.js`, `resource-control.js`, |
| 59 | `desk-leases.js`, `resource-index.js`. |
| 60 | - **Realtime:** [server/realtime.js](server/realtime.js) — `/device`, `/control`, `/watch`, the gate. |
| 61 | - **Web:** [server/web/](server/web/) — `home.js`, `routes.js`, `directory.js`, `docs.js`, |
| 62 | `layouts.js`, `session.js`, `qr.js`, `surface.js`. |
| 63 | - **Browser:** [public/panel.js](public/panel.js), `public/drive.js`, `public/phone-device.js`, |
| 64 | `public/try-device.js`, `public/sim-world.js`, `public/sim-view.js`, `public/embed-frame.js`. |
| 65 | - **Jobs/metering:** [server/jobs/](server/jobs/), [server/events/outbox.js](server/events/outbox.js). |
| 66 | - **Deploy:** [deploy/systemd/openvibe-bot.service](deploy/systemd/openvibe-bot.service), |
| 67 | [deploy/nginx/openvibe.bot.conf](deploy/nginx/openvibe.bot.conf). |
| 68 | |
| 69 | ## Commands |
| 70 | |
| 71 | ```bash |
| 72 | npm ci |
| 73 | cp .env.example .env |
| 74 | npm run dev # http://127.0.0.1:4630 |
| 75 | npm test # test/run.js, PGlite, in-process Network/Events stubs |
| 76 | npm run test:pg # same suite on PostgreSQL (BOT_TEST_STORE=pg) |
| 77 | npm test -- profiles pairing # filter by file name |
| 78 | ov test # the harness's sharded run of what the change reaches |
| 79 | node --check <file.js> |
| 80 | ``` |
| 81 | |
| 82 | ## Database and migrations |
| 83 | |
| 84 | PostgreSQL database `ov_bot` (host data role, ADR-035); PGlite in development/tests. Boot applies |
| 85 | `migrations/NNNN_*.sql` through the **owner** connection and then serves through the pooled runtime |
| 86 | role; the run is serialised by an advisory lock, so parallel starts are safe. Never edit an applied |
| 87 | migration; add a new number. The command audit is pruned after 30 days; job usage waits in |
| 88 | `run_usage_outbox` when Billing is not configured and is never dropped. |
| 89 | |
| 90 | ## Auth and the command gate |
| 91 | |
| 92 | - User tokens (SSO session / `ov_token` cookie) and service tokens are both accepted on `/api/v1`; |
| 93 | a **node token is refused** on person and service routes. A person acts only as themself — naming |
| 94 | anyone else is refused, never ignored. |
| 95 | - The **owner** can list a robot, add/remove operators, edit limits, set |
| 96 | the agent switch and flip streaming toggles. An operator drives within limits and cooldowns. An |
| 97 | open-queue robot gives a viewer a turn, and leaving promotes the next person at once. |
| 98 | - `svc:actor` may read/drive **only** the owner's robot and only with `robots.agent_control` on; |
| 99 | otherwise `403 bot.agent_not_allowed`. Every step passes the same gate as the owner's own commands. |
| 100 | - A machine runs only its owner's jobs: the dispatcher compares the job requester with the machine |
| 101 | owner and refuses anyone else with `bot.not_owner`, whatever the placer chose. |
| 102 | - Every command, allowed or refused, is audited. Pairing codes are one-time; `halt` always passes; |
| 103 | Stop is one optional part of a layout, and the tray's own stop stays local. |
| 104 | |
| 105 | ## Frontend loading model |
| 106 | |
| 107 | Server-rendered pages and plain forms, enhanced by no-build scripts (`public/panel.js` drives with |
| 108 | touch/keyboard/gamepad). There is no SPA, bundler or transpiler. `openvibe-shared` supplies the |
| 109 | Frame, showcase, arrange surface and serve handler (`/shared/*` from the pinned node_modules). The |
| 110 | front page uses its own CSP (`HOME_CSP`) for the Frame's Network calls; every other page keeps |
| 111 | `default-src 'self'`. Embedding takes both sides: `BOT_EMBED_ORIGINS` (bare https origins, no paths/ |
| 112 | wildcards) and the embedding page's `frame-src` must both allow the frame. |
| 113 | |
| 114 | ## Deploy |
| 115 | |
| 116 | Unit [deploy/systemd/openvibe-bot.service](deploy/systemd/openvibe-bot.service) runs |
| 117 | `/opt/openvibe.bot/server/index.js` on `127.0.0.1:4630`, env `/etc/openvibe/bot.env`; nginx |
| 118 | [deploy/nginx/openvibe.bot.conf](deploy/nginx/openvibe.bot.conf) terminates TLS and upgrades |
| 119 | `/device`, `/control` and `/watch` to the same port. The repo documents the unit and env, not an |
| 120 | `ovhost deploy` command — use the host's own installer. Readiness `/api/ready`, liveness |
| 121 | `/api/health`, both loopback-only. Env that refuses to boot when invalid: `BOT_PAIRING_AUTHORITY` |
| 122 | (`bot` or `network`), `BOT_EMBED_ORIGINS` (https origins only; `http://localhost` outside production). |
| 123 | |
| 124 | ## Tests |
| 125 | |
| 126 | `npm test` runs each `test/*.test.js` in its own process through `openvibe-shared/test-runner`, with |
| 127 | PGlite and in-process Network/Events stubs. It covers the device and operator WebSocket gates, the |
| 128 | REST routes, pairing paths, profiles and kits, the owner fence, the optional Stop, the outbox, routines, |
| 129 | webhooks, jobs/metering, the embed/watch limits, the simulator and the ONVIF camera against a stub. |
| 130 | `npm run test:pg` runs the same on PostgreSQL. `ov test` is the harness wrapper. |
| 131 | |
| 132 | ## Conventions |
| 133 | |
| 134 | - CommonJS, 4-space indent, single quotes, semicolons; domain logic in `server/domain/` and |
| 135 | `server/`, HTTP in `server/api`/`server/web`, injectable config/db/clock/fetch/log for tests. |
| 136 | - Robot profiles and layouts are data (`bot.robot-profile@1`, `ui.surface@1`, `ui.layout@1`), not |
| 137 | per-robot code; every control in a layout still goes through the same gate. |
| 138 | - Secrets: device credentials, pairing codes, publish keys and WHIP URLs never reach a log, an |
| 139 | outbox event, an API response after their one showing, or a file. |
| 140 | - Errors are problem+json with stable codes; egress only to configured hosts (ONVIF one host, no |
| 141 | redirects; webhooks public addresses only, no redirects). |
| 142 | |
| 143 | ## Pitfalls |
| 144 | |
| 145 | - **Stop is one optional part, off by default.** It sends the robot's own `halt`; nothing latches and nothing |
| 146 | clears. The tray's own Stop stays local, and `halt` always passes the gate. |
| 147 | - **Never widen the owner fence.** A person naming another subject, a service, a node token or |
| 148 | `svc:actor` without the owner's switch must be refused. |
| 149 | - **Device credentials are hashed and read-once.** Do not add a readable copy or a query-string form. |
| 150 | - **Webhooks are written in the transaction that emits the event**, signed and retried 30 s, 2, 10, |
| 151 | 30, 120 min; never post inline and lose the row. |
| 152 | - **ONVIF is one configured host, no embedded credentials, no redirects**, with secret refs only. |
| 153 | - **The simulator is not a device**: no jobs, not counted online, no outbox presence. |
| 154 | - **Embedding is opt-in** (`embed_public`), and `/api/v1` never exposes the flag. |
| 155 | - **Do not put a capability in a layout** — layouts arrange, the gate authorizes. |
| 156 | - **The front page CSP is separate**; a new Network/Frame asset must be added to `HOME_CSP` only, or |
| 157 | to the site-wide policy if it is used elsewhere. |
| 158 | |
| 159 | ## Documentation |
| 160 | |
| 161 | - [README.md](README.md) — the product, devices, limits, security and deploy. |
| 162 | - [docs/protocol.md](docs/protocol.md) — the device/control/watch WebSocket protocol. |
| 163 | - [docs/live-controls-migration.md](docs/live-controls-migration.md) — where Live's old controls went. |
| 164 | - [docs/controls-embed.md](docs/controls-embed.md) — embedding, tickets and CSP. |
| 165 | - [docs/ai-offers.md](docs/ai-offers.md), [docs/machine-access-relay.md](docs/machine-access-relay.md). |
| 166 | - ADR-043 is the binding contract; `openvibe-contracts` holds `bot.*` shapes and capabilities. |
| 167 |