Skip to content

feat(cloud-connection): publish the control plane's upgrade entry as an absolute upgradeUrl on /api/v1/runtime/config - #14590

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-14514-runtime-config-billing-url
Sep 2, 2026
Merged

feat(cloud-connection): publish the control plane's upgrade entry as an absolute upgradeUrl on /api/v1/runtime/config#14590
hotlong merged 1 commit into
mainfrom
claude/issue-14514-runtime-config-billing-url

Conversation

@hotlong

@hotlong hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #14514

The framework half of objectstack-ai/cloud#1850 (maintainer ruling 2026-09-02, option A). GET /api/v1/runtime/config can now carry the control plane's upgrade / billing entry as an absolute URL, declared by the host that owns the page, instead of the tenant Console composing it from cloudUrl plus a guessed console mount, app slug and page route (which landed on the control plane's API 404 — objectui#7273 already consumes "key present → render link, key absent → no link").

The key, for the cloud side to fill

  • Response key: top-level upgradeUrl (optional string), beside cloudUrl. Named after the concept the stack already spells upgrade_url (environment-lifecycle 402/403 envelopes) and upgradeUrl (environment-lifecycle.ts, objectui entitlements.ts), camel-cased like every sibling key on this payload.
  • Host option: upgradeUrl?: string on the exported RuntimeConfigPluginConfig. The cloud subclass (packages/objectos-runtime/src/cloud-runtime-config-plugin.ts extends the open plugin; objectos-stack.ts constructs it) passes e.g. upgradeUrl: 'https://cloud.example.com/_console/apps/cloud_control/page/pricing' after the pin bump. ⛔ Not done here — no cloud, no objectui, no other key on this payload touched.
  • Semantics, all pinned: declared → served verbatim (no trailing-slash trimming, no re-serialisation); undeclared or empty / whitespace-only → the key is absent (asserted on key presence, not on undefined); not an absolute http: / https: URL → refused and named at mount, no key served. The Console opens this URL from the tenant origin, so the control plane's current relative /settings/billing dialect would resolve against the wrong host and recreate the defect — it is refused, not forwarded, the same shape as branding.stage and the telemetry DSN in this file.
  • No env var, by design: the value belongs to the distribution whose control plane serves the page. Absent by default because a vanilla objectstack dev, a self-hosted box and an air-gapped deployment have no billing page.

Premise checks against origin/main

  • The payload is assembled AND mounted by RuntimeConfigPlugin itself on the raw Hono app (rawApp.get('/api/v1/runtime/config', handler)); there is no objectos-runtime wrapper that re-shapes it, so the new key sits at the same level as cloudUrl with nothing in between. PM assumption confirmed in substance, corrected in detail.
  • No packages/spec schema owns this payload — the only RuntimeConfigSchema in spec is the unrelated plugin-security runtime config. No spec touch, no artifact regeneration.
  • No field table for this payload exists on main (README or content/docs). The row now lives in a new README section (with a one-row field table, JSON and host examples) plus the plugin's Response-shape block; the changeset is minor on @objectstack/cloud-connection.
  • Type export: the key is declared on the already-exported RuntimeConfigPluginConfig. No response-side payload type is introduced — the payload has never had one, and typing it whole would touch every other key, which the ruling puts out of scope.

Contract review

Clause-② yes: this widens a public payload, so the PR carries needs:contract-review. The widening is additive and default-absent — a runtime that declares nothing serves a byte-identical payload.

Tests

New packages/cloud-connection/src/runtime-config-upgrade-url.test.ts — 22 cases: direction 1 (declared → verbatim, beside cloudUrl, not inside features / branding, independent of cloudUrl, survives JSON), direction 2 (undeclared → key NOT THERE, empty reads as unset and silent, no derivation from a resolved cloudUrl, siblings untouched), and the refusal group (four relative spellings, three non-http schemes, the warning names option + value + requirement, refusal leaves the rest of the payload intact, acceptance is silent).

Ablation, from the committed state: the spread was replaced with an always-present upgradeUrl: this.upgradeUrl (marker ABLATION-14514 counted 1, spread counted 0, blob 8bf0740c vs HEAD 418e09a9) → 11 of 22 red (every key-absence and refusal case); the JSON-round-trip case correctly stayed green because JSON.stringify drops undefined — which is exactly why the load-bearing assertions are on key presence. Restore via git checkout HEAD -- PATH: blob hash back to 418e09a9, git diff HEAD empty, marker count 0. No dist ablation needed: the suite imports ./runtime-config-plugin.js from source (vitest), not through exports.

Local verification at c0c1f03a7 (branch fast-forwarded onto origin/main 5563bfb32 before the final commit)

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh found no usable flock on this macOS host, so every locked run printed VERDICT command-exit N · UNLOCKED (declared); nothing was serialized, wall-clock figures are shared-box readings.

  • pnpm --filter '@objectstack/cloud-connection^...' build (dependency closure, rerun after the fast-forward) → VERDICT command-exit 0.
  • pnpm --filter @objectstack/cloud-connection buildVERDICT command-exit 0; DTS ⚡️ Build success; dist/index.d.ts and dist/index.d.cts carry upgradeUrl (6 mentions each); require('./dist/index.cjs') loads and exports RuntimeConfigPlugin.
  • pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2Test Files 28 passed (28), Tests 362 passed (362); VERDICT command-exit 0.
  • tsc --noEmit -p tsconfig.json --listFiles (the package has no typecheck script — a zero-match pnpm --filter … typecheck would have exited 0 having run nothing): exit 2 with 13 errors, all in three test files this PR does not touch (cloud-connection-plugin.test.ts 4, connection-credential-store.test.ts 7, marketplace-install-local-bundle.test.ts 2; TS2493 x11 + TS2550 x2). That is exactly the DEBT ledger's frozen entry for this package (errors: 13, "code-tier 11 (TS2493) + 2 config-tier"), so the count does not move. 0 errors in the two files this PR touches; the new test file IS in the program (--listFiles hit 1).
  • node scripts/check-nul-bytes.mjscheck-nul-bytes: OK (scanned 7923 text file(s) … no raw ASCII control bytes); control-byte self-scan of the four touched files: 0 hits.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths; derived from git at c0c1f03a7, 4 files) → 35 families. 31 print their own OK / holds line, among them check:route-envelope (matched on this very file), check:published-files, check:test-source-alias, check:type-source-resolution, check:type-check-coverage, check:doc-authoring, check:slot-lookup, check:logger-receiver-detach, check:plugin-teardown-shape, check-adr-0087-registration ("adds no declared-breaking changeset"), check-changeset-no-major, check-empty-changeset, check:objectui-changeset, check:pm-half-states (self-test), check-keyed-text-bounds, check-undeclared-dep-imports, check-comment-mask-adoption, check-system-context-census, check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:where-matcher, check:page-declaration-shape, check-ci-filter-parity, check-cross-package-test-inputs (both spellings), check-shard-attestation, docs-audit check-affected-docs + check-drift-comment, release-rehearsal-clone --self-test, check:changeset-gate-self-tests.
  • NOT MEASURED (4), each by the gate's own PREREQUISITE NOT MET exit 3, not a red: check-test-completeness.mjs (grades a saved turbo run test log CI tees; none locally), pm/check-half-states.mjs (anonymous GitHub rate limit exhausted on this egress; a repo-wide card patrol unrelated to this diff, matched only via the .changeset glob), check:dual-build-cjs-loads and check:type-check-debt --re-measure (both need every workspace package built; 35 / 23 packages have no dist/). For the last two, the reading that this diff CAN move was taken directly: the package's exports map is untouched, its rebuilt dist/index.cjs loads under require, and the debt count above is unchanged at 13. CI's Build Core / Lint & Repo Gates run all four on the full farm.
  • Declared narrowing on lint. eslint --no-inline-config --format json packages/cloud-connection → 43 files, 0 errors, 0 warnings (counts read from the JSON output). Untouched packages were not linted locally; that is safe because eslint.config.js enables no type-aware linting (its own comment: no parserOptions.project, no typed @typescript-eslint rules), so this diff cannot move a verdict on a file it does not contain. CI runs pnpm lint over the whole tree regardless.

Not addressed here (remains open)

  • objectstack-ai/cloud#1850's other half — the cloud subclass filling upgradeUrl, converging the control plane's two URL spellings, and the requested evaluation of retiring the relative upgrade_url in the environment-lifecycle envelopes — is cloud-side work after the pin bump.

Generated by Claude Code

…an absolute upgradeUrl on /api/v1/runtime/config

RuntimeConfigPlugin gains one optional host option, upgradeUrl, on the
exported RuntimeConfigPluginConfig, and the served payload gains one optional
top-level key of the same name beside cloudUrl. Declared, it is served
verbatim; undeclared (or empty), the key is absent; not an absolute http(s)
URL, it is refused and named at mount and no key is served.

Contract test covers both directions on key presence plus the refusal group;
README gains the field row and semantics; changeset minor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/cloud-connection/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cloud-connection/README.md) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b8562ff2620641def6a23234e97613da879fa8f6packageMentionDocs.

Which tree this was computed on

This run read content/docs from f5606c0cf0f3ae5a51d89249dae4a83c51ee3d28 — the merge of head c0c1f03a7f39d19bc0f3c5e824d4ed05a86d7f5f into base b8562ff2620641def6a23234e97613da879fa8f6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f5606c0cf0f3ae5a51d89249dae4a83c51ee3d28 && git checkout f5606c0cf0f3ae5a51d89249dae4a83c51ee3d28
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b8562ff2620641def6a23234e97613da879fa8f6 c0c1f03a7f39d19bc0f3c5e824d4ed05a86d7f5f && git checkout -B drift-repro b8562ff2620641def6a23234e97613da879fa8f6 && git merge --no-ff c0c1f03a7f39d19bc0f3c5e824d4ed05a86d7f5f

node scripts/docs-audit/affected-docs.mjs --json b8562ff2620641def6a23234e97613da879fa8f6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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.

cloud-connection:runtime config 载荷声明可选的「升级/计费入口」绝对 URL 键(cloud#1850 裁 A 的 framework 半边)

1 participant