openvibers/openvibe.bot Commits

Add the cutover runbook for the embed-public flag migration

OP OpenVibers committed 4 Oct 2026 · af8c8eb028 · 1 file, +139 −0

Requested by finish-f1. Handed to the local harness by the cloud job pr-OpenVibe.Bot-22; done by DeepSeek harness.

Changes

ddiff --git a/docs/cutover-pr-openvibe-bot-22.md b/docs/cutover-pr-openvibe-bot-22.md
nnew file mode 100644
iindex 0000000..3dd1edd
---- /dev/null
++++ b/docs/cutover-pr-openvibe-bot-22.md
@@@ -0,0 +1,139 @@
++# OpenVibe.Bot — cutover runbook for PR #22
++
++The harness marks OpenVibe.Bot **risk data** (the repository owns `migrations/`), so a PR that merges into it
++must carry a cutover manifest and a green rehearsal. PR #22 is the first step that actually adds a migration
++(`0005_embed_public.sql`), so this runbook is a real data cutover, not the no-op kind: what runs in which
++order, what is backed up, how the result is verified, and the way back.
++
++## Scope — what this PR actually changes
++
++PR #22 (plan T15 step R9, first half) adds the owner's opt-in to anonymous read-only embedding and the
++validated allow-list of pages that may frame it:
++
++- `migrations/0005_embed_public.sql` — **phase `expand`**: `ALTER TABLE robots ADD COLUMN embed_public boolean
++ NOT NULL DEFAULT false`. Additive only: one new column, `false` for every existing robot, nothing to backfill.
++ No row is touched and no event is written. There is no `contract` step in this PR; the column stays.
++- `server/config.js` — `BOT_EMBED_ORIGINS` (comma or space separated, default
++ `https://openvibe.live,https://www.openvibe.live`) is parsed into `config.embed.origins`, and
++ `frameAncestors(config)` builds the CSP `frame-ancestors` list. Each entry must be a bare https origin (no
++ path, query or wildcard; `http://localhost:<port>` / `http://127.0.0.1:<port>` only outside production). An
++ invalid entry **refuses to boot**.
++- `server/domain/index.js`, `server/web/routes.js`, `server/web/render.js` — `setEmbedPublic` and the owner-only
++ `POST /robots/:id/embed` (a same-origin form post, `embed_public=on|off`, 303 back to the panel; a
++ non-owner, a cross-site Origin, a bad value and a missing robot are refused and audited as usual). The flag is
++ web-only: `present.robot` never carries it, so `/api/v1` is unchanged.
++- `test/embed-flag.test.js`, `.env.example`, `README.md`, `STATUS.json`.
++
++The migration is the only data-shaped change and it is backward-compatible in both directions: the previous
++release's queries neither name `embed_public` nor depend on its absence (new rows take the column default), so
++an old release serves correctly against the migrated schema. Deploying this PR writes no rows; a row changes
++only when an owner flips the switch, and only that one boolean.
++
++## Cutover manifest (the harness writes it into the PR description)
++
++The merge gate reads `runbook` and `rehearsal` from a fenced `cutover` block in the PR description. The harness
++adds it from this file at the verified head; it resolves to:
++
++```cutover
++{"runbook": "docs/cutover-pr-openvibe-bot-22.md", "rehearsal": "openvibe-bot-22"}
++```
++
++- `runbook` — this file, the runbook the rehearsal follows.
++- `rehearsal` — the marker name `openvibe-bot-22`; the rehearsal writes
++ `ds/deploy/rehearsals/openvibe-bot-22.json` = `{"ok": true}` on the harness when it is green. That marker is
++ written by whoever runs the rehearsal, never by this runbook and never by the PR author.
++
++## What runs, in which order
++
++1. **Merge gate (harness).** Checks green, review SHIP, this runbook on the verified head, and
++ `ds/deploy/rehearsals/openvibe-bot-22.json` saying `{"ok": true}` for the pushed head. Only then does the
++ squash-merge run.
++2. **Backup.** Take the standard pre-deploy backup of Bot's PostgreSQL database (pgBackRest) and confirm the
++ latest restore point is current. This migration cannot lose or rewrite data, but the guard is unconditional:
++ no deploy starts on an unbacked database.
++3. **Deploy.** Bot deploys through its own pipeline (`ov deploy bot` queues the guarded deploy on
++ `openvibe-ovh`; unit `openvibe-bot.service`, layout `/opt/openvibe.bot`, port 4630). On boot the service
++ applies `migrations/` with the owner role on `DATABASE_DIRECT_URL` (this PR leaves **1 pending**: `0005`),
++ serialised by the SDK's advisory lock; the single `ALTER TABLE` is transactional, so it either applies
++ whole or not at all. It then serves on the pooled `DATABASE_URL`. The nginx vhost is unchanged; no reload is
++ needed. Set `BOT_EMBED_ORIGINS` in `/etc/openvibe/bot.env` first **only if** the framing pages differ from
++ the default OpenVibe.Live pair — an invalid value stops the service at boot, so set it before the restart,
++ not after.
++4. **Verify.** Run the checks in the next section before the deploy is called good. A failed readiness check
++ rolls the release back automatically.
++
++## The backup
++
++- Command: the standard pgBackRest backup of the Bot database (the same one every Bot deploy takes), then
++ `pgbackrest info` to confirm a restore point newer than the deploy start.
++- No import step applies: Bot is already on PostgreSQL (`migrations/0001_bot.sql` through
++ `0004_run_jobs.sql`, all applied on the base branch).
++- Row counts of `robots`, `devices`, `robot_operators`, `pairing_codes`, `command_audit` and
++ `bot_event_outbox` are noted before the deploy for the verification step. The new column starts `false` for
++ every row, so `SELECT count(*) FROM robots WHERE embed_public` is 0 immediately after the migration.
++
++## How the result is verified
++
++1. `openvibe-bot.service` is active after the restart (no crash loop); the journal shows `0005_embed_public.sql`
++ applied and no migration held.
++2. `GET http://127.0.0.1:4630/api/health` answers 2xx and `GET http://127.0.0.1:4630/api/ready` answers 2xx
++ (database, Valkey, Network readiness and the outbox relay).
++3. The migrations table lists `0005_embed_public.sql` as applied and **0 pending**; `robots.embed_public`
++ exists, is `NOT NULL DEFAULT false`, and `SELECT count(*) FROM robots WHERE embed_public` is 0 — so no
++ existing robot was opted in by the migration.
++4. With an intentionally invalid `BOT_EMBED_ORIGINS` (e.g. `https://*.evil.test` or `https://x.test/path`), a
++ scratch start refuses to boot; with the default or a valid list it boots. The list is only parsed and
++ validated in this PR — `frameAncestors(config)` is exported for the embed view that follows in a later step,
++ and the responses' CSP still carries the existing `frame-ancestors 'self'`, unchanged by this PR.
++5. Signed out, `GET https://openvibe.bot/robots`, `/pair/<id>` and `/panel/<id>` still answer HTTP 302 to
++ `/auth/login?next=<the page>`; `GET /install` is still a 302 to `BOT_INSTALLER_SOURCE_URL`.
++6. Signed in as a test person with a `sim.rover` robot: `/panel/<id>` shows the embed switch (and `/robots`
++ shows one per robot); posting `embed_public=on` answers 303 to `/panel/<id>` and the column becomes `true`;
++ posting `off` clears it. `GET /api/v1/robots/<id>` never contains `embed_public`. A member operator and an
++ unrelated person get 403 and the flag does not move; a cross-site `Origin` gets 403; a value other than
++ `on`/`off` is refused and a missing robot is 404 — with no flag change in any refusal.
++7. The row counts from the backup step are unchanged apart from anything the test person added in step 6;
++ `embed_public` moved only for the test robot and only while the test held it.
++
++If any of 1–5 fails, the deploy is not good: roll back (below) and investigate.
++
++## Way back (rollback and restore)
++
++- **Automatic:** while the deploy waits for readiness, a non-2xx `/api/ready` restores the previous release's
++ sha. The previous release never reads `embed_public` and never names `BOT_EMBED_ORIGINS`, so it serves
++ correctly against the migrated schema with no data step.
++- **Manual:** `ovhost rollback bot --to <previous sha>` on `openvibe-ovh`, then re-run the verification above.
++- **Configuration:** `BOT_EMBED_ORIGINS` is new; the previous release ignores it, so a rollback needs no change.
++ To return a still-running new release to its defaults, unset it (falls back to the OpenVibe.Live pair) or set
++ it to `https://openvibe.live,https://www.openvibe.live` and restart.
++- **Data:** do **not** drop the column to roll back. `embed_public` is additive (`NOT NULL DEFAULT false`), so
++ the previous release and this one both serve with it in place; dropping it is a destructive `contract` step
++ that belongs to a later PR, and doing it here would break a re-deploy of this release and every row a
++ rollback did not touch. Values written during the window are ordinary booleans the previous release ignores.
++ Restore from the pgBackRest backup taken in the backup step **only** if an unrelated data incident requires
++ it, coordinating with the database owner because a restore discards writes made after the snapshot.
++
++## Rehearsal on a copy of the data
++
++Whoever runs this runbook rehearses it **on a copy** — never on production — against a scratch PostgreSQL, with
++main's migrations, the repository's fixtures and this PR's head. The fenced block declares the commands the
++harness runs after it has applied the migrations:
++
++```rehearse
++# PR 22 adds migration 0005 (an additive robots.embed_public column); this proves the column, the owner-only
++# switch and the validated origin list against the migrated scratch schema, and that the panel still renders.
++node test/embed-flag.test.js
++node test/panel.test.js
++```
++
++1. The harness starts a scratch database, applies the base branch's migrations and the repository's fixtures,
++ then this PR's migrations — `0005_embed_public.sql` applies once, and a second run applies nothing.
++2. It runs `node test/embed-flag.test.js` (the column is `false` by default and absent from `/api/v1`; the
++ owner flips it on and off and the panel/list reflect it; members and outsiders, cross-site posts, bad values
++ and missing robots are refused; `setEmbedPublic` emits no event; the origin list defaults, accepts custom
++ lists, and refuses a wildcard, path, query, bare scheme or plain http) and `node test/panel.test.js` (the
++ panel page, its controls and the simulated robot still render against the migrated schema).
++3. It confirms `0005` applied, **0 pending** afterwards, and the fixture row counts unchanged.
++4. When all of it is green, record the marker `ds/deploy/rehearsals/openvibe-bot-22.json` = `{"ok": true}` on
++ the harness. **This runbook must not write that marker itself** — it is written by whoever performed the
++ rehearsal, from the runbook's exit code.