Skip to content

runtime(dispatcher): on the standalone server an anonymous non-POST to /api/v1/actions gets the adapter's 405 instead of the v18 401 floor — registerActionRoutes mounts POST only (the #23909 / #24024 gap, for /actions) #24228

Closed

Description

@objectstack-fleet

Filing gate ①: a measured defect at a public door. reach: the standalone server's own composition: HonoServerPlugin + createDispatcherPlugin over a kernel, a test auth service, a real socket, on the e614b2adb build. There, an anonymous GET or PUT /api/v1/actions/task/close is answered 405 METHOD_NOT_ALLOWED + Allow: POST by the HTTP adapter, with CORS on (the default) and off. The createHonoApp host answers the same request 401 UNAUTHENTICATED. A stock showcase boot is NOT MEASURED.

Filed by the domain:cli seat 2 (#22648) · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb, from #24023's dev report on PR #24227 (out-of-scope findings 1 and 2). ⛔ Not a claim; the lane and grade are triage's.

Reader who acts: the domain:cli seat (packages/runtime/src/dispatcher-plugin.ts, and packages/plugins/plugin-hono-server's unmatched-request answer), once triage grades it.

Contract

  • ADR-0138 D2: "Everything else answers 401 (UNAUTHENTICATED). The decision is taken once per domain, as the domain's first statement, never surface by surface."
  • packages/runtime/src/domains/actions.ts says of its own anonymous baseline: "Deliberately the FIRST statement, ahead of the 405: an anonymous caller learns the auth baseline and nothing about the route's shape." On the standalone server that statement never runs for a non-POST verb.

Where (origin/main b9ac35cbf, read by this seat)

Second member (the same mount family, measured by the same dev)

Done when

  • On the standalone server, an anonymous non-POST request to /api/v1/actions/... is answered 401 UNAUTHENTICATED with no Allow, as on a createHonoApp host. A signed-in one is still answered 405 + Allow: POST (PR fix(runtime): the keys and actions domains answer their 405 with an Allow header naming POST #24227's pin).
  • A route whose extra verbs exist only to refuse does not list them as supported in an Allow header, or the IHttpServer contract line is reconciled with RFC 9110. That second option touches packages/spec, so it would be the spec seat's.
  • Pins cover both, on a real socket.

Dedupe

MCP search_issues on this repository, closed included, at this filing:

None is /actions or the Allow content.

Dedupe words: actions anonymous 405 standalone; registerActionRoutes POST only every verb; Allow lists refusal-only mounts; OPTIONS keys 405 Allow every verb.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · target:v18 · area:access · domain:cli · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-17T23:52Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-18T00:29Z
    Session: session_01QyoWKK9RwUNxBxmevy9Neb
    Account: marchtian (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24228-actions-anonymous-floor
    Worktree: objectstack-issue-24228
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main 5a9a80bd2; line numbers move; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24228,
      "status": "done",
      "branch": "claude/issue-24228-actions-anonymous-floor",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24244",
      "session": "session_01QyoWKK9RwUNxBxmevy9Neb",
      "premise_still_valid": true,
      "summary": "Both members are fixed in packages/runtime/src/dispatcher-plugin.ts alone, with no spec or adapter change. The dispatcher now declares the paths whose domain decides every verb (the three POST /actions patterns on both bases, POST /keys, the three /notifications faces, GET /mcp/skill), and its existing setFallbackHandler seam hands every request the router cannot route on such a path to dispatch(), the way createHonoApp's catch-all does. Membership is the transport router's verdict through IHttpServer.resolveMountedRoute; there is no second matcher. So an anonymous non-POST to /api/v1/actions/... gets the domain's 401 with no Allow, a signed-in one gets the domain's 405 + Allow: POST, and the adapter never builds an Allow from refusal-only mounts on those paths. The PM's suggested route (refusal-only /actions mounts) was measured and rejected: OPTIONS with CORS off, and TRACE/PROPFIND even with CORS on, still reach the adapter's 405, whose Allow would have widened from POST to all six verbs. H2: the adapter cannot tell a refusal-only mount apart through the (path, handler) registration signature, but it does not need to. Residual outside this claim's surface: plugin-sharing's /share-links/* still lists refusal-only verbs (out_of_scope_findings 1, open_questions 1).",
      "tests": "All at final head 6c8625968 unless noted. The implementation blob 24648a251fc8 is the same at d60989609, 194b25dc2 and 6c8625968. (1) Full suite: pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 gave Test Files 413 passed (413), Tests 6411 passed | 19 skipped (6430). (2) pnpm --filter @objectstack/runtime typecheck: exit 0, check:test-typecheck OK, ledger unchanged; all 6 touched test files are in the tsconfig.test.json program (--listFiles). (3) Targeted, at 194b25dc2 (same runtime tree for the touched files): 10 files incl. the new actions-every-verb.hono.integration.test.ts (79 cases), keys/notifications every-verb, mcp-skill, PR 24227's control, fallback-absence-warn, routes, share-links-every-path, endpoint-fallback, streaming-fallback gave Test Files 10 passed (10), Tests 234 passed (234). (4) Reach measurement, raw node:http on a real socket, 4 compositions (CORS off/on x scoping off/on), 14 paths x 9 verbs x anonymous/signed-in = 1008 requests at base 2bcd406a2 and at head d60989609: 368 rows changed, every one from the adapter's 405 to the domain's answer (148 to 401, 172 to the domain's 405, 30 to the dispatcher's 404, 18 HEAD 405 to 401), and no served answer moved. createHonoApp parity measured in process (app.request, PROPFIND for TRACE). (5) Ablations, fix committed first, node scripts/ablation-replace.mjs WRAP plus an EXIT/INT/TERM trap restoring from HEAD; the pins import ./dispatcher-plugin.js from src, so no dist of the mutated module is on the path. Member 1: the 3 /actions declarations removed (anchor 1 to 0, blob 24648a251fc8 to 4bcfc24a0ac7) gave Tests 72 failed | 27 passed (99) (expected 405 to be 401, expected undefined to be 405); the keys pin and the 24227 control stayed green. Restore: blob == HEAD, git diff HEAD empty, marker 0. Member 2: the keys/notifications/mcp-skill declarations removed (3 nested anchors, each 1 to 0) gave Tests 20 failed | 124 passed (144) (expected 'DELETE, GET, HEAD, PATCH, POST, PUT' to be 'POST'; expected 'DELETE, GET, HEAD, POST' to be 'GET'; 405 to be 401/404); the actions pin stayed green. Restore proven at all 3 levels. (6) eslint --no-inline-config over the 7 changed .ts files: exit 0. test:repo not run locally: no file of that project changed, and it is declared to CI (Test Core).",
      "gates": "Local, at final head 6c8625968: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 commands. Each was run with its exit captured before any pipe and recorded as 'COMMAND :: exit N'. --ran prints '62 derived, 61 run, 1 NOT-MEASURED, 0 UNRUN' (exit 0). All 61 that ran exit 0, including check:route-envelope, check:nul-bytes, check:test-source-alias, check:type-check-coverage and check:type-check-debt. NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET (39 packages have no dist in this worktree); this diff changes no exports, package.json or build config, and CI's Lint & Repo Gates runs it. Not run: check-platform-checklist (no export renamed or removed); check:dev-prereqs and check:skill-frame-freshness (no packages/spec or frame change). Lane blind spot: pnpm exec eslint --no-inline-config over the 7 changed .ts files gave exit 0. CI on head 6c8625968 at PR open, read once: 23 check-runs. 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, the other 21 in_progress (Lint * 7 lanes, Type Check * 4 lanes, Check Changeset, Governed Surface Queue Guard, Check PR Size, card/claim guards, docs). Test Core / Build Core had not appeared yet. Not polled further.",
      "mcp_calls": "0",
      "api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24244/assignees [marchtian], read back byte-identical (15829 bytes); (2) comment, i.e. POST /repos/objectstack-ai/objectstack/issues/24228/comments (this report). Plus git push (not REST). No labels were written: the dispatch named none, and skip-changeset does not apply because a changeset ships.",
      "line_budget": "n/a, no skills/** path touched",
      "files_changed": [
        ".changeset/24228-actions-every-verb-anonymous-floor.md",
        "packages/runtime/src/dispatcher-plugin.ts",
        "packages/runtime/src/actions-every-verb.hono.integration.test.ts",
        "packages/runtime/src/keys-every-verb.hono.integration.test.ts",
        "packages/runtime/src/notifications-every-verb.hono.integration.test.ts",
        "packages/runtime/src/mcp-skill-method-not-allowed.hono.integration.test.ts",
        "packages/runtime/src/dispatcher-plugin.fallback-absence-warn.test.ts",
        "packages/runtime/src/domains/keys-actions-method-not-allowed-allow.test.ts"
      ],
      "members_measured": [
        "/api/v1/actions (3 shapes x unscoped and /environments/:id) - served POST - base: Allow 'POST' from the adapter on every non-POST verb, anonymous too (the 401 floor missing) - head: anonymous 401 with no Allow; signed-in domain 405 with Allow 'POST'; anonymous OPTIONS (CORS off) domain 405 with Allow 'POST' (preflight exemption, same as createHonoApp)",
        "/api/v1/keys - served POST - base (OPTIONS CORS off; TRACE/PROPFIND any CORS): 'DELETE, GET, HEAD, PATCH, POST, PUT' - head: anonymous 401 with no Allow; signed-in domain 'POST'",
        "/api/v1/notifications - served GET - base: 'DELETE, GET, HEAD, PATCH, POST, PUT' - head: no Allow (anonymous 401, signed-in 404 ROUTE_NOT_FOUND)",
        "/api/v1/notifications/read - served POST - base: same six - head: no Allow (401 / 404)",
        "/api/v1/notifications/read/all - served POST - base: same six - head: no Allow (401 / 404)",
        "/api/v1/mcp/skill - served GET - base (PUT, PATCH, OPTIONS CORS off, TRACE, PROPFIND): 'DELETE, GET, HEAD, POST' - head: domain 405 with 'GET'",
        "/api/v1/share-links (root) - served GET, POST - base and head: 'GET, HEAD, POST' - measured-clean, unchanged",
        "/api/v1/share-links/abc - served DELETE - base and head: 'DELETE, GET, HEAD, PATCH, POST, PUT' - NOT fixed (plugin-sharing family, outside the claim's file surface)",
        "/api/v1/share-links/abc/resolve - served GET - base and head: same six - NOT fixed (same reason)"
      ],
      "deviations": [
        "Route: the PM's suggested refusal-only GET/PUT/PATCH/DELETE mounts on /actions were not used. Measured: they would widen Allow on OPTIONS (CORS off) and TRACE/extension verbs (any CORS) from 'POST' to six verbs. The dispatcher's existing fallback seam is used instead, so no new route, no route-ledger row and no census move.",
        "PR 24227's control pin (domains/keys-actions-method-not-allowed-allow.test.ts): assertions untouched and green, but its docblock paragraph and one test title said the standalone /actions 405 is the adapter's, which this change makes false, so the wording was corrected. Flagged because H4 said 'unchanged'.",
        "mcp-skill-method-not-allowed.hono.integration.test.ts: its 'PUT falls through to the adapter (boundary)' case pinned the defect (Allow ['DELETE','GET','HEAD','POST']) and was replaced by PUT/PATCH/OPTIONS/TRACE domain pins.",
        "Runtime strings: one new boot warn (transport with setFallbackHandler but no resolveMountedRoute) and one clause appended to the existing no-setFallbackHandler warn. The regex pins on the existing warn still hold.",
        "The worktree was cut from origin/main 2bcd406a2 (main had moved past the dispatch base 5a9a80bd2; dispatcher-plugin.ts and the adapter are identical across that range), and origin/main 7be3cc728 was merged before the PR (merge 6c8625968, model-free trailers); install and build were refreshed after it.",
        "The first full-suite run (at 194b25dc2) spelled 'pnpm --filter @objectstack/runtime test -- --maxWorkers=2'. The bare '--' may have dropped the flag; the whole package ran (413 files). The final-head run used 'exec vitest run --project local --maxWorkers=2'.",
        "The first gate pass was cut by my own 590 s timeout after 51 commands (check:query-options-erasure interrupted, not recorded). The remaining 11 ran under nohup + tail --pid; each command is recorded exactly once.",
        "Labels: none written (none named; skip-changeset not applicable)."
      ],
      "open_questions": [
        {
          "question": "/share-links/* (plugin-sharing's family on the standalone server) still answers OPTIONS (CORS off) and TRACE/extension verbs (any CORS) with the adapter's 405, Allow 'DELETE, GET, HEAD, PATCH, POST, PUT' on /share-links/abc (served: DELETE) and /share-links/abc/resolve (served: GET). This PR's mechanism cannot reach it: the fallback seam is one handler and the dispatcher's, and a dispatcher answer there would make a second owner. The adapter cannot tell refusal-only mounts apart through IHttpServer's (path, handler) registration (measured). The PR body says 'Fixes #24228' as dispatched. Where does this residual go?",
          "options": [
            "A: keep 'Fixes #24228' (the triage-bound scope is /actions plus the dispatcher's /keys, /notifications and /mcp/skill), and file /share-links as its own member card in the plugin-sharing lane. Cost: one more card. Its route needs a seat ruling: a declared cross-plugin 'domain decides every verb' registry that the dispatcher's fallback consults (runtime + plugin-sharing; if it becomes a service contract, it is a spec contract), or the IHttpServer route (a registration marker or reconciling the Allow contract line with RFC 9110; domain:spec).",
            "B: rewrite the PR body to 'Part of #24228' (seat write) and keep this card open for /share-links. Cost: the card stays open on a member whose owner and route differ from this PR's.",
            "C: accept the residual as reachable only by exotic verbs or CORS off. Cost: Done-when line 2 is left unmet for one family."
          ],
          "recommendation": "A, because the residual has a different owner (plugin-sharing) and needs a different mechanism, which has to come from a ruling or from domain:spec. Folding it into this card would keep a closed-out fix open on a design question."
        }
      ],
      "out_of_scope_findings": [
        "class: a, reach: public door HTTP on the standalone server. OPTIONS /api/v1/share-links/abc with CORS off, and TRACE/PROPFIND with CORS on, answer 405 with Allow 'DELETE, GET, HEAD, PATCH, POST, PUT' while the route serves DELETE; /share-links/abc/resolve is the same while it serves GET. Measured at base 2bcd406a2 and head on a real socket (HonoServerPlugin + createDispatcherPlugin + registerShareLinkRoutes at kernel:ready). Seam: renderer: none, runtime: packages/plugins/plugin-sharing/src/share-link-routes.ts mountFamilyRoute catch-all, read by plugin-hono-server allowedMethodsForPath. Same family as this card: named here for the family's closing card per open_questions[0], not filed as a single-point card. Dedupe words: share-links Allow refusal-only; OPTIONS share-links 405 six verbs; share-link catch-all Allow lists every verb",
        "class: a, reach: public door HTTP on a createHonoApp host. HEAD /api/v1/mcp/skill answers 405 with Allow 'GET' while GET /api/v1/mcp/skill is served: the catch-all hands dispatch() the raw method 'HEAD', and handleMcpSkillRequest refuses every non-GET. Measured in process through createHonoApp's app.request (the host's HTTP pipeline), not a socket. The standalone server answers HEAD from the GET route. Seam: runtime: packages/runtime/src/domains/mcp.ts handleMcpSkillRequest method check, consumer: none. Dedupe words: HEAD mcp skill 405; createHonoApp HEAD domain non-GET; mcp skill HEAD Allow GET",
        "carrier: none - noted in the PR's Acceptance notes, not filed. On the /share-links root, the adapter's matchesRoutePattern does not count the '/share-links/*' pattern while the Hono router routes PUT /share-links to it, so allowedMethodsForPath and the router disagree. The Allow happens to be accurate there (GET, HEAD, POST); this is a read-only observation.",
        "carrier: none - noted, not filed. The signed-in non-face /notifications 404 is spelled ROUTE_NOT_FOUND on the standalone server and RESOURCE_NOT_FOUND on createHonoApp; this predates the PR and is already recorded in #24024's notes."
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24244 comment 6244569658 · domain:cli seat 2 · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-18T02:08Z · open question 1 decided A (/share-links filed as #24246; HEAD /mcp/skill filed as #24245); landing waits on every check of head 6c8625968.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24244 → cbfa5e433 (#24228, Fixes #24228) · domain:cli seat 2 · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-18T02:48Z

    Reading Result
    PR #24244 object merged: true, merge commit cbfa5e433, merged 2026-10-18T02:47Z through the merge queue (enqueued 2026-10-18T02:28Z)
    origin/main cbfa5e433 is an ancestor of origin/main, with a single parent (squash). The 8 files and +559 / −29 match what was reviewed at 6c8625968
    queue branch no gh-readonly-queue/main/*pr-24244* head remains

    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:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions