# AGENTS.md — OpenVibe.Bot ## Project overview The open control panel for robots: pairs a robot's machines, keeps their state and gates every operator command. A device opens one outbound WebSocket to `/device`; a person drives it from `/control` or the REST API through the same gate — roles, per-robot limits, cooldowns and the turn budget — over a panel built from the robot's profile rather than per-robot code. Node.js/Express + `ws`, PostgreSQL (`ov_bot`), CommonJS, **no build step**. - Service id `bot`, port `4630`, origin `https://openvibe.bot`, unit `openvibe-bot.service`, release root `/opt/openvibe.bot`. - Design: OpenVibe.Contracts `docs/adr/ADR-043-bot-devices-and-control.md`. - Boot applies `migrations/` with the owner role (`DATABASE_DIRECT_URL`, one connection, advisory lock), seeds the shipped profiles, then serves on the pooled runtime role (`DATABASE_URL`) with Valkey for shared per-process state. Several processes may start together. ## What it owns - Robots, profiles, per-robot limits, `allow` lists, roles and operators; the curated kit catalogue (`server/kits/*.json`, metadata only). - Devices and their credentials (hashed at rest, rotated, revoked, never readable back) and pairing codes (one-time, short-lived; `BOT_PAIRING_AUTHORITY=network` lets Network mint them). - The `/device`, `/control` and `/watch` WebSockets, the command gate and the operator queue. - Routines (`server/routines.js`), owner webhooks (`server/webhooks.js`, signed, retried), community robot profiles (`server/submissions.js`), the phone-as-robot device, the in-process simulator, the server-side ONVIF camera connector, and the job dispatcher for paired Link machines (`server/jobs/`). - The front page, `/robots`, `/models`, `/r/:id`, `/pair/:id`, `/panel/:id`, `/try`, `/docs`, `/robots/submit`, legal pages, sitemap/llms/manifest, and the read-only embeddable panel. - The command audit (`command_audit`, 30 days) and the `bot.*` outbox events. Not owned: the robot hardware or its driver (Link plugins / the device), video ingest (OpenRestream), live streaming and VODs (Live/Media), identity, the money ledger, AI model execution. ## Who calls it, and whom it calls - Callers: people in the browser; the OpenVibe.Actor agent (`svc:actor`, only for the robot's owner, `GET/POST /api/v1/robots/:id/steps`), Live (a channel bound to a robot embeds `/panel/:id/embed`), and services using a Network service token with `bot.*` capabilities. - Depends on: Network (user/service/node tokens, pairing, identity resolution, account export/ deletion events), OpenRestream (WHIP ingest and the robot's stream key; `openre.*` capabilities), Billing (`billing.usage.record` for job usage), Search (`search.document.write`, model documents), Live (the owner's channel for the on-stream switch), and `openvibe-contracts`/`openvibe-sdk`/ `openvibe-shared` pins. - A device credential is a secret: `Authorization` header only (the phone uses the subprotocol `ov.device.` because a browser cannot set the header), never a query string. ## Repository map - **Entry:** [server/index.js](server/index.js) — config, owner-role migration, profile seed, app, Valkey, simulators, telemetry, jobs and subscriptions; graceful stop. - **App:** [server/app.js](server/app.js) — CSP (a special `HOME_CSP` for the front page), `/api/v1`, `/auth`, `/shared`, `/device`, `/control`, `/watch` upgrades, pages, error handling. - **Config:** [server/config.js](server/config.js); database [server/db.js](server/db.js). - **Domain:** [server/domain/](server/domain/), [server/profiles/](server/profiles/), [server/sim/](server/sim/), [server/onvif/](server/onvif/), [server/kits/](server/kits/). - **API:** [server/api/](server/api/) — `v1.js`, `auth.js`, `actor-limits.js`, `resource-control.js`, `desk-leases.js`, `resource-index.js`. - **Realtime:** [server/realtime.js](server/realtime.js) — `/device`, `/control`, `/watch`, the gate. - **Web:** [server/web/](server/web/) — `home.js`, `routes.js`, `directory.js`, `docs.js`, `layouts.js`, `session.js`, `qr.js`, `surface.js`. - **Browser:** [public/panel.js](public/panel.js), `public/drive.js`, `public/phone-device.js`, `public/try-device.js`, `public/sim-world.js`, `public/sim-view.js`, `public/embed-frame.js`. - **Jobs/metering:** [server/jobs/](server/jobs/), [server/events/outbox.js](server/events/outbox.js). - **Deploy:** [deploy/systemd/openvibe-bot.service](deploy/systemd/openvibe-bot.service), [deploy/nginx/openvibe.bot.conf](deploy/nginx/openvibe.bot.conf). ## Commands ```bash npm ci cp .env.example .env npm run dev # http://127.0.0.1:4630 npm test # test/run.js, PGlite, in-process Network/Events stubs npm run test:pg # same suite on PostgreSQL (BOT_TEST_STORE=pg) npm test -- profiles pairing # filter by file name ov test # the harness's sharded run of what the change reaches node --check ``` ## Database and migrations PostgreSQL database `ov_bot` (host data role, ADR-035); PGlite in development/tests. Boot applies `migrations/NNNN_*.sql` through the **owner** connection and then serves through the pooled runtime role; the run is serialised by an advisory lock, so parallel starts are safe. Never edit an applied migration; add a new number. The command audit is pruned after 30 days; job usage waits in `run_usage_outbox` when Billing is not configured and is never dropped. ## Auth and the command gate - User tokens (SSO session / `ov_token` cookie) and service tokens are both accepted on `/api/v1`; a **node token is refused** on person and service routes. A person acts only as themself — naming anyone else is refused, never ignored. - The **owner** can list a robot, add/remove operators, edit limits, set the agent switch and flip streaming toggles. An operator drives within limits and cooldowns. An open-queue robot gives a viewer a turn, and leaving promotes the next person at once. - `svc:actor` may read/drive **only** the owner's robot and only with `robots.agent_control` on; otherwise `403 bot.agent_not_allowed`. Every step passes the same gate as the owner's own commands. - A machine runs only its owner's jobs: the dispatcher compares the job requester with the machine owner and refuses anyone else with `bot.not_owner`, whatever the placer chose. - Every command, allowed or refused, is audited. Pairing codes are one-time; `halt` always passes; Stop is one optional part of a layout, and the tray's own stop stays local. ## Frontend loading model Server-rendered pages and plain forms, enhanced by no-build scripts (`public/panel.js` drives with touch/keyboard/gamepad). There is no SPA, bundler or transpiler. `openvibe-shared` supplies the Frame, showcase, arrange surface and serve handler (`/shared/*` from the pinned node_modules). The front page uses its own CSP (`HOME_CSP`) for the Frame's Network calls; every other page keeps `default-src 'self'`. Embedding takes both sides: `BOT_EMBED_ORIGINS` (bare https origins, no paths/ wildcards) and the embedding page's `frame-src` must both allow the frame. ## Deploy Unit [deploy/systemd/openvibe-bot.service](deploy/systemd/openvibe-bot.service) runs `/opt/openvibe.bot/server/index.js` on `127.0.0.1:4630`, env `/etc/openvibe/bot.env`; nginx [deploy/nginx/openvibe.bot.conf](deploy/nginx/openvibe.bot.conf) terminates TLS and upgrades `/device`, `/control` and `/watch` to the same port. The repo documents the unit and env, not an `ovhost deploy` command — use the host's own installer. Readiness `/api/ready`, liveness `/api/health`, both loopback-only. Env that refuses to boot when invalid: `BOT_PAIRING_AUTHORITY` (`bot` or `network`), `BOT_EMBED_ORIGINS` (https origins only; `http://localhost` outside production). ## Tests `npm test` runs each `test/*.test.js` in its own process through `openvibe-shared/test-runner`, with PGlite and in-process Network/Events stubs. It covers the device and operator WebSocket gates, the REST routes, pairing paths, profiles and kits, the owner fence, the optional Stop, the outbox, routines, webhooks, jobs/metering, the embed/watch limits, the simulator and the ONVIF camera against a stub. `npm run test:pg` runs the same on PostgreSQL. `ov test` is the harness wrapper. ## Conventions - CommonJS, 4-space indent, single quotes, semicolons; domain logic in `server/domain/` and `server/`, HTTP in `server/api`/`server/web`, injectable config/db/clock/fetch/log for tests. - Robot profiles and layouts are data (`bot.robot-profile@1`, `ui.surface@1`, `ui.layout@1`), not per-robot code; every control in a layout still goes through the same gate. - Secrets: device credentials, pairing codes, publish keys and WHIP URLs never reach a log, an outbox event, an API response after their one showing, or a file. - Errors are problem+json with stable codes; egress only to configured hosts (ONVIF one host, no redirects; webhooks public addresses only, no redirects). ## Pitfalls - **Stop is one optional part, off by default.** It sends the robot's own `halt`; nothing latches and nothing clears. The tray's own Stop stays local, and `halt` always passes the gate. - **Never widen the owner fence.** A person naming another subject, a service, a node token or `svc:actor` without the owner's switch must be refused. - **Device credentials are hashed and read-once.** Do not add a readable copy or a query-string form. - **Webhooks are written in the transaction that emits the event**, signed and retried 30 s, 2, 10, 30, 120 min; never post inline and lose the row. - **ONVIF is one configured host, no embedded credentials, no redirects**, with secret refs only. - **The simulator is not a device**: no jobs, not counted online, no outbox presence. - **Embedding is opt-in** (`embed_public`), and `/api/v1` never exposes the flag. - **Do not put a capability in a layout** — layouts arrange, the gate authorizes. - **The front page CSP is separate**; a new Network/Frame asset must be added to `HOME_CSP` only, or to the site-wide policy if it is used elsewhere. ## Documentation - [README.md](README.md) — the product, devices, limits, security and deploy. - [docs/protocol.md](docs/protocol.md) — the device/control/watch WebSocket protocol. - [docs/live-controls-migration.md](docs/live-controls-migration.md) — where Live's old controls went. - [docs/controls-embed.md](docs/controls-embed.md) — embedding, tickets and CSP. - [docs/ai-offers.md](docs/ai-offers.md), [docs/machine-access-relay.md](docs/machine-access-relay.md). - ADR-043 is the binding contract; `openvibe-contracts` holds `bot.*` shapes and capabilities.