Skip to content

[finding] serve's ready banner prints Config: objectstack.config.ts on an OS_ARTIFACT_URL boot, where no config was loaded #8978

Closed

Description

@os-zhuang

Observation-class finding, no behaviour change requested. Found while adopting the artifact-pinned boot (#8368) on the ObjectOS EE image (cloud#1292). Filed unassigned.

What happens

With OS_ARTIFACT_URL set, serve resolves the reference, sets useArtifactFallback = true, and loads { mod: { default: {} } } — the objectstack.config.ts in the cwd is deliberately never executed. The boot diagnostics say so plainly:

Artifact (OS_ARTIFACT_URL): http://127.0.0.1:41541/hotcrm-2.2.2.json [remote, sha256 verified]
Booting from the artifact named by OS_ARTIFACT_URL (default host)...

But the ready banner a few lines later still reports:

  Config:  objectstack.config.ts
  Mode:    production
  Driver:  SqlDriver(better-sqlite3)  → :memory:

printServerReady({ configFile: relativeConfig, … }) is passed relativeConfig unconditionally, and relativeConfig is derived from args.config at the top of run() — before the artifact branch is decided. So the row names a file that exists, was found, and was not read.

Why it is worth a line of code

The banner is the surface an operator uses to answer "what is this container actually running", and on this path that is the whole question the feature turns on — the two-axis model means the config in the image and the artifact in the env are different apps. A row that names the file that did not boot points the reader at the wrong one, and it does it on the same screen that just said the opposite. It cost a reading during the cloud-side adoption: the row is the first thing that looks authoritative, and the plugin table is what actually settles it.

The neighbouring precedent is #4801 (cloud#1020), where the banner's tenancy row disagreed with the runtime wiring and every later investigation paid for it.

Suggested shape

Report the artifact on this path instead of the config — the resolved display is already in hand (pinnedArtifact.display), already redacted for a pre-signed URL, and already printed by the diagnostic line above. Something like Artifact: <display> (OS_ARTIFACT_URL) in the Config: slot, or an added row with the Config: row omitted when useArtifactFallback is set.

Same for the plain artifact-fallback path (no config authored, dist/objectstack.json), where the row currently names a config file that does not exist.

Reproduce

objectstack build
OS_ARTIFACT_URL=file://$PWD/dist/objectstack.json objectstack serve objectstack.config.ts
# banner: Config: objectstack.config.ts   ← never loaded

Verified at 8798cd2a60649f9a54d794a97da314d41be4ab7b.

Related: #8368, cloud#1292.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 16, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 16, 2026
  3. os-project-manager commented on Aug 16, 2026

    @os-project-manager
    Collaborator

    Triage (first-touch grading): promoted finding → pm:queue, domain:cli (lands on the packages/cli serve banner path), type Bug. Rationale: named landing site (printServerReady is fed relativeConfig unconditionally, decided before the artifact branch), repro included, and the misreport already cost a reading during the cloud-side adoption — precedent #4801 shows this banner-disagrees-with-wiring class is expensive downstream. The suggested shape in the card (report pinnedArtifact.display on the artifact paths, cover the plain artifact-fallback path too) is a suggestion, not a ruling.

    Size/model suggestion: S, sonnet — mechanical, verified by the existing serve-path tests plus one banner assertion.

    本评论来自分诊座位 Routine。


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 17, 2026
  5. claude commented on Aug 17, 2026

    @claude
    Contributor

    Claim: PM loop round 1
    Session: session_012WKSnqAaoqtW3QX7SSf1Vk
    Branch: claude/issue-8978-serve-banner-artifact-row
    Worktree: objectstack-issue-8978
    Domain: domain:cli
    File surface: packages/cli/src/commands/serve.ts, packages/cli/src/utils/format.ts (+ their sibling tests) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: sonnet — --tier reports no path-derived mandate; mechanical banner-row change, correctness decided by an assertion rather than by judgement, so the sonnet floor is the right tier. Clause ② not reachable from content (no accept/reject or public-surface change).
    Serial constraints cleared: packages/cli is free this batch — the other two cards dispatched with it land in packages/cloud-connection (#9070) and packages/create-objectstack (#9263); no overlap. ⚠️ serve.ts is a three-way mutually-exclusive slot in this seat's hot-file queue (#8368 / #8985 / #8978) — #8985 is deliberately held back this round and must not be dispatched until this lands. No same-day churn: last touch of either file is 445ae4d (2026-08-16).

    Premise re-verified on origin/main @ 68cd4e7 before dispatch: relativeConfig is derived at serve.ts:937 and passed unconditionally as configFile: relativeConfig at serve.ts:3418 — exactly the shape the card describes. The card is accurate.

    Generated by Claude Code


    Generated by Claude Code

  6. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    <!-- os-dev-report -->

    {
      "issue": 8978,
      "status": "done",
      "branch": "claude/issue-8978-serve-banner-artifact-row",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9347",
      "premise_still_valid": true,
      "summary": "serve's ready banner named a config file that was never read (OS_ARTIFACT_URL boot) or does not exist (plain artifact-fallback boot), because relativeConfig was passed to printServerReady unconditionally, derived before the artifact branch is decided. Added resolveBannerConfigRow (serve.ts) to decide the row: report the resolved artifact's already-redacted display on an OS_ARTIFACT_URL boot, omit the row on the other artifact-fallback paths (no safe display in hand), and report the config unchanged on the ordinary boot path. printServerReady (format.ts) now renders whichever of configFile/artifactSource it is given. Both files stayed within the declared file surface.",
      "tests": "Reverse verification (fix committed at e6e5d65d8, then `git checkout origin/main -- packages/cli/src/commands/serve.ts packages/cli/src/utils/format.ts` with tests untouched): `pnpm --filter '@objectstack/cli' exec vitest run src/commands/serve-banner-config-row.test.ts src/utils/format.config-artifact-row.test.ts` -> 7 of 8 failed (`TypeError: resolveBannerConfigRow is not a function`; and `expected 'Config:  undefined' to be undefined`). Restored via `git checkout HEAD -- <paths>` -> same command -> 8/8 pass. Full suite after merging current origin/main and rebuilding: `pnpm --filter '@objectstack/cli' typecheck` clean; `pnpm --filter '@objectstack/cli' test -- --maxWorkers=2` -> 'Test Files 126 passed (126) / Tests 1375 passed (1375)' (one e2e failed ECONNREFUSED on the first run under load, isolated re-run passed -- environmental, unrelated to this diff). Gates: `pnpm check:cross-package-test-inputs` OK, `node scripts/check-cross-package-test-inputs.mjs` OK, `node scripts/docs-audit/check-affected-docs.mjs` OK (self-test; real mapper's affected-docs.mjs flags 2 non-release docs via an unrelated pre-existing `artifactSource` symbol -- MetadataPlugin's boot-mode config option, not this PR's new ServerReadyOptions field -- confirmed by grep, no doc drift). Convention-triggered (new test files added): `pnpm check:query-options-erasure` OK (67 unswept, none new), `pnpm check:type-check-coverage` OK (64/77 covered), `pnpm check:type-check-debt` OK after `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'` (33 ledger entries re-measured, 1926 raw errors, none above recorded), `pnpm check:engine-double-contract` OK (315 pinned, 133 DEBT, 2 exempt, none new), `pnpm check:where-matcher` OK (253 matchers, 0 silently-wrong, none new). All gates re-run on the merged tree at final HEAD 97db3f31e. `node scripts/check-nul-bytes.mjs` OK (self-scanned; caught and fixed a raw ESC control byte the Write tool materialized from a \\u001b escape in one new test file before it was committed).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report (marker retry — the HTML-comment form of the marker gets eaten by the GitHub body sanitizer even inside code fences; this comment leads with the literal text instead, per AGENTS.md 字节与sanitizer纪律)

    {
      "issue": 8978,
      "status": "done",
      "branch": "claude/issue-8978-serve-banner-artifact-row",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9347",
      "premise_still_valid": true,
      "summary": "serve's ready banner named a config file that was never read (OS_ARTIFACT_URL boot) or does not exist (plain artifact-fallback boot), because relativeConfig was passed to printServerReady unconditionally, derived before the artifact branch is decided. Added resolveBannerConfigRow (serve.ts) to decide the row: report the resolved artifact's already-redacted display on an OS_ARTIFACT_URL boot, omit the row on the other artifact-fallback paths (no safe display in hand), and report the config unchanged on the ordinary boot path. printServerReady (format.ts) now renders whichever of configFile/artifactSource it is given. Both files stayed within the declared file surface.",
      "tests": "Reverse verification (fix committed at e6e5d65d8, then `git checkout origin/main -- packages/cli/src/commands/serve.ts packages/cli/src/utils/format.ts` with tests untouched): `pnpm --filter '@objectstack/cli' exec vitest run src/commands/serve-banner-config-row.test.ts src/utils/format.config-artifact-row.test.ts` -> 7 of 8 failed (`TypeError: resolveBannerConfigRow is not a function`; and `expected 'Config:  undefined' to be undefined`). Restored via `git checkout HEAD -- (paths)` -> same command -> 8/8 pass. Full suite after merging current origin/main and rebuilding: `pnpm --filter '@objectstack/cli' typecheck` clean; `pnpm --filter '@objectstack/cli' test -- --maxWorkers=2` -> 'Test Files 126 passed (126) / Tests 1375 passed (1375)' (one e2e failed ECONNREFUSED on the first run under load, isolated re-run passed -- environmental, unrelated to this diff). Gates: `pnpm check:cross-package-test-inputs` OK, `node scripts/check-cross-package-test-inputs.mjs` OK, `node scripts/docs-audit/check-affected-docs.mjs` OK (self-test; real mapper's affected-docs.mjs flags 2 non-release docs via an unrelated pre-existing `artifactSource` symbol -- MetadataPlugin's boot-mode config option, not this PR's new ServerReadyOptions field -- confirmed by grep, no doc drift). Convention-triggered (new test files added): `pnpm check:query-options-erasure` OK (67 unswept, none new), `pnpm check:type-check-coverage` OK (64/77 covered), `pnpm check:type-check-debt` OK after `pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'` (33 ledger entries re-measured, 1926 raw errors, none above recorded), `pnpm check:engine-double-contract` OK (315 pinned, 133 DEBT, 2 exempt, none new), `pnpm check:where-matcher` OK (253 matchers, 0 silently-wrong, none new). All gates re-run on the merged tree at final HEAD 97db3f31e. `node scripts/check-nul-bytes.mjs` OK (self-scanned; caught and fixed a raw ESC control byte the Write tool materialized from a u001b escape in one new test file before it was committed).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  8. claude commented on Aug 17, 2026

    @claude
    Contributor

    ACCEPT — PR #9347

    Reviewer of record: domain:cli PM seat, session session_012WKSnqAaoqtW3QX7SSf1Vk. Checked against GitHub and origin/main, not against the report's self-description.

    Checklist: draft ✅ · base main ✅ · first line Fixes #8978 ✅ · 5 changed files, all inside the declared surface ✅ · changeset @objectstack/cli: patch ✅ · path fork clean (no docs/adr/**, .claude/skills/**, skills/**) ✅ · no packages/spec/src/**, so no contract-review hold ✅ · all five required checks green, each read from its own job conclusion — TypeScript Type Check, ESLint, Test Core (4/4), Dogfood Regression Gate (4/4), Build Core — ⛔ not from an aggregate ✅

    Spot readings — the two load-bearing claims were verified, not accepted

    "The docs-drift rows come from a pre-existing artifactSource, not my new field." True. artifactSource already exists on main (serve.ts:1780, start.ts:202+) as MetadataPlugin's boot-mode config option, and both flagged pages document that (artifactSource: { mode: 'local-file', path: … }), not the new ServerReadyOptions field. A symbol-name collision, correctly diagnosed. The release-owned page the bot also listed is untouched, as the guardrail requires.

    configFile went from required to optional — does anything regress? Checked every caller rather than reasoning about it: exactly one production call site (serve.ts:3418); every other reference is a test that passes configFile explicitly, so the Config: row still renders for them. Nothing silently loses its row.

    Both defect paths are covered, which was the ruling. resolveBannerConfigRow is a pure exported function with three branches, and the new pins assert all of them — including "never emits BOTH a configFile and an artifactSource", which pins the invariant rather than only the happy path. Reverse verification predicted the direction before running (7 of 8 red against unfixed code, 8/8 after).

    ⭐ One judgement worth endorsing explicitly. On the plain artifact-fallback path the row is omitted rather than filled with OS_ARTIFACT_PATH, because that value may itself carry credentials and no redacted display is in hand there. Printing it would have satisfied the card's literal suggestion and leaked secrets into an operator-facing banner. Choosing a missing row over a possibly-credentialed one is the right trade, and it is documented at the site.

    Deviations

    • Report marker malformed. Both os-dev-report comments on this card are entity-escaped, so the machine-readable marker is absent and collection fell back to reading content. The report itself is complete and accurate; only the marker is affected. HTML comments are stripped or escaped on write — anything that must survive belongs in visible markdown.
    • A raw ESC control byte was materialised by the editing tool from an escape sequence in a new test file, and caught by check:nul-bytes before the commit. Self-caught, correctly handled.
    • Two non-required checks are red repo-wide right now (Check PR Size, Auto Label) from a codeload.github.com 429 incident affecting third-party action downloads. Neither is on this PR, and neither is in the ruleset's required set.

    ⇒ Flipping ready and enqueuing.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions