Skip to content

fix(objectql): give a runtime-authored object a metadata-service copy on a kernel with an environmentId, so both MCP doors refuse an object whose API exposure is off - #23716

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23689-env-kernel-runtime-object-exposure
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23689-env-kernel-runtime-object-exposure

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23689

Clause-②: no

reachable: true. On a kernel built with an environmentId, an object whose API exposure was off was served, read and write, through the MCP stdio door and the HTTP MCP data verbs. That covers the real os dev (env_local) and the single-environment runtime shape the cloud runs. The REST data door on the same boot refused the same objects. The walk reading is comment 6199252355 on the card, posted before any code changed.

The producer is packages/objectql. On this kernel shape the metadata service is sourced from the artifact and the boot bridge does not run. The copy follow from PR #23687 also returned early here. So the service held no copy of any runtime-authored object, exposed or not, and both gates read that absence as "no declaration". This PR keeps that copy where it is produced, on this shape too. packages/mcp and packages/runtime are untouched, and so are ObjectQLPlugin.start()'s order and the objectql:drivers-ready region (PR #23694).

Step 1: the walk, on base 69351e985a

The two shapes.

  • os dev as env_local, booted for real: examples/app-crm, objectstack dev --fresh from the built CLI. os dev takes resolvedMode = 'standalone', so createStandaloneStack composes the kernel. That is MetadataPlugin({ environmentId, artifactSource: local-file }) plus ObjectQLPlugin({ environmentId: 'env_local', hydrateMetadataFromDb: true, runPlatformMigrations: true }).
  • The cloud single-environment runtime: its host (apps/objectos-ee) lives in objectstack-ai/cloud, outside this session's repository scope, so it was not booted. In this repo the shape is the one ObjectQLPlugin's hydrateMetadataFromDb docblock names ("the cloud single-env tenant runtime on Turso"): an environmentId, hydration on, and an artifact-sourced service. createStandaloneStack composes exactly that for any OS_ENVIRONMENT_ID. It was measured in-process with those two plugin options verbatim, at env_local and at env_01walkcloudshape, and the two read identically.

The in-process harness is PR #23687's door harness with the kernel swapped. It uses the real MCPServerPlugin (its start() builds the stdio data bridge under enforceApiExposure) driven by JSON-RPC tools/call. The HTTP leg calls refuseUnexposedDataAction (the gate callData asks first under the HTTP MCP data verbs) with the kernel's own metadata service. The one faked seam is the sys_api_key row the stdio principal resolves from.

Real os dev, HTTP MCP door (POST /api/v1/mcp, as the seeded admin), base:

case REST GET /api/v1/data/X MCP query_records MCP create_record other
(a) created with apiEnabled: false OBJECT_API_DISABLED served (total: 0) served, a row written; the next query returns it list_objects does not name it
(b) saved exposed, a row via REST, then turned off OBJECT_API_DISABLED served, the REST row returned served, a second row written describe_object: Object "qa_walk_toggle" not found

In-process, both environmentId values, base:

case metadata.getObject stdio door HTTP gate
(a) created off ABSENT query_records served; create_record served, rows 1 to 2 ALLOWED (query, create)
(b) exposed, then off, same boot ABSENT (absent while exposed too) served; create wrote a row ALLOWED
(b') exposed, restart, then off ABSENT served ALLOWED
(c) then deleted ABSENT not listed; OBJECT_NOT_FOUND 404 n/a
(d) saved off, then a restart ABSENT served ALLOWED

Reading (d) is not in the card. An object saved off and then hydrated at the next boot has no copy, and no mutation fires for it after boot. So following mutations alone would leave it served.

The landing point, measured against the alternative

Can this service take a runtime copy without writing through a loader? Yes, so the dispatch's fork never came up. MetadataManager.registerInMemory sets the in-memory map only, with no loader write and no announce. unregister deletes through datasource: loaders only, and it is called under PR #23687's three bounds: the mutation says deleted, the registry no longer serves the name, and the copy is the runtime-authored one.

Producer (this PR) vs the gates reading the registry or the stored definition:

axis copy kept at the producer (taken) gates read the registry
business need (measured) Both gates, list_objects / describe_object, the AI tools and the MCP resources read this one service, and all of them were wrong on this shape. One producer change fixes every reader, and the after-walk below shows it. It would fix the gates it is written into and leave listing, describing, the AI tools and the resources blind to every runtime object. The gate half alone is about seven sites: enforceApiExposure, list, describe and the get_record resource reader in packages/mcp, plus refuseUnexposedDataAction, listObjects and describeObject in packages/runtime (domains/mcp.ts).
long-term The service carries one rule on every kernel shape: a runtime-authored object has a current copy. There is no per-shape exception left to rediscover. The service and the registry would stay disagreeing on this shape, and every new reader of the service would inherit the gap. IDataEngine does not declare registry, so each site needs an untyped reach-through.
AI-safety apiEnabled: false is enforced wherever it is read. That is "declared means enforced" at the producer, with no consumer tolerance. Readers would differ in which source they trust. That split is how the defect arose.
scope One file: packages/objectql/src/plugin.ts, about 60 lines. Cross-lane (domain:cli), two packages, and the dispatch says to stop before writing it.

What changed

  • packages/objectql/src/plugin.ts, the copy follow. followRuntimeObjectCopy no longer returns early when environmentId is set. A save refreshes the copy before it returns. A delete removes it under the same three bounds. What the follow touches is unchanged: only an object whose registry owner row is tenant-authored. The copy written is the object this kernel's own authoring door just saved, so the cross-project leakage the full bridge is gated against does not arise.
  • packages/objectql/src/plugin.ts, the boot half (bridgeRuntimeObjectsToMetadataService). On a kernel with an environmentId and hydrateMetadataFromDb, start() copies each hydrated runtime-authored object once, beside the existing bridge gate. hydrateMetadataFromDb is documented as set only on a kernel whose registry is its own. Only tenant-authored owner rows are copied, through registerInMemory, so a code package's objects reach the service from the artifact alone. The full boot bridge stays off on this shape. A copy that cannot land is logged once at error, naming the objects, the consequence and the way out. This is an else if on the existing bridge gate, after hydration; no step of start() moved.
  • Pins: packages/objectql/src/plugin-runtime-object-metadata-copy.test.ts gains a describe for this shape. packages/qa/dogfood/test/mcp-stdio-runtime-object-exposure.test.ts runs its door suite on both shapes and gains one case. Both reuse the drivers already in those files, so no new double is added to any ledger.
  • .changeset/23689-env-kernel-runtime-object-copy.md: a patch for @objectstack/objectql.

Pins, and the ablation

Producer pins (real kernel, createStandaloneStack's two options, the service's answers), new describe in plugin-runtime-object-metadata-copy.test.ts:

  • created after boot with exposure off: a copy at once, saying apiEnabled: false;
  • exposed, then turned off: the copy says so before the save returns;
  • saved off, then a restart: the boot gives the hydrated object its copy;
  • deleted: copy and listing entry gone;
  • a code package's object gets no copy from the boot pass or from a runtime edit beside it.

Door pins (real kernel, real MCPServerPlugin, in-process JSON-RPC), mcp-stdio-runtime-object-exposure.test.ts, now describe.each over no environmentId and environmentId env_local (createStandaloneStack):

  • CONTROL: an exposed runtime object is listed, described, queried and written.
  • Turned off after boot: query_records, get_record, create_record, update_record and delete_record are refused with OBJECT_API_DISABLED / 404, the table is untouched, and the exposed neighbour is still served.
  • Created after boot with exposure off: refused from the first call.
  • Saved off, then a restart: refused from the first call after the boot (new case).
  • Deleted: not listed, describe says not found, and data verbs answer OBJECT_NOT_FOUND.

The no-environmentId leg is the CONTROL that PR #23687's shape is unchanged.

Ablations. Every leg's mutation was proved on disk by scripts/ablation-replace.mjs: anchor 1 to 0, blob changed. Every restore was proved too: blob equal to HEAD 3751a6c6161c, and git diff HEAD empty.

leg mutation result
objectql 1 the follow's environmentId early return put back 4 failed, 6 passed. Red: created off, turned off, deleted, and the code-package pin's runtime-edit half. Green: the restart pin, which the boot pass covers, and the five no-environmentId pins.
objectql 2 the boot pass's call replaced by a comment 2 failed (the restart pin, and delete, whose precondition is the boot copy), 8 passed
door, mutant dist/ both mutations at once (the boot gate spelled && !String('ABLATION-23689-3b') so it compiles), objectql rebuilt, ablation-dist-preflight found the marker in 2 built files the environmentId shape: 5 failed. The no-environmentId shape: 5 passed.
door, restored objectql rebuilt, ablation-dist-preflight --absent: marker absent from all 14 built files, tree clean 10 passed

The first door attempt deleted the boot pass's call outright. The .d.ts build refused the then-unused private method, so that leg never ran a test and its reading is void. It was redone with the compile-safe spelling above. The objectql pins import plugin.ts from source, so their legs need no rebuild.

After the fix: the same walk on the real os dev (HTTP MCP door)

(a) created with apiEnabled:false   query_records / create_record   isError: "object is not exposed via the API"
(b) exposed, REST row, then off     query_records (exposed)         served; describe_object describes it (was "not found")
                                    after turn-off: query_records / create_record   isError: "object is not exposed via the API"
CONTROL exposed object              create_record served; list_objects names it (it did not before)
(c) deleted (DELETE /api/v1/meta/object/X)   query_records   OBJECT_NOT_FOUND 404

The HTTP door's refusal text carries no error code. That is #23528, which this PR does not address.

Verification (at 94604ee5, the last code commit; the gate list was re-derived there)

  • pnpm --filter @objectstack/objectql test: 426 files, 8145 tests passed.
  • pnpm --filter @objectstack/objectql typecheck (tsc --noEmit plus check:test-typecheck): exit 0.
  • plugin-runtime-object-metadata-copy.test.ts: 10 passed. mcp-stdio-runtime-object-exposure.test.ts (against rebuilt objectql dist/): 10 passed.
  • The exposure-gate suites: @objectstack/mcp stdio-data-bridge.exposure.test.ts plus plugin.record-resource-exposure.test.ts 35 passed, and @objectstack/runtime action-execution-calldata-relation-target-exposure.test.ts 13 passed.
  • pnpm --filter @objectstack/dogfood typecheck: exit 0, 0 errors. It was re-run after rebuilding the CLI closure with declarations.
  • Gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 94604ee51, with no paths (4 paths against merge base 69351e985), derives 68 families. All 68 were run and recorded with their exit codes. 64 exited 0 on the first pass. Four exited non-zero on that pass for reasons in this container, and each was re-run and exited 0:
    • check-engine-split-ratio.mjs --days 90 (exit 2) and check-plugin-teardown-shape.mjs --self-test (exit 3) refused on a shallow clone. Each passed after git fetch --shallow-since=2026-07-10 origin main.
    • check:dts-closure (exit 1, 28 packages missing .d.ts) and check:dual-build-cjs-loads (exit 3, packages not built) came from my own walk-time OS_SKIP_DTS=1 CLI build. Both passed after turbo run build --force over the CLI and CRM closure: check:dts-closure found 175 of 175 declaration files, and check:dual-build-cjs-loads probed 68 packages.
    • dispatch-gates --ran: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derived list re-read at the same head is byte-identical.
  • CI families beyond that list are read from this PR's check-runs.

Acceptance notes

  • The real-CLI stdio leg was walked in-process, not through a spawned os dev. The stdio transport would share the dev process's stdout with its banner. The in-process leg runs the same MCPServerPlugin stdio bridge over createStandaloneStack's plugin options. Carrier: none.
  • The cloud runtime itself (objectstack-ai/cloud's single-environment host) was not booted. This session's repository scope is objectstack-ai/objectstack. Its in-repo shape is measured as described above. Carrier: none.
  • Exposed runtime objects on this shape were also missing from list_objects / describe_object. That is the same absent copy, and it is fixed by the same change (see the after-walk). Noted, not filed.

Generated by Claude Code

… on a kernel with an environmentId too

On os dev (env_local), os serve/start and the cloud single-environment
runtime the metadata service is sourced from the artifact and the boot
bridge does not run, so a runtime-authored object had no copy there at
all and both MCP exposure gates served one saved with apiEnabled: false.

The copy follow from the onMetadataMutation seam now runs on every
kernel shape, and a kernel that hydrates its own sys_metadata copies the
hydrated runtime-authored objects once at boot. Code packages' objects
are never touched.

Claude-Session: https://claude.ai/code/session_01QBn4EzKV17Y1Ero6KDNZ45
Co-authored-by: Claude <noreply@anthropic.com>
…e environmentId kernel too

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectQLPlugin (symbol, a top-level class))

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

  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ObjectQLPlugin (symbol, a top-level class))

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
  • 1 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 — 17 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 53e3638ea21de79576ff093f9ef476d696941350 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 80311501c02070f72ddae4431403f95e5642aec3 — the merge of head 94604ee51bbdc3c5ba73d147604c4ec7e6e81816 into base 53e3638ea21de79576ff093f9ef476d696941350, 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 80311501c02070f72ddae4431403f95e5642aec3 && git checkout 80311501c02070f72ddae4431403f95e5642aec3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53e3638ea21de79576ff093f9ef476d696941350 94604ee51bbdc3c5ba73d147604c4ec7e6e81816 && git checkout -B drift-repro 53e3638ea21de79576ff093f9ef476d696941350 && git merge --no-ff 94604ee51bbdc3c5ba73d147604c4ec7e6e81816

node scripts/docs-audit/affected-docs.mjs --json 53e3638ea21de79576ff093f9ef476d696941350

⚠️ 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 53e3638ea21de79576ff093f9ef476d696941350 → 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

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 94604ee51bbdc3c5ba73d147604c4ec7e6e81816
Local-runs: none

Read: card #23689's body and all six comments (triage 6196324526, claim 6198937330, dispatch 6198975590, Walk reading 6199252355, grade change 6200228776, report 6200333099); PR #23716's body, its four-file list, and the net diff origin/main...94604ee51 (merge base 69351e985); the 41 check-runs on this head. In the cited code at the head I read ObjectQLPlugin.start(), followRuntimeObjectCopy, settleRuntimeObjectCopy, mirrorRuntimeObject and putObjectCopy (packages/objectql/src/plugin.ts), the per-engine registry (engine.ts), isTenantAuthored (metadata-core), MetadataManager.registerInMemory, MetadataPlugin's service and artifact load, resolvePluginOrder, MCPServerPlugin.start(), the stdio bridge's GATED_ACTIONS note, and PR #23694's plugin.ts hunk (cd86e41b0). Governing text: AGENTS.md "Add a changeset for anything that publishes" (a fix to a released package is patch; Clause-②: yes|no plus at most one arm), the spec's enable.apiEnabled ("Expose object via automatic APIs"), the hydrateMetadataFromDb option docblock, and the dispatch's bound (the producer only; ⛔ start()'s boot order and the objectql:drivers-ready region).

① Derived judgments

  • MCP stdio data verbs on an apiEnabled: false runtime-authored object, on an environmentId kernel. query_records, get_record, create_record, update_record and delete_record were served and are now refused with OBJECT_API_DISABLED / 404. Right. This is the meaning the spec declares for the key. REST refused the same objects on the same boot, and both MCP doors already refuse them on the no-environmentId shape (PR fix(objectql): keep the metadata service's copy of a runtime-authored object current after boot (MCP stdio exposure gate) #23687). The implementation now matches the declaration; no published accept set narrows.
  • HTTP MCP data verbs (refuseUnexposedDataAction). The gate answered ALLOWED and now refuses. Right, for the same reason: the gate reads the same service, and the fix is at that service's producer. The dispatch ruled out a consumer-side tolerance, and none was added: packages/mcp and packages/runtime are untouched.
  • list_objects / describe_object, the objectstack://objects resource, AI describe and metadata.getObject / listObjects on this shape. Before, these named no runtime-authored object. Now they name and describe every one. Right. That is the answer the bridge's own docblock gives the metadata service ("visible to AI tools and other consumers that query IMetadataService"), and it matches the no-environmentId shape. One precision the PR body leaves out: the body says "exposed" objects, but the listing and the describe now also cover objects with apiEnabled: false. That is schema only, not data. These two reads are deliberately not gated on exposure (the stdio bridge's GATED_ACTIONS note), exactly as on the other shape, so this is parity and not a defect.
  • The follow, armed on every environmentId kernel, including ones without hydrateMetadataFromDb. It copies only from this engine's own registry (engine.ts: a per-engine _registry, "so kernels are fully isolated") into this kernel's own service. The trigger is this kernel's own onMetadataMutation. It writes through registerInMemory. unregister stays under the three bounds fix(objectql): keep the metadata service's copy of a runtime-authored object current after boot (MCP stdio exposure gate) #23687 set, which this diff does not change: the mutation says deleted, the registry no longer serves the name, and the copy is tenant-authored. Right: no other environment's rows are reachable. start() has an older comment calling SchemaRegistry "a process-wide singleton". It is stale, it is not in this diff, and it does not bear on the change.
  • The boot pass (bridgeRuntimeObjectsToMetadataService). It runs only when environmentId is set and hydrateMetadataFromDb is on. It copies only names whose registry owner row satisfies isTenantAuthored (_provenance === 'org'). Code-package objects stay out, and a pin covers that (the shipped package's object has no copy, before or after a runtime edit beside it). The write is registerInMemory: on MetadataPlugin's NodeMetadataManager that is an in-memory map set plus a list-cache invalidation, with no loader write and no announce. The register(..., { notify: false }) fallback applies only to a service that lacks registerInMemory, which this one does not. The rows come from this kernel's own sys_metadata, under the option's own contract ("Set this ONLY when the kernel's registry is per-instance isolated AND sys_metadata lives on the kernel's own local driver"). Right: it copies nothing it must not, and nothing is written through to a loader.
  • A window before the copy exists. The pass sits on the exact line the full bridge occupies on the other shape: after Phase-2 hydration and Phase-3 sync, before kernel:ready. Before hydration, the registry does not hold the object either. In the composed stack, MCP is a capability provider. It is mounted after the stack plugins and declares no dependencies, and resolvePluginOrder walks in registration order, so ObjectQLPlugin.start() (and with it the copy) runs before MCP's stdio runtime.start(). The HTTP MCP door serves only once the listener is open. Right: any remaining pre-copy window is the one the no-environmentId bridge already has at the same line, not a new one.
  • The boot-copy failure line (ctx.logger.error, naming the objects, the consequence and the way out). This is operator output, not an accept set. Right.
  • Public API. No export, option, type or event is added or removed, and both new members are private. The no-environmentId behaviour is unchanged: that leg's pins stayed green under both ablations. Right.

② Semver level

The changeset is .changeset/23689-env-kernel-runtime-object-copy.md: '@objectstack/objectql': patch. Its body carries Clause-②: no, and so does the PR body. @objectstack/dogfood is private: true, and its file here is a test, so it needs no changeset. The level matches what the diff publishes. It is a fix inside a released package, with no new or removed export, option, event or schema key. The contrast is PR #23694: it added OBJECTQL_DRIVERS_READY_HOOK and the objectql:drivers-ready event, and so declared yes and minor. Check Changeset concluded success.

Clause-②: no holds. The refusals on both MCP doors return the implementation to apiEnabled: false's declared contract. REST on the same boot already enforced that contract, and so did both doors on the other kernel shape. The listing and describe change restores the metadata service's documented content on this shape, at parity with the other shape. No published contract is narrowed or widened, so neither arm applies. no (narrowing) would be wrong, because no declared accept set shrinks.

③ Boundary flags

  • open_questions[0]: is the boot copy's else if inside the dispatch's bound? Yes: A. The bound is start()'s boot order and the objectql:drivers-ready region. The new branch moves no step. It is an else if on the existing bridge gate, at the bridge's own position. fix(objectql,runtime): install datasourceMapping before the first schema sync, so a mapped boot keeps one store (#23649) #23694's hunk is one ctx.trigger('objectql:drivers-ready', ...) after ql.init() and before Phase 1, about fifty lines earlier and disjoint. Since the merge base, main has changed plugin.ts only by that hunk, and the PR reads mergeable_state: clean. The claim's file surface literally said "the copy follow only". The seat's grade change 6200228776 widened that to "copies hydrated runtime-authored objects once at boot on this shape", on the strength of reading (d), so the boot copy is in scope. Option B (kernel:ready) would open the window that A avoids.
  • open_questions[1]: is a cloud-side confirmation needed? A, with no escalation. The gates now coincide. Hydration runs when environmentId === undefined || hydrateMetadataFromDb, and in exactly those cases a copy follows: the full bridge, or this boot pass. A composition without hydrateMetadataFromDb puts no hydrated runtime object in the registry, so no door serves one. The mutation follow covers saves on every shape. The dev's conditional B therefore guards no case this diff leaves open. A cloud-lane walk stays the cloud lane's own choice and does not gate this PR.
  • open_questions[2]: Clause-②. A, no. See ②.
  • open_questions[3]: the primary checkout was detached to create the worktree. Noted. This is process only; it touches nothing in the diff.
  • out_of_scope_findings[0]. The HTTP door's refusal carries no error code. runtime: the /mcp data bridge's exposure gate (callData) refuses with a status and a message but no code, where the stdio face and REST answer OBJECT_API_DISABLED / OBJECT_API_METHOD_NOT_ALLOWED for the same declaration #23528 already tracks it, so this is correctly not filed.
  • out_of_scope_findings[1], and the acceptance notes. The stdio leg ran in-process over createStandaloneStack's plugin options rather than through a spawned os dev, and the cloud host was not booted. Both are accepted, since the in-repo shape is the one the hydrateMetadataFromDb docblock names. The door pin mcp-stdio-runtime-object-exposure.test.ts belongs to the dogfood suite. That suite runs only in the full battery (ci.yml, "THE TWO SPEEDS"), so the dogfood shards were skipped on this head. For the PR, its evidence is the dev's local 10-of-10 run and the mutant-dist ablation (5 red on the environmentId shape, 5 green on the CONTROL shape). It runs on main's full battery after merge. The producer pins in plugin-runtime-object-metadata-copy.test.ts ran in Test Core.
  • CI on this head. 41 check-runs, all completed: 35 success, 6 skipped, none failed or pending. The skipped six are Temporal Conformance, Dogfood Verify CLI, Console Pin Gate, Build Docs, the Dogfood Regression Gate shard matrix (full-battery only), and Packed-tarball smoke (opt-in). Test Core (all six shards), Build Core, every Type Check and Lint job, Governed Surface Queue Guard, Check Changeset, Spec property liveness, both single-claim guards and the card-claims-branch check are all success.

Implemented-by: session_01QBn4EzKV17Y1Ero6KDNZ45
Reviewed-by: session_01GP3comALHRcRBixS2TEnLF

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T20:22Z · ACCEPT

Head: 94604ee51bbdc3c5ba73d147604c4ec7e6e81816.

Item Reading
Shape PR → main. Line 1 is Fixes #23689, the only closing keyword; #23661, PR #23687 and #23528 are named without one.
Scope 4 files (+263 / −12): packages/objectql/src/plugin.ts (the copy follow, plus one boot else if that calls the new bridgeRuntimeObjectsToMetadataService), its pin, the dogfood door pin, and the changeset. That is the claim's surface (6198937330), widened to the boot copy by the measured reading (d) and the p1 grade (6200228776). The packages/qa test is declared to domain:cli on #6024 (6201359074). Nothing in packages/mcp or packages/runtime. No governed path (check-governed-merges.mjs --pr 23716: 0 of 4). Under the 3,000-line threshold.
Clause-② no, in the claim, the PR body and the changeset; @objectstack/objectql patch. Both MCP gates now refuse what apiEnabled: false declares, and REST already refused it. That brings the implementation back to its declared contract; no published accept set moves.
Contract review 6201494120, PASS at this head, TIER_DEFAULT (declaration face), the seat's review of record for a security p1 card. The seat checked the producer itself: mirrorRuntimeObject copies only owner rows that isTenantAuthored accepts; the boot pass sits in the existing bridge's if / else if, so no start() step moved; and the objectql:drivers-ready trigger (PR #23694) is untouched. putObjectCopy falls back to register (notify: false) where a service lacks registerInMemory. That is PR #23687's helper, unchanged, and the walk measured registerInMemory on this shape's service.
Walk Before (6199252355): the real os dev HTTP MCP door served an apiEnabled: false object, read and write, while REST refused it. The stdio door and the HTTP gate did the same in process, at env_local and at a cloud-shaped id. After (PR body): refused, the exposed CONTROL served, a deleted object OBJECT_NOT_FOUND.
Pins and controls 5 new producer pins (on the environmentId shape); the dogfood door pin over both kernel shapes plus a restart case. Ablations: the early return put back gave 4 red; the boot pass removed gave 2 red; at the door, mutant dist, the env shape went 5 red while the no-env CONTROL stayed green; the restore was 10 green.
Gates The dev's derived union at this head: 68 derived, 68 exit 0, --ran reconciled. CI on this head: 41 check-runs, 35 success, 6 skipped (the path-filtered roster), none failed. The dogfood shards skip on pull requests by design, so the door pin first runs in main's full battery after the merge. This seat reads that run.

Disposition:


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 20:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 20:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 76560a1 Oct 15, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23689-env-kernel-runtime-object-exposure branch October 15, 2026 20:55
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