fix(cli): refuse to serve a drifted vendored console under os dev (#7752) - #7783
Conversation
…7752) `packages/console/dist` is a gitignored local build that only `scripts/build-console.sh` (`pnpm objectui:build`) refreshes — `turbo run build` never touches it. Pull a branch that moves `.objectui-sha` and the pin advances while the dist stays frozen, so the server keeps serving a Console SPA the repo no longer pins. `pnpm check:console-sha` already fails on exactly this, but it is wired into the root `pnpm dev` / `dev:showcase` / `dev:crm` / `dev:todo` scripts only. Every other way to boot reaches the server without passing it: `objectstack dev` run inside an example dir, an example's own `dev` script (`objectstack dev --seed-admin`), a `.claude/launch.json` config driving `pnpm exec objectstack dev` — which is the path the QA runner takes. A sweep booted that way and spent its run measuring a console two days behind the pin; two of its clauses had to be recorded `blocked` once the gap was found. So the guard gets a second seat, on the boot path itself. Resolution now reports proven drift to its caller instead of warning about it in place, and `decideConsoleMount` turns that into a decision: under `os dev` the Console does not mount at all, and the boot prints the rebuild remediation. The stale bundle becomes unreachable rather than silently authoritative — `/_console/` 404s and the banner advertises no console URL — while the API keeps serving, so api/cli work is unaffected. Deliberately narrow: `isDev` only, and only on drift it can prove. A published install ships no `.objectui-sha` pin and the sibling-repo dev fallback writes no stamp, so both keep resolving exactly as before and no production or cloud deployment can reach the refusal. `OS_ALLOW_CONSOLE_DRIFT=1` boots the stale bundle deliberately. The runner-facing docs say the same thing from the other side: a 404 `/_console/` after this change reads "rebuild with `pnpm objectui:build`", never "console broken". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkvcB2Ei8Mpaa87N2US5wY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
…s/**` ships separately (#7752) Per the maintainer's 2026-08-11 ruling, changes under `.claude/skills/**` and `skills/**` are ADR-level and merge through the human channel. Carrying the runner note here would hold the whole guard behind that channel, so it moves to its own PR (branch `claude/issue-7752-skill-note`) and this one keeps only the code, tests, changeset, and the `RUNNER.md` trap row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkvcB2Ei8Mpaa87N2US5wY
Fixes #7752
Scope
The card's rebuild half (
pnpm objectui:build+ re-runningpage-variables-and-actionsclauses 4-5 and #7750's fix verification) happens on the QA runner's machine —packages/console/distis a gitignored local artifact and is not in this PR. What is in scope is the card's "Worth a second look" paragraph: the drift guard exists, and the boot path the QA run used never reaches it.The bypassed path, and the evidence
pnpm check:console-shais wired into four root scripts and nothing else:The QA runner does not use any of them.
docs/qa/platform-checklist/RUNNER.mddelegates booting to the dogfood-verification skill §0/§1, whose pre-flight is a named.claude/launch.jsonconfig driven bypreview_start. Both configs checked in today have the same shape:That is
objectstack devinvoked directly, so the root script — and with itcheck:console-sha— is never on the path. The same hole is wider than the launch file:examples/app-showcase's owndevscript isobjectstack dev --seed-admin, so evenpnpm -C examples/app-showcase devboots unguarded. Only the four root scripts carry seat 1.What the boot path did have was
warnOnConsoleShaDriftinpackages/cli/src/utils/console.ts— a single advisory line, after which the drifted bundle mounts and serves normally. That is the gap: a warning that scrolls past 47 loaded plugins and a seed summary is not a guard, and #7752 is what it costs.The fix — a second seat on the boot path itself
resolveConsolePathnow reports proven drift to its caller (onDrift) instead of warning about it in place, so exactly one message is emitted by whoever decides what to do.decideConsoleMountis that decision, and is the whole policy in one testable function: with drift proven andisDev, the Console does not mount.serve.tsprints the refusal block and moves on./_console/404s,/stops redirecting to it, the banner advertises no console URL (it is derived fromloadedPlugins.includes('ConsoleUI')), and the API still boots — soapi/cli/buildchecklist items are unaffected. The stale bundle is unreachable instead of silently authoritative.Remediation wording is the existing script's, verbatim in intent:
pnpm objectui:build(rebuild at the pinned SHA), withobjectui:refreshnamed only as the labelled wrong turn, since it would re-bump the pin to the local../objectuiHEAD.Deliberately narrow, so nothing outside the dev loop can reach it:
.objectui-shapin up-tree (published install, sibling-repo fallback)os serve/ productionOS_ALLOW_CONSOLE_DRIFT=1Why here rather than patching
.claude/launch.json: launch configs are authored fresh per run, so a seat there guards only the configs that exist today. Every boot — root script, example script, launch config, a barepnpm exec objectstack dev— goes throughserve.ts. Minimum intrusion, no bypass left.Verification
End-to-end, booting the runner's own command shape (
objectstack dev --ui -p <port> -d file:…fromexamples/app-showcase) against a fabricated stamp carrying the exact gap the card measured — dist09987b680…vs pin6314e87f2…:os devGET /_console/→ 404;GET /→ 404 (no redirect); banner Console line absent;GET /api/v1/health→ 200Console: http://localhost:3878/_console/;/_console/→ 200;/→ 302 →/_console/— no false positiveOS_ALLOW_CONSOLE_DRIFT=1/_console/→ 200 — escape hatch worksnode scripts/check-console-sha.mjs→ exit 1 on drift, 0 in syncResolution chain checked on the real path too (cwd
examples/app-showcase): resolvespackages/console, walks up to the repo.objectui-sha, decision{mount:false, refusedForDrift:true}.Unit: 19 tests pass across
packages/cli/src/utils/console.sha-drift.test.ts(new — the decision matrix, override parsing, and "no dist at all" staying distinguishable from "refused for drift" since they need different messages) andpackages/cli/test/console-resolve.test.ts(detection semantics, migrated todetectConsoleShaDrift).eslintclean on every touched file;tscreports nothing new in them.Runner-facing docs
The counterpart change, so the new 404 is not mis-read as a product bug: the dogfood skill §2 and the
stale-console-bundletrap row inRUNNER.mdnow say that a 404/_console/means "rebuild withpnpm objectui:build", never "console broken".Out of scope
No
.objectui-shabump (the pin is already correct), nopackages/consolechange, noscripts/pm/check-half-states.mjs(that is #7412), no release-notes edit.