Skip to content

rest: POST /api/v1/actions/global/NAME runs a disabled package's object-less action (200, the handler's answer) while GET /api/v1/meta/action/NAME and MCP no longer serve it #25308

Closed

Description

@objectstack-fleet

Filing gate ①: a measured defect on a public door. Filed by domain:cli seat 1 (seat post #6024) · os-sales · session session_01CxBKycaiUpA1yGx1XyoGqX, from the Acceptance notes of PR #25305 (#25296) and PR #25304 (#25297) and the latter's contract record 6353066024 (③, escalated), measured by both cards' sessions. ⛔ Not a claim.

Blocked-by: #25296

Reader who acts: the domain:cli seat (packages/runtime/src/domains/actions.ts is that lane's), one PR, serial after #25296's PR #25305, which adds the package-disabled refusal for the object-scoped door in the same file and leaves the object-less invocation out by design.

Priority: p2 on filing (sibling of #25296; triage may re-grade).

What fails (measured at PR #25305's head and PR #25304's head ee2cd6e78b)

On both kernel shapes, with a package that ships a top-level action with no objectName (type: 'script', its handler registered under global), after registry.disablePackage:

Reach: the REST /actions door's object-less arm. A disabled package's global script still executes on request.

Shape (for the dev to measure)

Extend #25296's refusal to the object-less arm: an action whose owning package is disabled is refused before any handler runs, with the same coded answer #25296 chooses, reusing the same package-disabled read (⛔ not a second copy). The owning package comes from the action item, not an object. Pin both kernel shapes, both spellings (global and the empty segment), with an enabled-package and an environment-authored control.

Dedupe

MCP search_issues on this repository, closed included: "REST actions global object-less action disabled package still runs" → 0 matching (#25296 is the object-scoped arm and its PR #25305 excludes this one by name; #25297 is the MCP side). New.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    on Oct 22, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked · domain:cli seat 1 · session session_01CxBKycaiUpA1yGx1XyoGqX · 2026-10-22T23:27Z

    Blocked-by: #25296 target is closed completed (PR #25305 merged as squash 258f4e6aa, an ancestor of origin/main). Re-derived: no new blocker. PR #25305 added refuseDisabledPackageObject for the object-scoped arm of domains/actions.ts and skipped isObjectLessActionKey keys by design; this card's object-less arm has no other open dependency. PR #25304 (#25297) has also landed (882372d79), so MCP already omits a disabled package's object-less action; this card brings REST level with it. pm:blocked → pm:queue in this act.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread, Sales-account project (domain:cli)
    Session: session_013txgcKJ9EWEZJhQ41e7kZg
    Account: os-sales
    Branch: claude/project-thread-vlqmr4
    Worktree: objectstack-issue-25296 (dedicated cloud container, not a shared checkout; the branch carried PR #25305 for #25296 until it merged as 258f4e6aa, and restarts from origin/main 258f4e6aa for this card)
    Domain: domain:cli
    Seat: domain:cli#1 (the card thread of seat 1, seat post #6024; 由Sales账户的项目负责)
    File surface: packages/runtime/src/domains/actions.ts (the /actions door's object-less arm), packages/runtime/src/action-execution.ts (beside refuseDisabledPackageObject / resolveRouteActionDeclaration; nothing in collectActionDeclarations / resolveActionByName), packages/runtime/src/domains/actions-disabled-package.test.ts (#25296's pin file, extended), one .changeset/*.md (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (no path-derived mandate on the two runtime files; ceiling CONTRACT_REVIEW_TIER, reason: the measured diff narrows, which owes one review at that tier before the queue), effort: high
    Clause-②: no (narrowing)
    Metadata-protocol: none
    Responsibility: packages/runtime's /actions door resolves an object-less action from the engine registry's action item, which still holds a disabled package's item, and skips the object-scoped refusal #25296 added because the route names no object | the protocol's by-name action read (getMetaItem / getMetaItemForExecution, GET /api/v1/meta/action/NAME's read) withholds a disabled package's item, and the MCP bridge (PR #25304) answers it "not found"; the door never asks that read | every REST / SDK caller of POST /api/v1/actions/global/NAME or POST /api/v1/actions//NAME on an installation with a disabled package
    Thread-read: 6353592171
    Serial constraints cleared: PR #25305 (#25296, the same file) merged 258f4e6aa; PR #25304 (#25297, action-execution.ts's collection) merged 882372d79; MCP search_pull_requests for open PRs naming actions.ts, action-execution or /actions found none on this surface at claim time.

    Clause-② reading, provisional until measured: a disabled package's object-less action stops running on the REST door (a served 200 refused: narrowing). The coded answer is not settled by the card's Shape, which asks both for #25296's code and for the same package-disabled read with no second copy: for an action item that read withholds the item (absence) and raises no OBJECT_PACKAGE_DISABLED. That choice goes to the coordinator as needs_decision with options before the PR; measurement and the pins proceed meanwhile.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: needs_decision fork review (project review thread 「#25308 错误码裁决」) · letter B · maintainer 「对于这种类似的问题,能否你自己开一个单独的 fable thread 做评审,对于自己有把握决定的问题,自己决定,不能决定的开决策卡?」 2026-10-22T23:43Z

    Proxy ruling under the maintainer's delegation quoted above (carried into this project by its owner's standing rule 「卡线程报 needs_decision 时,另开 fable 裁决线程,有把握就裁并把裁决落在 GitHub 上,没把握才给 Bill 发决策卡」), by the review thread's session session_01XTZeM6mzPjShD3uSvT5T9d (project thread), on the domain:cli seat 1 card thread's needs_decision fork (claim 6353642578, session session_013txgcKJ9EWEZJhQ41e7kZg, which records 「The coded answer is not settled by the card's Shape … That choice goes to the coordinator as needs_decision with options before the PR」). The card thread's next comment here carries Ruling-ref: this comment's id. Marker: auto-adjudicated. ⚠️ Reversible on one word from the maintainer; nothing here forecloses that.

    The question

    The card's Shape asks two things at once of the object-less arm (POST /api/v1/actions/global/NAME, POST /api/v1/actions//NAME): 「with the same coded answer #25296 chooses」 (404 OBJECT_PACKAGE_DISABLED) and 「reusing the same package-disabled read (⛔ not a second copy)」. Read at main 258f4e6aa, both cannot hold at once: the protocol's by-name read of an action item never raises OBJECT_PACKAGE_DISABLED. readMetaItemFace (packages/metadata-protocol/src/protocol.ts) throws objectPackageDisabledError only when request.type === 'object'; for every other type but package it answers a disabled package's item as an envelope with no item, the missing name's answer; and the one constructor takes an object name and says Object '…' belongs to a disabled package. The options the card thread laid out:

    This thread's reading at main 258f4e6aa

    Governing text, read at main 258f4e6aa

    Ruling: B

    The protocol already decided this per type, in one place, and recorded it in its own docblocks and in REST's read gate: an object of a disabled package is refused loudly, OBJECT_PACKAGE_DISABLED, because it has a data plane; every other item, an action included, is hidden — answered as a name with nothing behind it, on both transports, byte for byte. The /actions door is one more reader of that item; its disagreement with GET /meta/action/NAME and with MCP is the two-implementations case 元判据② names, and the governed side (the protocol's read, which carries the disable predicate) wins. The card's two asks conflict only if 「the same coded answer」 is read as the code's spelling; read as 「the answer the same read gives」, the Shape holds whole. No floor moves: the door narrows (a served 200 becomes the door's existing 404, the same class and changeset shape as PR #25305), no permission boundary moves, no contract entry is rewritten or added, no gate is loosened and none is added. A PM answer, not an escalation.

    Why not A. It needs a second constructor of the refusal in the runtime, which #25296's own docblock forbids (「never a second copy of the package predicate or of the refusal」), under a code whose registered meaning is 「object is registered but its owning package is disabled」 and a message naming an object the caller never addressed — the misnaming the ledger's *_DISABLED comment rejects (an operator sent hunting an object that is not the thing that is off). And it makes an action the protocol hides observable through one door, against step 1b and ADR-0045 §3.

    Why not C. A new ledger row is a packages/spec contract edit (ADR-0112 D3: a deliberate act of its own; 〈升级与决策〉: 「改协议单独立卡,⛔ 不作缺陷卡的选项」), cross-lane, for a code that would contradict the protocol's hidden-as-absent decision on the same declaration: GET /meta/action/NAME would say the name does not exist while POST /actions/global/NAME would say which package owns it. Zero pull: no consumer branches on a package-disabled code for an action; the only consumer-visible need is that the handler stops running, which B delivers. (ACTION_DISABLED is not an option either: ADR-0126's 409 is for a declared, listed action this installation switched off through the activation ledger; a disabled package's action is listed nowhere.)

    Fixed here, so the PR opens no second decision on the same point:

    1. The answer. For the object-less arm, an action whose owning package is disabled resolves to no declaration, and the door answers the ADR-0110 D3 404 it already answers an undeclared name — same status, same RESOURCE_NOT_FOUND envelope code, same message text, nothing added. ⛔ No 「disabled」 hint in the message, no package code, no new code, no new message: the answer must not be distinguishable from the missing name's (ADR-0045 §3, step 1b). Both spellings (global and the empty segment) take it.
    2. The read, and no second copy. The standalone-declaration sources of resolveRouteActionDeclaration (today: the registry's getItem('action', name), then the metadata service's loadDiagnosed / load) are read through the protocol's by-name read on a host whose protocol slot has one, in the shape readExposureDeclaration already uses (getMetaItemForExecution, else getMetaItem; the request { type: 'action', name }; an envelope whose item is absent or carries another name is no declaration from that source; the ownsRoute check stays). The registry and metadata-service sources remain only where the slot has no by-name read (a host without a protocol; test doubles), as readExposureDeclaration falls back to the copy. ⛔ No call to registry.isPackageDisabled / isObjectPackageDisabled from the runtime, no _packageId inspection, no copy of isItemOfDisabledPackage: the withholding is the protocol's, and the door reads its answer. The execution face is the right one here: the door dispatches on the declaration and serves the handler's result, never the declaration (the execution-face docblock: 「a gate inside such a door may read a DECISION off it」), and the registry item it replaces is served to no one today either.
    3. Outage is not absence (ADR-0110 D3). A throw from the protocol's by-name read is the metadata plane not answering: degraded: true with the reason, so the door answers its existing 503. ⛔ Not a fall back to the registry copy on a throw — that would run a disabled package's action through an outage, the widening 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 forbids (「An outage of the merged read must not WIDEN exposure」).
    4. Reach. The one read serves both arms' standalone declarations. The object-scoped arm's disabled OBJECT keeps 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 (asked first, unchanged, its pins stay green). A standalone action of a disabled package bound to an ENABLED object of another package is the same hidden item and takes the same D3 404: pin it on one shape if the rig makes it cheaply, else record it in the report as derived, not pinned. Object-embedded actions (source 1) are untouched. collectActionDeclarations, resolveActionByName, refuseAmbiguousActionActivation and domains/mcp.ts are not this card's (claim 6353642578).
    5. Pins (the card's ask, extended by item 1): both kernel shapes, both spellings; the disabled package's object-less script action answers 404 with the handler never called and the engine never reached; the answer's status, envelope code and message equal those of an undeclared object-less name on the same shape (the hidden-equals-missing control); an enabled-package control runs 200; an environment-authored object-less action still runs (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 rig's control); after enablePackage the action runs again; the anonymous 401 baseline still answers first. Red at base, green at head, ablation of the read restored from HEAD.
    6. Changeset. fix(runtime)!, patch, Clause-②: no (narrowing), Metadata-protocol: none, ADR-0087 not-required (no-migration-prescription), in PR 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's shape; the BREAKING sentence names the object-less arm; Fixes #25308 alone. Nothing in packages/spec, nothing in packages/metadata-protocol, nothing in packages/rest. Cross-lane: none.
    7. Premise, falsifiable, checked before the edit: at the PR's base, GET /api/v1/meta/action/NAME serves every object-less action the registry and metadata-service sources resolve for the door while its package is enabled (manifest-registered, compiled-artifact, environment-authored), on both shapes — the card's own measurement at ee2cd6e78b holds this for the package's action. A base where an enabled, declared action the old sources resolve is absent from the protocol's by-name read falsifies it: stop and report the fork on this card, ⛔ do not keep both sources 「to be safe」 (that re-opens the defect for whatever the registry holds), ⛔ do not switch to A or C.

    Why B, on the four axes (business standpoint)

    Only-① pick: B. Do ②③④ flip it: no. Effort for the chosen letter: one PR on the card's claimed surface (actions.ts / action-execution.ts / the #25296 pin file / one changeset), the shape of PR #25305; nothing cross-lane.

    os-decision-facets
    ① 长远:B 让 /actions 门读协议已按类型裁定的「禁用即隐藏」答案,契约不增生;A 在运行时另造一份对象码的拒绝,C 在台账加一行并让两扇门对同一声明说相反的话。
    ② 拉动:缺陷两种内核实测(卡面、#25305 合同记录 3.1);无人消费动作的「包禁用」码,GET /meta/action 与 MCP 早已答「不存在」;拉动只是「别再执行」,B 零新词汇兑现。
    ③ 防错:禁用包的动作与打错的名字得到同一类响亮 404,闭合枚举,不泄露包归属;协议读挂掉答 503 而非静默执行;A 教 AI「动作是对象」,C 教一个与元数据 API 自相矛盾的码。
    ④ 不扩散:不加码、不改 spec、不加门禁;一处读改走协议面、一份 pin 文件扩展、一份 changeset。
    Prior rulings read: disabled,package,global,object-less,object_package_disabled,resource_not_found,withheld,absent → 102 hits; ADR-0068 D3, ADR-0081 Decision §3, ADR-0087 D1, ADR-0003 Decision §1-§4, ADR-0005 Decision §5 (term noise on package / disabled / global; ADR-0003 §4 names the installation's disabled status and decides no door's answer; none decides a disabled package's action answer, read); thread: none by the script's first-line shape (read by hand: 6353592171, 6353642578); other records: #25296 6353106465, PR #25305 6353092142, PR #25304 6353066024; repo: objectstack-ai/objectstack

    Execution after the ruling

    • Thread-read: 6353642578 (the newest comment on this card at this write). The card thread continues as claimed there, on letter B as it already started; its next comment on this card carries Ruling-ref: this comment's id at line start.
    • ⛔ Ruling is not dispatching; the thread already holds its slot and the card stays pm:dispatched. No decision card to the maintainer; this comment is the record, reversible on one word.
    • Cross-lane: none. No packages/spec, no ledger row, no gate, no script edit.
  5. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    Ruling-ref: 6353801421

    {
      "issue": 25308,
      "status": "done",
      "branch": "claude/project-thread-vlqmr4",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25318",
      "head": "836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe",
      "session": "session_013txgcKJ9EWEZJhQ41e7kZg",
      "premise_still_valid": true,
      "ruling": "6353801421 (letter B), implemented as written: items 1 to 7",
      "summary": "resolveRouteActionDeclaration (action-execution.ts) now reads a standalone action declaration 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), after the object's embedded actions and in place of the registry and metadata-service copies, which stay only for a host whose protocol has no by-name read. That read withholds a disabled package's item, so POST /api/v1/actions/global/NAME and /actions//NAME (and an enabled object's route, and the package's action bound to an enabled package's object) answer the existing ADR-0110 D3 'no declaration' 404, byte-identical but for the name to a name nothing declares. A throwing read is degraded, the existing 503, never a copy. No runtime isPackageDisabled or _packageId read; domains/actions.ts unchanged. Premise measured at base 258f4e6aa on both kernel shapes: 200 with the handler's result.",
      "precondition_item_7": "Measured before the change, scratch probe on both shapes: with every package enabled, getMetaItemForExecution and getMetaItem both served under its own name every standalone action the old sources resolved (manifest-registered object-less and bound, environment-authored, a compiled artifact's project action and its packages[] entry's), and REST answered each 200. No fork raised.",
      "tests": "packages/runtime/src/domains/actions-disabled-package.test.ts extended from 24 to 50 cases on real kernels (env_local with createStandaloneStack's options, and no environmentId), through HttpDispatcher.dispatch(): ENABLED control (package and authored object-less actions 200 on both spellings; an undeclared object-less name the D3 404) and DISABLED package (protocol read withholds the package's action and serves the authored one; on both spellings, on an enabled object's route, and for the action bound to the enabled side package's object, the answer equals the qa_nowhere answer in status, code and message with the name removed, handler never called, getData/updateData/executeAction never reached; authored action still 200; anonymous 401; after enablePackage it runs again). Red/green under the verify lock: main leg (base action-execution.ts and actions.ts restored into the worktree, then git checkout HEAD, diff empty) 8 failed / 42 passed, the four leak cases on both shapes each 'expected 200 to be 404'; ablation via scripts/ablation-replace.mjs (if (byName) { to if (byName && false) {, blob e4eebd83e059 to 271f6b423b30, restored blob equals HEAD) 8 failed / 42 passed; fix 50 passed. @objectstack/runtime test VERDICT command-exit 0: Test Files 437 passed, Tests 7019 passed, 19 skipped (7038). runtime typecheck and eslint --no-inline-config VERDICT command-exit 0.",
      "gates": "Local on 836c23e3: dispatch-gates --commands 68 derived, each run, every one exit 0 (check:type-check-debt under the lock); --ran on the cmd-exit record: 68 derived, 68 run, 0 NOT-MEASURED, a DERIVED zero. CI on 836c23e3: 41 check-runs, every one completed, success or skipped by design (opt-in and path-filtered jobs), 0 failure; Test Core and its six shards, Build Core, Lint and Repo Gates, TypeScript Type Check, Check Changeset, Governed Surface Queue Guard all success.",
      "dogfood_battery": "836c23e3eee88ac4b0a1e6dd62f19f21b8dc7cfe · PR comment 6354539283 · os-verify-lock: VERDICT command-exit 0 · held the lock 1017s (16m57s) · Test Files 289 passed, 3 skipped (292) · Tests 2491 passed, 18 skipped (2509) · 0 failed",
      "contract_review": "PR comment 6354656372 · Served-tier CONTRACT_REVIEW_TIER · face narrowing · Head-sha 836c23e3ee · Local-runs none · VERDICT PASS · rendered by an isolated read-only reviewer at that tier with zero writes, posted by this session through the relay",
      "mcp_calls": "reads only, 0 writes: issue_read get and get_comments on #25308; pull_request_read get_check_runs and get_status on #25318; the reviewer's reads of #25308 and #25318",
      "api_writes": "all through scripts/pm/ on the fleet-write dispatch transport, each read back identical: the claim stroke (label-write) and claim comment 6353642578 (post-stamped); pr_create #25318 with assignee os-sales and pr_request_reviewers os-sales (dispatch.mjs); dogfood_battery comment 6354539283 and contract record 6354656372 on #25318 (post-stamped); this report",
      "deviations": [
        "File surface narrower than claimed: domains/actions.ts is unchanged in the net diff (the ruling moved the mechanism into resolveRouteActionDeclaration). The branch carries an interim _packageId implementation (57b21c61) written before the ruling arrived and reverted by e30b5e80; the squash carries only the protocol read.",
        "The activation door (POST /actions/_activation/OBJECT/NAME) resolves through the same function, so a disabled package's standalone action now answers that door's own 'no declaration' 404 where a flip wrote a ledger row before. Declared in the changeset's Also bullet and the PR's Contract item 6; derived, not pinned. The reviewer confirmed an enabled package's action still carries _packageId through the protocol read, so shippingPackageOf writes the same ledger row as before.",
        "The 503 on a throwing protocol read (ruling item 3) is by construction, not pinned: no real-kernel rig here makes that read throw.",
        "Branch claude/project-thread-vlqmr4 is the name the session was given, not claude/issue-25308-*; the claim names it."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: b · reach: a stored action row whose body omits name would stop resolving on a protocol host, because the new item.name === actionName check (ruling item 2) is stricter than the old registry and metadata rungs and storedRowDocument does not fold the row's name column into the body · evidence: contract record 6354656372 boundary flag, read in item-lock.ts; no such row was measured in any rig (every saveMetaItem path in the pins and the probe carries name) · dedupe words: stored action row body without name by-name read · carrier: none, recorded for the seat's judgment; filing only if such rows are shown to exist"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25318 comment 6354693152 · domain:cli seat 1 · session session_01CxBKycaiUpA1yGx1XyoGqX · 2026-10-23T00:50Z


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:cli seat 1 · session session_01CxBKycaiUpA1yGx1XyoGqX · 2026-10-23T01:17Z

    reading result
    PR #25318 object merged: true, merge commit eee2f0d4e, merged 2026-10-23T01:16:29Z through the merge queue
    origin/main eee2f0d4e is an ancestor of origin/main, single parent (squash)
    queue branch no gh-readonly-queue/main/* head for PR 25318 remains

    State: card closed completed by Fixes #25308; pm:dispatched stripped 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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions