Skip to content

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

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 #25297

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 action items that no object embeds (the object-less global actions among them) from the protocol's merged action listing, the one GET /api/v1/meta/action serves. They read the metadata service's loadMany('action') until now. That call reads the service's loaders alone, so it never held an action a package registers through the manifest service, nor an environment-authored one, while REST's POST /api/v1/actions/global/NAME (OBJECT/NAME for a bound one) ran both. The listing is read through the same readMergedItems the object listing uses since #25120 and #25139, not copied.

The defect, measured on PR #25291's head ae2b2bc5ff

collectActionDeclarations (packages/runtime/src/action-execution.ts) built its second shape, the standalone rows, from meta.loadMany('action'). MetadataManager.loadMany aggregates loader.loadMany(type) over the registered loaders and nothing else: not the in-memory register() registry and not sys_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. No DatabaseLoader is wired (an environmentId kernel skips it by design).

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 through the same dispatcher. Both shapes (environmentId env_local as createStandaloneStack composes it, and none) answered the same on every row:

the standalone action, and where it came from loadMany('action') GET /meta/action list_actions run_action REST POST /actions/global/NAME (OBJECT/NAME when bound)
a package's, registered through the manifest service (a plugin during the boot, or a marketplace install) absent listed not listed Action 'NAME' not found 200, the handler's answer
an environment-authored row (protocol.saveMetaItem, the PUT /api/v1/meta/action/NAME write) absent listed not listed Action 'NAME' not found 200
the same, bound to a package object (the merged object listing does not fold an authored row into the object's actions) absent listed not listed … not found on object 'OBJECT' 200
the compiled artifact's, its packages[] included (the artifact door and AppPlugin over one bundle) held listed listed run 200
the same, its package disabled (registry.disablePackage) still held omitted listed run 200
a manifest-registered package's, the package disabled absent omitted not listed not found 200

Scoped to objectName: 'global', run_action answered Action '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. The loadMany and GET /meta/action columns 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. buildMcpBridge gains readServedActions(): readMergedItems(protocol, 'action'), whose type union widens by 'action'. list_actions hands it to collectActionDeclarations, and run_action and resume_run pass it as readActions. It answers undefined on a host with no merged read, and the collection then reads loadMany('action') as before. resumeActionRun's wiring gains an optional readActions.
  • packages/runtime/src/action-execution.ts. collectActionDeclarations(deps, meta, objects?, actions?) and resolveActionByName(deps, meta, name, objectName?, objects?, actions?) take the standalone items as an optional last input; left out, they read meta.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 optional readActions. 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 by index.ts.
  • .changeset/25297-mcp-standalone-actions-merged-listing.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, on both kernel shapes:

  1. list_actions, for an object-less action a package registered through the manifest service, 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).
  2. run_action for 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's POST /actions/OBJECT/NAME 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. A package's top-level action bound to one of its objects. Old: collected from the object's 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 merged action listing too; the object-embedded declaration wins the clash, so its answer does not change.
  5. The three tools on a disabled compiled-artifact package's object-less action (the narrowing). Old: listed and run, from the artifact loader's copy. New: omitted from list_actions; run_action answers Action 'NAME' not found (… on object 'global' when scoped); resume_run refuses its runs 403 (derived from the empty candidate set, not pinned). As GET /api/v1/meta/action omits it and list_objects omits the package's objects.
  6. The three tools while the merged action listing 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 loadMany copy shown as the precondition). Object-embedded actions, a host with no protocol service or one without the merged listing (it reads loadMany('action')), every gate after resolution, and REST's /actions door, which still resolves through ql.getSchema, the registry and loadDiagnosed. 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.

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) and qa_glb_hidden (not exposed), and two environment-authored actions saved through the protocol after the boot, qa_authored_ping (object-less) and qa_authored_bound (bound to the package's object). For each of the four shape×registration rows: list_actions lists exactly qa_authored_ping and qa_glb_ping among the object-less actions; run_action qa_glb_ping answers the handler's result unscoped and scoped to global, and REST's door answers it 200; run_action qa_authored_ping likewise, as REST does; qa_authored_bound is listed on its object and run there, as REST runs it; qa_glb_hidden is resolved and refused at the exposure gate; and a name nothing declares is still Action 'qa_nowhere' not found. For a disabled package on both shapes: list_actions lists qa_authored_ping alone, and run_action answers qa_glb_ping "not found", scoped and unscoped, while the authored one still runs. For a compiled artifact booted as createStandaloneStack boots one (the artifact door and an AppPlugin over the same bundle), on both shapes: a precondition shows the metadata service's loaders hold both object-less actions; as a control, list_actions lists and run_action runs both, as REST does; with the artifact's packages[] entry disabled, the loaders still hold its action (precondition), list_actions lists only the project's own, and run_action answers 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 a metadata service whose loadMany holds none: run_action starts the screen flow and resume_run completes it, getMetaItems({ type: 'action' }) asked and loadMany never; list_actions lists it. With a merged action listing that throws (the object listing still served): list_actions, run_action and resume_run fail with its error, loadMany is not asked, and the engine's resume is not called. The harness's standalone option 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 ee2cd6e7 over origin/main fb7501bc6 (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 by scripts/ablation-replace.mjs (anchor 1 to 0, blob changed; restored blob equals HEAD, git diff HEAD empty); the main leg wrote the base blobs of the two files and restored them by checkout from 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 31 on the four rigs every case but the control (20); the two disabled-package rigs (4, the authored action missing); the two disabled-artifact-package cases on each shape (4); the three unit cases
listactions list_actions hands no standalone items 14 every real-kernel case that reads list_actions, but the artifact control (12), the unit listing case, the throwing case
invoke run_action hands no standalone items 22 the four run_action cases on the four rigs (16), the disabled-package and disabled-artifact-package run_action cases (4), the unit resume case (its run_action start), the throwing case
resume resume_run hands no standalone items 2 the unit resume case (403), the throwing case
collect collectActionDeclarations ignores the items it is given 30 every case the main leg failed but the throwing one (the bridge still asks the throwing listing)
fix ee2cd6e7 0 172 passed

Local verification

On ee2cd6e7, this PR's head, over origin/main fb7501bc6. 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, 172 passed.
  • @objectstack/runtime unit 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, and check:test-typecheck OK).
  • 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 one 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-adr-0087-registration.mjs --base origin/main on this head: exit 0 (one declared-breaking changeset, not-required (no-migration-prescription)).
  • node scripts/check-closing-keyword-parity.mjs --body on this body: exit 2, its "closing declarations found" answer: the one Fixes line at the top of this body and nothing else, bound by the duplicate-fix guard's and the part-of guard's parsers.
  • The full dogfood battery (owed by the ! 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 the dogfood_battery comment.

Acceptance notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK

…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>
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 22, 2026 22:23
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 7 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 5801a4f2c9df6a10f808063d7c3b001b7d4381c7 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a1a56b0e15a4d83c27ea51d53c9fb507f2460ef5 — the merge of head ee2cd6e78bbe061d844f8b50ffd9d324d0fb27bb into base 5801a4f2c9df6a10f808063d7c3b001b7d4381c7, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

dogfood_battery

dogfood_battery: ee2cd6e78bbe061d844f8b50ffd9d324d0fb27bb · os-verify-lock: VERDICT command-exit 0 · held the lock 927s (15m27s) · 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 fb7501bc6, 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, on this head: 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 22:23:41Z and ran for 924.87 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: ee2cd6e78bbe061d844f8b50ffd9d324d0fb27bb
Local-runs: none
Scope: .changeset/25297-mcp-standalone-actions-merged-listing.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/src/domains/actions.ts, packages/runtime/src/standalone-stack.ts, packages/metadata/src/metadata-manager.ts, packages/metadata/src/plugin.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/metadata-diagnostics.ts, packages/spec/src/kernel/metadata-type-redaction.ts, packages/objectql/src/registry.ts, packages/objectql/src/action-governance.ts, content/docs/ai/actions-as-tools.mdx, scripts/check-adr-0087-registration.mjs, scripts/pm/clause2-line.mjs, AGENTS.md

Inputs read: card #25297 (body, comments 6351973228 and the claim 6352041231), PR #25304 (body, file list, comments 6352627434 and the dogfood_battery record 6352879348), the net diff fb7501bc66..ee2cd6e78b (merge base confirmed as fb7501bc66, 5 files, +426/-19), the cited code at the head, and the head's 41 check-runs, read last.

① Derived judgments

  1. The published declaration surface of @objectstack/runtime does not move — right. packages/runtime/package.json publishes one exports entry, ., onto dist/index.*; src/index.ts exports none of collectActionDeclarations, resolveActionByName, invokeBusinessAction, resumeActionRun, buildMcpBridge or readMergedItems, and re-exports nothing from ./action-execution.js or ./domains/mcp.js (its HttpDispatcher, AppPlugin and collectBundleActions exports are untouched by the diff). Each of the four changed functions gains one optional trailing input (actions, or the readActions wiring member); readMergedItems is module-private and only its type union widens by 'action'; McpMergedMetadataRead is unchanged; the bridge's listActions, runAction and resumeRun signatures are unchanged. No accept set an author writes against moves: the diff touches no packages/spec file and no Zod schema.

  2. The merged action listing is read through the one existing unwrap, not a second copy of the merge — right. readServedActions is readMergedItems(await getProtocol(), 'action'), the same function the skill read and readMergedObjects go through; the diff adds no second parse of getMetaItems's answer, and the three outcomes are the documented ones: undefined on structural absence (no protocol in the slot or no getMetaItems verb) falls back to loadMany('action'), a throw travels to the caller, and a non-array answer without items reads as an empty listing, exactly as for object.

  3. Each numbered item of the PR body's Contract list reads right against the code.

    • Item 1 (list_actions, a manifest-registered package's object-less action or an environment-authored one). Old: MetadataManager.loadMany iterates this.loaders alone (metadata-manager.ts), so neither the registry's item nor a sys_metadata row was in shape 2. New: the merged listing is the registry's listItems('action'), the environment's overlay rows and metadataService.list('action') merged by readFlattenedMetaItems, and the listing still passes through isSystemObjectName, isHeadlessInvokableAction, the handler probe, actionAiExposureError and actionPermissionError in listActions. Right.
    • Item 2 (run_action). Old: the two messages Action 'NAME' not found and Action 'NAME' not found on object 'OBJECT' are invokeBusinessAction's own on a null resolution. New: resolved from the same collection, then the exposure, permission, activation (disabledActionRefusal), declared-param, confirmation and subject-record gates run unchanged. Right.
    • Item 3 (resume_run). Old: an empty candidate set throws firstRefusal ?? resumeDoorError(..., 403); new: the standalone flow action is a candidate and resumeAdmissionRefusal admits it. Pinned on doubles, as the item says. Right.
    • Item 4 (a package's top-level action bound to one of its objects). The registry's action map holds bound top-level actions (boundCodeActions in registry.ts reads them), and the merged object listing folds the same action into the object's actions[] (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); collectActionDeclarations keys shape 1 first into seen as objectName:name and the standalone loop continues on a seen key, so the object-embedded copy wins and the answer does not change. Right.
    • Item 5 (the narrowing: a disabled compiled-artifact package's object-less action). The artifact door registers every packages[] body's items with that body's own id (registerArtifactBodyCollections with artifactPackageId(body), plugin.ts, ADR-0130 D4) into both its MemoryLoader and the manager, so loadMany('action') keeps the copy after a disable while the merged listing re-applies isItemOfDisabledPackage on its final set for every type but package (protocol.ts). So it is omitted from list_actions, run_action answers the two not-found spellings, and resume_run refuses 403 from the empty candidate set, as item 5 says ("derived, not pinned" for the last). Right.
    • Item 6 (the merged action listing throws). readMergedItems lets the throw travel; listActions awaits readServedActions() before calling collectActionDeclarations, and the two wirings await readActions?.() the same way, so the protocol's error reaches the caller and loadMany is never asked; old behaviour was the try/catch answering none. Right.
  4. The narrowing is stated wherever it should be — right. PR title fix(runtime)!: and body Clause-②: no (narrowing); Contract item 5 named as the narrowing; changeset summary with ! and "a disabled package's is omitted", its Clause-②: no (narrowing) line, its BREAKING banner naming exactly that narrowing, the "Also changed" bullet and the "What to do" bullet; the test file's docblock names the last table row as the narrowing; collectActionDeclarations's docblock ("an artifact package's action outlived the package's disable there") and readServedActions's docblock ("a disabled artifact package's still was") carry it in the code. One wording note, not a defect: the changeset's "Also changed" bullet spells only the unscoped Action 'NAME' not found; the scoped spelling is in the PR body.

  5. The "Unchanged" claims hold — right. Object-embedded actions: shape 1 of collectActionDeclarations is untouched. Object-embedded wins on a clash: the seen logic is untouched. REST's /actions door: resolveRouteActionDeclaration still resolves through ql.getSchema or registry.getObject, registry.getItem('action', name) and loadDiagnosed, and domains/actions.ts is not in the diff. The activation-ambiguity caller: refuseAmbiguousActionActivation calls collectActionDeclarations(deps, meta) with neither input, so it reads listObjects() and loadMany('action') as before. A host with no protocol service: getProtocol() answers undefined, readMergedItems answers undefined, readServedActions answers undefined, actions is falsy and the loadMany branch with its try/catch runs as before. The compiled artifact's actions: the merged listing lays the registry and overlay rows over metadataService.list(type), which merges the loaders with the register() registry, so the artifact door's copy is served where its package is enabled; pinned as the control on both shapes.

  6. Reading the served face (getMetaItems) for a declaration run_action executes is sound against getMetaItemsForExecution's documented rule — right. Both faces are readFlattenedMetaItems; audience decides only whether decorateMetadataItems (a per-item _diagnostics member) and per-type redaction are applied, plus the served-only flowRegistrationsOf on the response envelope, so the item set is identical by construction ("every source, merge and filter above that line is shared"). BUILTIN_METADATA_TYPE_REDACTORS holds datasource alone, so an action body is served unredacted, and the fields the gates and dispatch read (name, objectName/object, type, target, ai, requiredPermissions, the declared params, _packageId) are the stored ones. The execution face's docblock says never to call it from a door that answers a caller, HTTP and MCP named, so the served face is the documented read for this door. Residual, not a defect: a redactor registered at runtime for action through registerMetadataTypeRedactor would redact every door's copy including this one; none exists in-tree.

  7. Each tool now makes two merged reads per call (object and action), and on an unscoped kernel each merged read hydrates overlay rows into the registry — a pre-existing class of side effect since 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 object read and the skill read, not a new one. Noted, not a finding.

② Semver level

'@objectstack/runtime': patch matches what the diff publishes. Metadata-protocol: none is right: no spec key, authorable spelling, metadata type, default or stored shape moves. Under the Post-Task rule in AGENTS.md ("a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition") the changeset keeps the **BREAKING** banner and exactly one disposition marker, the adr-0087 comment marker not-required (no-migration-prescription), in the shape the gate's header documents. The marker's own category reasoning matches the code: the package publishes, no ADR-0087 id is named, and nothing exported is removed or narrowed. no-migration-prescription is honest statement-against-statement: the body prescribes nothing to rewrite ("What to do. Nothing."), and the one consumer-visible change is a read the bridge no longer answers. Clause-②: no (narrowing) is line-initial in both the PR body and the changeset body, the spelling readClause2Line reads as declared with the narrowing arm; breakingDeclaration reads three signals on this changeset (the banner, the ! summary, the arm) and the disposition it then asks for is present. Check Changeset and Lint & Repo Gates (the job carrying check:adr-0087-registration and the changeset level gate) are success on the head. The Clause-②: no (narrowing) line is right: nothing an author writes widens, and the one accept-set change is the served read a disabled package's object-less action no longer gets.

③ Boundary flags

PR body's Acceptance notes:

Card's own asks:

Gates on the head: all 41 check-runs are completed; the five required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard) and all six Test Core shards are success; Check Changeset is success; the skipped runs are the jobs that skip on pull requests by design (the three Dogfood Regression Gate shards, Console Pin Gate, Temporal Conformance, Build Docs, two opt-in harnesses). The full dogfood battery the ! and the (narrowing) arm owe was run on this head and posted as the dogfood_battery record 6352879348: Test Files 289 passed / 3 skipped, Tests 2491 passed / 18 skipped, 0 failed. No gate-carrying run is in progress or unsuccessful.

Residuals for the implementer, none of them a FAIL: content/docs/ai/actions-as-tools.mdx still describes the listing as a walk of "every registered object's actions[]" and never names object-less actions or the global object key, before and after this PR; a docs-only follow-up could say so. The ablation legs are the PR's own report; this review is read-only and takes the head's check-runs as the gate verdicts.

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-22T22:55Z · head ee2cd6e78b

item reading
Shape 5 files, +426 / −19 (diff read by the seat). domains/mcp.ts adds readServedActions (the merged action listing through the one readMergedItems unwrap, undefined on a host with none) and passes it beside readServedObjects 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 or undefined, shape 2 reads loadMany('action') as before, and "object-embedded wins" is unchanged. REST's /actions door and refuseAmbiguousActionActivation pass neither and are unchanged. Inside claim 6352041231.
Premise #25297, filed by this seat from PR #25291's contract escalation; Blocked-by: #25139 closed by PR #25291 (fb7501bc6), unblocked by this seat at 21:41Z. The card's environment-authored control was red at the base (no loader reads sys_metadata on either shape); corrected in the claim, judged right by the contract record.
Contract review CONTRACT_REVIEW_TIER PASS on this head, 6353066024 (Clause-②: no (narrowing)).
Dogfood battery 6352879348 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 6353080174: 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: standalone items from the merged action listing on all three tools, a disabled compiled-artifact package's object-less action omitted, a throwing merged listing failing the tools, the loadMany fallback on a host with no merged read, REST's door untouched. Metadata-protocol: none holds.
Escalations REST's POST /actions/global/NAME still runs a disabled package's object-less action: PR #25305 (#25296) excludes that arm by name, so this seat filed #25308 (Blocked-by: #25296) in this act. refuseAmbiguousActionActivation is already #25296's ask. The actions-as-tools.mdx gap 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 #25291 merged).
Cross-lane touches None. packages/runtime is this lane's.
Landing Not governed (check-governed-merges.mjs --pr 25304, +426 / −19). 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