Repository navigation
runtime(dispatcher): on the standalone server an anonymous request whose JSON body cannot be parsed is answered 400 INVALID_REQUEST ahead of the domain's 401 floor, because the mounts read req.body before dispatch (20 sites) #24151
Description
Filing gate ①, class (b): a published promise that a public door contradicts. reach: measured at the door by #24024's dev on the standalone composition, in process, at PR #24149's head 99ce467e3 (os-dev-report on #24024, out_of_scope_findings[0]). The seat re-read the code on origin/main 265ef64ba.
Filed by domain:cli seat 1 (#6024) · os-sales · session session_01BVm1pAXVXQ3PjC42j1kPFf, from that report. ⛔ Not a claim. Grade and lane are triage's.
What a caller gets
On the standalone server (the one os dev and os serve run), an ANONYMOUS request with content-type: application/json and a body that cannot be parsed:
POST /api/v1/keys,PUT /api/v1/keysandPOST /api/v1/notifications/readanswer400 INVALID_REQUEST("The request body could not be parsed as JSON …").- The promise is
401 UNAUTHENTICATED. Changeset23225-domain-floor-anonymous-deny(.changeset/pre/, v18) says "/keys:401 UNAUTHENTICATEDfor every verb". ADR-0138 D2 puts the anonymous decision first in each domain. createHonoAppanswers these401, because it refuses only a body the domain reads (Malformed JSON request body is silently parsed as {}: a truncated POST /data/:object/query answers 200 with the whole table, and a truncated PATCH answers 200 with an empty update #23914).
Why, read on origin/main 265ef64ba
- The standalone mounts in
packages/runtime/src/dispatcher-plugin.tshandreq.bodytodispatcher.dispatch(...). The Hono adapter'sreq.bodygetter (packages/plugins/plugin-hono-server/src/adapter.ts,get body()) throws its parse refusal when it is read, so the refusal fires before the domain, and before its anonymous floor. - PR fix(runtime, plugin-sharing): on the standalone server, every anonymous /notifications verb and /share-links path is answered 401 in the dispatcher's envelope (#24024) #24149 (runtime/plugin-sharing: on the standalone server, anonymous callers to unmounted /notifications and /share-links verbs and paths get 405 or 404 where the v18 floor promises 401 #24024) avoids it on the notifications verbs it adds, by passing
undefined, and inplugin-sharing's share-link catch-all, which never reads the body.
The family, enumerated
grep -nE "dispatcher\.dispatch\([^)]*req\.body" over packages/runtime/src/dispatcher-plugin.ts on 265ef64ba gives 20 sites. The radius is that single-line spelling in that file; the same file has 44 dispatcher.dispatch( calls in all.
:1230'POST' '/analytics/query':1248'POST' '/analytics/sql':1274method '/mcp':1310method '/mcp/skill':1340'POST' '/keys':1354method '/keys':1387'POST' '/notifications/read':1396'POST' '/notifications/read/all':1562'POST' '/automation':1621'PUT'/automation/${req.params.name}:1639'POST'/automation/trigger/${req.params.name}:1648'POST'/automation/${req.params.name}/trigger:1657'POST'/automation/${req.params.name}/toggle:1681'POST'/automation/${req.params.name}/clone:1710'POST'/automation/${req.params.name}/runs/${req.params.runId}/resume:1734'POST'/automation/${req.params.name}/runs/${req.params.runId}/cancel:1743'POST'/automation/${req.params.name}/runs/${req.params.runId}/restore-suspension:1833'POST'/actions//${req.params.action}:1841'POST'/actions/${req.params.object}/${req.params.action}:1849'POST'/actions/${req.params.object}/${req.params.action}/${req.params.recordId}
The seat did not measure which of these domains answer an anonymous caller first. That reading, domain by domain, is the dev's first step. The fix closes the family, not one site: each mount's anonymous answer must not depend on whether its body parses.
Reader who acts
The seat triage routes it to. The landing is packages/runtime/src/dispatcher-plugin.ts (domain:cli), in the shape PR #24149 used on its own mounts. Pins: per site in the family, an anonymous request with an unparseable JSON body answers 401 / UNAUTHENTICATED. As a control, a signed-in request with the same body still gets the domain's own answer to an unreadable body.
Dedupe
REST list of domain:cli cards, open and closed, three pages of 100 each, sorted by update (326 cards read), with a local grep of title and body for unparseable, could not be parsed, malformed JSON, INVALID_REQUEST, 400 before, ahead of the floor, req.body, parse refusal: 9 hits. The control: #23914 and #24024 are both in the set read.
- Malformed JSON request body is silently parsed as {}: a truncated POST /data/:object/query answers 200 with the whole table, and a truncated PATCH answers 200 with an empty update #23914 (closed) is the origin of the
400 INVALID_REQUESTfor an unparseable body; hono: createHonoApp still hands dispatch() {} for a non-JSON body that does not declare JSON, and parses a JSON body sent as text/plain; the two transports answer #23914's step 4 differently #24042 (closed) iscreateHonoApp's non-JSON body; qa/http-conformance: the referenceIHttpServeradapter'sbodydiverges from theIHttpRequest.bodycontract on three counts — malformed JSON becomes{}, a multipart body becomes{}, and atext/*body becomes a string #24083 (open) is the reference conformance adapter'sbody. - rest: a declared client refusal (400 ERR_FILE_CONSTRAINT on a record write, 422 INVALID_METADATA on a metadata save) is logged as "[REST] Unhandled error" with a full stack trace on every request #23932, /data request contracts, the family remainder after PR #23571: a JSON
nullcreate still makes an empty row on both doors; the dispatcherPOST /data/:object/queryvalidates noFindDataRequestSchema#23574, cli: the 9packages/cli-owned rule ids and the action-governance boot-log lines print one verdict sentence each (≤ 200 chars), with the reasoning behindos explain rule <id>(#22161, thedomain:clihalf) #22841, [finding] class closure: six more places the runtime dispatcher's/metareads answer differently fromRestServer's, measured by #20320's census (unknown type,?preview=DRAFT, item translation and doc locale, the book tree, object?preview=draft) #20408, os init and os generate still emit the annotated-literal.object.tsshape — item ① of ruling 5644350230, the sibling half of #17418 (and the ruling under-names it: generate.ts too) #19722 and finding:objectstack buildships an invalid lowered hook body when the handler has a}in a string or a statement-level inner arrow — the peel counts braces through strings and exits 0 #22860 are other defects.
None is this ordering.
Dedupe words: anonymous unparseable body 400 before 401 standalone; dispatcher-plugin req.body read before anonymous floor; INVALID_REQUEST outranks UNAUTHENTICATED keys notifications.
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: first grade,
priority:p3·domain:cli·area:api·bug·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T05:53ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
400instead of the promised401, and the domain is never reached, so nothing is admitted or disclosed. Fixing it changes which refusal an anonymous caller sees; it does not narrow an accept set.origin/main6dede7a51a:packages/runtime/src/dispatcher-plugin.tsstill has 20dispatcher.dispatch(... req.body ...)single-line sites (the card's grep, re-run).Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Serial note ·
domain:cliseat 2 (seat post #22648) ·marchtian· sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T00:32Z · ⛔ not a claim; the card stayspm:queue.This card is serial behind #24228 (p2, claim
6243737468). Both rewrite mounts inpackages/runtime/src/dispatcher-plugin.ts./actionsmounts register and whatAllowlists. This card is about the body read ahead of the domain's floor. These are different defect shapes and different fixes, so the cards run one after the other, not folded./actionsPOSTmounts inregisterActionRoutes(:1898,:1906,:1914on5a9a80bd2). 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 is about to rewrite those mounts, possibly mounting every verb, so the site list grows or moves. Re-run the card's grep on themainthat carries 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 before claiming.727091c82) made the dispatcher's observability Proxy wrapPUT/PATCH, so aPUTsite'sreq.bodyread now runs inside that wrapper.5a9a80bd2.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Re-price after #24228 landed ·
domain:cliseat 2 (seat post #22648) ·marchtian· sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T02:49Z · ⛔ not a claim; the card stayspm:queue.The card this one waited on, #24228, landed as PR #24244 (
cbfa5e433). The question the deferral note6243761215owed: did that landing make this card easier, harder, unnecessary, or leave it unchanged?origin/maincbfa5e433, the card's grep still finds 20dispatcher.dispatch(… req.body …)sites inpackages/runtime/src/dispatcher-plugin.ts, the same 20 at shifted lines. The three/actionsPOSTmounts are still among them.dispatch()withundefinedas the body, "readingreq.bodywould let the transport's400for a body it cannot parse answer ahead of the floor's401". That covers only verbs no route mounts. The 20 sites here are mountedPOST/PUTroutes whose domains do read the body once the caller is admitted. So this card still needs the body to be read after the domain's anonymous decision (e.g. a lazy body), not dropped. That is the dev's first measurement, as the card says.400-before-401on these mounts is untouched by 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.It is dispatched when this seat has a free slot, by the take order.
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 2 · 2026-10-18T02:56Z
Session:
session_01QyoWKK9RwUNxBxmevy9NebAccount:
marchtian(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24151-body-after-anonymous-floorWorktree:
objectstack-issue-24151Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/maincbfa5e433, where the card's grep still finds the 20 sites at:1282–:1963; line numbers move; stop on a breach and explain it in the report):packages/runtime/src/dispatcher-plugin.ts: the 20dispatcher.dispatch(… req.body …)mounts, and only how each hands its body todispatch().8e9fd41b2): four moredispatch()mounts in the same file with the same defect, which the card's single-line grep missed: the/ai/*wildcard (:1909oncbfa5e433), the/packagesregistrar (:1553, 16 mounts) and the two/securityregistrars (:2006_activation,:2020permission-sets/:name/clone). Their domains (ai,packages,security) read the deferred body right after their anonymous decision. Base evidence: anonymous + unparseable400, and anonymous + well-formed401. These are fixed in place under the four conditions: the same defect class, the same mechanical change, no other claim on the file, and the same gate families.dispatch()and the domains need so the body is read only after the domain's anonymous decision, inpackages/runtime(http-dispatcher.tsandsrc/domains/*), only as far as the measured route requires.packages/plugins/plugin-hono-server/src/adapter.ts, itsreq.bodygetter, read; edited only if the route needs a non-throwing or deferred read there.plugin-hono-servertest files where they fit.patchchangeset per package whose published behaviour moves.packages/specpath, includingIHttpServer's body contract (if the route needs it, stop and report: that isdomain:spec's);packages/hono(createHonoApp, read only: its Malformed JSON request body is silently parsed as {}: a truncated POST /data/:object/query answers 200 with the whole table, and a truncated PATCH answers 200 with an empty update #23914 rule, commit76d9dca5f, is the parity reference);cbfa5e433; it already passes no body);readGatedSessionUser;Container & model:
M,mode:subagent,model: TIER_DEFAULT(dispatch-gates.mjs --tier: "Model tier — no path-derived mandate")Clause-②: no
Basis for the Clause-② line: triage
6230399933: "an answer-order defect … it does not narrow an accept set". An anonymous request with an unparseable body moves from400to401; both are refusals. A signed-in caller with the same body still gets the domain's own refusal.Responsibility:
packages/runtime's standalone mounts readreq.bodybeforedispatch(), and the Hono adapter's getter throws its400 INVALID_REQUESTparse refusal on read, so the refusal answers ahead of the domain's ADR-0138 D2 anonymous floor | none:createHonoAppanswers the same request401because it refuses only a body the domain reads (Malformed JSON request body is silently parsed as {}: a truncated POST /data/:object/query answers 200 with the whole table, and a truncated PATCH answers 200 with an empty update #23914), and PR fix(runtime, plugin-sharing): on the standalone server, every anonymous /notifications verb and /share-links path is answered 401 in the dispatcher's envelope (#24024) #24149 avoided it only on the mounts it added | every anonymous caller of the standalone server (os dev,os serve) that sends an unparseable JSON body; measured by runtime/plugin-sharing: on the standalone server, anonymous callers to unmounted /notifications and /share-links verbs and paths get 405 or 404 where the v18 floor promises 401 #24024's dev (PR fix(runtime, plugin-sharing): on the standalone server, every anonymous /notifications verb and /share-links path is answered 401 in the dispatcher's envelope (#24024) #24149,99ce467e3) on/keysand/notifications/readThread-read: 6244895606
Serial constraints cleared:
6243761215), landed atcbfa5e433; re-price6244895606.dispatcher-plugin.ts,http-dispatcher.ts,plugin-hono-server/src/adapter.tsorpackages/hono.packages/runtime/src/security/resolve-execution-context.ts, which is disjoint. [maintainer] refactor(rest): splitpackages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050 13c (this seat) is inpackages/rest. test budget (domain:cli): 119 test file(s) over 10 s of wall in the hourly full battery (Part of #23151) #23181 slice 5 (this seat) is test files inverify,restandcreate-objectstack.Scope: p3, inside this seat's p1/p2/p3 scope (the maintainer, in this session's chat: 「暂时只处理 p1 p2 任务」, then 「p3 任务也可以开始处理」).
Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{ "issue": 24151, "status": "done", "branch": "claude/issue-24151-body-after-anonymous-floor", "pr": "https://github.com/objectstack-ai/objectstack/pull/24250", "session": "session_01QyoWKK9RwUNxBxmevy9Neb (subagent: the parent PM session id, as the container stamps it)", "premise_still_valid": true, "summary": "The standalone mounts in packages/runtime/src/dispatcher-plugin.ts now hand dispatch() deferRequestBody(req) instead of req.body, and each of the nine domains they reach (analytics, mcp, keys, notifications, automation, actions, ai, packages, security) reads it with readDispatchedBody(body) as the statement right after its anonymous decision, so the body is read after the decision, not dropped. New module packages/runtime/src/deferred-request-body.ts (not exported from the package index): a deferred value reads nothing until resolved and throws UnresolvedRequestBodyRead on any unresolved read, so a domain that forgets the resolve fails loudly instead of acting on an invented empty body. No spec, packages/adapters/hono or plugin-hono-server change. Measured on a real socket: every one of the card's 20 sites plus 4 call sites the card's grep missed (the /ai/* wildcard, the /packages registrar, the two /security registrars; same defect, fixed in place) answered an anonymous unparseable JSON body 400 INVALID_REQUEST at base cbfa5e433 and answers the domain's 401 UNAUTHENTICATED at head 8e9fd41b2, the same as for a well-formed body. A signed-in caller still gets the transport's 400 INVALID_REQUEST refusal, except where the domain answers without reading a body: /mcp/skill (its 405) and /mcp when MCP is switched off or not installed (404 / 501). H2 partly falsified: createHonoApp hands a refuse-on-read Proxy (refusedOnRead), and dispatch() accepts any body shape, but a standalone mount cannot build that value without reading req.body. A lazily forwarding Proxy is not faithful for an array, null or primitive JSON body (an MCP JSON-RPC batch branches on Array.isArray), hence the explicit one-line domain reads. H3 confirmed (no adapter change). H4 confirmed with the two named exceptions.", "tests": "(1) New pins, packages/runtime/src/dispatcher-body-after-anonymous-floor.hono.integration.test.ts (real Hono socket, LiteKernel, scoping on, 60 URLs x 2 pins) + deferred-request-body.test.ts (5): 'Test Files 2 passed (2) / Tests 125 passed (125)' at bde3873ed. (2) Full runtime suite, pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2: 'Test Files 415 passed (415) / Tests 6536 passed | 19 skipped (6555)', VERDICT command-exit 0, at bde3873ed. The final head 8e9fd41b2 adds only the .changeset file, so packages/runtime is byte-identical. (3) --project repo (error-envelope.conformance.test.ts reads dispatcher-plugin.ts as text): 'Test Files 3 passed (3) / Tests 808 passed (808)' at 8e9fd41b2. (4) pnpm --filter @objectstack/runtime typecheck: exit 0; check:test-typecheck OK (27 files / 190 errors / 68 signatures held, the new test files add none). (5) Base measurement, the same probe at cbfa5e433 over 32 site/verb rows: anonymous+unparseable 400 INVALID_REQUEST on all 32; anonymous+well-formed 401 UNAUTHENTICATED on all except /mcp/skill (405); signed-in+unparseable 400 on all 32. (6) Ablation, once, as the dispatch asked: the fix committed first; node scripts/ablation-replace.mjs WRAP on deferred-request-body.ts, anchor 'return deferred;' replaced by 'return req.body; // ABLATION-24151-EAGER' (anchor x1 to x0, blob 7d7452c90112 to dfaf76e93a8e). The pin file imports ./dispatcher-plugin.js from src by relative path, so no dist is on the path and no build or dist preflight applies. Result: 'Tests 62 failed | 58 passed (120)'. Red: all 60 anonymous pins, plus the 2 signed-in /mcp/skill pins, whose expected answer is the domain's own 405, which an eager read turns into 400. Green: the 58 signed-in refusal controls. Restore: blob after restore 7d7452c90112 == HEAD, git diff HEAD empty. (7) Scratch probe at head, file deleted before commit: /mcp with no MCP service 501 NOT_IMPLEMENTED and with OS_MCP_SERVER_ENABLED=false 404 RESOURCE_NOT_FOUND, anonymous and signed in, whatever the body. Anonymous POST /api/v1/apps/ns/x: 400 INVALID_REQUEST with an unparseable body, 404 ENDPOINT_NOT_FOUND with a well-formed one.", "gates": "Local subset at 8e9fd41b2. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands. Each was run with its exit code captured before any pipe: 64 exit 0, plus pnpm check:dual-build-cjs-loads :: exit 3, PREREQUISITE NOT MET (it reads every workspace package's dist; only the runtime closure is built here), so NOT MEASURED, reason: prerequisite; CI's. --ran with exit codes: '65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. Roster families whose list sits under a changed directory, run anyway: check:route-ledger-census, check:error-code-casing, check:authz-resolver, check:filter-alias-parity, node scripts/check-changeset-fixed.mjs, each exit 0. Lane blind spot: pnpm exec eslint --no-inline-config over the 13 changed .ts files, exit 0. check:dev-prereqs and check:skill-frame-freshness: not owed (no packages/spec and no frame file touched). CI, read once at PR open, not re-run locally and not waited on: 23 check-runs on 8e9fd41b2. 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, the other 21 in_progress (Lint · ESLint, Lint · repo & tooling gates, Type Check · workspace/source/consumer/debt, Check Changeset, Governed Surface Queue Guard, The card this PR closes must claim this branch, and the rest). Test Core, Build Core and the dogfood shards had not yet appeared.", "mcp_calls": "0 — no MCP GitHub tool was called (reads went through gh api REST; writes through the fleet relay)", "api_writes": "2 relay strokes, each one seat-side POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24250/assignees [marchtian], run 39095422272, read-back 11479 bytes sent = stored; (2) comment, i.e. POST /repos/objectstack-ai/objectstack/issues/24151/comments (this os-dev-report). Plus git push of the branch (not REST). No labels written: the dispatch named none, and a changeset is present, so no skip-changeset.", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: anonymous POST /api/v1/mcp on the standalone composition (HonoServerPlugin + dispatcher plugin, LiteKernel, in process, head 8e9fd41b2; the handler code is unchanged from cbfa5e433) answers 501 NOT_IMPLEMENTED when no MCP service is installed and 404 RESOURCE_NOT_FOUND with OS_MCP_SERVER_ENABLED=false, not 401 · evidence: ADR-0138 D2 'Everything else answers 401 (UNAUTHENTICATED). The decision is taken once per domain, as the domain's first statement'; handleMcpRequest (packages/runtime/src/domains/mcp.ts) checks isMcpServerEnabled() (404) and the mcp service (501) ahead of its 401. The analytics, ai, notifications and automation domains each record why the 401 must come before a capability answer. Caveat for the filer: D2 lists MCP under 'outside the guest model by construction' (it never admits an anonymous caller), so whether the first-statement order binds it is the triage's reading · Seam: spec: none (ADR-0138 D2; @objectstack/core shouldDenyAnonymous) → runtime: packages/runtime/src/domains/mcp.ts handleMcpRequest · dedupe words: mcp anonymous 501 before 401; mcp disabled 404 anonymous; handleMcpRequest anonymous decision not first statement", "class: a · reach: anonymous POST /api/v1/apps/ns/x on the standalone composition, in process, head 8e9fd41b2 (fallback code unchanged from cbfa5e433): unparseable JSON body 400 INVALID_REQUEST, the same request well-formed 404 ENDPOINT_NOT_FOUND; signed in, the same pair · evidence: the declarative-endpoint fallback in packages/runtime/src/dispatcher-plugin.ts (setFallbackHandler, the execution.request object passed to runAppEndpointStep) reads req.body while building its argument, before runAppEndpointStep matches an endpoint or applies authRequired. The same defect shape as #24151, on a seam that is not a dispatch() mount, so outside this claim; ③ and ④ of the in-place exemption hold, ② does not (no ruling pins that seam's shape, and the step's input mapping reads request.body after its policies, which needs its own measurement). An anonymous caller of a declared authRequired: true endpoint getting 400 where 401 is owed is inferred, NOT MEASURED (it needs a declared endpoint) · dedupe words: declarative endpoint fallback req.body before authRequired; apps endpoint unparseable body 400 before 404; runAppEndpointStep request.body read eagerly" ], "line_budget": "n/a — skills/** not touched", "deviations": [ "Claim surface: dispatcher-plugin.ts was claimed for 'the 20 dispatcher.dispatch(… req.body …) mounts'. 4 more call sites in the same file were changed under the bounded in-place exemption (same defect class; the same mechanical change; no other claim on the file; same gate families): the /ai/* wildcard (:1909 on cbfa5e433, 5 verbs x 2 bases), the /packages registrar (:1553, 16 mounts x 2 bases), and the /security registrars (:2006 _activation, :2020 permission-sets clone). Base evidence: each answered anonymous + unparseable 400 INVALID_REQUEST and anonymous + well-formed 401. The seat declares these on the claim's file surface. The domains the extra sites reach (ai, packages, security) are inside the claim's src/domains/* line.", "H4 says a signed-in caller's answer is unchanged. It moved in two named places, both where the domain answers without reading a body, as createHonoApp answers them: /mcp/skill POST and DELETE (400 to the domain's 405, pinned) and /mcp with MCP switched off or not installed (400 to 404 / 501; measured at head, base 400 by construction). Everywhere else a signed-in caller gets the base answer.", "The full runtime suite ran at bde3873ed, not at the final head 8e9fd41b2. The final commit adds only .changeset/24151-body-after-anonymous-floor.md, so the packages/runtime tree is identical; the gate union, the repo-project tests and eslint ran at 8e9fd41b2.", "test:repo (runtime's --project repo) was run locally although the derivation did not name it: error-envelope.conformance.test.ts reads dispatcher-plugin.ts as text.", "check:dual-build-cjs-loads NOT MEASURED locally (PREREQUISITE NOT MET: needs every package's dist; building the whole workspace is CI's)." ], "files_changed": [ ".changeset/24151-body-after-anonymous-floor.md", "packages/runtime/src/deferred-request-body.ts", "packages/runtime/src/deferred-request-body.test.ts", "packages/runtime/src/dispatcher-body-after-anonymous-floor.hono.integration.test.ts", "packages/runtime/src/dispatcher-plugin.ts", "packages/runtime/src/domains/actions.ts", "packages/runtime/src/domains/ai.ts", "packages/runtime/src/domains/analytics.ts", "packages/runtime/src/domains/automation.ts", "packages/runtime/src/domains/keys.ts", "packages/runtime/src/domains/mcp.ts", "packages/runtime/src/domains/notifications.ts", "packages/runtime/src/domains/packages.ts", "packages/runtime/src/domains/security.ts" ], "domains_measured": [ { "site": ":1282 POST /analytics/query, :1300 POST /analytics/sql", "domain": "analytics", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":1326 /mcp (POST, GET, DELETE)", "domain": "mcp", "anonymous_first": "no: the isMcpServerEnabled() 404 and the service-probe 501 come first (finding 1)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED with MCP on and installed; 404 / 501 otherwise, body unread" }, { "site": ":1362 /mcp/skill (POST, GET, DELETE)", "domain": "mcp/skill", "anonymous_first": "no anonymous decision: public by design", "base": "400 INVALID_REQUEST", "head": "405 METHOD_NOT_ALLOWED (its own answer to a non-GET)" }, { "site": ":1395 POST /keys, :1409 /keys (GET, PUT, PATCH, DELETE)", "domain": "keys", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":1447 POST /notifications/read, :1456 POST /notifications/read/all", "domain": "notifications", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":1676 :1735 :1753 :1762 :1771 :1795 :1824 :1848 :1857 /automation (x9)", "domain": "automation", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":1947 :1955 :1963 POST /actions (x3)", "domain": "actions", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":1909 /ai/* wildcards (outside the card grep)", "domain": "ai", "anonymous_first": "yes (decided once, ahead of every capability answer; a route's auth: false opens it)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":1553 /packages registrar, 16 mounts (outside the card grep)", "domain": "packages", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" }, { "site": ":2006 POST /security/_activation/:type/:name, :2020 POST /security/permission-sets/:name/clone (outside the card grep)", "domain": "security", "anonymous_first": "yes (first statement)", "base": "400 INVALID_REQUEST", "head": "401 UNAUTHENTICATED" } ] }Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24250 comment
6245468236·domain:cliseat 2 · sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T04:04Z · claim revised for the four extra mounts; filed #24251 and #24252; landing waits on every check of head8e9fd41b2.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed: PR #24250 →
8eccf519d(#24151,Fixes #24151) ·domain:cliseat 2 ·marchtian· sessionsession_01QyoWKK9RwUNxBxmevy9Neb· 2026-10-18T04:47Zmerged: true, merge commit8eccf519d, merged 2026-10-18T04:46Z through the merge queue (enqueued 2026-10-18T04:24Z)origin/main8eccf519dis an ancestor oforigin/main, with a single parent (squash). The 14 files and +521 / −25 match what was reviewed at8e9fd41b2gh-readonly-queue/main/*pr-24250*head remains6244946396(revised in place for four more mounts) → report6245430651→ ACCEPT6245468236on PR fix(runtime): on the standalone server, an anonymous request whose JSON body cannot be parsed gets the domain's 401, not the transport's 400 #24250 → pointer6245473623. No cross-lane declaration was owed: every file is inpackages/runtime.dispatch()a deferred body (deferRequestBody,packages/runtime/src/deferred-request-body.ts), and each of the nine domains reads it withreadDispatchedBodyright after its anonymous decision. An unresolved read throws loudly. So an anonymous caller gets the domain's401whatever its body, on the card's 20 sites plus the/ai/*,/packagesand two/securitymounts the grep missed. A signed-in caller with an unparseable body still gets the transport's400.completedbyFixes.pm:dispatchedis stripped in this act; the other labels stay./mcpanswers404/501ahead of401) and runtime(dispatcher): the declarative-endpoint fallback reads req.body while building the step input, before any endpoint is matched or authRequired applies, so an unparseable body answers 400 where the same request answers 404 (and, inferred, 401) #24252 (the declarative-endpoint fallback reads the body before matching).HEAD /mcp/skill) was waiting on this card. Under the maintainer's serial rule it is the next card, taken once the cards in flight at that instruction close.Generated by Claude Code