Repository navigation
fix(runtime)!: the /mcp bridge's list_actions, run_action and resume_run read the actions no object embeds from the merged action listing, so a package's object-less action and an environment-authored one are listed and run - #25304
Conversation
…ed action listing, so a package's object-less action and an authored one are listed and run
list_actions, run_action and resume_run collected the action items no object
embeds from metadata.loadMany('action'), which reads the metadata service's
loaders alone: the compiled artifact's actions, but not an action a package
registers through the manifest service nor an environment-authored row. They
now collect them from the protocol's merged action listing, the read
GET /api/v1/meta/action serves, through the same readMergedItems the object
listing uses; a host with no merged read still reads loadMany. A disabled
artifact package's action, which the loader copy kept listing, is omitted.
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 a1a56b0e15a4d83c27ea51d53c9fb507f2460ef5 && git checkout a1a56b0e15a4d83c27ea51d53c9fb507f2460ef5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5801a4f2c9df6a10f808063d7c3b001b7d4381c7 ee2cd6e78bbe061d844f8b50ffd9d324d0fb27bb && git checkout -B drift-repro 5801a4f2c9df6a10f808063d7c3b001b7d4381c7 && git merge --no-ff ee2cd6e78bbe061d844f8b50ffd9d324d0fb27bb
node scripts/docs-audit/affected-docs.mjs --json 5801a4f2c9df6a10f808063d7c3b001b7d4381c7
|
dogfood_battery
Owed because the title carries
Generated by Claude Code |
Contract reviewServed-tier: Inputs read: card #25297 (body, comments 6351973228 and the claim 6352041231), PR #25304 (body, file list, comments 6352627434 and the ① Derived judgments
② Semver level
③ Boundary flagsPR body's Acceptance notes:
Card's own asks:
Gates on the head: all 41 check-runs are Residuals for the implementer, none of them a FAIL: Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25297
Clause-②: no (narrowing)
Metadata-protocol: none
The HTTP MCP bridge's three business-action tools,
list_actions,run_actionandresume_run, now collect theactionitems that no object embeds (the object-lessglobalactions among them) from the protocol's mergedactionlisting, the oneGET /api/v1/meta/actionserves. They read themetadataservice'sloadMany('action')until now. That call reads the service's loaders alone, so it never held an action a package registers through themanifestservice, nor an environment-authored one, while REST'sPOST /api/v1/actions/global/NAME(OBJECT/NAMEfor a bound one) ran both. The listing is read through the samereadMergedItemsthe object listing uses since #25120 and #25139, not copied.The defect, measured on PR #25291's head
ae2b2bc5ffcollectActionDeclarations(packages/runtime/src/action-execution.ts) built its second shape, the standalone rows, frommeta.loadMany('action').MetadataManager.loadManyaggregatesloader.loadMany(type)over the registered loaders and nothing else: not the in-memoryregister()registry and notsys_metadata. On these rigs, and in the in-repo compositions, the loaders are the filesystem loader and, on an artifact boot, the artifact door's in-memory loader. NoDatabaseLoaderis wired (anenvironmentIdkernel skips it by design).Real kernels (
ObjectKernel+MetadataPlugin+ObjectQLPluginover a better-sqlite3SqlDriver), the bridge the real/mcpentry hands the MCP service (HttpDispatcher.handleMcp), and REST's door through the same dispatcher. Both shapes (environmentIdenv_localascreateStandaloneStackcomposes it, and none) answered the same on every row:loadMany('action')GET /meta/actionlist_actionsrun_actionPOST /actions/global/NAME(OBJECT/NAMEwhen bound)manifestservice (a plugin during the boot, or a marketplace install)Action 'NAME' not found200, the handler's answerprotocol.saveMetaItem, thePUT /api/v1/meta/action/NAMEwrite)Action 'NAME' not found200actions)… not found on object 'OBJECT'200packages[]included (the artifact door andAppPluginover one bundle)200registry.disablePackage)200manifest-registered package's, the package disablednot found200Scoped to
objectName: 'global',run_actionansweredAction 'NAME' not found on object 'global'on the first two rows. The MCP columns are what the pins read; the bold cells are what the main leg below fails on. TheloadManyandGET /meta/actioncolumns are the pins' preconditions and a scratch probe over the same rig, not committed. The last column of the last two rows is REST's door running a disabled package's action, the defect #25296 tracks.What changed
packages/runtime/src/domains/mcp.ts.buildMcpBridgegainsreadServedActions():readMergedItems(protocol, 'action'), whose type union widens by'action'.list_actionshands it tocollectActionDeclarations, andrun_actionandresume_runpass it asreadActions. It answersundefinedon a host with no merged read, and the collection then readsloadMany('action')as before.resumeActionRun's wiring gains an optionalreadActions.packages/runtime/src/action-execution.ts.collectActionDeclarations(deps, meta, objects?, actions?)andresolveActionByName(deps, meta, name, objectName?, objects?, actions?)take the standalone items as an optional last input; left out, they readmeta.loadMany('action')as before (its failure still answers none), so REST's activation-ambiguity check, the one other caller, is unchanged.invokeBusinessAction's wiring gains an optionalreadActions. An object-embedded declaration still wins over a standalone one with the same object and name, and every gate after collection is untouched. None of these functions is published byindex.ts..changeset/25297-mcp-standalone-actions-merged-listing.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, on both kernel shapes:list_actions, for an object-less action a package registered through themanifestservice, or an environment-authored action (object-less, or bound to an object). 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(… not found on object 'global', or on its object, when scoped). New: resolved and dispatched behind the same gates, as REST'sPOST /actions/OBJECT/NAMEruns 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.actions, where the registry carries it (mcp: on an environmentId kernel, list_actions / run_action cannot see a package object's actions when the package registered during boot — collectActionDeclarations and action resolution read metadata.listObjects() #25139). New: collected there and from the mergedactionlisting too; the object-embedded declaration wins the clash, so its answer does not change.list_actions;run_actionanswersAction 'NAME' not found(… on object 'global'when scoped);resume_runrefuses its runs403(derived from the empty candidate set, not pinned). AsGET /api/v1/meta/actionomits it andlist_objectsomits the package's objects.actionlisting throws. Old:loadMany('action'), a failure of which answered none. New: the protocol's error reaches the caller, as a throwing merged object listing does since mcp: on an environmentId kernel, list_actions / run_action cannot see a package object's actions when the package registered during boot — collectActionDeclarations and action resolution read metadata.listObjects() #25139; nothing falls back. Pinned with a throwing double, not on a real store outage.Unchanged: the compiled artifact's object-less actions are listed and run as before (pinned as a control, its
loadManycopy shown as the precondition). Object-embedded actions, a host with noprotocolservice or one without the merged listing (it readsloadMany('action')), every gate after resolution, and REST's/actionsdoor, which still resolves throughql.getSchema, the registry andloadDiagnosed. The published surface does not move:@objectstack/runtime'sindex.tsexports none of the four functions, and each gains only an optional trailing input.Pins
packages/runtime/src/domains/mcp-package-object-discovery.test.ts, 38 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 (144 in the file). A package of two objects with two top-level object-less actions,qa_glb_ping(exposed to AI, a registered handler) andqa_glb_hidden(not exposed), and two environment-authored actions saved through the protocol after the boot,qa_authored_ping(object-less) andqa_authored_bound(bound to the package's object). For each of the four shape×registration rows:list_actionslists exactlyqa_authored_pingandqa_glb_pingamong the object-less actions;run_action qa_glb_pinganswers the handler's result unscoped and scoped toglobal, and REST's door answers it200;run_action qa_authored_pinglikewise, as REST does;qa_authored_boundis listed on its object and run there, as REST runs it;qa_glb_hiddenis resolved and refused at the exposure gate; and a name nothing declares is stillAction 'qa_nowhere' not found. For a disabled package on both shapes:list_actionslistsqa_authored_pingalone, andrun_actionanswersqa_glb_ping"not found", scoped and unscoped, while the authored one still runs. For a compiled artifact booted ascreateStandaloneStackboots one (the artifact door and anAppPluginover the same bundle), on both shapes: a precondition shows the metadata service's loaders hold both object-less actions; as a control,list_actionslists andrun_actionruns both, as REST does; with the artifact'spackages[]entry disabled, the loaders still hold its action (precondition),list_actionslists only the project's own, andrun_actionanswers the package's "not found", scoped and unscoped, and still runs the project's own.packages/runtime/src/mcp-resume-run.test.ts, 3 new cases, doubles (28 in the file). With a merged listing that serves an object-less flow action and ametadataservice whoseloadManyholds none:run_actionstarts the screen flow andresume_runcompletes it,getMetaItems({ type: 'action' })asked andloadManynever;list_actionslists it. With a mergedactionlisting that throws (the object listing still served):list_actions,run_actionandresume_runfail with its error,loadManyis not asked, and the engine'sresumeis not called. The harness'sstandaloneoption and'action-throws'arm are additive; every existing case runs as before.Red before, green after. Each leg's failing set was itemized before it ran; all six runs read both pin files (172 cases), on this PR's head
ee2cd6e7overorigin/mainfb7501bc6(PR #25291's merge). A first pass on a local commit over PR #25291's unmerged head, before the bound-action case was added, answered the same legs with the same itemized sets (27, 10, 18, 2, 26, 0 of 168). 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 wrote the base blobs of the two files and restored them by checkout from HEAD,git diff HEADempty. These files import the runtime's source, so nodist/is involved.action-execution.tsanddomains/mcp.ts, with the new pinslist_actionshands no standalone itemslist_actions, but the artifact control (12), the unit listing case, the throwing caserun_actionhands no standalone itemsrun_actioncases on the four rigs (16), the disabled-package and disabled-artifact-packagerun_actioncases (4), the unit resume case (itsrun_actionstart), the throwing caseresume_runhands no standalone items403), the throwing casecollectActionDeclarationsignores the items it is givenee2cd6e7Local verification
On
ee2cd6e7, this PR's head, overorigin/mainfb7501bc6. 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, 172 passed.@objectstack/runtimeunit suite (pnpm --filter @objectstack/runtime test):VERDICT command-exit 0; Test Files 436 passed (436); Tests 6969 passed, 19 skipped (6988).pnpm --filter @objectstack/runtime typecheck:VERDICT command-exit 0(tsc --noEmit, andcheck:test-typecheckOK).pnpm lint(eslint . --no-inline-config):VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 68 families derived; each run, every oneexit 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-adr-0087-registration.mjs --base origin/mainon this head: exit 0 (one declared-breaking changeset,not-required (no-migration-prescription)).node scripts/check-closing-keyword-parity.mjs --bodyon this body: exit 2, its "closing declarations found" answer: the oneFixesline at the top of this body and nothing else, bound by the duplicate-fix guard's and the part-of guard's parsers.!and the(narrowing)arm; the PR's Dogfood shards skip on pull requests) runs once on this head after the PR opens, and its counts are posted on this PR as thedogfood_batterycomment.Acceptance notes
loadMany('action')holds the environment-authored rows. It does not on either shape: the row is insys_metadata, which no loader of the metadata service reads here, so the authored action was invisible to MCP by the same cause, and this change makes it listed and run. The control that is green at the base, and stays green, is the compiled artifact's object-less action; the pins carry both.manifest-registered package's was never there, so its disable already hid it from MCP.200on the last two rows of the table). That is rest: POST /api/v1/actions/OBJECT/NAME runs a disabled package's script action (200, the handler's answer) while the data plane refuses its object 404 OBJECT_PACKAGE_DISABLED #25296's door, not this one.run_actionresolves its declaration fromgetMetaItems, the served face that answers callers, aslist_actionslists from it and as mcp: on an environmentId kernel, list_actions / run_action cannot see a package object's actions when the package registered during boot — collectActionDeclarations and action resolution read metadata.listObjects() #25139 resolves object-embedded actions.actionhas no registered redactor, so the served body is the stored one today.getMetaItemsForExecutionis documented as never to be called from an MCP door.objectand once foraction.loadMany('action').refuseAmbiguousActionActivation(domains/actions.ts) passes no items, so its reading is unchanged. Not MCP, and not measured here.claude/project-thread-dkk3hd, the name the session was given, notclaude/issue-25297-…; the Duplicate Fix Guard may say so. It carried 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 (mcp: on an environmentId kernel, list_actions / run_action cannot see a package object's actions when the package registered during boot — collectActionDeclarations and action resolution read metadata.listObjects() #25139) until that merged, and was restarted fromorigin/mainfor this card.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK