Repository navigation
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
Conversation
… 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>
Claude-Session: https://claude.ai/code/session_01QBn4EzKV17Y1Ero6KDNZ45 Co-authored-by: Claude <noreply@anthropic.com>
…me-object copy Claude-Session: https://claude.ai/code/session_01QBn4EzKV17Y1Ero6KDNZ45 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: 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 ① Derived judgments
② Semver levelThe changeset is
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对 ·
|
| 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:
- open_questions: [0] A (the boot
else ifis inside the dispatch's bound, as the record judges). [1] A (the in-repo shape reading; the cloud host is outside this repo). [2] A (no). [3] noted. out_of_scope_findings: the bare refusal text without an error code at the HTTP MCP door rides 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 (carrier). The real-CLI stdio door not being spawned is an Acceptance note.- Grade: the card is p1
securitysince 6200228776.
Generated by Claude Code
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 realos 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 themetadataservice 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/mcpandpackages/runtimeare untouched, and so areObjectQLPlugin.start()'s order and theobjectql:drivers-readyregion (PR #23694).Step 1: the walk, on base
69351e985aThe two shapes.
os devasenv_local, booted for real:examples/app-crm,objectstack dev --freshfrom the built CLI.os devtakesresolvedMode = 'standalone', socreateStandaloneStackcomposes the kernel. That isMetadataPlugin({ environmentId, artifactSource: local-file })plusObjectQLPlugin({ environmentId: 'env_local', hydrateMetadataFromDb: true, runPlatformMigrations: true }).apps/objectos-ee) lives inobjectstack-ai/cloud, outside this session's repository scope, so it was not booted. In this repo the shape is the oneObjectQLPlugin'shydrateMetadataFromDbdocblock names ("the cloud single-env tenant runtime on Turso"): anenvironmentId, hydration on, and an artifact-sourced service.createStandaloneStackcomposes exactly that for anyOS_ENVIRONMENT_ID. It was measured in-process with those two plugin options verbatim, atenv_localand atenv_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(itsstart()builds the stdio data bridge underenforceApiExposure) driven by JSON-RPCtools/call. The HTTP leg callsrefuseUnexposedDataAction(the gatecallDataasks first under the HTTP MCP data verbs) with the kernel's ownmetadataservice. The one faked seam is thesys_api_keyrow the stdio principal resolves from.Real
os dev, HTTP MCP door (POST /api/v1/mcp, as the seeded admin), base:GET /api/v1/data/Xquery_recordscreate_recordapiEnabled: falseOBJECT_API_DISABLEDtotal: 0)list_objectsdoes not name itOBJECT_API_DISABLEDdescribe_object:Object "qa_walk_toggle" not foundIn-process, both
environmentIdvalues, base:metadata.getObjectquery_recordsserved;create_recordserved, rows 1 to 2OBJECT_NOT_FOUND404Reading (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.registerInMemorysets the in-memory map only, with no loader write and no announce.unregisterdeletes throughdatasource:loaders only, and it is called under PR #23687's three bounds: the mutation saysdeleted, 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:
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.enforceApiExposure, list, describe and theget_recordresource reader inpackages/mcp, plusrefuseUnexposedDataAction,listObjectsanddescribeObjectinpackages/runtime(domains/mcp.ts).IDataEnginedoes not declareregistry, so each site needs an untyped reach-through.apiEnabled: falseis enforced wherever it is read. That is "declared means enforced" at the producer, with no consumer tolerance.packages/objectql/src/plugin.ts, about 60 lines.domain:cli), two packages, and the dispatch says to stop before writing it.What changed
packages/objectql/src/plugin.ts, the copy follow.followRuntimeObjectCopyno longer returns early whenenvironmentIdis 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 anenvironmentIdandhydrateMetadataFromDb,start()copies each hydrated runtime-authored object once, beside the existing bridge gate.hydrateMetadataFromDbis documented as set only on a kernel whose registry is its own. Only tenant-authored owner rows are copied, throughregisterInMemory, 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 aterror, naming the objects, the consequence and the way out. This is anelse ifon the existing bridge gate, after hydration; no step ofstart()moved.packages/objectql/src/plugin-runtime-object-metadata-copy.test.tsgains a describe for this shape.packages/qa/dogfood/test/mcp-stdio-runtime-object-exposure.test.tsruns 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 inplugin-runtime-object-metadata-copy.test.ts:apiEnabled: false;Door pins (real kernel, real
MCPServerPlugin, in-process JSON-RPC),mcp-stdio-runtime-object-exposure.test.ts, nowdescribe.eachoverno environmentIdandenvironmentId env_local (createStandaloneStack):query_records,get_record,create_record,update_recordanddelete_recordare refused withOBJECT_API_DISABLED/ 404, the table is untouched, and the exposed neighbour is still served.OBJECT_NOT_FOUND.The no-
environmentIdleg 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 HEAD3751a6c6161c, andgit diff HEADempty.environmentIdearly return put backenvironmentIdpins.dist/&& !String('ABLATION-23689-3b')so it compiles),objectqlrebuilt,ablation-dist-preflightfound the marker in 2 built filesenvironmentIdshape: 5 failed. The no-environmentIdshape: 5 passed.objectqlrebuilt,ablation-dist-preflight --absent: marker absent from all 14 built files, tree cleanThe first door attempt deleted the boot pass's call outright. The
.d.tsbuild 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 importplugin.tsfrom source, so their legs need no rebuild.After the fix: the same walk on the real
os dev(HTTP MCP door)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 --noEmitpluscheck:test-typecheck): exit 0.plugin-runtime-object-metadata-copy.test.ts: 10 passed.mcp-stdio-runtime-object-exposure.test.ts(against rebuiltobjectqldist/): 10 passed.@objectstack/mcpstdio-data-bridge.exposure.test.tsplusplugin.record-resource-exposure.test.ts35 passed, and@objectstack/runtimeaction-execution-calldata-relation-target-exposure.test.ts13 passed.pnpm --filter @objectstack/dogfood typecheck: exit 0, 0 errors. It was re-run after rebuilding the CLI closure with declarations.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat94604ee51, with no paths (4 paths against merge base69351e985), 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) andcheck-plugin-teardown-shape.mjs --self-test(exit 3) refused on a shallow clone. Each passed aftergit fetch --shallow-since=2026-07-10 origin main.check:dts-closure(exit 1, 28 packages missing.d.ts) andcheck:dual-build-cjs-loads(exit 3, packages not built) came from my own walk-timeOS_SKIP_DTS=1CLI build. Both passed afterturbo run build --forceover the CLI and CRM closure:check:dts-closurefound 175 of 175 declaration files, andcheck:dual-build-cjs-loadsprobed 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.Acceptance notes
os dev. The stdio transport would share the dev process's stdout with its banner. The in-process leg runs the sameMCPServerPluginstdio bridge overcreateStandaloneStack's plugin options. Carrier: none.objectstack-ai/cloud's single-environment host) was not booted. This session's repository scope isobjectstack-ai/objectstack. Its in-repo shape is measured as described above. Carrier: none.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