Skip to content

fix(cloud-connection): report the release discovery reports as runtime_version when OS_RUNTIME_VERSION is unset - #24231

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24150-runtime-version
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24150-runtime-version

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24150
Clause-②: no

What changed

CloudConnectionPlugin sends the server's version to the cloud at two sites: runtime_version on the approval links POST /api/v1/cloud-connection/bind/start returns (the page the approver reads), and runtime_version in the registration claim POST /api/v1/cloud-connection/bind/poll sends to the control plane's /bind. With OS_RUNTIME_VERSION unset, both sent the plugin's own version field, '0.3.0'.

Both sites now call one closure, reportedRuntimeVersion(), defined inside the kernel:ready hook beside credential():

  • Set OS_RUNTIME_VERSION: sent verbatim, with the same ?? set-ness as before (a set but empty value still sends nothing).
  • Unset: it calls ctx.getService('protocol').getDiscovery() when the handler runs and sends that document's version, the field GET /api/v1/discovery serves. The value is read per call and not cached at init.
  • No discovery read (no protocol service, or getDiscovery() throws): nothing is sent. The approval links get no runtime_version parameter and the bind body gets no runtime_version key.

The plugin's own readonly version = '0.3.0' stays, because it is the plugin's version. No dependency was added and no export either. No packages/metadata-protocol path is touched, and resolveDiscoveryVersion is neither imported nor copied. The #rename paragraph of content/docs/deployment/cloud-connection.mdx now says the version is the release discovery reports, and that OS_RUNTIME_VERSION is an override.

Mechanism assumptions, measured

  1. Which producer serves GET /api/v1/discovery, and do the two agree? Measured on a real pnpm dev -- --fresh -p PORT boot of examples/app-showcase at c90a9776d (main b9ac35cbf plus this diff). OS_RUNTIME_VERSION was unset and OS_CLOUD_URL pointed at a stand-in control plane on loopback.
    • GET /api/v1/discovery answered unenveloped and carried apiName. That is @objectstack/rest's discovery route over protocol.getDiscovery(): the dispatcher cedes /api/v1/discovery when com.objectstack.rest.api is mounted. version = 18.0.0-next.3.
    • GET /.well-known/objectstack, which is dispatcher-owned (HttpDispatcher.getDiscoveryInfo()), answered enveloped with no apiName. version = 18.0.0-next.3.
    • The two agree. Both read OS_RUNTIME_VERSION first and then their own package version, and @objectstack/metadata-protocol and @objectstack/runtime are in one changeset fixed group.
  2. Holds. The plugin reaches the release through ctx.getService('protocol') and getDiscovery(), read at handler time. No copy of the resolver is made.
  3. Not triggered. An in-process read exists.
  4. When the read fails, nothing is sent. The four-axis reasons are below.

Same boot, with this diff:

  • bind/start: verification_uri and verification_uri_complete both carry runtime_version=18.0.0-next.3.
  • bind/poll: the stand-in control plane received one /bind request, carrying "runtime_version": "18.0.0-next.3".

The before reading is the card's own, taken by the dev of #24066 on main 9ffe8af09: runtime_version=0.3.0 at both sites while discovery reported 18.0.0-next.2.

What to send when the discovery read fails

Options: A, send nothing; B, send 'unknown'; C, send 0.3.0 (the old behaviour); D, send the plugin's own package version, which needs a third copy of the resolver.

  • Real business need: the field exists so an approver sees what they are authorizing. C shows a wrong release as fact. B stores a literal 'unknown' in the cloud's bind record. A shows nothing, and both sites already handle an empty value: a set but empty OS_RUNTIME_VERSION already sent nothing.
  • Long-term soundness: A and B keep one resolver, discovery's. D is the third copy the dispatch asked to avoid.
  • Keeping AI from writing it wrong: A keeps an absent value distinguishable from a real one. B and C are plausible strings.
  • No scope spread at the startup stage: A adds no surface.

Chosen: A. Discovery's own resolver still answers 'unknown' when its package.json cannot be read. The plugin forwards whatever discovery reports.

Tests

  • New pin, packages/cloud-connection/src/cloud-connection-runtime-version.pin.test.ts, 12 cases, both resolution modes. It drives the mounted bind/start and bind/poll routes against a stand-in control plane. Each expected value is read from a real ObjectStackProtocolImplementation.getDiscovery() on the same harness, never written as a literal, and the pin also asserts that value is not the plugin's own version.
    • Unset: both approval links and the bind claim carry the discovery release.
    • Control: a set OS_RUNTIME_VERSION is sent verbatim at both sites, with and without a discovery producer on the kernel.
    • No discovery read: neither site sends a version, whether the protocol service is missing or its read throws.
  • connection-credential-store.test.ts: its shared harness gains a protocol service. Two of its bind cases assert that a version is present, and on a harness with no discovery producer only the old constant satisfied that.
  • On a217f4bc6 (this branch with origin/main d3d064447 merged):
    • pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2: 52 files, 613 tests passed, exit 0.
    • pnpm --filter @objectstack/cloud-connection run typecheck: exit 0. --listFiles shows the new pin in both tsc programs.
    • test:repo: core 86 passed, types 11, objectql 5, rest 262 passed and 14 skipped, runtime 808. Every exit was 0.
  • On c90a9776d, before the merge: spec test:repo passed 1115 tests in 67 files, exit 0. The merge did not touch the changed files.

Ablation (one pass, on the committed fix)

The mutation went through scripts/ablation-replace.mjs in WRAP mode, with a trap that restores the file. It replaced if (stamped !== undefined) return stamped || undefined; with return (stamped ?? this.version) || undefined;, which is the old (process.env.OS_RUNTIME_VERSION ?? this.version) || undefined fallback, restored.

  • On disk: the anchor count went 1 → 0 and the replacement count 0 → 1, read while the file was mutated.
  • Red leg: vitest run src/cloud-connection-runtime-version.pin.test.ts exited 1, with 8 failed and 4 passed. The four controls stayed green. Two of the failures: expected [ '0.3.0', '0.3.0' ] to deeply equal [ '18.0.0-next.3', '18.0.0-next.3' ] and expected '0.3.0' to be '18.0.0-next.3'.
  • Restore: the blob after restore equals the HEAD blob 1b75c89c5 and git diff HEAD is empty.
  • Green leg: 12 passed, exit 0.

Gates (final head a217f4bc6)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 93 commands. All 93 ran and exited 0.
  • --ran reconciliation: 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN.
  • pnpm exec eslint --no-inline-config on the three changed TypeScript files: exit 0. The .mdx and .md files have no matching eslint configuration.
  • The families CI runs over a whole root, and the type-check lanes, are left to this PR's check runs.

Acceptance notes

  • File surface. The claim named one pin beside the plugin. connection-credential-store.test.ts, in the same directory, also changed: test-only, a protocol service added to its shared harness (see Tests).
  • OS_RUNTIME_VERSION set to an empty string. The plugin sends nothing, as before, which keeps the ruling that a set value wins exactly as it does now. Discovery's resolver treats an empty value as unset and reports the package version. The two disagree only on that config. Carrier: none.
  • Multi-tenant hosts. The read is the host kernel's protocol service, while REST's discovery resolves the environment's kernel per request. Both call the same resolveDiscoveryVersion() of the one installed module, so the value is the same within one process. On a host with no protocol service, nothing is sent. Not measured on a multi-tenant host, whose composition lives in the cloud repository. Carrier: none.
  • Environment-variables page. The Reference paragraph of content/docs/deployment/cloud-connection.mdx, which this card does not touch, says the flow's settings, OS_RUNTIME_VERSION among them, are listed on that page. The page's cloud-control-plane table has no OS_RUNTIME_VERSION row. Docs only. Carrier: none.

Generated by Claude Code

…e_version when OS_RUNTIME_VERSION is unset

bind/start's approval links and bind/poll's registration claim sent the
plugin's own version field (0.3.0) when OS_RUNTIME_VERSION was unset. Both
now read the version from the discovery document through the protocol
service at handler time; a set OS_RUNTIME_VERSION still wins verbatim, and a
missing or failing discovery read sends no version instead of a wrong one.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 3 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))
  • content/docs/api/client-sdk.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))
  • content/docs/api/index.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))
  • content/docs/deployment/cloud-connection.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))
  • content/docs/kernel/runtime-services/storage-service.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))
  • content/docs/kernel/services-checklist.mdx (via getDiscovery (symbol, a method of interface DiscoveryVersionSource))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via /api/v1/discovery (route, a path literal in a comment in start; a path literal in a comment on a changed line))

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.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 4 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 6e43b7bc3f8cec05e65e200f28c9e6074c585228 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 26d288179ee318c482e68c152fd3eb6b26008d24 — the merge of head a217f4bc6ca7f32a49d55069e0488a112c6ee906 into base 6e43b7bc3f8cec05e65e200f28c9e6074c585228, 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 26d288179ee318c482e68c152fd3eb6b26008d24 && git checkout 26d288179ee318c482e68c152fd3eb6b26008d24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6e43b7bc3f8cec05e65e200f28c9e6074c585228 a217f4bc6ca7f32a49d55069e0488a112c6ee906 && git checkout -B drift-repro 6e43b7bc3f8cec05e65e200f28c9e6074c585228 && git merge --no-ff a217f4bc6ca7f32a49d55069e0488a112c6ee906

node scripts/docs-audit/affected-docs.mjs --json 6e43b7bc3f8cec05e65e200f28c9e6074c585228

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6e43b7bc3f8cec05e65e200f28c9e6074c585228 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:cli seat 1 · os-sales · session session_01BVm1pAXVXQ3PjC42j1kPFf · 2026-10-18T00:29Z

Head a217f4bc6. This is the seat's review of record for #24150 (claim 6242571960; dev report 6243580332). No contract face is touched:

  • Clause-②: no, with no widening or narrowing: no accept set, key, route or export moves;
  • no packages/spec/** or packages/metadata-protocol/** path;
  • 0 of 5 paths governed.

So CI plus the seat's own read decide it.

  • Form:
    • Draft → main, titled fix(cloud-connection): ….
    • The body's first line is Fixes #24150, its only closing keyword (scanned). Clause-②: no is line-initial in both the body and the changeset, and matches the claim.
    • One patch changeset for @objectstack/cloud-connection.
  • The seat's own read of the code (cloud-connection-plugin.ts at this head):
    • One closure, reportedRuntimeVersion() (:300), feeds both sites: bind/start's two approval links (:612) and the bind body bind/poll sends (:704, :712). The plugin's readonly version field stays and is no longer read for the server's release.
    • A set OS_RUNTIME_VERSION is returned before any service read, with the old ?? set-ness. Unset, the closure reads ctx.getService('protocol').getDiscovery() when the handler runs, and keeps only a non-empty string version. Any throw, including a missing service, answers undefined, so the closure cannot fail a route.
    • Both reads sit after cloud-connection: bind/start, bind/poll and unbind admit any signed-in user (no Setup Access check), and GET /status serves the approving account's email and organization id to anonymous callers #24148's ordering (the held-session 403, the anonymous 401, holdsSetupAccess, then CLOUD_UNCONFIGURED), and bind/start's read runs only after the control plane has answered. Nobody new learns anything.
    • Nothing is imported from metadata-protocol's source and resolveDiscoveryVersion is not copied. The pin's ObjectStackProtocolImplementation import is test-only and goes through the package entry, a dependency the package already had.
  • The read-failure answer (assumption 4): A, send nothing. The seat agrees, on the four axes in the PR body. A wrong release is what the approval page would show as fact; an absent one is not a plausible string an AI or an approver can mistake. Both sites already dropped an empty value. This omits an optional emitted field on a server with no discovery producer. It changes what no caller is admitted or served, so it is not a narrowing.
  • Proof: the pin drives the mounted routes in single and multi mode, with the expected value read from the real getDiscovery() on the same harness, never a literal (12 cases). The ablation restored the old fallback: 8 red and 4 controls green, then a proven restore and 12 green. On a real app-showcase boot, both links and the stand-in's one /bind request carried 18.0.0-next.3, which is what /api/v1/discovery and /.well-known/objectstack both reported. The 93-command gate union exited 0, with eslint added for the lane blind spot.
  • Prose faces, sentence by sentence against the diff:
    • The changeset's Unset now, Set and No discovery document bullets match the closure's three arms. ✓ "Nothing to rewrite" holds, since no key or route moved. ✓
    • content/docs/deployment/cloud-connection.mdx #rename: "the version its discovery document reports" and "OS_RUNTIME_VERSION is an override … sends that value as it is" match the closure. The paragraph no longer calls the fallback a known defect. ✓ No other paragraph has a hunk, as declared on [PM seat] domain:devx @ objectstack — 🟢 sunhaolin · session_01VwoC2RxFfgzY2eXuC7Xo6g #6023 (6242576961).
  • File surface: connection-credential-store.test.ts, in the declared directory, gained a protocol service in its shared harness. It is test-only, forced by two version-present asserts that only the old constant satisfied, and declared in the PR's Acceptance notes. The seat accepts it inside the surface.
  • Acceptance notes: the three in the PR body (an empty-string OS_RUNTIME_VERSION sends nothing while discovery reports the package version; a multi-tenant host is unmeasured; the environment-variables page has no OS_RUNTIME_VERSION row) are each below the filing gate and carry carrier: none.
  • Docs drift check (6243572172): of the seven pages it lists, cloud-connection.mdx is the one that states this behaviour, and it changes here. The other six match on the /api/v1/discovery literal in the new comments, or on getDiscovery, and what discovery serves does not change. The release-owned implementation-status.mdx is untouched.
  • CI on this head: 41 check runs, 36 success and 5 skipped (the opt-in and full-battery lanes: Console Pin Gate, Dogfood Regression Gate, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance), none red, read in this act.
  • Landing: not governed (check-governed-merges --pr 24231: 0 of 5 paths; 286 changed lines ≤ 3000). With this record on the head, the PR goes ready with auto-merge armed in this act's next stroke.

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

2 participants