openvibers/openvibe.bot AGENTS.md
AGENTS.md
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, port4630, originhttps://openvibe.bot, unitopenvibe-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,
allowlists, 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=networklets Network mint them). - The
/device,/controland/watchWebSockets, 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 thebot.*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 withbot.*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.recordfor job usage), Search (search.document.write, model documents),
Live (the owner's channel for the on-stream switch), andopenvibe-contracts/openvibe-sdk/
openvibe-sharedpins. - A device credential is a secret:
Authorizationheader only (the phone uses the subprotocol
ov.device.<credential>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_CSPfor the front page),
/api/v1,/auth,/shared,/device,/control,/watchupgrades, 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
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 <file.js>
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_tokencookie) 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:actormay read/drive only the owner's robot and only withrobots.agent_controlon;
otherwise403 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 withbot.not_owner, whatever the placer chose. - Every command, allowed or refused, is audited. Pairing codes are one-time;
haltalways 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 inserver/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, andhaltalways passes the gate. - Never widen the owner fence. A person naming another subject, a service, a node token or
svc:actorwithout 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/v1never 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_CSPonly, 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-contractsholdsbot.*shapes and capabilities.