From 91147b403ba2acbd4bb03da3c556159e37ec86e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 28 Aug 2026 20:27:11 +0100 Subject: [PATCH] =?UTF-8?q?deploy/2026.08.27.02=20=E2=80=94=20a=20plan=20w?= =?UTF-8?q?ith=20commands,=20and=20a=20schema=20claim=20that=20is=20checke?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026.08.27.01 was REFUSED before a single host was contacted, and the commander was right twice over. its plan described the release instead of instructing the rollout — no step list at all, where deploy/README.md defines a plan as "what to run, in order, on which hosts" and steps 4 and 5 of the procedure are plan-driven. and it asserted "nothing here changes a schema" on the day dav/schema.ts was deleted out of core. that second one was not a formatting mistake. dav_app_passwords now lives in plugins/caldav, and gen-plugin-schemas follows plugin DIRECTORIES — so on a host without that directory the table is in the database and not in the schema, and `db:push` proposes dropping it. on alpha that is eight device credentials. so this plan answers it from a survey rather than a guess. alpha has the plugin and 8 rows; the other six have no plugin and an empty leftover table; zero hosts run officer-marketplace, which retires the `tags` column question entirely. the orphan table is dropped EXPLICITLY, guarded by a count, and skipped on alpha — never as a db:push side effect. step 6 is new and would have been missed: officer-opencode has to restart or none of the skills, agent-identity or credential work in this release does anything. ALSO THE SIGN-OUT. a single 401 from any endpoint wiped the token from both storages and reloaded — which signs the owner out of every tab and keeps them there, since nothing is left to recover from. that is the right response to a revoked session and far too violent a response to one endpoint answering 401 for its own reasons. it now confirms against /auth/me first, and treats an unreachable server as emphatically not a revocation: rebooting the box must not end the session. reported mid-demo, with nothing in the logs and a freshly signed token working against every route the app loads. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- deploy/2026.08.27.02.md | 78 +++++++++++++++++++++++++++ package.json | 2 +- src/workspaces/hooks/src/useClient.ts | 39 ++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 deploy/2026.08.27.02.md diff --git a/deploy/2026.08.27.02.md b/deploy/2026.08.27.02.md new file mode 100644 index 00000000..50043c0f --- /dev/null +++ b/deploy/2026.08.27.02.md @@ -0,0 +1,78 @@ +# 2026.08.27.02 + +Everything in `2026.08.27.01`, which was **refused before any host was contacted** — its plan named no +commands and claimed "no schema change", which was false. This plan replaces it. `2026.08.27.01` stays +claimed and undeployed; do not try to revive it. + +Plus one fix made because of that refusal: a single 401 no longer signs the owner out of every tab. + +## The schema question, answered per host + +`officer_db/src/dav/schema.ts` was **deleted** — `dav_app_passwords` moved into `plugins/caldav`. +`gen-plugin-schemas` follows plugin DIRECTORIES, so the table is only declared on a host that has +`plugins/caldav/`. Everywhere else `bun db:push` will propose **dropping** it. + +Surveyed 2026-08-28, every host: + +| Host | `plugins/caldav/` | `dav_app_passwords` | +| --- | --- | --- | +| `alpha` | present | **8 rows** | +| `fofos` | absent | present, 0 rows | +| `macbookpro` | absent | present, not read (bun at `/opt/homebrew/bin`) | +| `macbruno` | absent | present, 0 rows | +| `production-pertento-bridge` | absent | present, 0 rows | +| `edge-pertento-bridge` | absent | present, 0 rows | +| `deadpool-rezio-bridge` | absent | not read (ssh refused at survey time) | + +So: **alpha pushes normally.** Everywhere else the table is a leftover of a feature that has left core, +and it is empty — but it is dropped **explicitly, with a guard**, not as a `db:push` side effect. + +`officer_marketplace` and its new `tags` column: **no host runs `officer-marketplace`** — checked, zero +of seven. Nothing to apply. If that changes, the column has no push script and must be added by hand. + +## Every host + +1. `git fetch origin --tags` then `git merge --ff-only deploy/2026.08.27.02` +2. **Only if `plugins/caldav/` is ABSENT on this host** — retire the orphan table, refusing to if it + holds anything: + ``` + psql "$POSTGRES_URL" -Atc "select count(*) from dav_app_passwords" + # 0 → drop it: psql "$POSTGRES_URL" -c "drop table dav_app_passwords" + # anything else → STOP. Report the count and do not continue on this host. + ``` + Read the variable name from `.env` — several hosts use `OFFICERDEV_POSTGRES_URL`. +3. `bun db:push < /dev/null` — **stdin closed on purpose**, so a push needing an answer fails instead of + hanging. Expect **no changes** after step 2. Anything destructive: stop and report. +4. `bun run plugins:update` +5. `pm2 restart officer` +6. `pm2 restart officer-opencode` — this release rewires how OpenCode gets its skills, its agent identity + and its credential, and all of that is built when the sidecar boots. Without this the code ships and + nothing about OpenCode changes. + +**`alpha`:** skip step 2 entirely — it has the plugin and 8 real credentials in that table. Step 3 runs +normally and must report no changes. + +**`macbookpro`:** prepend `/opt/homebrew/bin` to PATH for `bun`; run `pm2` separately. Asleep is a skip +with a note. + +**`deadpool-rezio-bridge`:** login is `br`, checkout `/home/br/officerdev/platform`. SSH refused once +during the survey — if it refuses again, note it and move on rather than retrying in a loop. + +**Do not restart `officer-claude-code`.** It ends every live agent session on the host, nothing here needs +it, and it is the owner's call. + +## Risk + +Moderate, and concentrated in step 2 — the only destructive command in this release. It is guarded by a +count and it is skipped on the one host where the table has contents. + +After the rollout, two things are worth a look: `useThemeAsset` now reads in `requestAnimationFrame`, so +a themed logo or wallpaper failing to appear points here; and the notify router's mount order changed, so +device registration is worth one check (it forwards correctly on `officer-dev`). + +## Hosts + +alpha · fofos · macbookpro · macbruno · production-pertento-bridge · edge-pertento-bridge · +deadpool-rezio-bridge + +`officer-dev` is skipped: it is the host running the rollout. diff --git a/package.json b/package.json index a6356af4..0a52016a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "officer", - "version": "2026.08.27.01", + "version": "2026.08.27.02", "private": true, "type": "module", "workspaces": [ diff --git a/src/workspaces/hooks/src/useClient.ts b/src/workspaces/hooks/src/useClient.ts index df34eb85..32f85e79 100644 --- a/src/workspaces/hooks/src/useClient.ts +++ b/src/workspaces/hooks/src/useClient.ts @@ -206,6 +206,45 @@ const handleRevokedSession = (url: string) => { if (revocationHandled) return; revocationHandled = true; + /* + Guard 2b: CONFIRM the session is dead before destroying it. + + A 401 from any one endpoint used to be enough to wipe the token out of both storages and reload — + which signs the owner out of every tab, and keeps them signed out, because there is then no token + anywhere to recover from. That is the right response to a revoked session and far too violent a + response to one endpoint answering 401 for its own reasons. + + Reported 2026-08-27, mid-demo: every refresh and every new tab came back signed out, with nothing in + the logs and a freshly signed token working fine against every route the app loads. One transient + 401 is indistinguishable from a revocation at the call site — so ask the one endpoint whose whole + job is to answer it. + + `/auth/me` is exempt from this handler (the `/auth/` guard above), so this cannot recurse. + */ + void (async () => { + try { + // Derived from the URL that just failed, because the base is passed per call rather than held in + // config — a relative guess would be wrong on any host that mounts the API elsewhere. + const probe = /\/api\//.test(url) ? url.replace(/\/api\/.*$/, '/api/auth/me') : '/api/auth/me'; + const res = await fetch(probe, { headers: { Authorization: `Bearer ${theToken}` } }); + // Still good: that 401 belonged to one endpoint, not to the session. Leave everything alone and + // let the next revocation try again. + if (res.ok) { + revocationHandled = false; + return; + } + } catch { + // The server is unreachable — a restart, a dropped network. Emphatically not a revocation, and + // wiping the session because the box was rebooting is the failure this guard exists to stop. + revocationHandled = false; + return; + } + destroySession(); + })(); +}; + +/** Actually end it: forget the token everywhere a load could read it back, then reload once. */ +const destroySession = () => { theToken = null; // Every key `createClient` reads, or the next load signs straight back in with the dead token. for (const key of ['PERTENTO_EDITOR_AUTH_TOKEN', 'BEARER_TOKEN']) {