Repository navigation
fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated - #25243
Conversation
Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
…ct type The repo's lint refuses a service lookup erased to `any`; the precondition reads `getObject` off the `metadata` slot, whose contract is IMetadataService. Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
…te contract The engine-double contract gate refuses a double whose findOne, update or delete is looser than ObjectQL's. The double opens each with the shared dispatch asserts, and the pinned ledger records the file. Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 12 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 084af0baf71dfbfaf5537ebadfd7c5f77605ce3f && git checkout 084af0baf71dfbfaf5537ebadfd7c5f77605ce3f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 42b60b91bdee4963a3a64be57f71d729946d649a 476460a86c89fccb17e4dd9173d5a4165722edd7 && git checkout -B drift-repro 42b60b91bdee4963a3a64be57f71d729946d649a && git merge --no-ff 476460a86c89fccb17e4dd9173d5a4165722edd7
node scripts/docs-audit/affected-docs.mjs --json 42b60b91bdee4963a3a64be57f71d729946d649a
|
Contract reviewServed-tier: Inputs: card #25138 (body; pointer 6328231985; claim 6341568965; ① Derived judgments
Verdict on the face: every change narrows or holds; no public surface moves; the one widening the HTTP review found on the twin is absent here, by the same mechanism that closed it there. This head is the narrowing its ② Semver level
Sentence by sentence: the title — true, modulo the pre-existing ③ Boundary flagsReport 6343835289:
The PR body's Acceptance notes, each judged: the two above; "describe reads the whole listing" — noted; file surface — the claim names New from this review, non-blocking, to carry: (1) the dogfood pin is unexecuted by CI on this head by design (① 9) — the landing push's full battery is where it first runs under CI; (2) a one-shot warn on the no-copy outage fall-open (① 4), the same note the HTTP record carried; (3) relation-target parity between the three doors on a disabled package's target (① 6); (4) the What a merge needs beyond this record: the five required contexts are already Implemented-by: VERDICT: PASS Generated by Claude Code |
dogfood_battery
Owed because the title carries
Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25138
Clause-②: no (narrowing)
Metadata-protocol: none
The stdio MCP transport (
objectstack mcp,OS_MCP_STDIO_ENABLED) now reads objects from the protocol's merged view, as the/mcpHTTP bridge does since #25120.list_objectsanddescribe_objectanswer from the listingGET /api/v1/meta/objectserves. The API-exposure gate every stdio data verb and the record resource ask reads the object by name through the protocol, the readRestServer's own gate makes. So a package object declaredapiEnabled: falseis refused on stdio as REST refuses it, and every object of a disabled package is refused404 OBJECT_PACKAGE_DISABLEDon every verb. Nothing is copied into themetadataservice.The defect, measured on base
123ff6f4e2createStdioDataBridgeansweredlist_objectsfrommetadataService.listObjects()anddescribe_objectfromgetObject().enforceApiExposure, the gate on all six data verbs, on the record resource (plugin.tsgetRecord) and on nested-relation targets, read the samegetObject()and treated a missing copy as "no declaration", that is, allowed. That service holds a copy of an object only where something put one there. On a kernel with anenvironmentId(createStandaloneStack:os devasenv_local,os serve,os start; and the cloud single-environment runtime) the full boot bridge is off, so an object a package registers in theSchemaRegistryduring the boot gets no copy.Real kernels (
ObjectKernel+MetadataPlugin+ObjectQLPlugin, the same composition the HTTP pins use), driven through the realMCPServerPluginstdio session over an in-process transport speaking a stdio client's JSON-RPC. The fixture package has two objects, one declaredapiEnabled: false, registered through the kernel'smanifestservice:list_objectsdescribe_objectapiEnabled: falseobjectenv_local, during the boot{ objects: [], totalCount: 0 }Object "qa_pkg_open" not foundquery_records,aggregate_recordsandcreate_recordanswered,get_recordread the store,update_recordanddelete_recordreached it (RECORD_NOT_FOUNDfor the absent id)Record "r_any" not found)env_local, installed after the boot (source: 'marketplace')404 OBJECT_API_DISABLED404environmentId, during the boot404 OBJECT_API_DISABLED404environmentId, installed after the boot404 OBJECT_API_DISABLED404The first row is the card's reading. The other three are controls: something already copied the package's objects there, so they were right and must stay right.
A disabled package (
registry.disablePackage), on both shapes. The protocol omits its objects from its listing and refuses them by name404 OBJECT_PACKAGE_DISABLED, and REST refuses them on every/dataroute. Every stdio data verb asks the ENGINE, never the protocol, and the engine serves them. All six verbs and the record resource served the exposed object on both shapes, and theapiEnabled: falseobject where it had no copy (env_local); where it had one, the copy refused it404 OBJECT_API_DISABLED. On the shape that copies, the copy outlives the disable, solist_objectslisted both objects anddescribe_objectdescribed them. This is the posture the HTTP bridge'saggregatearm had before #25136 closed it, here on every verb.What changed
packages/mcp/src/stdio-data-bridge.ts.readMergedObjectsreadsprotocol.getMetaItems({ type: 'object' }), unwrapped by the samemetaItemsArraythe skill read on this transport uses.listObjectsserves it;describeObjectanswers from the same listing, so every listed object can be described and a name the listing omits is "not found". A host with noprotocolservice, or one withoutgetMetaItems, reads themetadataservice as before. A merged read that throws travels to the caller, as on the HTTP bridge; nothing falls back.listObjectsDiagnosedkeeps itspartialverdict fromdiagnoseObjectListRead(metadataService).readObjectDeclarationreads the object by name through the protocol, on the execution face (getMetaItemForExecution) where the protocol has one andgetMetaItemotherwise: the item under its own name is the definition; nothing, or an item under another name, is "no declaration" as before; a refusalOBJECT_PACKAGE_DISABLEDis THROWN; any other throw judges themetadataservice's copy, the source this gate read before, so an outage never widens exposure. A host whose protocol has no by-name read keeps reading the copy.enforceApiExposuretakesStdioObjectReads({ metadataService, protocol }) in place of the bare service, reads throughreadObjectDeclaration, and lets the disabled-package refusal through its fail-opencatch. The relation-target gate and the write-response strip (internal: truefields omitted from thecreate_record/update_recordecho) read the same definition.GATED_ACTIONSdocblock no longer says the HTTP bridge reads discovery "straight off the metadata service" (the stale wording fix(runtime)!: the /mcp bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25136's notes left on this card).packages/mcp/src/plugin.ts. The bridge and the record resource's gate get theprotocolservice through one accessor that resolves it at each use (ctx.getService('protocol'),undefinedwhen absent or throwing), never captured instart().packages/mcp/src/mcp-server-runtime.ts.metaItemsArrayis exported within the package for the bridge;index.tsdoes not publish it.packages/mcp/src/stdio-data-bridge.exposure.test.ts,plugin.record-resource-exposure.test.ts. The two direct callers ofenforceApiExposurepass{ metadataService }; their assertions are unchanged, and on a host with no protocol the gate reads the copy exactly as before.scripts/engine-double-contract.pinned.json. Records the new unit pin file's engine double (written bycheck-engine-double-contract.mjs --write)..changeset/25138-mcp-stdio-package-objects.md. Apatchfor@objectstack/mcp, with the BREAKING banner and the ADR-0087 dispositionnot-required (no-migration-prescription); both declaration lines are in the body.Contract
Every input whose answer changes, old to new, on the stdio transport:
list_objectson a kernel with anenvironmentId, for an object a package or plugin registered during the boot. Old: omitted,totalCountshort. New: listed; the non-sys_set is whatGET /api/v1/meta/objectlists.describe_objecton such an object. Old:Object "NAME" not found. New: described.query_records,get_record,aggregate_records,create_record,update_record,delete_recordand theobjectstack://objects/NAME/records/IDresource on such an object declaredenable.apiEnabled: false. Old: served, the writes reaching the engine. New:404 OBJECT_API_DISABLED, the engine never asked;405 OBJECT_API_METHOD_NOT_ALLOWEDfor a verb anapiMethodswhitelist leaves out. The codes REST and the HTTP bridge answer.create_record/update_recordon such an object with aninternal: truefield. Old: the field echoed in the response. New: omitted, as on every other object.metadataservice still holds a copy. Old: listed and described. New: omitted and not found, as REST answers.apiEnabled: falseserved where it had no copy and refused404 OBJECT_API_DISABLEDwhere it had one. New:404 OBJECT_PACKAGE_DISABLED, the protocol's own error, refused at the gate with the engine never asked. No refusal's status moves.metadataservice's copy. Old: discovery and the gate read the copy. New: they read the definition REST serves.list_objects/describe_objectwhile the protocol's merged listing throws (the metadata store cannot be read). Old: themetadataservice's listing. New: the protocol's error travels to the client (its503 SERVICE_UNAVAILABLE, whatGET /api/v1/meta/objectanswers); nothing falls back. Pinned with a throwing double, not on a real store outage.Unchanged: objects that already had a copy (every object on a kernel without an
environmentId, a marketplace install on any kernel, a runtime-authored object) are listed and gated as before. A host with noprotocolservice, or one without the merged reads, reads themetadataservice. The gate's outage branch judges the copy. A system caller (isSystem) still bypasses the gate.Pins
packages/qa/dogfood/test/mcp-stdio-package-object-discovery.test.ts, 70 cases, real kernels. For each of the four rows above (10 each):list_objectslists both package objects with a completetotalCount;describe_objectanswers both,enableFeatures.apiEnabled: falseincluded, and a name nothing serves is still "not found"; each of the six data verbs refuses the gated object{ code: 'OBJECT_API_DISABLED', status: 404 }with no enginefind/findOne/insert/update/delete/count/aggregatecall on it; the record resource refuses it; the exposed object is queried and created, andcreate_recorddoes not echo itsinternal: truefield. For a disabled package on both shapes (15 each, the copy's existence a precondition on the shape that copies): neither object is listed nor described; each of the six verbs and the record resource refuses both objects{ code: 'OBJECT_PACKAGE_DISABLED', status: 404 }, the engine never reached.packages/mcp/src/stdio-data-bridge.merged-object-reads.test.ts, 39 cases, doubles. The engine double'sfindOne,updateanddeleteopen with the sharedassertEngine*dispatch asserts. Each branch of the two reads: listing from the merged read where the copy is absent, the bare-array answer, thepartialverdict kept, a throwing merged read propagated; the gate on all six verbs and the record resource's gate, the served face where there is no execution face, the served definition over a stale copy, an item under another name; the disabled-package refusal on every verb with and without a copy, the system caller still let through; the outage branch judging the copy and falling open with none; the relation-target gate and both write-response strips.Red before, green after, measured on
eb3b93d9bd. The later commits change one pin title (fbb65d51), type two test doubles (2c6cb414) and one lookup (1261513f), and route the unit double through the engine asserts (476460a8); no source blob or assertion moved, and both pin files pass in full on the final head (above). Each leg's direction was predicted before it ran. Mutations were landed and restored byscripts/ablation-replace.mjs(anchor 1 to 0, blob changed; restored blob equals HEAD,git diff HEADempty); the main leg swapped the three source files to their base blobs under anEXIT/INT/TERMtrap and checked blob equality and an emptygit diff HEADafter. The dogfood file consumes@objectstack/mcp'sdist/, so every leg rebuilt it andscripts/ablation-dist-preflight.mjsproved the leg's marker reacheddist/(and, after the restore, that none remained, exit 0 for each).stdio-data-bridge.ts,plugin.ts,mcp-server-runtime.tsat base blobscf59e76683,18a0afbe7f,3ef6ce641eenv_localduring-boot rows; all 28 disabled-package verb and resource rows; the no-environmentIddisabled package's listingreadMergedObjectsanswersundefined(themetadataservice read)env_localduring-boot list and describe; the no-environmentIddisabled package's listingreadObjectDeclarationalways reads the copyenv_localduring-boot six verbs, record resource and write-response strip; all 28 disabled-package rowsOBJECT_PACKAGE_DISABLEDread as an outage again (judged by the copy)eb3b93d9bdThe first attempt at the discovery and refusal legs was a no-op reading. Their marker was a bare string expression (
void 'ABLATION_…'), which the bundler drops, so the preflight answered exit 1 (marker absent fromdist/) although the source mutation landed. They were re-run with a marker the bundler keeps (aglobalThismember read): preflight exit 0, and the counts above are those runs (the same counts as the first attempt). The dogfood row now titled "the package's exposed object is queried and written, and create_record does not echo its internal: true field" was titled "CONTROL: …" in those runs;fbb65d51renamed it because it also pins the strip.Local verification
On
476460a86c, the final commit, overorigin/main123ff6f4e2(unmoved since the branch was cut), with the workspace built on1261513f93(turbo run build --filter=!@objectstack/docs, 76 tasks,VERDICT command-exit 0). Every heavy run went through the verify lock, and each line cites itsVERDICT.@objectstack/mcpunit suite (pnpm --filter @objectstack/mcp test): 41 files, 518 tests passed, 0 failed (VERDICT command-exit 0).pnpm --filter @objectstack/mcp typecheck: exit 0 (tsc --noEmit, andcheck:test-typecheckOK: 6 files, 53 errors, 8 pinned signatures held in the ledger).pnpm --filter @objectstack/dogfood typecheckwithNODE_OPTIONS=--max-old-space-size=8192: exit 0. A first run at the default heap died at exit 134 (out of memory), before any type verdict.pnpm lint(eslint . --no-inline-config): exit 0 on1261513f93. The first run (on2c6cb4141b) found one error, ametadataservice lookup typedanyin the dogfood pin's precondition;1261513f93types itIMetadataService. The last commit touches one unit pin file, andeslint --no-inline-configon it answers exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 79 commands on476460a86c(the pinned ledger brings 9 more families than the earlier tree's 70), each exit code captured before any pipe, all exit 0.--ranreconciliation:✓ dispatch-gates --ran: 79 derived famil(ies) accounted for — 79 run, 0 NOT-MEASURED (a DERIVED zero — all 79 recorded an exit code and none of them is 3).node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD, with PAYLOAD apull_requestevent carrying this body: exit 0.✓ This diff introduces no major bump.and✓ LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol none ....node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0 (in the derived list).node scripts/check-closing-keyword-parity.mjs --bodyon this body binds#25138alone.pnpm --filter @objectstack/dogfood test, no shard), on1261513f93:VERDICT command-exit 0; Test Files 288 passed, 3 skipped (291); Tests 2468 passed, 18 skipped (2486); no failure. The last commit changes one@objectstack/mcpunit pin file and the engine-double ledger (scripts/engine-double-contract.pinned.json), neither of which the battery reads (the battery consumes@objectstack/mcp'sdist/, and a test file is not in it), so it was not re-run.2c6cb4141b, found three things, each handled before the pass above:check:engine-double-contractrefused the unit pin's engine double, whosefindOne/update/deletewere looser than ObjectQL's (476460a8, the last commit, opens each with the sharedassertEngine*asserts and records the file in the pinned ledger, as the gate's own message prescribes);check-plugin-teardown-shape.mjs --self-testanswered 3 because this shallow clone lacked its pinned fixture commit (fetched);check:dual-build-cjs-loadsanswered 3 before the full build.Acceptance notes
objectstack://objectsandobjectstack://objects/NAME(bridgeResourcesinpackages/mcp/src/mcp-server-runtime.ts) read themetadataservice. Measured at this PR's head on theenv_localduring-boot fixture:resources/readofobjectstack://objectsanswers{ objects: [], totalCount: 0 }, and ofobjectstack://objects/qa_pkg_openanswers404 RESOURCE_NOT_FOUND, whilelist_objectsanddescribe_objectnow list and describe both. Reach: the stdioresources/readdoor, measured. It reveals no record and serves no data verb, so it was left for its own card rather than widened in here.404 OBJECT_PACKAGE_DISABLEDwhen a condition's target belongs to a disabled package, because the engine would read that target to lower the condition. Code-read, not pinned.env_localduring-boot shape the strip read the missing copy, socreate_recordechoed aninternal: truefield. Pinned by the exposed-object row, which the gate leg turns red.describe_objectreads the whole served listing per call, as the HTTP bridge does since fix(runtime)!: the /mcp bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25136.plugin.ts(the accessor) andmcp-server-runtime.ts(one export) besidestdio-data-bridge.ts, and the real-kernel pins live inpackages/qa/dogfood, where the stdio exposure twin already lives.claude/project-thread-dkk3hd, the name the session was given, notclaude/issue-25138-…; the Duplicate Fix Guard may say so.os-sales, before the fleet-write relay was found available in this container. This PR and its follow-up writes go through the relay.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK