Skip to content

fix(runtime)!: REST's /actions door resolves a standalone action through the protocol's by-name read, so a disabled package's object-less action answers the "no declaration" 404 instead of running - #25318

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-vlqmr4
Oct 23, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25308

Clause-②: no (narrowing)
Metadata-protocol: none
Ruling-ref: 6353801421

REST's POST /api/v1/actions/global/NAME (and its empty-segment spelling /actions//NAME) no longer runs an object-less action whose owning package is disabled. resolveRouteActionDeclaration now reads a standalone action through the protocol's by-name action read, in readExposureDeclaration's shape (getMetaItemForExecution, else getMetaItem; { type: 'action', name }; the item under its own name or nothing; ownsRoute kept). That read withholds a disabled package's item, as GET /api/v1/meta/action/NAME does, so such an action resolves to no declaration and the door answers its existing ADR-0110 D3 "no declaration" 404: the same status, code and message, but for the name, that a name nothing declares gets. The registry and metadata-service copies are read only on a host whose protocol has no by-name read. The answer is the one ruling 6353801421 on the card chose (letter B).

The defect, measured on base 258f4e6aa

#25296 (PR #25305) refuses an action on an object of a disabled package 404 OBJECT_PACKAGE_DISABLED, and skips object-less keys by design: they name no object to ask about. The declaration resolution then read the engine registry's action items (else the metadata service's rows), where a disabled package's items stay, so an object-less action resolved and dispatched. It resolves on an object route too, because ownsRoute accepts an object-less owner on any route, and so does a package's top-level action bound to another, enabled package's object (the registry's boundCodeActions leaves it off that object, so the embedded read misses and the standalone read found it).

Real kernels (ObjectKernel + MetadataPlugin + ObjectQLPlugin over a better-sqlite3 SqlDriver), in the two shapes #25296's pin file already boots (env_local with createStandaloneStack's options, and no environmentId), driven through the runtime dispatcher's dispatch(). The fixture package gains a top-level action with no objectName (type: 'script', its handler registered under global) and a top-level action bound to a second, enabled package's object; the environment authors one object-less action through protocol.saveMetaItem.

request, as a signed-in caller, the package disabled base, both shapes this PR, both shapes
GET /meta/action/NAME's read (protocol.getMetaItem, unchanged) item withheld same
POST /actions/global/NAME 200 { pong: 'package' }, the handler ran the "no declaration" 404
POST /actions//NAME 200, the handler ran the "no declaration" 404
POST /actions/ENABLED_OBJECT/NAME (the object-less action on an enabled object's route) 200, the handler ran the "no declaration" 404
POST /actions/ENABLED_OBJECT/BOUND (the package's action bound to the enabled package's object) 200, the handler ran the "no declaration" 404
the environment-authored object-less action, both spellings 200 200
POST /actions/global/NAME, anonymous 401 401
the package's action, both spellings, after registry.enablePackage 200 200

The ruling's precondition (item 7), measured before the change

With every package ENABLED, on both shapes, both protocol faces (getMetaItemForExecution and getMetaItem) served under its own name every standalone action the old sources resolved, and REST answered each 200: a manifest-registered object-less action and a manifest-registered bound action (_packageId in the registry), an environment-authored object-less action (no _packageId), and a compiled artifact's object-less actions, the project's own (AppPlugin over loadArtifactBundle, no _packageId) and its packages[] entry's. With those packages disabled, both faces withheld the packaged ones (item absent, envelope packageId the disabled package) and still served the authored and the project's own. Scratch probe, not committed; no enabled, declared action was missing from the protocol read, so no fork was raised.

What changed

  • packages/runtime/src/action-execution.ts. resolveRouteActionDeclaration: after the object's embedded actions (unchanged), a standalone declaration comes from the protocol's by-name action read when the protocol slot has one. A read that throws is the metadata plane failing to answer: degraded, so the door's existing 503, ⛔ never a fall-back to a copy, which would run a disabled package's action during the outage. The registry and metadata-service sources stay, below, for a host whose protocol has no by-name read. No runtime call of isPackageDisabled, no _packageId read, no copy of the protocol's predicate. Signature unchanged; nothing newly published.
  • .changeset/25308-rest-global-actions-disabled-package.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.

domains/actions.ts is unchanged: its "no declaration" 404 answers the absent declaration as it answers any other.

Contract

Every input whose answer changes, old to new, where the protocol slot has a by-name read and the action the route names is not embedded in the route's object:

  1. POST /actions/global/NAME or /actions//NAME, NAME a disabled package's object-less script action. Old: 200, its handler's result (the body ran). New: the "no declaration" 404; the handler is not called.
  2. POST /actions/OBJECT/NAME, OBJECT an enabled object, NAME a disabled package's object-less action. Old: 200, the handler ran. New: the "no declaration" 404.
  3. POST /actions/OBJECT/NAME, NAME a disabled package's top-level action bound to OBJECT of an enabled package. Old: dispatched. New: the "no declaration" 404.
  4. Any of 1 to 3 gated by requiredPermissions the caller lacks, switched off in the activation ledger, a declarative update or a flow action. Old: 403, 409 ACTION_DISABLED, the record load's answer, or a dispatch. New: the "no declaration" 404. Not pinned separately: the resolution answers before every gate that reads the declaration, by construction.
  5. The protocol's by-name read throws. Old: the registry's copy (else the metadata service's) answered, and the action ran. New: the door's 503 ("the metadata plane is unavailable … Refusing rather than running it ungated"). Not pinned: no rig here makes that read throw.
  6. The activation door, POST /actions/_activation/OBJECT/NAME, on a disabled package's standalone action. It resolves through the same function. Old: a ledger row was written. New: that door's own "no declaration" 404 ("there is nothing to switch"). Derived from the shared resolution, not pinned. Re-enable the package to switch it.
  7. A standalone action whose environment row overrides a code one. The protocol's layered read serves the merged item (environment row over code), where the registry copy served the code item. Derived, not pinned; the probe above found no enabled action where the two disagree on presence.

Unchanged: the anonymous baseline (401) and the 405 stay first; an action on an object of a disabled package still answers #25296's 404 OBJECT_PACKAGE_DISABLED first; an object's embedded actions resolve as before; an enabled package's object-less actions and an environment-authored one run as before (pinned on both shapes and spellings); re-enabling the package restores its actions at once, because nothing records the answer (pinned); a host whose protocol has no by-name read. The cost is one by-name protocol read per invocation of a standalone action, the read /meta/action/NAME makes.

Pins

packages/runtime/src/domains/actions-disabled-package.test.ts, extended from 24 to 50 cases on real kernels. The fixture gains the object-less action and its handler under global, an environment-authored object-less action, an optional enabled side package, and (with it) the package's action bound to the side package's object. Per shape:

  • ENABLED control. The package's and the authored object-less actions answer 200 with their handlers' results on both spellings. An object-less name nothing declares answers the "no declaration" 404.
  • DISABLED package. Precondition: the protocol's by-name read withholds the package's action and serves the authored one. On both spellings, the package's action answers exactly what qa_nowhere answers (status, code and message with the name taken out). The handler is never called, and getData, updateData and executeAction are never reached. The same holds on the enabled side package's object route, and for the package's action bound to that object. The authored action still answers 200 on both spellings. The anonymous caller still answers 401. After enablePackage, the package's action runs again on both spellings.

Red before, green after. The directions were predicted before each run, under the verify lock. The test imports the runtime's source, so no dist/ is involved.

leg mutation result which failed
main base's action-execution.ts and domains/actions.ts (git restore --source=258f4e6a, worktree only), restored by git checkout HEAD, git diff HEAD empty 8 failed, 42 passed the four DISABLED leak cases (two spellings, the enabled object's route, the cross-package bound action) on both shapes, each expected 200 to be 404
fix the protocol read skipped (if (byName) { to if (byName && false) { through scripts/ablation-replace.mjs, blob e4eebd83e059 to 271f6b423b30; restored blob equals HEAD, git diff HEAD empty) 8 failed, 42 passed the same eight
none none 50 passed none

Local verification

On 836c23e3ee, over origin/main 258f4e6aa. Every heavy run went through the verify lock; each line cites its VERDICT.

  • pnpm exec turbo run build --filter='@objectstack/dogfood^...' --filter=@objectstack/driver-turso --concurrency=3: VERDICT command-exit 0 (the runtime's closure; driver-turso because the runtime's test typecheck reads it).
  • The pin file (pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/actions-disabled-package.test.ts): VERDICT command-exit 0, 50 passed.
  • @objectstack/runtime unit suite (pnpm --filter @objectstack/runtime test): VERDICT command-exit 0; Test Files 437 passed (437); Tests 7019 passed, 19 skipped (7038). The existing action-door suites (action-activation-*, actions-hook-refusal-sentence.ledger, anonymous-gate-actions-automation, tenancy-posture-outage-gates, scoped-service-miss-attribution, actions-every-verb.hono.integration, action-governance-scope-divergence, meta-overlay-read-your-writes) are in that count, unchanged.
  • pnpm --filter @objectstack/runtime typecheck: VERDICT command-exit 0 (tsc --noEmit, and check:test-typecheck OK).
  • eslint --no-inline-config on the changed source and test files: VERDICT command-exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 68 families derived, each run, every one exit 0 (check:type-check-debt under the lock).
  • --ran reconciliation: 68 derived, 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3).
  • The full dogfood battery is owed once on the final head (the ! and the (narrowing) arm); its counts follow as a PR comment.

Acceptance notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg

…ctions/global/NAME

Claude-Session: https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg
Co-authored-by: Claude <noreply@anthropic.com>
…ct-less action as undeclared, 404, before any handler runs

Claude-Session: https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg
Co-authored-by: Claude <noreply@anthropic.com>
…s through the protocol)

Claude-Session: https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg
Co-authored-by: Claude <noreply@anthropic.com>
… the protocol's by-name read, which withholds a disabled package's

Claude-Session: https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 00:20
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

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 21a25dd9f1af51c23c4b7d703ec3a3793a9f3fa5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5e9d99854e6a4df7c7f46a0fb612da547f1b9091 — the merge of head 836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe into base 21a25dd9f1af51c23c4b7d703ec3a3793a9f3fa5, 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 5e9d99854e6a4df7c7f46a0fb612da547f1b9091 && git checkout 5e9d99854e6a4df7c7f46a0fb612da547f1b9091
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 21a25dd9f1af51c23c4b7d703ec3a3793a9f3fa5 836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe && git checkout -B drift-repro 21a25dd9f1af51c23c4b7d703ec3a3793a9f3fa5 && git merge --no-ff 836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe

node scripts/docs-audit/affected-docs.mjs --json 21a25dd9f1af51c23c4b7d703ec3a3793a9f3fa5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

dogfood_battery · head 836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe

  • Command: pnpm --filter @objectstack/dogfood test under scripts/pm/os-verify-lock.sh, no shard, no path, NODE_OPTIONS=--max-old-space-size=8192, after pnpm exec turbo run build --filter='@objectstack/dogfood^...' --filter=@objectstack/driver-turso --concurrency=3 (VERDICT command-exit 0).
  • os-verify-lock: VERDICT command-exit 0 · held the lock 1017s (16m57s) · waited 0s
  • Test Files 289 passed, 3 skipped (292) · Tests 2491 passed, 18 skipped (2509) · 0 failed.

Owed once on the final head by the ! in the title and the (narrowing) arm; the PR's Dogfood shards skip on pull requests.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe
Local-runs: none
Scope: packages/runtime/src/action-execution.ts, packages/runtime/src/domains/actions-disabled-package.test.ts, .changeset/25308-rest-global-actions-disabled-package.md + packages/runtime/src/domains/actions.ts (the invocation door and handleActionActivationWrite, shippingPackageOf), packages/metadata-protocol/src/protocol.ts (getMetaItem, getMetaItemForExecution, readMetaItemFace, readMetaItem steps 1 to 3 and its protection merge, isItemOfDisabledPackage, objectPackageDisabledError, canonicalizeMetaRequestType, readItemFromMetadataService, saveMetaItem), packages/metadata-protocol/src/item-lock.ts (storedRowDocument), packages/metadata-core/src/code-artifact-provenance.ts (isCodeArtifactBody, isTenantAuthored), packages/runtime/src/http-dispatcher.ts (resolveService), packages/runtime/src/meta-overlay-read-your-writes.test.ts, packages/runtime/src/action-governance-scope-divergence.test.ts, packages/runtime/src/http-dispatcher.actions-type-dispatch.test.ts, packages/runtime/src/domains/tenancy-posture-outage-gates.test.ts, scripts/check-adr-0087-registration.mjs (the disposition list), AGENTS.md (Clause-② and changeset rules), card 25308 body and comments 6353592171, 6353642578, 6353801421 (the ruling), PR 25318 body, file list, net diff against merge base 258f4e6, and the head's check-runs read twice.

Face: narrowing (Clause-②: no (narrowing) on the PR body and the changeset, both at line start). reviewFace returns no hits for the file list (no schema, no governed path), so the floor is every path of the diff outside the prose faces; the changeset is judged in ②. The net diff is the squash content: 32 added lines in resolveRouteActionDeclaration plus its docblock, the pin file, and the changeset. The interim _packageId implementation (57b21c6) is absent from the net diff: no isPackageDisabled, isObjectPackageDisabled or _packageId read is added to the runtime.

① Derived judgments

  1. The answer for a disabled package's object-less action (ruling item 1): right. On a host whose protocol slot has getMetaItemForExecution or getMetaItem, the standalone declaration is that read's item; readMetaItemFace answers a disabled package's action as { ...envelope, item: undefined } (protocol.ts, the isItemOfDisabledPackage branch), so the resolver returns { action: undefined, obj } and domains/actions.ts renders its existing ADR-0110 D3 deps.error(..., 404). Nothing new is said: no hint, no package code, no new code. Pinned on both kernel shapes and both spellings (global, the empty segment), handler never called, getData/updateData/executeAction never reached.
  2. Byte-identity with a name nothing declares: right, on both spellings and on both doors. The invocation door's 404 is one template with the name interpolated twice; the pins strip every occurrence (split(name).join('NAME')) and compare status, envelope code (the status-derived RESOURCE_NOT_FOUND) and message against qa_nowhere on the same shape and spelling. The activation door's 404 is likewise one template per door ("has no declaration — there is nothing to switch on/off"), identical for withheld and missing, read in handleActionActivationWrite.
  3. Reach (ruling item 4): right and declared. The object-less action on an ENABLED object's route and the package's top-level action bound to another, enabled package's object both take the same D3 404 (the ownsRoute check is kept; both are the same hidden item). Pinned on both shapes. An action on an object of a disabled package still answers 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 404 OBJECT_PACKAGE_DISABLED first: refuseDisabledPackageObject runs before the resolution in actions.ts and is untouched. Object-embedded actions (source 1) are untouched.
  4. No ENABLED, declared standalone action stops resolving (the undeclared-widening question): right, with one bounded residual in ③. The protocol's readMetaItem consults, in order, the environment's stored row (step 1), the metadata service (step 2, readItemFromMetadataService, the old rung 3's source) and this.engine.registry.getItem(type, name) (step 3, the old rung 2's very call), so every source the old rungs 2 and 3 resolved from is inside the one read; the ruling's item 7 premise was probed by the dev for manifest, authored and compiled artifacts. The served item still carries _packageId (the stored row's package_id is stamped, and mergeArtifactProtection grafts the artifact's _packageId, _packageVersion, _provenance), so shippingPackageOf on the activation door writes the same ledger row for an enabled package's action, and isCodeArtifactBody/isTenantAuthored read the same keys. canonicalizeMetaRequestType cannot throw for action (a declared type); action has no bundle member, so isWantedMember is a no-op.
  5. Outage answers 503 where a copy ran before (ruling item 3): right and declared. A throw from the by-name read returns degraded: true with the reason, and both doors render their existing 503 ("the metadata plane is unavailable ... Refusing rather than running it ungated" / "... writing an activation row ..."). Before, on a protocol host, the registry copy answered first even during a metadata outage. The changeset's "Now" bullet declares it. The protocol's own 503 (throwMetadataServiceUnavailable) fires only when the whole chain resolved nothing and the service reported degraded, and rethrowUnlessMetadataStoreUnprovisioned rethrows a store read that did not happen, so the outage surface is "the metadata plane could not answer", not every warning. No fall-back to a copy on a throw: correct per 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 docblock ("An outage of the merged read must not WIDEN exposure").
  6. The activation door's changed answer: declared. POST /actions/_activation/OBJECT/NAME resolves through the same function; a disabled package's standalone action now takes that door's own "no declaration" 404 where it wrote a ledger row before. The changeset's "Also" bullet states it with the remedy (re-enable the package).
  7. Precedence for a host without a by-name read: holds. byName is undefined when the slot is unmapped or partial, and the registry and metadata-service rungs follow, unchanged. The two mocked suites that drive resolveRouteActionDeclaration with doubles (meta-overlay-read-your-writes.test.ts: resolveService answers undefined; action-governance-scope-divergence.test.ts: resolveService answers only metadata) therefore keep pinning the legacy copies, and the real-kernel pin file pins the protocol arm; neither mocked suite was weakened.
  8. Service resolution: right. deps.resolveService(requestContext, 'protocol', envId) is the same chain readExposureDeclaration uses (HttpDispatcher.resolveService: the request's kernel, then the default kernel's scoped slot, then the singleton), so on a shared-kernel host the environment's protocol answers, and the pin file's env_local shape exercises it.
  9. Re-enable restores at once: right and pinned. The disable predicate is applied in readMetaItemFace after the cached row read, so no cache or ledger records the answer; enablePackage then runs the action on both spellings.
  10. Anonymous 401 and 405 stay first: right. The anonymous gate runs in dispatch() ahead of the door; pinned on the disabled shape.
  11. Public surface: nothing added. resolveRouteActionDeclaration keeps its signature and return shape; no export moves; packages/spec, packages/metadata-protocol and packages/rest are untouched; no ledger row. Matches ruling item 6.

② Semver level

'@objectstack/runtime': patch, fix(runtime)!, Clause-②: no (narrowing) and Metadata-protocol: none at line start in the changeset body and the PR body, the BREAKING banner naming the object-less arm, and one ADR-0087 marker not-required (no-migration-prescription) with its reasoning: this is the AGENTS.md shape for a non-protocol breaking change (patch with the banner and the disposition). The disposition is in check-adr-0087-registration.mjs's accepted list, and Check Changeset is green on the head. The changeset's Before/Now/Also/Unchanged bullets match the diff as read: object-less arm on both spellings, the enabled object's route, the bound-elsewhere action, the 503 on a failed read, the activation door, the environment-authored control, re-enable, the host without a by-name read. Nothing in the diff publishes more than the changeset says; nothing the changeset says is unimplemented. Fixes #25308 alone. Level and arms: right.

③ Boundary flags

  1. PR Contract item 5 (the by-name read throws, 503), "not pinned": answered by construction. The catch returns degraded: true and both doors render 503 on degraded. The only existing 503 pin (http-dispatcher.actions-type-dispatch.test.ts) mocks the resolver itself, so no suite exercises a throwing protocol double; the real-kernel rig cannot make the read throw. Accepted as derived; a mocked protocol throw would be a cheap pin for a later card, not owed here.
  2. PR Contract item 6 (the activation door): answered. Read in handleActionActivationWrite: same function, that door's own 404, declared in the changeset. For an ENABLED package's action the ledger row's packageId is unchanged because the protocol item carries _packageId (①.4).
  3. PR Contract item 7 (an environment row overriding a code action): answered as no change in presence. The registry's bare slot holds the hydrated stored row since the meta overlay: a just-saved overlay is listed but not dispatchable for a short window — a cache between saveMeta and resolveRouteActionDeclaration lags the write #4521 write-through, and the protocol serves the same row with the artifact's protection envelope grafted; the bodies differ in envelope keys only, and ownsRoute, requiredPermissions, type, operation and target are read from the same stored body.
  4. A residual narrowing the ruling prescribes but the PR does not name: a stored action row whose body carries no name. The new arm requires item.name === actionName (ruling item 2's shape, the same readExposureDeclaration applies to objects). storedRowDocument serves the stored body verbatim (the row's name column is not folded in), and the old rungs 2 and 3 never checked name (the scope-divergence suite's C2 shape, measured there for view). On a protocol host such an action row would stop resolving while GET /meta/action/NAME still serves it under the request's name. ADR-0110 D1 makes the declarative name the identity and the ruling fixed the check, so this is not a defect of the diff; recorded for the seat as derived, not pinned. If a real store holds action rows with no body name, that is a card of its own (a fold at the read, or a repair of the rows), not a reason to keep the copies.
  5. Cost: one by-name protocol read per invocation of a standalone action, through the overlay cache the /meta read already uses. Declared in the PR body; not a contract term.
  6. Dev-report claims not re-run here (read-only shape): the red-at-base and ablation legs, the 68 derived gate families, the runtime suite counts. The head's check-runs are the gate verdicts, read twice. At the first read Test Core (3/6) was in progress; at the end every run is completed: success on Build Core, Test Core (1/6) through (6/6) and the aggregate Test Core, Dogfood Regression Gate, TypeScript Type Check, Lint & Repo Gates and every Lint/Type Check sub-job, Check Changeset, Check PR Size, Check Documentation Links, Governed Surface Queue Guard, the three claim guards, Part-of guard, Flag docs, Auto Label, filter; skipped (opt-in or path-filtered): Dogfood Verify CLI, Console Pin Gate, Dogfood Regression Gate shards, Temporal Conformance, Build Docs, Packed-tarball smoke, 18.x upgrade harness. No failure, no neutral.
  7. PR state: draft, mergeable_state: blocked; the PR body's branch-name note (claude/project-thread-vlqmr4, named in claim 6353642578) is consistent with the green "card must claim this branch" check. Informational.

Implemented-by: claude/project-thread-vlqmr4
Reviewed-by: session_013txgcKJ9EWEZJhQ41e7kZg

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-23T00:49Z · head 836c23e3ee

item reading
Shape 3 files, +239 / −7 (diff read by the seat). resolveRouteActionDeclaration reads a standalone declaration through the protocol's by-name action read (the execution face where the protocol has one) whenever that read exists, and consults neither the registry nor the metadata-service copy then; a thrown read returns degraded: true (the door's existing 503), never a fall-back. An item the protocol withholds, a disabled package's, is no declaration, so both doors answer their existing "no declaration" 404. domains/actions.ts is unchanged; on a host with no by-name read the two copies are read as before. Inside claim 6353642578.
Premise and ruling #25308, filed by this seat from the PR #25304 / #25305 escalations. The coded answer was a needs_decision fork; ruling 6353801421 (letter B, auto-adjudicated, reversible by the maintainer) chose the by-name read and the existing 404 over a second refusal constructor. Report 6354670842 carries Ruling-ref: 6353801421; the diff implements B as ruled.
Contract review CONTRACT_REVIEW_TIER PASS on this head, 6354656372 (Clause-②: no (narrowing)).
Dogfood battery 6354539283 on this head: 289 files / 2491 tests, 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: 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 / Unchanged / What to do match the hunk: both spellings and the cross-package bound case answer the "no declaration" 404, a failing read the 503, OBJECT_PACKAGE_DISABLED still first for an object of a disabled package, the activation door now answering its own 404 for a disabled package's standalone action (declared in Also and Contract item 6), environment-authored and enabled-package actions unchanged. Metadata-protocol: none holds.
Contract ③.4 (residual) A stored action row whose body carries no name would stop resolving on a protocol host, because the ruled shape compares item.name. Derived, not measured in any rig, and ADR-0110 D1 makes the body name the identity: not filed (filing gate ① needs a measured defect). A real store holding such rows is a card of its own (a fold at the read or a row repair), not a reason to keep the copies.
Accepted deviations Branch claude/project-thread-vlqmr4 (the #25296 card thread's branch, named in the claim).
Cross-lane touches None. packages/runtime is this lane's.
Landing Not governed (check-governed-merges.mjs --pr 25318, +239 / −7). 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