Repository navigation
fix(runtime)!: the /mcp bridge's list_actions, run_action and resume_run read an object's actions from the listing list_objects serves, so a package object's actions are listed and run - #25291
Conversation
…ting list_objects serves list_actions, run_action and resume_run collected object-embedded actions from the metadata service's listObjects(). On a kernel with an environmentId that service holds no copy of an object a package registers during the boot, so its actions were neither listed nor resolved while list_objects listed the object and REST's /actions door ran them. The bridge now hands the three tools the object definitions list_objects lists (the protocol's merged listing, the metadata service only on a host with none). collectActionDeclarations and resolveActionByName take that listing as an optional input; invokeBusinessAction and resumeActionRun take a reader for it. A disabled package's actions are omitted and not found, as describe_object answers its objects. Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 27 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 63ce253db78716e9ada4fe3f7667d3d0b3561e52 && git checkout 63ce253db78716e9ada4fe3f7667d3d0b3561e52
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1eb1afae071530f66f4d93d416e7f452ceab3de3 ae2b2bc5ff2b3256968339d75810dc1fceeb22e4 && git checkout -B drift-repro 1eb1afae071530f66f4d93d416e7f452ceab3de3 && git merge --no-ff ae2b2bc5ff2b3256968339d75810dc1fceeb22e4
node scripts/docs-audit/affected-docs.mjs --json 1eb1afae071530f66f4d93d416e7f452ceab3de3
|
dogfood_battery
Owed because the title carries
Generated by Claude Code |
Contract reviewServed-tier: Read-only on the head: the net diff ① Derived judgmentsThe published declaration surface of The merged listing is shared, not copied — right. The PR body's Contract list, each item read against old and new code:
The narrowing is stated where it should be. The changeset's title clause ("a disabled package's are omitted as its objects are"), its BREAKING banner, its "Also changed" bullet and its "What to do" line; the PR body's Contract item 4 and its The "Unchanged" claims hold. REST's ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25139
Clause-②: no (narrowing)
Metadata-protocol: none
The HTTP MCP bridge's three business-action tools,
list_actions,run_actionandresume_run, now collect the actions declared on objects from the object definitionslist_objectslists: the protocol's merged listing, the oneGET /api/v1/meta/objectserves (#25120). They read themetadataservice'slistObjects()until now, so on a kernel with anenvironmentIdthey saw none of the actions on an object a package registers during the boot, whilelist_objectslisted the object and REST's/actionsdoor ran the actions. The listing is read once per call through the same functionlist_objectsuses, not copied.The defect, measured on base
cc8d886d4collectActionDeclarations(packages/runtime/src/action-execution.ts) built its object-embedded rows frommeta.listObjects(), andlist_actions,run_action(throughresolveActionByName) andresume_runall read it. Themetadataservice 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) nothing copies an object a package registers in theSchemaRegistryduring the boot.Real kernels (
ObjectKernel+MetadataPlugin+ObjectQLPluginover a better-sqlite3SqlDriver), the bridge the real/mcpentry hands the MCP service (HttpDispatcher.handleMcp), and REST's door driven through the same dispatcher (POST /actions/OBJECT/NAME). The fixture package: an object with a declarative update exposed to AI (mark_done), a script action exposed to AI whose handler is registered (ping), a declarative update not exposed (hidden_done), and a top-level action bound to the object (bound_done); and anapiEnabled: falseobject with a declarative update exposed to AI (gated_done).list_actionsrun_action mark_doneon a recordrun_action pingPOST /actions/qa_act_open/mark_donepingenv_local, during the bootAction 'mark_done' not found on object 'qa_act_open'Action 'ping' not found on object 'qa_act_open'200, the record updated200,{ pong: true }env_local, installed after the boot (source: 'marketplace'){ pong: true }200, updated200environmentId, during the boot{ pong: true }200, updated200environmentId, installed after the boot{ pong: true }200, updated200The MCP columns of the first row are the pins' main leg below, verbatim. The REST columns and the disabled-package reading come from a scratch probe over the same rig and fixture (object prefix
qa_pkg), not committed.Without
objectName,run_action mark_doneansweredAction 'mark_done' not foundon the first row and resolved on the other three. The bound top-level action rides inside the object'sactionson every shape (in the merged listing and in the copy alike), so it followed the same split.A disabled package (
registry.disablePackage). On the shape without anenvironmentIdthe boot copied the objects into themetadataservice, actions and all, and the copy outlives the disable:list_actionslisted all four andrun_action pingran the handler ({ pong: true }), whilelist_objectsanddescribe_objectomit the objects since #25120 andGET /api/v1/meta/objectomits them. Onenv_localthere is no copy, so the bridge already listed none and answered "not found".What changed
packages/runtime/src/domains/mcp.ts.buildMcpBridgegainsreadServedObjects(): the merged listing (readMergedObjects, unchanged from mcp: list_objects / describe_object cannot see any package-installed object (a marketplace app's whole schema) — the HTTP bridge reads the metadata service, not the SchemaRegistry REST serves #25120), or themetadataservice'slistObjects()on a host with no merged read.list_objectsnow lists through it (its answer does not change), andlist_actions,run_actionandresume_runcollect object-embedded actions from it.resumeActionRun's wiring gains an optionalreadObjects.packages/runtime/src/action-execution.ts.collectActionDeclarations(deps, meta, objects?)andresolveActionByName(deps, meta, name, objectName?, objects?)take the object listing as an optional last input; left out, they readmeta.listObjects()as before, so REST's activation-ambiguity check, the one other caller, is unchanged.invokeBusinessAction's wiring gains an optionalreadObjects. Standaloneactionrows are still read frommeta.loadMany('action'), and an object-embedded declaration still wins a clash. None of these functions is published byindex.ts..changeset/25139-mcp-actions-merged-object-view.md. Apatchfor@objectstack/runtime, 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
POST /api/v1/mcp:list_actionson a kernel with anenvironmentId, for an action declared on an object a package or plugin registered during the boot (inside the object, or a top-level action bound to it). Old: not listed. New: listed, behind the same filters (headless-invokable, a registered handler for a script action,ai.exposed,requiredPermissions).run_actionfor such an action. Old:Action 'NAME' not found on object 'OBJECT', orAction 'NAME' not foundwithoutobjectName. New: resolved and dispatched behind the same gates as on every other shape, as REST's/actionsdoor runs it.resume_runof a run started by such a flow action. Old: refused403(no flow action it could see targets that flow). New: admitted throughrun_action's gates. Pinned with doubles, not on a real automation engine.metadataservice still holds a copy of its objects (the narrowing; every kernel without anenvironmentId, and a marketplace install on any kernel). Old: listed;run_actionran a script action's handler, and refused a declarative update at the record load (RECORD_NOT_FOUND). New: omitted fromlist_actions;run_actionanswersAction 'NAME' not found on object 'OBJECT';resume_runrefuses their runs403. Asdescribe_objectanswers their objects "not found".metadataservice's copy. Old: its actions came from the copy. New: from the definition REST serves.metadataservice's listing. New: the protocol's error reaches the caller, aslist_objectslets it since mcp: list_objects / describe_object cannot see any package-installed object (a marketplace app's whole schema) — the HTTP bridge reads the metadata service, not the SchemaRegistry REST serves #25120; nothing falls back. Pinned with a throwing double, not on a real store outage.Unchanged: actions on objects that already had a copy are listed and run as before. Standalone
actionrows, a host with noprotocolservice or one without the merged listing (it reads themetadataservice), every gate after resolution, and REST's/actionsdoor, which still resolves throughql.getSchemaand the registry. The published surface does not move:@objectstack/runtime'sindex.tsexports none of the four functions, and each gains only an optional trailing input. The parity target the card names is met: on every row of the table above,run_actionnow answers what REST's door answers the same request (pinned: the declarative update, an absent record, the script action).Pins
packages/runtime/src/domains/mcp-package-object-discovery.test.ts, 36 new cases on real kernels, in mcp: list_objects / describe_object cannot see any package-installed object (a marketplace app's whole schema) — the HTTP bridge reads the metadata service, not the SchemaRegistry REST serves #25120's rig (106 in the file). For each of the four rows above:list_actionslists exactlygated_done,bound_done,mark_doneandpingon the package's two objects, and nothidden_done;run_action mark_doneupdates a seeded record (read back throughget), and REST's door updates another,200; withoutobjectNameon an absent record both answerRECORD_NOT_FOUND/404;bound_doneupdates a record;pinganswers{ pong: true }through the handler, as REST does;hidden_doneis resolved and refused at the exposure gate; and a name nothing declares is stillAction 'qa_nowhere' not found on object 'qa_act_open'. For a disabled package on both shapes:describe_objectanswers null (precondition),list_actionslists none of the package's actions, andrun_actionanswers "not found" formark_done,pingandbound_done, scoped and unscoped; on the shape without anenvironmentIda precondition shows the copy still holds the actions. The rig'sbridgeFortakes an optional caller (default unchanged), so these cases carry atenantIdand can seed records.packages/runtime/src/mcp-resume-run.test.ts, 3 new cases, doubles (25 in the file). With a merged listing that serves the object and ametadataservice that lists nothing:run_actionstarts the screen flow andresume_runcompletes it, themetadataservice'slistObjectsnever asked;list_actionslists the object's three exposed and permitted flow actions. With a merged listing that throws:list_actions,run_actionandresume_runfail with its error, themetadataservice is not asked, and the engine'sresumeis not called. The harness'smergedoption is additive; every existing case runs as before.Red before, green after. Each leg's direction was predicted before it ran; all six runs read both pin files (131 cases), on
c5e95a9dovercc8d886d4. The final head is that commit cherry-picked onto the newerorigin/main(nopackages/runtimefile moved between the two bases) with one change in the pin file: a precondition's local is typed instead ofany, which lint flagged. Both pin files were re-run on the final head (below). Mutations were landed and restored byscripts/ablation-replace.mjs(anchor 1 to 0, blob changed; restored blob equals HEAD,git diff HEADempty). These files import the runtime's source, so nodist/is involved.action-execution.tsanddomains/mcp.ts, with the new pinsenv_localduring-boot cases, the four no-environmentIddisabled-package cases, the threeresume_runcaseslist_actionscollects from themetadataservicelist_actionscases (env_localduring the boot; disabled without anenvironmentId), the unit listing case, the throwing caserun_actionresolves against themetadataserviceenv_localduring-bootrun_actioncases, the three disabled-packagerun_actioncases, the unit resume case (itsrun_actionstart), the throwing caseresume_runadmits against themetadataservice403), the throwing casecollectActionDeclarationsignores the listing it is givenc5e95a9dLocal verification
On
ae2b2bc5ff, the final commit, overorigin/mainbda53bb0ba, unless a line says otherwise. Every heavy run went through the verify lock, and each line cites itsVERDICT.pnpm turbo run build --filter='@objectstack/dogfood^...' --concurrency=2(the runtime's closure included):VERDICT command-exit 0, 66 of 66 tasks.npx vitest run src/domains/mcp-package-object-discovery.test.ts src/mcp-resume-run.test.tsinpackages/runtime):VERDICT command-exit 0, 131 passed.@objectstack/runtimeunit suite (pnpm --filter @objectstack/runtime test):VERDICT command-exit 0; Test Files 435 passed (435); Tests 6924 passed, 19 skipped (6943).pnpm --filter @objectstack/runtime typecheck:VERDICT command-exit 0(tsc --noEmit, andcheck:test-typecheckOK).pnpm lint(eslint . --no-inline-config):VERDICT command-exit 0. The first lint of the changed files, onc5e95a9d, answered one error (a service-lookup result erased toanyin a pin's precondition), fixed before the final head.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 68 families derived; each run, every oneexit 0(a first pass onefcb85dcovercc8d886d4, the same five files, also answered 68 of 68exit 0).--ranreconciliation:68 derived, 68 run, 0 NOT-MEASURED, exit 0.node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD, with PAYLOAD apull_requestevent carrying this title and body: exit 0 (✓ LEVEL AXIS:no (narrowing),Metadata-protocol: none, a non-protocol change atpatch).node scripts/check-closing-keyword-parity.mjs --bodyon this body: exit 2, its "closing declarations found" answer:Fixes #25139and nothing else, bound by the duplicate-fix guard's and the part-of guard's parsers.pnpm --filter @objectstack/dogfood test, no shard,NODE_OPTIONS=--max-old-space-size=8192):VERDICT command-exit 0; Test Files 289 passed, 3 skipped (292); Tests 2491 passed, 18 skipped (2509); no failure. Owed by the!and the(narrowing)arm; the PR's Dogfood shards skip on pull requests.Acceptance notes
/actionsdoor runs a disabled package's script action.POST /actions/OBJECT/pinganswered200 { pong: true }for a disabled package's object on both shapes (its declarative update is refused at the record load), while the data plane refuses the object404 OBJECT_PACKAGE_DISABLED. After this change the MCP door answers "not found" there; the REST door is a different door and a different card.objectName(aglobalaction) is in the registry'sactionitems and REST'sPOST /actions/global/NAMEruns it, butcollectActionDeclarationsreads standalone rows only frommeta.loadMany('action'), which holds the environment-authored rows alone, solist_actionsnever lists it andrun_actionanswersAction 'NAME' not foundon both shapes. Not this card's shape defect.metadataservice.refuseAmbiguousActionActivation(domains/actions.ts) is the one other caller ofcollectActionDeclarations; it passes no listing, so its reading on anenvironmentIdkernel is unchanged. Not MCP, and not measured here.list_objectsanddescribe_objectdo since mcp: list_objects / describe_object cannot see any package-installed object (a marketplace app's whole schema) — the HTTP bridge reads the metadata service, not the SchemaRegistry REST serves #25120.claude/project-thread-dkk3hd, the name the session was given, notclaude/issue-25139-…; the Duplicate Fix Guard may say so. It carried fix(mcp)!: the objectstack://objects resources read the stdio transport's merged object view, so a package's objects are listed and described #25268 (mcp stdio: on an environmentId kernel, theobjectstack://objectsresources read the metadata service, so a package registered during boot is listed as no object and itsobjectstack://objects/NAMEanswers 404 #25245) until that merged, and was restarted fromorigin/mainfor this card.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK