Repository navigation
fix(runtime)!: the notification inbox, API-key mint and share-link management domains deny an anonymous caller as their first statement (ADR-0138 D2 slice 1, #23225) - #23286
Conversation
…ide anonymity once, as their first statement (ADR-0138 D2) WIP: the domain-level anonymous baseline moves ahead of each domain's capability probe / method check; the share-link token faces (class 2) stay admitted by the capability token. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…tion conformance matrix, and gate-pin the three domain floors WIP: six door-class rows proven by the anonymous door sweep, three domain-floor rows with gate pins, the forms family and three dispatcher domains burnt out of the population baseline (30 to 26), the census re-measured, and the module doc of assemble-execution-context.ts points at ADR-0138 D9 and the D2 list. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…at the domain refuses an anonymous caller first; re-measure the census notes Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…-closed-door-list
…hey serve a user and have no system-context arm Passing isSystem added three elevation read sites the system-context page does not carry, for no behaviour: each domain already refused a system context with no user. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…-closed-door-list
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 44 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d6de8e609febc89ec06ab2246fca297ab14a0a8 && git checkout 3d6de8e609febc89ec06ab2246fca297ab14a0a8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 99e944daf26ac1d2d50e90c5b631349ab45f664f 8f838b7460d216f7d8a18c9bdff2e11995368fed && git checkout -B drift-repro 99e944daf26ac1d2d50e90c5b631349ab45f664f && git merge --no-ff 8f838b7460d216f7d8a18c9bdff2e11995368fed
node scripts/docs-audit/affected-docs.mjs --json 99e944daf26ac1d2d50e90c5b631349ab45f664f
|
Contract reviewServed-tier: Review of PR #23286 (card #23225, ADR-0138 card E2 slice 1: D2 only) at the head above, rendered 2026-10-13T14:41Z inside the adopting seat's session by an isolated read-only reviewer. Inputs: the card's body and every comment (triage Check-runs on the head, latest run per name, read 2026-10-13T14:40Z after four polls over ten minutes: every concluded run is ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged. The three floors: right. In For No signed-in caller's answer changes: case D of the suite pins, for each domain, that a member reaches every face with the same service calls ( The system-context census: right. At the merge base all three domains already required a user id: The six class rows: right. Each row names D2's enforcing site by the function D2's table names, and each symbol exists at this head: The new-route pin: right. The dispatcher-domain ledger key is minted by the probe over The census and the baseline: right. The The five existing tests moved to a signed-in caller: right, with one nuance. Each keeps its original assertion's meaning: ② Semver level
The tension the dev named: ③ Boundary flagsEvery dev flag and every
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #23225
Clause-②: no (narrowing)
ADR-0138 card E2, slice 1: D2 only — the closed list of doors recorded in the authorization conformance matrix, and denial by domain. D2b (the publish refusal of an anonymous flow door whose target runs as
system) is slice 2 of the same card and is not in this PR; the card stays open for it.What lands
1. The six door classes, recorded in
AUTHZ_CONFORMANCEpackages/qa/dogfood/test/authz-conformance.matrix.tsgains oneenforcedrow per D2 class, each naming its enforcing site as D2's table does:anonymous-door-public-formRestServer.registerFormEndpoints; thepublicFormGrantbranch ofSecurityPluginrest-family:…:forms(both routes of the family are the class)anonymous-door-share-linkregisterShareLinkRoutes(plugin-sharing)anonymous-door-public-bookisPublicAudienceRead(rest meta-item-read-gate)anonymous-door-endpoint-object-operationauthRequired: falseobject_operationendpointsresolveExecutionContext(the guest envelope, D9); the CRUD gateanonymous-door-endpoint-flowauthRequired: falseflowendpoints@objectstack/service-automationanonymous-door-public-read-fileacl: 'public_read'file downloadregisterStorageRoutes, atauthorizeDownloadEach class's admission is pinned by the existing proving file,
anonymous-door-sweep.dogfood.test.ts(the record's criterion-2 sweep), which now claims the six rows (authz-row:header lines) and which every row cites. That file already asserts each class on a real subject, admitted and refused: the admitted leg is the row's pin, the refused leg (an undeclared form, a revoked token, an org-audience book, a session-gated endpoint of the same type, a private file) its standing negative control. All six areHIGH_RISK, so the proof is mandatory. The newADR-0138 D2block inauthz-conformance.test.tsholds the list closed: exactly six door-class rows, eachenforced, each naming D2's enforcing site, each high-risk and proven by the sweep, and the sweep claims exactly those six. Its negative controls: a seventh door-class row is red, a missing one is red (and orphans the sweep's claim), a row that stops citing its proof or naming its site is red.2. "A new route in a governed domain answers 401"
Pinned through the matrix's own two mechanisms, and stated at the granularity each delivers:
3. Denial by domain: the census, and three moves
Every dispatcher domain's anonymous decision was measured. Domain-level (the decision is the domain's first statement):
/actions,/automation(with the ADR-0069 allow-list row for the signed inbound hook),/analytics, and, in files the in-flight #23214 slice 1 holds,/packages,/i18n,/securityand/meta(with class 3's carve-out). Three domains decided it per face, below another answer, and move to the domain-level deny here, each with the matrix shape the/analyticsand/i18nmoves used (a floor row, a gate pin that goes STALE when the floor is deleted, the domain's ledger key classified):/notifications(handleNotificationRequest): the decision sat below the inbox capability probe./keys(handleKeysRequest): below the method check./share-links(handleShareLinksRequest, the dispatcher twin of the plugin-sharing mount): inside the management faces, below the service probe. The two token faces stay admitted — the one exception D2 names for this domain (class 2), selected past the floor by the same predicate that marks them public.Each floor is asked about the user id alone: these domains serve a user and have no system-context arm, so passing the system flag would add an elevation read site with no behaviour.
Not moved here, and why (each raised for the seat in the report):
createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 1 carry no domain-level decision of their own; their move is listed for after that slice lands./ai(handleAIRequest) takes the decision once at entry but applies it per route: a registered AI route declaringauth: falseis admitted to an anonymous caller. No route in this repository declares it; moving the floor would retire a route-level opt-out the runtime pins today, and anauth: falseroute is a door D2 does not list. That is a decision, not a move, so it is raised asneeds_decision./webhooks/redeliveris decided by its member (plugin-webhooks), by design: its refusals answer the same bytes at the dispatcher and at the self-hosted mount. Raised, not moved.enforceAuth); only the metadata family has a family-wide guarded registrar. D2's "domain" is read here as the dispatcher domain the runtime face's guest envelope reaches; the REST reading is raised, not acted on.The population baseline shrinks 30 to 26 (
forms,/notifications,/keys,/share-linksleave classified); the probe census is re-measured (23 probes over 18 files, 29 covers keys).4. The module doc of
assemble-execution-context.tsCites ADR-0138 D9 as the record of the 2026-08-08 ruling, and the D2 list as the closure of its adoption rule. Doc only; no code line moves (cross-lane, declared on #6367).
Changeset
.changeset/23225-domain-floor-anonymous-deny.md,@objectstack/runtime: minor,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription). The three moves change what an anonymous caller is answered on compositions where another answer used to come first (a 501 capability answer with no inbox or no share-link service; a 405 for a wrong verb on the key mint) to the 401 D2 states; nothing a signed-in caller is served changes. Per the claim, a move that changes an answer takes the narrowing grade, and one contract-tier review is owed before the queue.@objectstack/coremoves a comment only (fast track: no changeset);packages/qa/dogfoodis private. The gates that decide:check:adr-0087-registration,check:changeset-no-major,check:empty-changeset.Ablations (one-time proofs; no permanent test file)
All four through
scripts/ablation-replace.mjs(anchor must hit; blob hash checked before and after; restore proven asblob == HEADand an emptygit diff HEAD), underos-verify-lock.sh, each wrapped in a restore trap, from the committed tree2d84837006:isPublicAudienceReadmade to answerfalsefor every request (markerABLATION_23225_CLASS3); anchor 1 to 0, blob41952c07b37ato41c05ff602de;@objectstack/restrebuilt, andablation-dist-preflightfound the marker in 2 built files. The sweep: 2 failed / 18 passed — exactly class 3's two admitted legs (the public book and its tree; a doc the public book claims); its refused leg and every other class stayed green. Restore leg: blob back to HEAD, rest rebuilt, preflight--absent(marker in 0 of 6 built files, tree clean), sweep 20 / 20./notifications). The floor's condition replaced byfalse(marker count 1, the gate's call spelling count 0). Runtime unit suite: 14 failed / 33 passed — every/notificationsA, B and C case, under both anonymous shapes. Matrix:STALE covers — notifications:domains/notifications.ts:anonymous-gate, aDEAD PROBEonnotifications.ts, the census control vanished — 9 failed / 95 passed. Restore leg: blob back to HEAD; 47 / 47 and 104 / 104.UNCLASSIFIED surface — dispatcher-domain:route-ledger.ts:/anon-scratch(and the docblock figure pins: 88 to 89 rows, 23 to 24 domains) — 8 failed / 53 passed./notificationsdomain (scratch). One row added under the existing domain: zero UNCLASSIFIED — only the docblock row-count pin moved (2 failed). This is the matrix header's measured bound, re-measured: a route inside an existing domain mints no key; ablation 2 is what holds it.Expected directions were declared first and observed as declared: red on the admitted leg only (1), red on the floor's pins (2), UNCLASSIFIED (3), no UNCLASSIFIED (4).
Gates
At
8f838b7460(the final tree,origin/mainmerged):node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths: 69 commands; each run with its exit code captured before any pipe: 69 / 69 exit 0 (check:type-check-debt: "OK — 26 raw tsc error(s) total, none above its recorded number"). Reconciled with--ran: "69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN".authz-conformance.test.ts+authz-probe-blind-spot.test.ts+anonymous-door-sweep.dogfood.test.tsafter a closure build (64 / 64 tasks): 3 files, 124 / 124;@objectstack/dogfoodtypecheck green. Runtime: the 8 touched or ordering-sensitive files, 405 / 405;@objectstack/runtimetypecheck (withcheck:test-typecheckOK) and@objectstack/coretypecheck green. The full runtime--project localsuite was 368 files, 5269 passed, 19 skipped at2d84837006; the later merge touched no runtime, rest, core or dogfood file.eslint --no-inline-config --format jsonover the 16 changed.tsfiles: 16 files, 0 errors, 0 warnings, none ignored. The population is eslint's own (no file reported ignored), the count is read from the JSON output, and the config enables no type-aware linting (noparserOptions.project), so this diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's.Acceptance notes
@objectstack/dogfood, which runs onmain's full battery, not per PR./share-linksdispatcher domain is unobservable on the standalone showcase boot (plugin-sharing mounts its own routes); its pins are the runtime unit suite.Generated by Claude Code