Skip to content

fix(cli): refuse to serve a drifted vendored console under os dev (#7752) - #7783

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-7752-console-dist-guard
Aug 11, 2026
Merged

fix(cli): refuse to serve a drifted vendored console under os dev (#7752)#7783
huangyiirene merged 2 commits into
mainfrom
claude/issue-7752-console-dist-guard

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7752

Scope

The card's rebuild half (pnpm objectui:build + re-running page-variables-and-actions clauses 4-5 and #7750's fix verification) happens on the QA runner's machine — packages/console/dist is 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-sha is wired into four root scripts and nothing else:

dev          :: node scripts/check-dev-prereqs.mjs && pnpm check:console-sha && pnpm --filter @objectstack/example-showcase dev
dev:showcase :: … && pnpm check:console-sha && …
dev:crm      :: … && pnpm check:console-sha && …
dev:todo     :: … && pnpm check:console-sha && …

The QA runner does not use any of them. docs/qa/platform-checklist/RUNNER.md delegates booting to the dogfood-verification skill §0/§1, whose pre-flight is a named .claude/launch.json config driven by preview_start. Both configs checked in today have the same shape:

// .claude/launch.json — "showcase-dogfood-3777"
"runtimeExecutable": "pnpm",
"runtimeArgs": ["-C", "examples/app-showcase", "exec", "objectstack", "dev",
                "--ui", "--seed-admin", "-p", "3777", "-d", "file:/tmp/…/data.db"]

That is objectstack dev invoked directly, so the root script — and with it check:console-sha — is never on the path. The same hole is wider than the launch file: examples/app-showcase's own dev script is objectstack dev --seed-admin, so even pnpm -C examples/app-showcase dev boots unguarded. Only the four root scripts carry seat 1.

What the boot path did have was warnOnConsoleShaDrift in packages/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

  • resolveConsolePath now 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.
  • decideConsoleMount is that decision, and is the whole policy in one testable function: with drift proven and isDev, the Console does not mount. serve.ts prints the refusal block and moves on.
  • Result: /_console/ 404s, / stops redirecting to it, the banner advertises no console URL (it is derived from loadedPlugins.includes('ConsoleUI')), and the API still boots — so api / cli / build checklist 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), with objectui:refresh named only as the labelled wrong turn, since it would re-bump the pin to the local ../objectui HEAD.

Deliberately narrow, so nothing outside the dev loop can reach it:

situation behavior
dev, stamp ≠ pin refuse to mount, print remediation
dev, stamp = pin mount (no message)
dist unstamped / no .objectui-sha pin up-tree (published install, sibling-repo fallback) unprovable → mount, unchanged
os serve / production advisory warning, unchanged
OS_ALLOW_CONSOLE_DRIFT=1 mount the stale bundle deliberately

Why 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 bare pnpm exec objectstack dev — goes through serve.ts. Minimum intrusion, no bypass left.

Verification

End-to-end, booting the runner's own command shape (objectstack dev --ui -p <port> -d file:… from examples/app-showcase) against a fabricated stamp carrying the exact gap the card measured — dist 09987b680… vs pin 6314e87f2…:

case result
drift, os dev refusal block printed; GET /_console/404; GET /404 (no redirect); banner Console line absent; GET /api/v1/health200
stamp = pin (no other change) no drift message; banner Console: http://localhost:3878/_console/; /_console/200; /302/_console/ — no false positive
drift + OS_ALLOW_CONSOLE_DRIFT=1 advisory warning only; /_console/200 — escape hatch works
seat 1 unchanged node scripts/check-console-sha.mjs → exit 1 on drift, 0 in sync

Resolution chain checked on the real path too (cwd examples/app-showcase): resolves packages/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) and packages/cli/test/console-resolve.test.ts (detection semantics, migrated to detectConsoleShaDrift). eslint clean on every touched file; tsc reports 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-bundle trap row in RUNNER.md now say that a 404 /_console/ means "rebuild with pnpm objectui:build", never "console broken".

Out of scope

No .objectui-sha bump (the pin is already correct), no packages/console change, no scripts/pm/check-half-states.mjs (that is #7412), no release-notes edit.

…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
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 7:38pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The vendored packages/console/dist is built from a pin two days behind .objectui-sha — QA runs measure a console the repo no longer pins

2 participants