Skip to content

fix(runtime)!: REST's /actions door refuses an action on an object whose owning package is disabled 404 OBJECT_PACKAGE_DISABLED, before any handler runs - #25305

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

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-vlqmr4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25296

Clause-②: no (narrowing)
Metadata-protocol: none

REST's POST /api/v1/actions/OBJECT/NAME now asks, before it reads the action's declaration, whether the object it names is served. An object whose owning package is disabled is refused 404 OBJECT_PACKAGE_DISABLED, with the error the protocol raises for it on the data plane and on GET /api/v1/meta/object/NAME, before any gate, record load or handler. The question is asked through the by-name object read the data verbs' exposure gate already makes (readExposureDeclaration), so neither the package predicate nor the refusal is copied.

The defect, measured on base 5801a4f2c9

The /actions door (packages/runtime/src/domains/actions.ts, the one owner of the route) resolved the action's declaration through resolveRouteActionDeclaration, which reads ql.getSchema(OBJECT) and the registry's action items. A disabled package's objects stay registered there, so the declaration resolved and the door dispatched, while every other door treats the package as gone.

Real kernels (ObjectKernel + MetadataPlugin + ObjectQLPlugin over a better-sqlite3 SqlDriver), in the two shapes the #25120 rig pins (env_local with createStandaloneStack's options, and no environmentId), driven through the runtime dispatcher's dispatch(). The fixture package, registered through the manifest service during the boot and then disabled (registry.disablePackage): one object carrying a script action ping (its handler registered), a declarative update mark_done, a script action admin_ping gated by requiredPermissions: ['manage_metadata'], and a top-level declarative update bound_done bound to the object.

request, as a signed-in caller without manage_metadata base, both shapes this PR, both shapes
GET /data/OBJECT (the data plane, unchanged) 404 OBJECT_PACKAGE_DISABLED same
POST /actions/OBJECT/ping 200 { pong: true }, the handler ran 404 OBJECT_PACKAGE_DISABLED
POST /actions/OBJECT/ping/r_any 200, the handler ran 404 OBJECT_PACKAGE_DISABLED
POST /actions/OBJECT/mark_done with a recordId refused only at the record load 404 OBJECT_PACKAGE_DISABLED
POST /actions/OBJECT/bound_done with a recordId refused only at the record load 404 OBJECT_PACKAGE_DISABLED
POST /actions/OBJECT/admin_ping 403 (the D4 gate read the declaration) 404 OBJECT_PACKAGE_DISABLED
POST /actions/OBJECT/qa_nowhere the "no declaration" 404 (no code) 404 OBJECT_PACKAGE_DISABLED
the same ping, anonymous 401 401 (the anonymous baseline stays first)
the same ping, after registry.enablePackage 200 200

The base column is the pins' main leg below.

What changed

  • packages/runtime/src/action-execution.ts. refuseDisabledPackageObject(deps, requestContext, object, scopeId?): it asks readExposureDeclaration (the protocol's by-name object read on its execution face, which refuseUnexposedDataAction already asks) and rethrows only the protocol's OBJECT_PACKAGE_DISABLED refusal (isObjectPackageDisabledRefusal). Any other answer is not its to give: an absent object is the declaration resolution's to answer, and so is an unreadable metadata plane (ADR-0110 D3). Not published by index.ts.
  • packages/runtime/src/domains/actions.ts. handleActionsRequest asks it once the engine is resolved and the activation write door has had its turn, before the declaration resolution; a refusal is answered through deps.errorFromThrown(err, 404), the exit every other domain catch uses, so the envelope carries the protocol error's own code and status. An object-less invocation (POST /actions/global/NAME, POST /actions//NAME) names no object and is not asked about.
  • .changeset/25296-rest-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.

Contract

Every input whose answer changes, old to new, on POST /api/v1/actions/OBJECT/NAME[/RECORD] where OBJECT's owning package is disabled, on every kernel shape:

  1. A script action with a registered handler. Old: 200, the handler's result (its body ran, isSystem-elevated as a script body always is). New: 404 OBJECT_PACKAGE_DISABLED; the handler is not called.
  2. A declarative update (embedded, or a top-level action bound to the object). Old: refused at the record load. New: 404 OBJECT_PACKAGE_DISABLED before the record load.
  3. An action gated by requiredPermissions the caller lacks. Old: 403. New: 404 OBJECT_PACKAGE_DISABLED. The data plane also answers the package refusal ahead of any permission check, and the refusal names the object, which the data plane already discloses to the same caller.
  4. An action switched off in the activation ledger. Old: 409 ACTION_DISABLED (after D4). New: 404 OBJECT_PACKAGE_DISABLED. Not pinned separately: the gate sits ahead of the ledger consult by construction, as it sits ahead of D4 in case 3.
  5. A name nothing declares. Old: the "no declaration" 404 with its prescription. New: 404 OBJECT_PACKAGE_DISABLED.
  6. A flow action. Old: dispatched through the automation service (or 503 without one). New: 404 OBJECT_PACKAGE_DISABLED. Not pinned separately: the rig has no automation service, and the gate runs before the type dispatch for every type.

Unchanged: the anonymous baseline (401) and the 405 stay first; an enabled package's actions run as before behind every gate (pinned as the control on both shapes); re-enabling the package restores its actions at once, because nothing records the refusal (pinned); an object-less action; the activation door (POST /actions/_activation/OBJECT/NAME); a host whose protocol has no by-name read (no refusal, as before). The cost is one by-name object read per invocation on an object route, the read the data verbs' gate already makes per call.

Pins

Red before, green after. The directions were predicted before each run. Both mutated legs ran the pin file under the verify lock on 99d375e8 (this PR's head); the test imports the runtime's source, so no dist/ is involved.

leg mutation result which failed
main the base tree's action-execution.ts and domains/actions.ts (git restore --source=5801a4f2, worktree only), restored by git checkout HEAD, git diff HEAD empty 12 failed, 12 passed the six refusal cases on both shapes
helper refuseDisabledPackageObject swallows the refusal too (scripts/ablation-replace.mjs: anchor 1 to 0, blob 9e202cd4e8a7 to 0711946f42aa; restored blob equals HEAD, git diff HEAD empty) 12 failed, 12 passed the same twelve
fix none 24 passed none

Local verification

On 99d375e84c, over origin/main 5801a4f2c9. 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, 66 of 66 tasks (the runtime's closure included; 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, 24 passed.
  • @objectstack/runtime unit suite (pnpm --filter @objectstack/runtime test): VERDICT command-exit 0; Test Files 437 passed (437); Tests 6952 passed, 19 skipped (6971).
  • pnpm --filter @objectstack/runtime typecheck: VERDICT command-exit 0 (tsc --noEmit, and check:test-typecheck OK). A first run before driver-turso was built answered one TS2307 in organization-ownership-boot-guard.turso-remote.kernel.test.ts (a missing dist, not this diff).
  • eslint --no-inline-config on the three changed source and test files: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 68 families derived, each run, every one exit 0. Two first answered exit 3 (prerequisites, not verdicts): check-plugin-teardown-shape --self-test until its pinned fixture commit was fetched, and check:dual-build-cjs-loads until the eight dists it named were built. check:type-check-debt ran 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

  • Measured, not changed here: a disabled package's object-less action still runs through REST. A top-level action with no objectName, shipped by the package (type: 'script', its handler registered under global), answered 200 with its handler's result on POST /actions/global/NAME and POST /actions//NAME after registry.disablePackage, on both shapes, while GET /api/v1/meta/action/NAME answered 404 RESOURCE_NOT_FOUND. Scratch probe over this PR's rig, not committed. The card's shape is the object route and its OBJECT_PACKAGE_DISABLED answer; an object-less action has no object to refuse, and which answer it owes (the "no declaration" 404, as the by-name /meta read answers it, is the likely reading) is the next card's to settle. The mcp: a package's object-less (global) action is invisible to list_actions / run_action on every kernel shape, while REST's POST /api/v1/actions/global/NAME runs it #25297 card moves MCP's object-less actions onto the merged action listing, which omits them.
  • refuseAmbiguousActionActivation was not needed. The card asked whether this fix needs the activation-ambiguity check to read the served listing. It does not: the fix reads no collection, and the check serves the activation door, which this PR leaves alone. Whether that check sees a package object's actions on an environmentId kernel is unchanged and unmeasured here.
  • The activation door is untouched. Flipping a disabled package's action in the activation ledger still works; it writes a ledger row and runs nothing.
  • Branch name. This PR's branch is claude/project-thread-vlqmr4, the name the session was given, not claude/issue-25296-…; the Duplicate Fix Guard may say so. The claim (comment 6352124313) names it.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg

…s 404 OBJECT_PACKAGE_DISABLED

Claude-Session: https://claude.ai/code/session_013txgcKJ9EWEZJhQ41e7kZg
Co-authored-by: Claude <noreply@anthropic.com>
…ing package is disabled 404 OBJECT_PACKAGE_DISABLED before any handler runs

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/system-context.mdx (via handleActionsRequest (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 37cc8788866e86f41f115d6158e885afabd1e7d7 — the merge of head 99d375e84c7ba42293d674b0e06f41d316309d5d 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 37cc8788866e86f41f115d6158e885afabd1e7d7 && git checkout 37cc8788866e86f41f115d6158e885afabd1e7d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5801a4f2c9df6a10f808063d7c3b001b7d4381c7 99d375e84c7ba42293d674b0e06f41d316309d5d && git checkout -B drift-repro 5801a4f2c9df6a10f808063d7c3b001b7d4381c7 && git merge --no-ff 99d375e84c7ba42293d674b0e06f41d316309d5d

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 · head 99d375e84c7ba42293d674b0e06f41d316309d5d

  • 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 (66 of 66 tasks, VERDICT command-exit 0).
  • os-verify-lock: VERDICT command-exit 0 · held the lock 1055s (17m35s) · 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: 99d375e84c7ba42293d674b0e06f41d316309d5d
Local-runs: none
Scope: packages/runtime/src/domains/actions.ts, packages/runtime/src/action-execution.ts, packages/runtime/src/domains/actions-disabled-package.test.ts + packages/metadata-protocol/src/protocol.ts (objectPackageDisabledError, isObjectOfDisabledPackage, getMetaItem / getMetaItemForExecution / readMetaItemFace, assertObjectRegistered), packages/runtime/src/http-dispatcher.ts (errorFromThrown, error), packages/types/src/thrown-http-error.ts (resolveThrownHttpError), packages/spec/src/api/error-code-ledger.zod.ts (OBJECT_PACKAGE_DISABLED row), packages/objectql/src/action-governance.ts (isObjectLessActionKey), packages/runtime/src/index.ts and packages/runtime/package.json (the published entry), packages/runtime/vitest.config.ts and vitest.repo-tests.json (which project runs the new file), .changeset/25296-rest-actions-disabled-package.md (prose face, judged in ② only). Read at the head with git show and the GitHub read tools; nothing built or run.

① Derived judgments

The gate's position, read in handleActionsRequest at the head. The statements run: anonymous baseline (401 ANONYMOUS_DENY) → deferred body read → 405 with Allow → path shape 400 → default environment → engine resolution (503 Data engine not available) → the activation write door (_activation) → the new gate → resolveRouteActionDeclaration (D3 503 degraded, "no declaration" 404, D4 403, activation ledger 409 ACTION_DISABLED) → headless type 400 → flow 503 → param contract 400 → record load → dispatch. So the PR body's "the anonymous baseline and the 405 stay first" and "the activation door is untouched" are right: both sit above the gate, unchanged, and the pin file measures the 401 ordering on both shapes. Right.

Reuse, as the card's Shape demands. refuseDisabledPackageObject calls readExposureDeclaration (the by-name read refuseUnexposedDataAction already makes: protocol.getMetaItemForExecution, else protocol.getMetaItem, else the metadata service's copy) and rethrows only what isObjectPackageDisabledRefusal recognises. The predicate lives once in the protocol (isObjectOfDisabledPackage, asked by readMetaItemFace and by assertObjectRegistered) and the refusal is the protocol's one constructor (objectPackageDisabledError: status 404, code OBJECT_PACKAGE_DISABLED, object). Neither is copied into the runtime. collectActionDeclarations, resolveActionByName and the MCP bridge are untouched, as the claim's serial constraint against #25297 required. Right.

The wire envelope. The door answers through deps.errorFromThrown(err, 404): resolveThrownHttpError takes the thrown status (404) and keeps the spelled code because OBJECT_PACKAGE_DISABLED is a registered member of the closed ledger (error-code-ledger.zod.ts), so no demotion to RESOURCE_NOT_FOUND and no declaredCode sibling. The same exit and the same body the /data door answers for the same object, which the precondition case pins. Right.

Can it widen? No. The function swallows every throw but the one code; readExposureDeclaration returns an item or undefined for an enabled or unknown object and never throws for them; an object of no package has no _packageId to be disabled. An enabled package's object cannot be refused (pinned as the control on both shapes: ping 200 with the handler run once, the two updates 200, admin_ping 403, qa_nowhere 404 with a code that is not the package code). An object-less key (global, *, empty) is skipped by isObjectLessActionKey, the same predicate the handler-key rotation uses. The isSystem caller is not exempted here, where refuseUnexposedDataAction exempts it; that is right, because assertObjectRegistered on the data plane refuses a system caller the same way, and the brief's "every REST / SDK caller" covers it. Right.

Does an outage of the by-name read change any answer? No. readMetaItemFace asks isObjectOfDisabledPackage(this.engine.registry, name) synchronously before readMetaItem touches any store, so a metadata-plane outage cannot mask the refusal; it can only make the later read throw, which the gate swallows and the declaration resolution's D3 trichotomy then answers as before. The one shape that skips the refusal is a host whose protocol slot has no by-name read (the copy path, which never throws this code), and the PR body declares that unchanged. No fail-open on an outage where a refusal is owed. Right.

Is every changed answer named? The six Contract rows are each right in direction and in old/new value (the 200 script run, the record-load refusal, the D4 403, the ledger 409, the "no declaration" 404, the flow 503), and rows 4 and 6 are correctly derived by construction from the gate's position. The list is incomplete by three rows of the same class, each a later gate's verdict that the package refusal now precedes for a disabled package's object: (a) the D3 degraded 503 for a standalone declaration the metadata service could not confirm; (b) the headless-type 400 for a declared url / modal / form / api action; (c) the ADR-0104 param-contract 400. Every one is old-refusal-to-new-refusal, so none changes the narrowing arm, the semver level or the migration prescription; the changeset's "Now" bullet ("before any gate reads the action's declaration and before any record load or dispatch") already covers them generically. Recorded in ③ as a PR-body amendment, not a defect. Wrong by omission, in the prose only.

Narrowing. A served answer (200, the handler's result) is refused; nothing that was refused is now served. Clause-②: no (narrowing) on the body and the changeset is the correct arm; nothing in the diff widens. Right.

Are the pins real pins? Yes. actions-disabled-package.test.ts drives the real HttpDispatcher.dispatch() over real kernels (MetadataPlugin + ObjectQLPlugin over better-sqlite3) in both shapes, registers the package through the manifest service during the boot, and disables it through the registry switch the protocol's predicate reads. Each refusal case asserts status and code, the qa_ping handler's call count unchanged, and spies on protocol.getData / protocol.updateData / engine.executeAction never called; the anonymous 401 ordering and the re-enable leg are pinned; the enabled control covers each of the five declared answers on both shapes. The file is under the local vitest project (not in vitest.repo-tests.json), so the Test Core shards on the head ran it; all six shards and the standalone Test Core run are success. The ablation legs in the PR body (12 failed / 12 passed on the base tree and on a swallowing helper) are consistent with the six cases × two shapes. Right.

Check-runs on the head. Every run that ran concluded success (Build Core, Test Core 1–6 and the aggregate, the four Type Check jobs, the ten Lint jobs, Check Changeset, Governed Surface Queue Guard, the three single-writer / card-claim guards, Docs Links, Flag docs, PR Size, Auto Label); the skipped ones (Dogfood shards, Console Pin, Build Docs, Temporal, the two opt-ins) skip on pull requests by design, and the fleet's dogfood_battery comment on this head reports the full battery VERDICT command-exit 0, 289 files passed, 0 failed.

② Semver level

patch for @objectstack/runtime with the BREAKING banner and the ADR-0087 marker not-required (no-migration-prescription) is the level AGENTS.md prescribes: Metadata-protocol: none takes patch, and "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". Metadata-protocol: none is right: no spec key, value, type or default moves. Both declaration lines are line-initial in the body and the changeset, and Check Changeset is success on the head.

The published surface does not move. packages/runtime/src/index.ts re-exports nothing from ./action-execution.js (its only star export is @objectstack/core; every other line is a named export from other modules), and package.json publishes the single . entry, so refuseDisabledPackageObject is unreachable from the package's published entry, as the changeset states. Nothing exported is removed or narrowed, so the "no exported declaration is removed or narrowed" clause of the marker is true. Right.

③ Boundary flags

  1. The object-less finding is a reproducible defect a seat must file under Prime Directive chore: version packages #10, not an acceptance note. The code confirms the PR's measurement without a run: isObjectLessActionKey(objectName) skips the gate for POST /actions/global/NAME and POST /actions//NAME, and resolveRouteActionDeclaration then reads the registry's standalone action item, which isItemOfDisabledPackage hides only inside the protocol's own /meta reads, never in the registry, so a disabled package's object-less script handler still runs 200 while GET /api/v1/meta/action/NAME answers it absent. The directive names the evidence class ("a reproducible defect ... on a public door") and the action ("file an issue ... never bury a defect"). The PR's deferral to "the next card" is the correct scoping for this card (the card's Shape is the object route and the OBJECT_PACKAGE_DISABLED answer, which an object-less action has no object for), so it does not block this head; the dispatching seat files the card (gate ①, with this PR's rig named as the repro and the "no declaration" 404 as the likely owed answer), and the seat should say so on the card thread before merge. Escalated, not blocking.
  2. refuseAmbiguousActionActivation not needed: confirmed. It collects from the metadata service for the activation write door only (step 4 of handleActionActivationWrite); the invocation path never calls it, and the fix reads no collection. Whether that check sees a package object's actions on an environmentId kernel remains the open measurement the PR records; it belongs to the activation door, not this card. Answered.
  3. The activation door untouched: confirmed by position (the _activation return precedes the gate). One observation for the seat, no action owed here: POST /actions/_activation/OBJECT/NAME on a disabled package's object still resolves the declaration from the registry and writes a ledger row; it runs nothing, so it is not a served-code defect, and the card did not ask for it.
  4. PR-body amendment owed (no new head needed): add the three omitted Contract rows from ① (D3 degraded 503, headless-type 400, param-contract 400, each now 404 OBJECT_PACKAGE_DISABLED for a disabled package's object). The changeset's generic "Now" sentence already covers them, so the changeset file stands.
  5. Docs drift advisory: content/docs/permissions/system-context.mdx names handleActionsRequest; the diff changes no system-context behaviour (the gate is not isSystem-conditional, and the page documents the system-context bypass, which this door never had for the package refusal), so no edit is owed; the seat may confirm with a glance.
  6. Branch name: the diff was produced on claude/project-thread-vlqmr4, named by the card's claim comment (6352124313); signed below as the implementation identity.

Implemented-by: claude/project-thread-vlqmr4
Reviewed-by: isolated-reviewer-for-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-22T22:59Z · head 99d375e84c

item reading
Shape 4 files, +342 / −0 (diff read by the seat). action-execution.ts adds refuseDisabledPackageObject, which rethrows only the protocol's own OBJECT_PACKAGE_DISABLED from readExposureDeclaration (the by-name read the data verbs' exposure gate makes; no second copy of the predicate or of the refusal) and lets every other answer pass to the declaration resolution. domains/actions.ts calls it after the _activation return and ahead of D4, the ledger, the "no declaration" 404 and any record load or dispatch, for every key isObjectLessActionKey does not mark. Inside claim 6352124313.
Premise #25296, filed by this seat from PR #25291's contract escalation.
Contract review CONTRACT_REVIEW_TIER PASS on this head, 6353092142 (Clause-②: no (narrowing)).
Dogfood battery 6352923809 on this head: lock VERDICT command-exit 0, 289 files, 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 6353106465: 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 / Unchanged / What to do match the hunks: the gate's position ahead of every declaration-reading gate, the anonymous baseline first, re-enable restoring at once, the object-less arm and the activation door untouched. Metadata-protocol: none holds.
Contract amendment (contract ③.4, carried here) The relay has no PR-body edit, so the seat records the three rows the PR body's Contract table omits, each for an object of a disabled package: 7. a degraded metadata plane (ADR-0110 D3), old 503, new 404 OBJECT_PACKAGE_DISABLED only where the by-name read still answers the refusal (an unreadable plane passes unanswered, so 503 stands there); 8. a type with no headless dispatch, old 400, new 404 OBJECT_PACKAGE_DISABLED; 9. a params-contract violation, old 400, new 404 OBJECT_PACKAGE_DISABLED. The changeset's "Now" sentence already covers them, so the file stands.
Escalations ③.1, a disabled package's object-less action still runs through POST /actions/global/NAME and POST /actions//NAME: filed by this seat as #25308 (Blocked-by: #25296) before the queue. ③.3, the activation door still writes a ledger row for a disabled package's object: runs nothing, noted, not filed.
Accepted deviations Branch claude/project-thread-vlqmr4 (named in claim 6352124313).
Cross-lane touches None. packages/runtime is this lane's.
Landing Not governed (check-governed-merges.mjs --pr 25305, +342 / −0). Ready + auto-merge through the relay in this act. Serial note: PR #25304 touches action-execution.ts in a different function; both merge clean against main, and the queue rebuilds each on the other.

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