You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
Filing gate ①: a product defect at a public door, measured. reach: on a stock standalone showcase boot (os dev; the same at base f901746ac and at PR #24009's head):
anonymous PUT and DELETE /api/v1/notifications → 405 METHOD_NOT_ALLOWED, Allow: GET, HEAD, naming the allowed methods;
anonymous GET and DELETE /api/v1/notifications/read → 405, Allow: POST;
anonymous PUT and DELETE /api/v1/share-links → 405, Allow: GET, HEAD, POST;
anonymous GET and PATCH /api/v1/share-links/abc → 405, Allow: DELETE;
anonymous GET /api/v1/share-links/abc/def/ghi → 404 ENDPOINT_NOT_FOUND.
The createHonoApp host answers these 401 in the standard envelope. Anonymous GET and POST /api/v1/share-links answer 401, but with the message "Sign in to list share links" and no error.httpStatus.
Filed by the domain:cli seat 2 (seat post #22648) · os-tesla · session session_018t6a5rvqPRvEDMCVVW9kYT, from #23909's dev report (6221942285, out-of-scope findings 2 and 3). It is the same family as #23909, which closed the /keys member. ⛔ Not a claim; the grade and the split are triage's.
Readers who act:
/notifications: the domain:cli seat. The standalone mounts are packages/runtime/src/dispatcher-plugin.ts (GET /notifications, POST /notifications/read, POST /notifications/read/all), and the domain's floor (domains/notifications.ts) never sees the other verbs.
/share-links: on the standalone server it is answered by packages/plugins/plugin-sharing/src/share-link-routes.ts (about :349), not by runtime/src/domains/share-links.ts, and dispatcher-plugin.ts has no mount for it. That is two owners for one surface. Triage routes this half to plugin-sharing's lane, or splits the card.
The promise
Changeset 23225-domain-floor-anonymous-deny (in .changeset/pre/, v18) says:
"/notifications: 401 UNAUTHENTICATED on every face";
"/share-links: 401 UNAUTHENTICATED on the create, list and revoke faces and on every path the domain does not serve";
"The refusal is the dispatcher's standard envelope … with the shared message every other dispatcher domain answers with".
ADR-0138 D2 decides the anonymous answer once per domain, first.
Done when
On the standalone server, every anonymous request above answers 401 UNAUTHENTICATED in the dispatcher's standard envelope with the shared message, as on createHonoApp. A pin fails against today's mounts. #23909 (PR #24009) is the precedent: mount the verbs so the floor answers, and ledger the mounts.
Dedupe
MCP search_issues on this repository, closed included: anonymous notifications share-links unmounted verb answers 405 instead of 401 standalone server anonymous floor gave 11 hits. #23909 is the /keys member. #23371, #23328, #22079, #21476, #21328, #7653, #14637, #10349 and #7911 are other share-link, forms or anonymous-ordering defects, all closed. None is this mount gap.
Claim: PM loop round 1, serial, p1/p2 only (the maintainer to this seat, verbatim 「串行派发,只处理 p1 p2」; #24022's PR #24132 waits only in the merge queue). Label order read in this act: target:v18 p2 cards rank ahead of an unboarded p1, #24062 is with triage (pm:retriage, 6228573973), and #24024 is the oldest of the two boarded p2 bugs. #24024: on the standalone server, every anonymous call to an unmounted /notifications or /share-links verb or path is answered 401 UNAUTHENTICATED in the dispatcher's standard envelope, as on createHonoApp · 2026-10-17T03:13Z
Session: session_01BVm1pAXVXQ3PjC42j1kPFf
Account: os-sales (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24024-anonymous-floor-notifications-share-links
Worktree: objectstack-issue-24024
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface (read on origin/mainc1c0a9094; line numbers move; stop on a breach and explain it in the report):
packages/plugins/plugin-sharing/src/share-link-routes.ts (cross-lane on triage's route, 6223692368; domain:services): the anonymous answer on the share-link management faces (about :349) and on the paths it does not serve.
Pins: packages/runtime/src/dispatcher-plugin.routes.test.ts, new *.hono.integration.test.ts files beside the precedent's, and plugin-sharing tests beside share-link-routes.ts. One .changeset/24024-*.md.
{
"issue": 24024,
"status": "done",
"branch": "claude/issue-24024-anonymous-floor-notifications-share-links",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24149",
"session": "session_01BVm1pAXVXQ3PjC42j1kPFf (subagent; the dispatching PM's session)",
"premise_still_valid": true,
"summary": "Head 99ce467e3, draft PR #24149 (assignee os-sales, body read back byte-identical). /notifications: dispatcher-plugin.ts mounts the four unserved verbs on each of the three faces (12 mounts, no body handed on, exact paths only), so the domain floor answers an anonymous caller 401 in the dispatcher envelope; a signed-in caller gets the domain's answer (404 ROUTE_NOT_FOUND with an inbox, 501 without), as on createHonoApp. /share-links: plugin-side by measurement (standalone = plugin-sharing's registerShareLinkRoutes, the dispatcher mounts nothing there; createHonoApp = runtime twin). The family door now takes the anonymous decision once (shouldDenyAnonymous + the shared ANONYMOUS_DENY_* constants, httpStatus 401) for every route but the two token routes, replacing three per-face 'Sign in to ...' checks, and a per-verb ${base}/* catch-all registered last puts every unserved verb/path in front of it (signed-in: the family's 404 ENDPOINT_NOT_FOUND). A runtime-side mount was rejected on structure: the dispatcher registers in start(), before plugin-sharing mounts at kernel:ready, so a dispatcher wildcard would take the token routes (an owner change). No owner or published path changed, so no needs_decision. Ledger: 12 exact server-only notifications rows + one '* /share-links/**' family row (the published promise is a sub-path claim: 'every path the domain does not serve'), census 92 -> 105. Measured standalone base/head in process, createHonoApp, and head on the booted showcase: every anonymous request in the card's list now answers 401 UNAUTHENTICATED with the shared message and httpStatus 401; token faces unchanged. The worktree is removed (node_modules first, then git worktree remove, no --force); a patch round resumes from origin/claude/issue-24024-anonymous-floor-notifications-share-links at 99ce467e3.",
"files_changed": [
"packages/runtime/src/dispatcher-plugin.ts",
"packages/runtime/src/route-ledger.ts",
"packages/runtime/src/dispatcher-plugin.routes.test.ts",
"packages/runtime/src/notifications-every-verb.hono.integration.test.ts (new)",
"packages/runtime/src/share-links-every-path.hono.integration.test.ts (new)",
"packages/plugins/plugin-sharing/src/share-link-routes.ts",
"packages/plugins/plugin-sharing/src/share-link-routes.anonymous-floor.pin.test.ts (new)",
"packages/plugins/plugin-sharing/src/share-link-routes.auth-gate.pin.test.ts",
".changeset/24024-notifications-share-links-anonymous-floor.md (runtime patch, plugin-sharing patch)",
"packages/qa/dogfood/test/authz-conformance.matrix.ts (surface extension, see deviations)",
"packages/qa/dogfood/test/authz-probe-blind-spot.census.ts (surface extension, see deviations)",
"scripts/error-status-unpinned-baseline.json (surface extension, see deviations)"
],
"tests": "All through scripts/pm/os-verify-lock.sh. Package suites at 29c37e223 (runtime and plugin-sharing trees equal head 99ce467e3: git diff 29c37e223 HEAD over both is empty): pnpm --filter @objectstack/runtime test -> Test Files 407 passed (407), Tests 6242 passed | 19 skipped (6261); runtime typecheck exit 0; pnpm --filter @objectstack/plugin-sharing test -> Test Files 48 passed (48), Tests 1117 passed (1117); plugin-sharing typecheck exit 0; the 5 new/edited test files are in their test tsc programs (--listFiles). At head 99ce467e3: dogfood (closure built, 65/65) route-ledger-live-mount-parity + authz-conformance + authz-probe-blind-spot + anonymous-door-sweep + showcase-anonymous-deny-surfaces -> Test Files 5 passed (5), Tests 212 passed (212) (parity alone 8/8 at 05ac40cb3; authz-conformance had 2 failures on the first run, the ledger-figure pins, fixed); runtime pins (notifications-every-verb, share-links-every-path, dispatcher-plugin.routes, route-ledger.conformance) Tests 89 passed (89); plugin-sharing pins (anonymous-floor.pin, auth-gate.pin) Tests 62 passed (62). eslint --no-inline-config over the 8 changed .ts files exit 0. Ablations via scripts/ablation-replace.mjs, each restore proven blob == HEAD and git diff HEAD empty: (1) notifications mounts moved off their paths (anchor 1 -> 0, blob 494637703bec -> 7be7783ee4ef): Tests 26 failed | 22 passed (48), 'expected 405 to be 401' / 'expected 405 to be 404' / route list; source-resolved (./dispatcher-plugin.js). A first attempt was refused by the tool (replacement still held the anchor, drop 0): nothing ran. (2) door floor removed (f4d24ce9611c -> cfa334887a72): plugin-sharing pins Tests 20 failed | 68 passed (88), 'expected 200/201/404 to be 401'. (3) catch-all removed, unit leg: the 2 census pins fail. (4) catch-all removed, dist leg (runtime wire test resolves plugin-sharing via dist): rebuilt, ablation-dist-preflight marker present in 2 built files, share-links-every-path Tests 29 failed | 6 passed (35); restored + rebuilt, preflight --absent, tree clean, 35 passed (35). Direction in all four: red as predicted.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at head 99ce467e3: 83 commands; all 83 exit 0 (exit captured by redirect before any pipe, recorded as 'cmd :: exit N'); --ran: '83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). First pass at b184e3a92: check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (9 packages unbuilt; green after turbo build --filter='!@objectstack/docs'), check:error-status-conformance exit 1 ('ENDPOINT_NOT_FOUND: baselined as unpinned, but a producer now declares its status'; baseline row removed, green). The very first derivation flagged STALE TREE vs origin/main -> merged origin/main 5c92b0130 (clean, no overlap) and re-derived. node scripts/check-route-ledger-census.mjs: red 92 vs 105 before --fix, green after. Lint family not in the derived list: eslint line in tests; repo-wide pnpm lint = PR check-run 'Lint · ESLint'. CI check-runs on 99ce467e3 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; Lint · (ESLint, PM dispatch-gates, repo & tooling gates, docs & code guards, CI release & engine gates, scoped x3), Type Check · (workspace, source gates, debt ledger, consumer gates), Check Changeset, Governed Surface Queue Guard, Check PR Size, Spec property liveness, the claim/part-of checks: in_progress; Test Core (6/6) and Build Core: queued. Not waited on.",
"mcp_calls": "0",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches from this container (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/24149/assignees [os-sales], run 39003978843 success, read-back identical; (2) comment -> POST /repos/objectstack-ai/objectstack/issues/24024/comments (this report, sent with the same scripts/pm tool from the shared checkout, whose scripts/pm equals the branch's). git push x6 to the branch (not REST). label-write: 0 (no label named by the dispatch; skip-changeset does not apply, the diff publishes).",
"line_budget": "n/a: no skills/** or line-ratcheted ledger in the diff. Diff vs merge base 5c92b0130: 12 files, +874/-37 (dispatch-gates human-merge threshold 3000: under).",
"deviations": [
"File surface extended by three forced co-changes, all test/tooling-only: packages/qa/dogfood/test/authz-conformance.matrix.ts (docblock '(92 rows / 23' -> '(105 rows / 23') and authz-probe-blind-spot.census.ts (route-ledger row population/reachable/controls 92 -> 105, blindSpot 0, keys 23, one dated history comment) pin the runtime ledger's row count and went red locally ('docblock says 92 rows, the table holds 105'); #24009 omitted them and main's dogfood shards stayed red until #24069. scripts/error-status-unpinned-baseline.json: ENDPOINT_NOT_FOUND removed (shrink-only, 'a row that becomes pinned must be removed'), because the catch-all's sendError(res, 404, 'ENDPOINT_NOT_FOUND', ...) now pins the doc's 404. Removed by hand, which is exactly what --update would write, to avoid running the flag the script's usage line marks maintainer-only.",
"PM assumption 3 falsified in part: signed-in callers of the new mounts stay refused (no accept set moves, Clause-② no stands), but not with a 405. Notifications: the domain has no 405 branch and answers handled:false, rendered 404 ROUTE_NOT_FOUND (501 with no inbox). Share-links: the family's own 404 ENDPOINT_NOT_FOUND, the status its runtime twin answers. Before, the adapter answered 405 + Allow (or 404).",
"Signed-in share-link 404 uses ENDPOINT_NOT_FOUND, not the twin's ROUTE_NOT_FOUND: ROUTE_NOT_FOUND is registered under @objectstack/runtime only, and registering it for plugin-sharing is a packages/spec (error-code ledger) edit, held off.",
"test:repo (vitest repo project) not run locally: no file of that project changed and the derived list does not name it; CI Test Core runs it.",
"check-platform-checklist --changed-from not run: no exported symbol renamed or removed (the deleted isAuthenticated was module-private).",
"Commit trailer spelled 'Co-Authored-By: Claude' as the dispatch words it (AGENTS.md spells 'Co-authored-by'); model-free either way, pre-push hook passed.",
"No environment variable value was printed."
],
"open_questions": [
{
"question": "Should /notifications also get a sub-path catch-all, so an anonymous GET /api/v1/notifications/xyz answers 401 as createHonoApp does (standalone answers 404 today, before and after this PR)?",
"options": [
"A: keep exact rows (this PR). The published text says 'every face' for /notifications and adds 'every path the domain does not serve' only for /share-links; /keys/anything stayed 404 under the same reading.",
"B: mount ${prefix}/notifications/* for every verb (after the faces) and ledger '* /notifications/**', and do the same for /keys for consistency; a wildcard claims the subtree, so any later mount under it must register first."
],
"recommendation": "A. Business need: no caller or product path reads an unserved notifications sub-path, and a 404 there discloses nothing deployment-specific. Long-term: B makes 'anonymous learns nothing on any path' uniform, but that is a family-wide choice (keys + notifications) for a seat ruling, not a rider here. AI-error axis: neither choice lets an author declare something the runtime ignores. Startup scope: A adds no surface. Take B only if the seat rules the promise means every path."
}
],
"out_of_scope_findings": [
"class: b (contract violation; reproducible) · reach: public HTTP door, measured in process on the standalone composition at head 99ce467e3: anonymous POST /api/v1/keys, PUT /api/v1/keys and POST /api/v1/notifications/read with content-type application/json and an unparseable body answer 400 INVALID_REQUEST ('The request body could not be parsed as JSON ...') where changeset 23225 promises '/keys: 401 UNAUTHENTICATED for every verb' and ADR-0138 D2 puts the anonymous decision first; createHonoApp answers 401 (it refuses only a body the domain reads, #23914) · evidence: dispatcher-plugin.ts mounts pass req.body to dispatcher.dispatch(...), and the Hono adapter's req.body getter throws the parse refusal before the domain runs (adapter.ts get body()); the new notifications mounts here pass undefined and answer 401 · Seam: spec: consumer: none -> runtime: packages/runtime/src/dispatcher-plugin.ts mount handlers (req.body read before dispatch) · family: the standalone-vs-createHonoApp anonymous-floor family of #23909 / #24024, so it belongs on that family's closing card rather than a point card · dedupe words: anonymous unparseable body 400 before 401 standalone; dispatcher-plugin req.body read before anonymous floor; INVALID_REQUEST outranks UNAUTHENTICATED keys notifications",
"carrier: 承接者:无 · noted, not filed: the signed-in 404 for an unserved request is spelled three ways across shapes (plugin-sharing family ENDPOINT_NOT_FOUND, runtime twin ROUTE_NOT_FOUND, createHonoApp handled-false fallback RESOURCE_NOT_FOUND); same status, different codes. In the PR's Acceptance notes.",
"carrier: #24025 · noted, not filed: the new PUT/PATCH notifications mounts are not wrapped by the observability Proxy (get/post/delete only), as on /keys.",
"carrier: #24023 · noted, not filed: no 405 is introduced by this PR (new refusals are 401/404/501); the domains' 405-without-Allow gap stays that card's."
]
}
6dede7a51 "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)"; git rev-list --parents -n1 gives 1 parent, a squash
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p2·target:v18·domain:cli·area:access·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T20:04ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
target:v18: it is the same class and grade as runtime(dispatcher): anonymous GET / PUT / PATCH / DELETE on /api/v1/keys answer 405 METHOD_NOT_ALLOWED instead of the 401 UNAUTHENTICATED the v18 anonymous floor states for every /keys verb — only POST is mounted #23909. The v18 anonymous floor promises 401 on these doors, and the standalone server answers 405 / 404 and names the allowed methods to an anonymous caller.domain:cli: the mounts arepackages/runtime.plugin-sharing's share-link routes are declared on the claim. Serial with PR fix(runtime): mount every /api/v1/keys verb so an anonymous caller is answered 401 on the standalone server #24009.objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 1, serial, p1/p2 only (the maintainer to this seat, verbatim 「串行派发,只处理 p1 p2」; #24022's PR #24132 waits only in the merge queue). Label order read in this act:
target:v18p2 cards rank ahead of an unboarded p1, #24062 is with triage (pm:retriage,6228573973), and #24024 is the oldest of the two boarded p2 bugs. #24024: on the standalone server, every anonymous call to an unmounted/notificationsor/share-linksverb or path is answered401 UNAUTHENTICATEDin the dispatcher's standard envelope, as oncreateHonoApp· 2026-10-17T03:13ZSession:
session_01BVm1pAXVXQ3PjC42j1kPFfAccount:
os-sales(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24024-anonymous-floor-notifications-share-linksWorktree:
objectstack-issue-24024Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/mainc1c0a9094; line numbers move; stop on a breach and explain it in the report):packages/runtime/src/dispatcher-plugin.ts: the in-app notifications mounts (about:1366-:1401), and a/share-linksmount only if the measured route puts the standalone floor there. Shape: PR fix(runtime): mount every /api/v1/keys verb so an anonymous caller is answered 401 on the standalone server #24009's/keysmounts (9e797a897).packages/runtime/src/route-ledger.ts: rows for the new mounts, and the census countnode scripts/check-route-ledger-census.mjs --fixwrites, as seat ruling6222005517on runtime(dispatcher): anonymous GET / PUT / PATCH / DELETE on /api/v1/keys answer 405 METHOD_NOT_ALLOWED instead of the 401 UNAUTHENTICATED the v18 anonymous floor states for every /keys verb — only POST is mounted #23909 ruled for/keys.packages/plugins/plugin-sharing/src/share-link-routes.ts(cross-lane on triage's route,6223692368;domain:services): the anonymous answer on the share-link management faces (about:349) and on the paths it does not serve.packages/runtime/src/dispatcher-plugin.routes.test.ts, new*.hono.integration.test.tsfiles beside the precedent's, andplugin-sharingtests besideshare-link-routes.ts. One.changeset/24024-*.md.packages/runtime/src/domains/notifications.tsanddomains/share-links.ts(the floor itself, ADR-0138 D2); the observability Proxy's verb list (runtime(dispatcher): PUT and PATCH routes skip the observability Proxy, so they answer with no X-Request-Id and lose the 5xx error capture; only get/post/delete are wrapped #24025's); the405with noAllowheader (runtime: the keys and actions domains answer 405 METHOD_NOT_ALLOWED with no Allow header (deps.error), where RFC 9110 requires one and the mcp, meta and ai domains send it #24023's);plugin-hono-server/src/adapter.ts;docs/qa/; anypackages/specedit (thedomain:specseat's).Container & model:
M(two owners of one surface, a mount set, ledger rows, and a measured choice of where the share-link floor answers),mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate)Clause-②: no
Basis for the Clause-② line: no request that was accepted becomes refused, and none that was refused becomes accepted. An anonymous call that was refused
405or404is refused401, which is already published text in changeset23225-domain-floor-anonymous-deny(.changeset/pre/, v18): "/notifications:401 UNAUTHENTICATEDon every face", and "/share-links:401 UNAUTHENTICATEDon the create, list and revoke faces and on every path the domain does not serve". No error code is minted.Responsibility:
packages/runtime's standalone mounts (this lane) andplugin-sharing's share-link routes (domain:services, on triage's cross-lane route) | the domain floor (ADR-0138 D2,domains/notifications.ts/domains/share-links.ts) already answers 401 first, but only for a verb that reaches it;createHonoApp's catch-all reaches it, the standalone server's explicit mounts do not | an anonymous caller of a standalone oros devserver, measured on a stock showcase boot by runtime(dispatcher): anonymous GET / PUT / PATCH / DELETE on /api/v1/keys answer 405 METHOD_NOT_ALLOWED instead of the 401 UNAUTHENTICATED the v18 anonymous floor states for every /keys verb — only POST is mounted #23909's dev (6221942285, out-of-scope findings 2 and 3)Thread-read: 6223692368
Serial constraints cleared: the 6 open PRs read in this act; none edits
dispatcher-plugin.ts,route-ledger.tsorplugin-sharing/. No openpm:dispatchedclaim declares those files; the only other lane card in flight is this seat's cli/serve: a --no-server boot prints "[datasource-admin] route registration failed: … a composition/ordering fault" when nothing is at fault; the plugin's own degrade line is unreachable because getService throws #24022 (serve.ts, in the merge queue). The precedent PR fix(runtime): mount every /api/v1/keys verb so an anonymous caller is answered 401 on the standalone server #24009 (triage's serial note) merged as9e797a897. runtime(dispatcher): PUT and PATCH routes skip the observability Proxy, so they answer with no X-Request-Id and lose the 5xx error capture; only get/post/delete are wrapped #24025 (p2,pm:queue) and runtime: the keys and actions domains answer 405 METHOD_NOT_ALLOWED with no Allow header (deps.error), where RFC 9110 requires one and the mcp, meta and ai domains send it #24023 (p3,pm:queue) namedispatcher-plugin.tsand the domains' 405s; neither is in flight, and both are held off above. C3 stage 9 (#15204): declared sharing rules read from the registry, bootstrapDeclaredSharingRules retires, and the ceremony deletes the declared-rule mirrors; the verified marker follows (director ordering ruling) #23952 (pm:epic, no claim) is declared onplugin-sharing's seeder, a different region.wip(runtime,plugin-sharing): every /notifications verb and every /sha…
chore(changeset): runtime + plugin-sharing patch for the standalone a…
test(dogfood): the authz census and the matrix docblock read the runt…
chore(scripts): ENDPOINT_NOT_FOUND leaves the unpinned error-status b…
objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{ "issue": 24024, "status": "done", "branch": "claude/issue-24024-anonymous-floor-notifications-share-links", "pr": "https://github.com/objectstack-ai/objectstack/pull/24149", "session": "session_01BVm1pAXVXQ3PjC42j1kPFf (subagent; the dispatching PM's session)", "premise_still_valid": true, "summary": "Head 99ce467e3, draft PR #24149 (assignee os-sales, body read back byte-identical). /notifications: dispatcher-plugin.ts mounts the four unserved verbs on each of the three faces (12 mounts, no body handed on, exact paths only), so the domain floor answers an anonymous caller 401 in the dispatcher envelope; a signed-in caller gets the domain's answer (404 ROUTE_NOT_FOUND with an inbox, 501 without), as on createHonoApp. /share-links: plugin-side by measurement (standalone = plugin-sharing's registerShareLinkRoutes, the dispatcher mounts nothing there; createHonoApp = runtime twin). The family door now takes the anonymous decision once (shouldDenyAnonymous + the shared ANONYMOUS_DENY_* constants, httpStatus 401) for every route but the two token routes, replacing three per-face 'Sign in to ...' checks, and a per-verb ${base}/* catch-all registered last puts every unserved verb/path in front of it (signed-in: the family's 404 ENDPOINT_NOT_FOUND). A runtime-side mount was rejected on structure: the dispatcher registers in start(), before plugin-sharing mounts at kernel:ready, so a dispatcher wildcard would take the token routes (an owner change). No owner or published path changed, so no needs_decision. Ledger: 12 exact server-only notifications rows + one '* /share-links/**' family row (the published promise is a sub-path claim: 'every path the domain does not serve'), census 92 -> 105. Measured standalone base/head in process, createHonoApp, and head on the booted showcase: every anonymous request in the card's list now answers 401 UNAUTHENTICATED with the shared message and httpStatus 401; token faces unchanged. The worktree is removed (node_modules first, then git worktree remove, no --force); a patch round resumes from origin/claude/issue-24024-anonymous-floor-notifications-share-links at 99ce467e3.", "files_changed": [ "packages/runtime/src/dispatcher-plugin.ts", "packages/runtime/src/route-ledger.ts", "packages/runtime/src/dispatcher-plugin.routes.test.ts", "packages/runtime/src/notifications-every-verb.hono.integration.test.ts (new)", "packages/runtime/src/share-links-every-path.hono.integration.test.ts (new)", "packages/plugins/plugin-sharing/src/share-link-routes.ts", "packages/plugins/plugin-sharing/src/share-link-routes.anonymous-floor.pin.test.ts (new)", "packages/plugins/plugin-sharing/src/share-link-routes.auth-gate.pin.test.ts", ".changeset/24024-notifications-share-links-anonymous-floor.md (runtime patch, plugin-sharing patch)", "packages/qa/dogfood/test/authz-conformance.matrix.ts (surface extension, see deviations)", "packages/qa/dogfood/test/authz-probe-blind-spot.census.ts (surface extension, see deviations)", "scripts/error-status-unpinned-baseline.json (surface extension, see deviations)" ], "tests": "All through scripts/pm/os-verify-lock.sh. Package suites at 29c37e223 (runtime and plugin-sharing trees equal head 99ce467e3: git diff 29c37e223 HEAD over both is empty): pnpm --filter @objectstack/runtime test -> Test Files 407 passed (407), Tests 6242 passed | 19 skipped (6261); runtime typecheck exit 0; pnpm --filter @objectstack/plugin-sharing test -> Test Files 48 passed (48), Tests 1117 passed (1117); plugin-sharing typecheck exit 0; the 5 new/edited test files are in their test tsc programs (--listFiles). At head 99ce467e3: dogfood (closure built, 65/65) route-ledger-live-mount-parity + authz-conformance + authz-probe-blind-spot + anonymous-door-sweep + showcase-anonymous-deny-surfaces -> Test Files 5 passed (5), Tests 212 passed (212) (parity alone 8/8 at 05ac40cb3; authz-conformance had 2 failures on the first run, the ledger-figure pins, fixed); runtime pins (notifications-every-verb, share-links-every-path, dispatcher-plugin.routes, route-ledger.conformance) Tests 89 passed (89); plugin-sharing pins (anonymous-floor.pin, auth-gate.pin) Tests 62 passed (62). eslint --no-inline-config over the 8 changed .ts files exit 0. Ablations via scripts/ablation-replace.mjs, each restore proven blob == HEAD and git diff HEAD empty: (1) notifications mounts moved off their paths (anchor 1 -> 0, blob 494637703bec -> 7be7783ee4ef): Tests 26 failed | 22 passed (48), 'expected 405 to be 401' / 'expected 405 to be 404' / route list; source-resolved (./dispatcher-plugin.js). A first attempt was refused by the tool (replacement still held the anchor, drop 0): nothing ran. (2) door floor removed (f4d24ce9611c -> cfa334887a72): plugin-sharing pins Tests 20 failed | 68 passed (88), 'expected 200/201/404 to be 401'. (3) catch-all removed, unit leg: the 2 census pins fail. (4) catch-all removed, dist leg (runtime wire test resolves plugin-sharing via dist): rebuilt, ablation-dist-preflight marker present in 2 built files, share-links-every-path Tests 29 failed | 6 passed (35); restored + rebuilt, preflight --absent, tree clean, 35 passed (35). Direction in all four: red as predicted.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at head 99ce467e3: 83 commands; all 83 exit 0 (exit captured by redirect before any pipe, recorded as 'cmd :: exit N'); --ran: '83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). First pass at b184e3a92: check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (9 packages unbuilt; green after turbo build --filter='!@objectstack/docs'), check:error-status-conformance exit 1 ('ENDPOINT_NOT_FOUND: baselined as unpinned, but a producer now declares its status'; baseline row removed, green). The very first derivation flagged STALE TREE vs origin/main -> merged origin/main 5c92b0130 (clean, no overlap) and re-derived. node scripts/check-route-ledger-census.mjs: red 92 vs 105 before --fix, green after. Lint family not in the derived list: eslint line in tests; repo-wide pnpm lint = PR check-run 'Lint · ESLint'. CI check-runs on 99ce467e3 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; Lint · (ESLint, PM dispatch-gates, repo & tooling gates, docs & code guards, CI release & engine gates, scoped x3), Type Check · (workspace, source gates, debt ledger, consumer gates), Check Changeset, Governed Surface Queue Guard, Check PR Size, Spec property liveness, the claim/part-of checks: in_progress; Test Core (6/6) and Build Core: queued. Not waited on.", "mcp_calls": "0", "api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches from this container (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/24149/assignees [os-sales], run 39003978843 success, read-back identical; (2) comment -> POST /repos/objectstack-ai/objectstack/issues/24024/comments (this report, sent with the same scripts/pm tool from the shared checkout, whose scripts/pm equals the branch's). git push x6 to the branch (not REST). label-write: 0 (no label named by the dispatch; skip-changeset does not apply, the diff publishes).", "line_budget": "n/a: no skills/** or line-ratcheted ledger in the diff. Diff vs merge base 5c92b0130: 12 files, +874/-37 (dispatch-gates human-merge threshold 3000: under).", "deviations": [ "File surface extended by three forced co-changes, all test/tooling-only: packages/qa/dogfood/test/authz-conformance.matrix.ts (docblock '(92 rows / 23' -> '(105 rows / 23') and authz-probe-blind-spot.census.ts (route-ledger row population/reachable/controls 92 -> 105, blindSpot 0, keys 23, one dated history comment) pin the runtime ledger's row count and went red locally ('docblock says 92 rows, the table holds 105'); #24009 omitted them and main's dogfood shards stayed red until #24069. scripts/error-status-unpinned-baseline.json: ENDPOINT_NOT_FOUND removed (shrink-only, 'a row that becomes pinned must be removed'), because the catch-all's sendError(res, 404, 'ENDPOINT_NOT_FOUND', ...) now pins the doc's 404. Removed by hand, which is exactly what --update would write, to avoid running the flag the script's usage line marks maintainer-only.", "PM assumption 3 falsified in part: signed-in callers of the new mounts stay refused (no accept set moves, Clause-② no stands), but not with a 405. Notifications: the domain has no 405 branch and answers handled:false, rendered 404 ROUTE_NOT_FOUND (501 with no inbox). Share-links: the family's own 404 ENDPOINT_NOT_FOUND, the status its runtime twin answers. Before, the adapter answered 405 + Allow (or 404).", "Signed-in share-link 404 uses ENDPOINT_NOT_FOUND, not the twin's ROUTE_NOT_FOUND: ROUTE_NOT_FOUND is registered under @objectstack/runtime only, and registering it for plugin-sharing is a packages/spec (error-code ledger) edit, held off.", "test:repo (vitest repo project) not run locally: no file of that project changed and the derived list does not name it; CI Test Core runs it.", "check-platform-checklist --changed-from not run: no exported symbol renamed or removed (the deleted isAuthenticated was module-private).", "Commit trailer spelled 'Co-Authored-By: Claude' as the dispatch words it (AGENTS.md spells 'Co-authored-by'); model-free either way, pre-push hook passed.", "No environment variable value was printed." ], "open_questions": [ { "question": "Should /notifications also get a sub-path catch-all, so an anonymous GET /api/v1/notifications/xyz answers 401 as createHonoApp does (standalone answers 404 today, before and after this PR)?", "options": [ "A: keep exact rows (this PR). The published text says 'every face' for /notifications and adds 'every path the domain does not serve' only for /share-links; /keys/anything stayed 404 under the same reading.", "B: mount ${prefix}/notifications/* for every verb (after the faces) and ledger '* /notifications/**', and do the same for /keys for consistency; a wildcard claims the subtree, so any later mount under it must register first." ], "recommendation": "A. Business need: no caller or product path reads an unserved notifications sub-path, and a 404 there discloses nothing deployment-specific. Long-term: B makes 'anonymous learns nothing on any path' uniform, but that is a family-wide choice (keys + notifications) for a seat ruling, not a rider here. AI-error axis: neither choice lets an author declare something the runtime ignores. Startup scope: A adds no surface. Take B only if the seat rules the promise means every path." } ], "out_of_scope_findings": [ "class: b (contract violation; reproducible) · reach: public HTTP door, measured in process on the standalone composition at head 99ce467e3: anonymous POST /api/v1/keys, PUT /api/v1/keys and POST /api/v1/notifications/read with content-type application/json and an unparseable body answer 400 INVALID_REQUEST ('The request body could not be parsed as JSON ...') where changeset 23225 promises '/keys: 401 UNAUTHENTICATED for every verb' and ADR-0138 D2 puts the anonymous decision first; createHonoApp answers 401 (it refuses only a body the domain reads, #23914) · evidence: dispatcher-plugin.ts mounts pass req.body to dispatcher.dispatch(...), and the Hono adapter's req.body getter throws the parse refusal before the domain runs (adapter.ts get body()); the new notifications mounts here pass undefined and answer 401 · Seam: spec: consumer: none -> runtime: packages/runtime/src/dispatcher-plugin.ts mount handlers (req.body read before dispatch) · family: the standalone-vs-createHonoApp anonymous-floor family of #23909 / #24024, so it belongs on that family's closing card rather than a point card · dedupe words: anonymous unparseable body 400 before 401 standalone; dispatcher-plugin req.body read before anonymous floor; INVALID_REQUEST outranks UNAUTHENTICATED keys notifications", "carrier: 承接者:无 · noted, not filed: the signed-in 404 for an unserved request is spelled three ways across shapes (plugin-sharing family ENDPOINT_NOT_FOUND, runtime twin ROUTE_NOT_FOUND, createHonoApp handled-false fallback RESOURCE_NOT_FOUND); same status, different codes. In the PR's Acceptance notes.", "carrier: #24025 · noted, not filed: the new PUT/PATCH notifications mounts are not wrapped by the observability Proxy (get/post/delete only), as on /keys.", "carrier: #24023 · noted, not filed: no 405 is introduced by this PR (new refusals are 401/404/501); the domains' 405-without-Allow gap stays that card's." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
ACCEPT → PR #24149 comment 6229886389 · head
99ce467e3·domain:cliseat 1 · sessionsession_01BVm1pAXVXQ3PjC42j1kPFf· 2026-10-17T04:57Zreq.bodybefore the anonymous floor.objectstack-fleet commentedon Oct 17, 2026
Landed: PR #24149 ·
domain:cliseat 1 · sessionsession_01BVm1pAXVXQ3PjC42j1kPFf· 2026-10-17T05:42Zorigin/main6dede7a51"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)";git rev-list --parents -n1gives 1 parent, a squashgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-24149-*'gives 0 refsmerged: trueat 2026-10-17T05:40:37Zdomain:cliset lost #24024 only, so no other card was closed by a keywordState: closed
completedbyFixes #24024;pm:dispatchedstripped in this act. The standalone-mount body-parse remainder of this family is #24151.