Skip to content

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

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-dkk3hd
Oct 22, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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_action and resume_run, now collect the actions declared on objects from the object definitions list_objects lists: the protocol's merged listing, the one GET /api/v1/meta/object serves (#25120). They read the metadata service's listObjects() until now, so on a kernel with an environmentId they saw none of the actions on an object a package registers during the boot, while list_objects listed the object and REST's /actions door ran the actions. The listing is read once per call through the same function list_objects uses, not copied.

The defect, measured on base cc8d886d4

collectActionDeclarations (packages/runtime/src/action-execution.ts) built its object-embedded rows from meta.listObjects(), and list_actions, run_action (through resolveActionByName) and resume_run all read it. The metadata service holds a copy of an object only where something put one there. On a kernel with an environmentId (createStandaloneStack: os dev as env_local, os serve, os start; and the cloud single-environment runtime) nothing copies an object a package registers in the SchemaRegistry during the boot.

Real kernels (ObjectKernel + MetadataPlugin + ObjectQLPlugin over a better-sqlite3 SqlDriver), the bridge the real /mcp entry 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 an apiEnabled: false object with a declarative update exposed to AI (gated_done).

shape, when the package registered list_actions run_action mark_done on a record run_action ping REST POST /actions/qa_act_open/mark_done REST ping
env_local, during the boot none of the four Action 'mark_done' not found on object 'qa_act_open' Action 'ping' not found on object 'qa_act_open' 200, the record updated 200, { pong: true }
env_local, installed after the boot (source: 'marketplace') all four the record updated { pong: true } 200, updated 200
no environmentId, during the boot all four the record updated { pong: true } 200, updated 200
no environmentId, installed after the boot all four the record updated { pong: true } 200, updated 200

The 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_done answered Action 'mark_done' not found on the first row and resolved on the other three. The bound top-level action rides inside the object's actions on 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 an environmentId the boot copied the objects into the metadata service, actions and all, and the copy outlives the disable: list_actions listed all four and run_action ping ran the handler ({ pong: true }), while list_objects and describe_object omit the objects since #25120 and GET /api/v1/meta/object omits them. On env_local there is no copy, so the bridge already listed none and answered "not found".

What changed

  • packages/runtime/src/domains/mcp.ts. buildMcpBridge gains readServedObjects(): 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 the metadata service's listObjects() on a host with no merged read. list_objects now lists through it (its answer does not change), and list_actions, run_action and resume_run collect object-embedded actions from it. resumeActionRun's wiring gains an optional readObjects.
  • packages/runtime/src/action-execution.ts. collectActionDeclarations(deps, meta, objects?) and resolveActionByName(deps, meta, name, objectName?, objects?) take the object listing as an optional last input; left out, they read meta.listObjects() as before, so REST's activation-ambiguity check, the one other caller, is unchanged. invokeBusinessAction's wiring gains an optional readObjects. Standalone action rows are still read from meta.loadMany('action'), and an object-embedded declaration still wins a clash. None of these functions is published by index.ts.
  • .changeset/25139-mcp-actions-merged-object-view.md. A patch for @objectstack/runtime, with the BREAKING banner and the ADR-0087 disposition not-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:

  1. list_actions on a kernel with an environmentId, 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).
  2. run_action for such an action. Old: Action 'NAME' not found on object 'OBJECT', or Action 'NAME' not found without objectName. New: resolved and dispatched behind the same gates as on every other shape, as REST's /actions door runs it.
  3. resume_run of a run started by such a flow action. Old: refused 403 (no flow action it could see targets that flow). New: admitted through run_action's gates. Pinned with doubles, not on a real automation engine.
  4. The three tools on a disabled package's actions, where the metadata service still holds a copy of its objects (the narrowing; every kernel without an environmentId, and a marketplace install on any kernel). Old: listed; run_action ran a script action's handler, and refused a declarative update at the record load (RECORD_NOT_FOUND). New: omitted from list_actions; run_action answers Action 'NAME' not found on object 'OBJECT'; resume_run refuses their runs 403. As describe_object answers their objects "not found".
  5. An object whose served definition differs from the metadata service's copy. Old: its actions came from the copy. New: from the definition REST serves.
  6. The three tools while the protocol's merged listing throws. Old: the metadata service's listing. New: the protocol's error reaches the caller, as list_objects lets 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 action rows, a host with no protocol service or one without the merged listing (it reads the metadata service), every gate after resolution, and REST's /actions door, which still resolves through ql.getSchema and the registry. The published surface does not move: @objectstack/runtime's index.ts exports 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_action now 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_actions lists exactly gated_done, bound_done, mark_done and ping on the package's two objects, and not hidden_done; run_action mark_done updates a seeded record (read back through get), and REST's door updates another, 200; without objectName on an absent record both answer RECORD_NOT_FOUND / 404; bound_done updates a record; ping answers { pong: true } through the handler, as REST does; hidden_done is resolved and refused at the exposure gate; and a name nothing declares is still Action 'qa_nowhere' not found on object 'qa_act_open'. For a disabled package on both shapes: describe_object answers null (precondition), list_actions lists none of the package's actions, and run_action answers "not found" for mark_done, ping and bound_done, scoped and unscoped; on the shape without an environmentId a precondition shows the copy still holds the actions. The rig's bridgeFor takes an optional caller (default unchanged), so these cases carry a tenantId and 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 a metadata service that lists nothing: run_action starts the screen flow and resume_run completes it, the metadata service's listObjects never asked; list_actions lists the object's three exposed and permitted flow actions. With a merged listing that throws: list_actions, run_action and resume_run fail with its error, the metadata service is not asked, and the engine's resume is not called. The harness's merged option 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 c5e95a9d over cc8d886d4. The final head is that commit cherry-picked onto the newer origin/main (no packages/runtime file moved between the two bases) with one change in the pin file: a precondition's local is typed instead of any, which lint flagged. Both pin files were re-run on the final head (below). Mutations were landed and restored by scripts/ablation-replace.mjs (anchor 1 to 0, blob changed; restored blob equals HEAD, git diff HEAD empty). These files import the runtime's source, so no dist/ is involved.

leg mutation failed which
main the base tree's action-execution.ts and domains/mcp.ts, with the new pins 13 the six env_local during-boot cases, the four no-environmentId disabled-package cases, the three resume_run cases
listactions list_actions collects from the metadata service 4 the two real-kernel list_actions cases (env_local during the boot; disabled without an environmentId), the unit listing case, the throwing case
invoke run_action resolves against the metadata service 10 the five env_local during-boot run_action cases, the three disabled-package run_action cases, the unit resume case (its run_action start), the throwing case
resume resume_run admits against the metadata service 2 the unit resume case (403), the throwing case
collect collectActionDeclarations ignores the listing it is given 12 every real-kernel case the main leg failed, the unit resume and listing cases
fix c5e95a9d 0 131 passed

Local verification

On ae2b2bc5ff, the final commit, over origin/main bda53bb0ba, unless a line says otherwise. Every heavy run went through the verify lock, and each line cites its VERDICT.

  • pnpm turbo run build --filter='@objectstack/dogfood^...' --concurrency=2 (the runtime's closure included): VERDICT command-exit 0, 66 of 66 tasks.
  • Both pin files (npx vitest run src/domains/mcp-package-object-discovery.test.ts src/mcp-resume-run.test.ts in packages/runtime): VERDICT command-exit 0, 131 passed.
  • @objectstack/runtime unit 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, and check:test-typecheck OK).
  • pnpm lint (eslint . --no-inline-config): VERDICT command-exit 0. The first lint of the changed files, on c5e95a9d, answered one error (a service-lookup result erased to any in 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 one exit 0 (a first pass on efcb85dc over cc8d886d4, the same five files, also answered 68 of 68 exit 0).
  • --ran reconciliation: 68 derived, 68 run, 0 NOT-MEASURED, exit 0.
  • node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD, with PAYLOAD a pull_request event carrying this title and body: exit 0 (✓ LEVEL AXIS: no (narrowing), Metadata-protocol: none, a non-protocol change at patch).
  • node scripts/check-closing-keyword-parity.mjs --body on this body: exit 2, its "closing declarations found" answer: Fixes #25139 and nothing else, bound by the duplicate-fix guard's and the part-of guard's parsers.
  • The full dogfood battery, once, on the final head (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


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/authorization.mdx (via buildMcpBridge (symbol, a top-level function))
What this run could not see
  • 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 — 27 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 1eb1afae071530f66f4d93d416e7f452ceab3de3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 63ce253db78716e9ada4fe3f7667d3d0b3561e52 — the merge of head ae2b2bc5ff2b3256968339d75810dc1fceeb22e4 into base 1eb1afae071530f66f4d93d416e7f452ceab3de3, 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 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

⚠️ 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 1eb1afae071530f66f4d93d416e7f452ceab3de3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

dogfood_battery

dogfood_battery: ae2b2bc5ff2b3256968339d75810dc1fceeb22e4 · os-verify-lock: VERDICT command-exit 0 · held the lock 979s (16m19s) · waited 0s (shared-box seconds) · Test Files 289 passed / 3 skipped (292) · Tests 2491 passed / 18 skipped (2509) · 0 failed

Owed because the title carries ! and the changeset declares Clause-②: no (narrowing) (.claude/agents/os-dev.md, Definition of done). The Dogfood Regression Gate shards are skipped on pull requests, so this is the only full run. It ran on this PR's head.

  • Head. Run in the card's checkout at the PR head above, over origin/main bda53bb0ba, with a clean tree before and after (git status --porcelain empty both times).
  • Closure. pnpm turbo run build --filter='@objectstack/dogfood^...' --concurrency=2 under scripts/pm/os-verify-lock.sh: 66 of 66 tasks, VERDICT command-exit 0. The runtime is in that closure, so the suite ran this PR's @objectstack/runtime build.
  • Battery. pnpm --filter @objectstack/dogfood test under the lock, with no OS_TEST_SHARD and no path, NODE_OPTIONS=--max-old-space-size=8192. It started 20:06:51Z and ran for 977.73 s.
  • Skips. 3 files and 18 tests were skipped by the suite's own environment guards. The diff touches none of them, and no test was skipped or narrowed by hand.
  • Failures. None.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ae2b2bc5ff2b3256968339d75810dc1fceeb22e4
Local-runs: none
Scope: .changeset/25139-mcp-actions-merged-object-view.md, packages/runtime/src/action-execution.ts, packages/runtime/src/domains/mcp.ts, packages/runtime/src/domains/mcp-package-object-discovery.test.ts, packages/runtime/src/mcp-resume-run.test.ts + packages/runtime/src/index.ts, packages/runtime/package.json, packages/runtime/tsup.config.ts, packages/runtime/src/http-dispatcher.ts, packages/runtime/src/domains/actions.ts, packages/metadata-protocol/src/protocol.ts, packages/mcp/src/mcp-http-tools.ts, content/docs/ai/actions-as-tools.mdx, content/docs/ai/connect-mcp.mdx, docs/adr/0016-studio-package-authoring-and-publish.md, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs

Read-only on the head: the net diff bda53bb0ba..ae2b2bc5ff (5 files, +329/-22), card #25139's body and its one comment (the claim 6348839472), the PR body and file list, the 41 check-runs on the head (every gate-carrying one success: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Check Changeset, Dogfood Regression Gate; the rest success or skipped), and the cited code at the head through git show. The check-run conclusions are the gate verdicts; nothing was built, run or re-run here.

① Derived judgments

The published declaration surface of @objectstack/runtime does not move — right. packages/runtime/src/index.ts at the head re-exports nothing from ./action-execution.js or ./domains/mcp.js (its action-named exports are collectBundleActions from app-plugin.js at line 85 and actionBodyRunnerFactory from sandbox/index.js at line 271; its only star export is @objectstack/core, line 296); tsup.config.ts builds src/index.ts alone and package.json publishes the . entry alone. So collectActionDeclarations, resolveActionByName, invokeBusinessAction and resumeActionRun are internal, and each gains only an optional trailing input (objects?, objects?, wiring.readObjects?, wiring.readObjects?): additive even where a consumer reaches them. HttpDispatcher.buildMcpBridge(context) still answers any, and the bridge's listActions(), runAction(name, input) and resumeRun(runId, input) shapes packages/mcp/src/mcp-http-tools.ts wires (wireBridgeTools, line 624) are unchanged. McpMergedMetadataRead and readMergedItems are untouched. Clause-② no holds on the declaration axis.

The merged listing is shared, not copied — right. buildMcpBridge adds ONE closure, readServedObjects (domains/mcp.ts 603–608): the #25120 readMergedObjects → readMergedItems → protocol.getMetaItems({ type: 'object' }), or the metadata service's listObjects() when that read answers undefined (structural absence). listObjectSummaries (so list_objects and listObjectsDiagnosed), listActions (line 813), runAction (readObjects: readServedObjects, line 854) and resumeRun (line 863) all consume it; describeObject keeps its by-name path on the same readMergedObjects. list_objects's answer is byte-identical to before (the two-branch body moved into the closure unchanged).

The PR body's Contract list, each item read against old and new code:

  1. list_actions, environmentId kernel, boot-registered package's object-embedded or bound action — right. Old: collectActionDeclarations(deps, meta) read meta.listObjects(), which holds no copy of such an object on that shape (the 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 reading, restated in the rig's docblock table), so shape 1 yielded nothing for it. New: the listing list_objects serves is handed in (mcp.ts:813), and every filter after collection is untouched in the diff: isSystemObjectName fail-closed, isHeadlessInvokableAction, the registered-handler probe for the script arm, actionAiExposureError, actionPermissionError, the flow getFlow summary. Pinned on real kernels on the four shape×registration rigs (listed = gated_done, bound_done, mark_done, ping; not hidden_done).
  2. run_action for such an action — right. invokeBusinessAction resolves through resolveActionByName(deps, meta, name, objectName, await readObjects?.()) (action-execution.ts:2618) and then runs the unchanged gate chain in its unchanged order: system-object, headless, ai.exposed, ADR-0066 D4, the ADR-0126 activation consult, the ADR-0104 param contract, the confirmation gate, the RLS subject load. The two "not found" messages are the same two strings as before. Pinned with REST's door on the same kernel (mark_done on a seeded record, RECORD_NOT_FOUND/404 on an absent one, ping → { pong: true }), plus bound_done, the exposure refusal of hidden_done, and the qa_nowhere control.
  3. resume_run — right. Step 3 of resumeActionRun collects the candidates from the same listing (mcp.ts:1034); with an empty candidate set the old answer was firstRefusal ?? 403 ("no flow action targets that flow there"), now the candidate is admitted through resumeAdmissionRefusal. Pinned with doubles (mcp-resume-run.test.ts, merged: 'listing': the metadata service's listObjects never asked, getMetaItems({ type: 'object' }) asked).
  4. A disabled package's actions where the metadata service still holds a copy — right, and it is the narrowing. getMetaItems drops every item of a disabled package from the FINAL merged set (protocol.ts:10663, the isItemOfDisabledPackage filter ADR-0016 §9.5 point 2 prescribes; the by-name read refuses the object too, protocol.ts:11473), and collectActionDeclarations's objects ?? (await meta?.listObjects?.()) ?? [] never falls back from a non-nullish array, so the copy is not consulted. list_actions omits them, run_action answers the unchanged "not found" strings, resume_run has no candidate and refuses 403. Pinned on real kernels on both shapes for list_actions and run_action (scoped and unscoped), with describe_object null as the precondition and, on the no-environmentId shape, the copy proven to still hold the actions. The resume_run 403 for this case follows from the code (empty candidates), not from a pin — see ③.
  5. A served definition that differs from the metadata service's copy — right by construction: the actions now come from the row REST serves; not pinned, and nothing in the diff could make it otherwise.
  6. A throwing merged listing — right. readMergedItems lets a throw travel ("never a fallback to the un-merged read", its docblock); readServedObjects adds no catch; invokeBusinessAction evaluates await readObjects?.() before any gate, and resumeActionRun at step 3, before resume(). Pinned with the throwing double for all three tools, the metadata service never asked and the engine's resume never called.

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 Clause-②: no (narrowing) line; the commit message; the readServedObjects docblock ("on none the listing omits (a disabled package's)"); the pins' describe titles. The PR title carries only the widening half — the changeset title is the published sentence, so this is cosmetic. One published page is short of it: content/docs/ai/connect-mcp.mdx line 390 (the "An action is missing from list_actions" troubleshooting row) lists five causes and not "its object is not in list_objects (a disabled package's)" — a documentation follow-up, not a contract defect: on the environmentId shape that row was already short for the same reason before this PR, and actions-as-tools.mdx (lines 52–61: AI-exposed and permitted, "exactly the path the REST /actions/... route uses") is the promise the widening half restores.

The "Unchanged" claims hold. REST's /actions door: domains/actions.ts's request handler resolves through ql.getSchema and the registry; its one collectActionDeclarations call is refuseAmbiguousActionActivation (line 345), which passes no listing and so reads the metadata service exactly as before (its docblock already calls it best-effort). Standalone action rows: meta.loadMany('action') and the object-embedded-wins seen set are untouched. A host with no protocol service, or one without getMetaItems: readMergedItems answers undefined and readServedObjects reads meta.listObjects(). Actions on objects that already had a copy: the merged listing is the registry plus the environment's rows (readFlattenedMetaItems), the superset the copy was taken from; the no-environmentId rigs (boot and marketplace) list and run the same four actions as before. The stdio bridge (packages/mcp/src/stdio-data-bridge.ts) carries no action seam, so there is no second action path to move.

② Semver level

'@objectstack/runtime': patch is right under the level axis check-changeset-no-major.mjs enforces (maintainer ruling A, 2026-10-21): the level follows the metadata protocol alone, Metadata-protocol: none must grade every entry patch, and nothing an author writes against moves here — no spec key, no authorable spelling, no stored shape. The breaking-ness carriers for the window are both present: the BREAKING banner naming the narrowed reads, and the ADR-0087 disposition marker not-required (no-migration-prescription), a category CATEGORIES holds, whose one mechanical check (no migration prescription in the body) the "What to do. Nothing." text satisfies. Clause-②: no (narrowing) and Metadata-protocol: none are on the PR body and in the changeset body alike, in the fixed spelling clause2-line.mjs reads. Check Changeset and Lint & Repo Gates concluded success on this head. The title is fix(runtime)!: with the !, matching the banner.

③ Boundary flags

  • PR acceptance note 1 (REST's /actions door runs a disabled package's script action) and note 2 (an object-less global action is invisible to MCP on every shape): answered as out of this card's shape, escalated as filings. Both are reproducible defects the measurement found, and neither note cites a card. Prime Directive chore: version packages #10 files such a defect rather than noting it; the dispatching seat files or cites the two cards before the queue. The diff is not changed by either.
  • Note 3 (refuseAmbiguousActionActivation still collects from the metadata service): answered. Verified at domains/actions.ts:345; unchanged, best-effort by its own docblock, not an MCP door. Its blind spot on an environmentId kernel (a package-declared duplicate it cannot see) predates this PR and is not widened.
  • Note 4 (one merged read per call): answered. listActions one readServedObjects(); invokeBusinessAction one readObjects(); resumeActionRun one readObjects(); the same cost list_objects and describe_object pay 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.
  • Note 5 (branch claude/project-thread-dkk3hd, not claude/issue-25139-…): answered. The claim 6348839472 names that branch and this session; the check "The card this PR closes must claim this branch" concluded success; Implemented-by below carries it.
  • Card ask — declarations and resolution from the merged object view: met. collectActionDeclarations and resolveActionByName take the listing; list_actions, run_action and resume_run pass the one list_objects serves.
  • Card ask — REST /actions parity measured: met. The PR body's four-row table, and in the pins the REST door is driven on the same kernel for the declarative update, the absent record and the script action on all four rigs.
  • Card ask — pins on both kernel shapes: met. SHAPES × REGISTRATIONS (environmentId env_local as createStandaloneStack composes it, and no environmentId; boot and marketplace install), plus the two disabled-package rigs; Test Core ran them green on this head.
  • Residual, not blocking: resume_run's 403 for a disabled package's run (Contract item 4) is derived from the empty candidate set, not pinned on a real engine — consistent with how the PR pins item 3 with doubles. The connect-mcp.mdx troubleshooting row named in ① is a docs follow-up for the lane.

Implemented-by: claude/project-thread-dkk3hd
Reviewed-by: session_01TgbP2QERdKmh147ohacHbK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:cli seat 1 (seat post #6024) · os-sales · session session_01CxBKycaiUpA1yGx1XyoGqX · 2026-10-22T21:15Z · head ae2b2bc5ff

item reading
Shape 5 files, +329 / −22 (diff read by the seat). domains/mcp.ts adds readServedObjects (the merged listing, else the metadata service's), now the one read behind list_objects, and passes it to list_actions, run_action and resume_run. collectActionDeclarations, resolveActionByName, invokeBusinessAction and resumeActionRun each gain only an optional trailing input (none published by index.ts); left out, they read the metadata service as before, so REST's /actions door and refuseAmbiguousActionActivation are unchanged. Standalone action rows still read loadMany('action'). Inside claim 6348839472.
Premise #25139, the actions half of the family #25120 / #25138 / #25245; serial after PR #25268 (merged 72fad9223).
Contract review CONTRACT_REVIEW_TIER PASS on this head, 6351371491 (Clause-②: no (narrowing)).
Dogfood battery 6350754884 on this head: lock VERDICT command-exit 0, 289 files / 2491 tests passed, 0 failed (owed: ! title and a narrowing arm).
Gates (check-runs on this head) 41 runs: 34 success, 7 skipped, 0 failed. mergeable_state: clean. Report 6351392996: mcp_calls reads only, writes all through the relay.
Changeset, read per sentence fix(runtime)! patch with the BREAKING banner and an ADR-0087 not-required reason. Before / Now / Also changed / Unchanged / What to do each match the hunks: object-embedded actions collected from the served listing on all three tools, a disabled package's omitted (not found, resume_run 403), a throwing merged listing failing the tools as list_objects does, standalone rows and REST's door unchanged. Metadata-protocol: none holds.
Escalations (contract ③, Prime Directive #10) Filed by this seat before the queue: #25296 (REST /actions runs a disabled package's script action) and #25297 (a package's object-less global action is invisible to MCP; Blocked-by: #25139). The connect-mcp.mdx troubleshooting row is handed to domain:devx on #6023.
Accepted deviations Branch claude/project-thread-dkk3hd (the MCP card thread's branch, restarted from origin/main after PR #25268 merged).
Cross-lane touches None. packages/runtime is this lane's.
Landing Not governed (check-governed-merges.mjs --pr 25291). Ready + auto-merge through the relay in this act.

Generated by Claude Code

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