Repository navigation
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
Conversation
…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>
…t-less action 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>
…ckage's object 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>
…read Claude-Session: https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 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
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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 |
|
dogfood_battery · head
Owed once on the final head by the Generated by Claude Code |
Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
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.resolveRouteActionDeclarationnow reads a standalone action through the protocol's by-nameactionread, inreadExposureDeclaration's shape (getMetaItemForExecution, elsegetMetaItem;{ type: 'action', name }; the item under its own name or nothing;ownsRoutekept). That read withholds a disabled package's item, asGET /api/v1/meta/action/NAMEdoes, 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'sactionitems (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, becauseownsRouteaccepts 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'sboundCodeActionsleaves it off that object, so the embedded read misses and the standalone read found it).Real kernels (
ObjectKernel+MetadataPlugin+ObjectQLPluginover a better-sqlite3SqlDriver), in the two shapes #25296's pin file already boots (env_localwithcreateStandaloneStack's options, and noenvironmentId), driven through the runtime dispatcher'sdispatch(). The fixture package gains a top-level action with noobjectName(type: 'script', its handler registered underglobal) and a top-level action bound to a second, enabled package's object; the environment authors one object-less action throughprotocol.saveMetaItem.GET /meta/action/NAME's read (protocol.getMetaItem, unchanged)itemwithheldPOST /actions/global/NAME200 { pong: 'package' }, the handler ran404POST /actions//NAME200, the handler ran404POST /actions/ENABLED_OBJECT/NAME(the object-less action on an enabled object's route)200, the handler ran404POST /actions/ENABLED_OBJECT/BOUND(the package's action bound to the enabled package's object)200, the handler ran404200200POST /actions/global/NAME, anonymous401401registry.enablePackage200200The ruling's precondition (item 7), measured before the change
With every package ENABLED, on both shapes, both protocol faces (
getMetaItemForExecutionandgetMetaItem) served under its own name every standalone action the old sources resolved, and REST answered each200: a manifest-registered object-less action and a manifest-registered bound action (_packageIdin the registry), an environment-authored object-less action (no_packageId), and a compiled artifact's object-less actions, the project's own (AppPluginoverloadArtifactBundle, no_packageId) and itspackages[]entry's. With those packages disabled, both faces withheld the packaged ones (itemabsent, envelopepackageIdthe 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-nameactionread when theprotocolslot has one. A read that throws is the metadata plane failing to answer:degraded, so the door's existing503, ⛔ 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 ofisPackageDisabled, no_packageIdread, no copy of the protocol's predicate. Signature unchanged; nothing newly published..changeset/25308-rest-global-actions-disabled-package.md. Apatchfor@objectstack/runtime, with the BREAKING banner and the ADR-0087 dispositionnot-required (no-migration-prescription); both declaration lines are in the body.domains/actions.tsis unchanged: its "no declaration"404answers the absent declaration as it answers any other.Contract
Every input whose answer changes, old to new, where the
protocolslot has a by-name read and the action the route names is not embedded in the route's object:POST /actions/global/NAMEor/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.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.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.requiredPermissionsthe caller lacks, switched off in the activation ledger, a declarative update or aflowaction. 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.503("the metadata plane is unavailable … Refusing rather than running it ungated"). Not pinned: no rig here makes that read throw.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.Unchanged: the anonymous baseline (
401) and the405stay first; an action on an object of a disabled package still answers #25296's404 OBJECT_PACKAGE_DISABLEDfirst; 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/NAMEmakes.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 underglobal, 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:200with their handlers' results on both spellings. An object-less name nothing declares answers the "no declaration"404.qa_nowhereanswers (status, code and message with the name taken out). The handler is never called, andgetData,updateDataandexecuteActionare 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 answers200on both spellings. The anonymous caller still answers401. AfterenablePackage, 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.action-execution.tsanddomains/actions.ts(git restore --source=258f4e6a, worktree only), restored bygit checkout HEAD,git diff HEADemptyexpected 200 to be 404if (byName) {toif (byName && false) {throughscripts/ablation-replace.mjs, blobe4eebd83e059to271f6b423b30; restored blob equals HEAD,git diff HEADempty)Local verification
On
836c23e3ee, overorigin/main258f4e6aa. Every heavy run went through the verify lock; each line cites itsVERDICT.pnpm exec turbo run build --filter='@objectstack/dogfood^...' --filter=@objectstack/driver-turso --concurrency=3:VERDICT command-exit 0(the runtime's closure;driver-tursobecause the runtime's test typecheck reads it).pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/actions-disabled-package.test.ts):VERDICT command-exit 0, 50 passed.@objectstack/runtimeunit 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, andcheck:test-typecheckOK).eslint --no-inline-configon the changed source and test files:VERDICT command-exit 0.node scripts/pm/dispatch-gates.mjs --commands: 68 families derived, each run, every oneexit 0(check:type-check-debtunder the lock).--ranreconciliation:68 derived, 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3).!and the(narrowing)arm); its counts follow as a PR comment.Acceptance notes
actionitem the two cannot both hold. 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 read is the by-name OBJECT read, which throwsOBJECT_PACKAGE_DISABLED. The by-name ACTION read throws nothing: it withholds the item. The choice went to the coordinator asneeds_decision, and ruling 6353801421 chose the read's own answer (absent: the D3404, no hint, no package code, no new code). This PR implements that ruling as written.registry.isPackageDisabled(action._packageId)at the door (commit57b21c61). The ruling forbids that read (item 2), ande30b5e80reverts it. The squash carries only the protocol read.claude/project-thread-vlqmr4, the name the session was given, notclaude/issue-25308-…; the Duplicate Fix Guard may say so. The claim (comment 6353642578) names it.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg