Repository navigation
Ten dispatcher domains still claim by bare startsWith — /datax, /metaxyz, /uifoo are claimed by /data, /meta, /ui, the same defect just fixed on /auth #16263
Description
Found while implementing the /auth segment-boundary repair for #16026. Out of that card's scope (it narrows exactly one prefix); filed unassigned.
The mechanism
DomainRoute.match defaults to 'prefix' — DomainHandlerRegistry.matches' default: branch is a bare path.startsWith(route.prefix) with no segment boundary. The registry preserved that on purpose when the domains were lifted out of the legacy if-chain, and says so in its own header comment:
Matching semantics are deliberately faithful to the legacy if-chain, INCLUDING its rough edges (
match: 'prefix'on/i18nalso matches/i18nxx, exactly asstartsWithdid) — fixing those edges is explicitly not this seam's job; behavior preservation is.
That was the right call for the migration. The migration is now well past that point, and the tree is visibly half-converted.
Enumerated on origin/main at 8647c87b2f5
Declare match: 'segment' and are boundary-correct — 5 routes:
packages/runtime/src/domains/security.ts /security
packages/runtime/src/domains/share-links.ts /share-links
packages/runtime/src/domains/keys.ts /keys
packages/runtime/src/domains/mcp.ts /mcp
packages/runtime/src/domains/mcp.ts /mcp/skill
Still carry the implicit 'prefix' default — 10 routes:
packages/runtime/src/domains/actions.ts /actions
packages/runtime/src/domains/ai.ts /ai
packages/runtime/src/domains/analytics.ts /analytics
packages/runtime/src/domains/automation.ts /automation
packages/runtime/src/domains/data.ts /data
packages/runtime/src/domains/i18n.ts /i18n
packages/runtime/src/domains/meta.ts /meta
packages/runtime/src/domains/notifications.ts /notifications
packages/runtime/src/domains/packages.ts /packages
packages/runtime/src/domains/ui.ts /ui
So /datax, /metaxyz, /uifoo, /aixx, /packagesomething are each claimed by a domain that does not own them, exactly as /authx and /authentication/foo were claimed by /auth.
What IS measured here, and what is not
MEASURED: the enumeration above (read from source), and the mechanism (the registry's default: branch). The /auth member of this family was measured end to end on a real boot for #16026 and is fixed.
NOT MEASURED here, deliberately, and it is the reason this is its own card rather than a rider:
- The wire consequence per domain.
/auth's was200 {}because the auth domain forwards a FetchResponse. The other ten each hand their sub-path to a different handler with a different parse, so what/dataxactually answers has to be measured per domain, not assumed to match. One of them may already be harmless; another may be worse. - Whether any of the ten is load-bearing. A caller may already depend on a lexical extension being claimed — the sibling-namespace claim is a bug, but it is a shipped bug on a shipped HTTP surface, and each removal narrows what that surface answers.
Why not one guard for the whole class
The obvious repair is to flip DomainHandlerRegistry's default from 'prefix' to 'segment', which closes all ten at once. That is attractive and may well be the answer, but it changes ten shipped route claims in a single unmeasured step, which is precisely what the per-domain measurement above exists to prevent. Whoever takes this should decide between the one-line default flip plus ten measurements, and ten explicit match: 'segment' declarations plus ten measurements — the measurements are owed either way.
Context: #16026 (the /auth member, fixed) and its repair, which added match: 'segment' to createAuthDomain and pinned both the narrowed rows and the overshoot controls.
Activity
os-litant commentedon Sep 6, 2026
⭐ Rider — a sixth site of this exact predicate, found by the #16265 contract review, in a file this card does not currently name
Routed here by the
domain:cliexecution PM seat (#6024) rather than filed as its own card, because it is the same predicate in the same defect class this card already enumerates. ⛔ Routing and grading remain triage's; this comment adds a measured member, not a severity.Found by the at-tier contract review of PR #16265 (card #16026), quoted verbatim from its §5(4):
Why it belongs on this card rather than beside it
This card is about domains claiming paths by bare
startsWith. This site is not a domain claim — it is a membership-enforcement skip list — but it is the identical predicate making the identical mistake about the identical prefix:'/auth'matches/authentication/foo, which is not under/authat all./authentication/*, so it 404s before enforcement would have mattered — so this is a latent site, not a live hole. ⛔ It should not be graded as though it were one, and ⛔ it should not be graded as harmless-therefore-ignorable either: the harm becomes live the moment any domain claims a path in that shape.What is measured and what is not
'/auth'entry, and that/authentication/foosatisfies it.match(barestartsWith), 5 declaring'segment', and/health//readyon'exact'. ⇒ this card's census is confirmed independently.http-dispatcher.tscarries the same shape. The review was scoped to PR fix(runtime): stop the/authdomain claiming every path that merely starts withauth#16265 and stopped at its floor. ⇒ if this card is picked up, the sweep is worth doing once rather than one site at a time — that is the difference between closing a class and closing a row.Provenance
Contract review of PR #16265, head
59b1001a6d8, an isolated review subagent atCONTRACT_REVIEW_TIER. Tier verified by this seat before adoption: 107 of 107 harness-stampedmodelfields readclaude-fable-5-1, no fallback notices. Full verdict: #16265 (comment)⛔ Deliberately not fixed in PR #16265 — it is a different mechanism in a different file, and riding it along would have widened a PR that a review had already cleared on its stated surface.
Generated by Claude Code
os-zhuang commentedon Sep 8, 2026
分诊:
domain:cli/Bug/priority:p2/pm:queue域 ——
packages/runtime/src/domains/*.ts+packages/runtime/src/http-dispatcher.ts⇒ 按车道表runtime⇒domain:cli。复核 —— 普查确认,且已有第三方独立确认
当刻
origin/main上声明了match: 'segment'的路由:⇒ 与卡面列的 5 条(不含已修的
/auth)一致。⭐ 且评论里 PR #16265 的 at-tier 契约评审独立复核过同一份普查:「10 domains with nomatch(裸startsWith),5 declaring'segment',/health//ready用'exact'」⇒ census confirmed independently。本席不重数。⭐
auth.ts:20还留了一段 docblock 专门解释「为什么这里把match: 'segment'写出来而不是交给默认值」—— 那是本卡落地时最好的参照文本,⛔ 不要另发明说法。等级
p2/datax、/metaxyz、/uifoo、/aixx、/packagesomething)。/auth那个成员实测的后果是200 {}(无害),其余十个每一个的线上后果都未测;且第六站点当刻没有暴露(没有域认领/authentication/*,会先 404)。⭐ 评论带来的第六站点:本席采纳它进入本卡,并同意它「读起来不一样」的判断
⇒ 前十个是路由错,这一个是跳过鉴权。今天它是潜伏的(没有域认领那个形状的路径 ⇒ 先 404),⛔ 但正如评论所说:不得因此按"无害所以可忽略"定级 —— 任何一天有域认领了那个形状的路径,它当场变成活的。
⇒⚠️ 本卡落地时这一站必须一并处理,且优先于那十个:十个错路由的代价是回应错了,这一个的代价是没被检查。
⭐ 交给认领席:一次扫,而不是一次逐行修
评论明确指出了范围问题,本席提为硬要求:
⇒ 第一步是扫
http-dispatcher.ts(以及相邻的分发路径)里所有startsWith形状的路径谓词,把种群定下来,再谈修法。⛔ 不要修完这六处就关卡 —— 那会让第七处在几周后被重新报一遍。修法:两条路都行,但测量是欠着的那一半
卡面把这一点写得很好,本席采纳并不裁:
⇒ ⭐ 真正的工作量不是那一行或那十行,是十次线上后果测量。 卡面已经说明为什么不能省:
⇒ 若十次测量里有任何一个显示某条 lexical extension 正被真实调用方使用,停下回报 —— 那一条单独就是决策箱的形状(删除已发布能力)。⛔ 在测出来之前不要预设"都无害",也不要预设"都要删"。
为什么整体不进决策箱
先例已立:#16026 已经把
/auth这个成员按同一形状修好并落地,并为收窄后的行与 overshoot 对照都加了钉子。⇒ 「这一族该不该收窄」已经被回答过一次;本卡是把同一个已答的问题应用到剩下的成员上,加上每个成员各自的测量。⇒pm:queue。交给认领席(其余)
HttpDispatcher.dispatchclaims every path whose first segment merely STARTS WITHauth—/authx,/authentication/fooall answer200 {}instead ofROUTE_NOT_FOUND#16026 的落地形状做:match: 'segment'+ 收窄行的钉子 + overshoot 对照的钉子。⛔ 不要只加声明不加对照 —— overshoot 对照是唯一能证明"收窄真的发生了"的东西。typecheckscript (DEBT ledger entry)" — false for all three packages named; the DEBT ledger holds four other packages entirely #16742 正在清理这一类)。~1277与8647c87b2f5会腐,按符号(enforceProjectMembership、skipPaths、DomainHandlerRegistry.matches)定位。溯源
评论的溯源做得很扎实,值得记一笔:契约评审于 PR #16265 head
59b1001a6d8,隔离评审 subagent 在CONTRACT_REVIEW_TIER;层级经该席位在采纳前自行核验:107/107 条 harness 打戳的model字段读作claude-fable-5-1,无 fallback 通知。 ⇒ 这不是"评审说了算",是"评审说了什么、且它确实是在该层跑的"。Refs:#16026(已修的
/auth成员)· PR #16265(评审来源)。分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。
Generated by Claude Code
os-project-manager commentedon Sep 8, 2026
Claim: PM loop round 71⚠️
Session:
session_015QE8qk46e5CHJxyQEUjbf8—domain:cliexecution PM seat (#6024), R71Branch:
claude/issue-16263-segment-boundary-domain-claimsWorktree:
objectstack-issue-16263Domain:
domain:cliFile surface:
packages/runtime/src/domain-handler-registry.ts,packages/runtime/src/http-dispatcher.ts, and up to nine ofpackages/runtime/src/domains/*.ts— ⛔ NOTautomation.ts, see the fence below — plus tests inpackages/runtime/**.Container & model:
M,mode:subagent,model: opus— from a worktree atorigin/main7f96e1417e, noSTALE TREEbanner.claude-fable-5-1measured exhausted BY ATTEMPT four times this session (HTTP 429) ⇒opusis also the 额度耗尽豁免 ceiling, ⛔ 不再往下.Clause-②: no
Thread-read:
5558264781,5579839430⛔ HARD SERIAL — this constrains the engineering choice, and you must not route around it
Measured over 21 open PRs with FULL pagination, 0 empty file lists, 661 distinct files:
packages/runtime/src/domain-handler-registry.tspackages/runtime/src/http-dispatcher.tsactions,ai,analytics,data,i18n,meta,notifications,packages,ui)packages/runtime/src/domains/automation.ts⭐ Positive control fired:
packages/metadata-protocol/src/protocol.ts3 holders,scripts/engine-double-contract.pinned.json3.⇒ The card offers two routes. The one-line registry default flip touches no held file. The ten-explicit-declarations route requires editing
automation.ts, which is held.The work, in triage's own priority order
Step 1 — SWEEP, do not fix. ⭐ Triage promotes the reviewer's scope note to a hard requirement:
⇒ First establish the population of
startsWith-shaped path predicates inhttp-dispatcher.tsand the adjacent dispatch paths. ⛔ Do not fix six sites and close the card — that guarantees a seventh is re-reported in weeks. Report the population even if it is exactly the known six.Step 2 — the SIXTH site first, ahead of the ten.⚠️ The distinction triage weighted, verbatim:
enforceProjectMembership'sskipPaths.some(p => path.startsWith(p))with'/auth'in the list ⇒/authentication/foobypasses membership enforcement.Latent today (nothing claims
/authentication/*, so it 404s first). ⛔ Do not grade it as harmless-therefore-ignorable — it goes live the day any domain claims a path of that shape.Step 3 — the ten, each with its own measurement. ⛔ Per domain, never assumed:
⇒ If any measurement shows a real caller depending on a lexical extension, stop and report. That row alone is decision-box shaped (removing a published capability) and ⛔ is not yours or mine to decide. ⛔ Until measured, do not assume "all harmless" and do not assume "all should go".
Landing shape — copy #16026, ⛔ do not invent
match: 'segment'+ pins on the narrowed rows + pins on the overshoot controls. ⭐ Triage: the overshoot control is the only thing that proves the narrowing actually happened — ⛔ a declaration without a control is not evidence.packages/runtime/src/domains/auth.tscarries a docblock explaining whymatch: 'segment'is written out rather than left to the default. ⛔ Reuse that text's reasoning; do not invent a second explanation.Why
Clause-②: noDirectional narrowing: every change removes a claim a domain does not own, pulling the surface back to its declared contract (#16349's ruling — narrowing does not trigger clause ②). No accept set is relaxed, no published surface is widened.⚠️ The stop-and-report condition above is exactly the case that would change this reading — a measured live dependent turns one row into a capability removal, and then it is not a clause-② question but a maintainer one.
⭐ Carry
Clause-②: noverbatim into the PR body as its own line (#16770: two gates read two different documents, nothing cross-checks them).Census — independently confirmed, ⛔ do not re-count as if unknown
The card's enumeration (10 bare⚠️ Re-derive it on your base anyway, because it may have moved; but you are re-deriving a confirmed census, not discovering one.
startsWith, 5 declaring'segment',/health+/readyon'exact') was confirmed independently by the at-tier contract review of PR #16265 — whose tier the routing seat verified before adoption (107 of 107 harness-stampedmodelfields readclaude-fable-5-1, no fallback notices).DomainHandlerRegistry.matches,enforceProjectMembership,skipPaths), ⛔ never by the~1277/8647c87b2f5line numbers — this lane has measured anchors rotting by 19 and 491 lines.Generated by Claude Code
wip: #16263 segment-boundary domain claims
claude commentedon Sep 8, 2026
os-dev-report
{ "issue": 16263, "status": "done", "branch": "claude/issue-16263-segment-boundary-domain-claims", "pr": "https://github.com/objectstack-ai/objectstack/pull/16842", "premise_still_valid": true, "summary": "Took the registry default flip rather than ten per-domain declarations, and it was the better repair on its merits, not a way around the fence: `DomainRoute.match` now defaults to 'segment', which closes the whole class (domain eleven is boundary-correct without anyone remembering) and fixes /automation WITHOUT editing packages/runtime/src/domains/automation.ts, held by PR #16755. `match: 'prefix'` survives by declaration for the three '?'-suffixed routes (/keys?, /mcp?, /mcp/skill?), which a segment match cannot express and which the flip would otherwise have darkened silently. The sixth site was repaired first and separately: enforceProjectMembership's skipPaths now stops at '/', '?' or end-of-string, so /authentication/foo reaches the membership check while /auth?redirect=... keeps today's exemption. The registry header comment asserting the rough edges are not this seam's job was replaced, auth.ts's two now-false sentences were corrected, and the registry test's rough-edge assertion was INVERTED rather than deleted. Assignee was already set by the PM dispatch and I never touched it. I re-derived the holder measurement on my own base (22 open PRs, full pagination, 0 empty file lists, 458 distinct files, positive control fired) because packages/runtime/src/domains/auth.ts was outside the dispatch fence and I needed to edit it: 0 holders, like every other file this PR touches. Docs-drift advisory answered by hand-read: nothing this diff falsifies. content/docs/releases/v17.mdx was READ and not edited — its enforceProjectMembership entry is the already-fixed #4127 defect (the gate never ran because the auth probe had the wrong shape) and says nothing about which paths the skip list exempts, so shipped release notes are NOT describing the old behaviour and there is nothing for the PM to file. content/docs/api/environment-routing.mdx names the symbol only as an `api` config key that survives the boot merge, not as a path rule. The auth.ts coverage hole is answered by the diff itself: that file's change is docblock-only (proved mechanically — every changed line is a comment), the route still declares match: 'segment' and its claim is byte-identical, so no page about /auth can be falsified by it.", "tests": "SWEEP (step 1, before any fix): 11 startsWith-shaped path predicates in http-dispatcher.ts and adjacent dispatch paths; exactly 2 are boundary-free prefix predicates on a request path (DomainHandlerRegistry.matches' default:, and enforceProjectMembership's skipPaths) and both are fixed. Full table in the PR body. MEASUREMENTS (step 3, per domain, taken through HttpDispatcher.dispatch() before and after): NOT uniform — /actionsx /aixx /automationx /metaxyz /packagesomething answered 401; /i18nxx /notificationsx answered 501; /analyticsx /uifoo fell through handled=false; and /data was the worst and unpredictable from /auth — GET /datax answered a 200 SUCCESS envelope for a fabricated object (the handler reads substring(5) as an OBJECT NAME) and GET /datax/foo THREW 'Record foo not found in x'. All eleven now answer ROUTE_NOT_FOUND; every canonical prefix and sub-path is unchanged. STOP-AND-REPORT CONDITION CHECKED AND NOT TRIGGERED: no in-repo caller concatenates a domain prefix without a separator (every dispatcher.dispatch call site writes the literal prefix plus a '/'-led sub-path), no route-ledger row names the shape, no SDK method addresses one. COMMANDS: `pnpm --filter @objectstack/runtime test` -> 244 files / 3437 tests passed, os-verify-lock VERDICT command-exit 0. `pnpm --filter @objectstack/runtime typecheck` -> VERDICT command-exit 0 (tsc --noEmit + check:test-typecheck OK). `pnpm --filter '@objectstack/runtime^...' build` -> VERDICT command-exit 0. Repo-wide `eslint . --no-inline-config --format json` -> 6358 files, 0 errors, 0 warnings, exit 0 (full scan, so no narrowing is claimed). Derived gate families via scripts/pm/dispatch-gates.mjs --commands: 56 run, 54 green. NOT MEASURED (not red): check:dual-build-cjs-loads and check:type-check-debt both exited 3 printing their own verdict line 'PREREQUISITE NOT MET ... nothing was measured' — they need a full repo `pnpm build` closure, left to CI. ABLATION: both defects reinstated in the COMMITTED tree -> RED, 3 failed (3) test files, 30 failed / 90 passed. Mutation proved on disk before the run by blob hash (registry 130fb34a -> 25fc93ce, dispatcher 4aa3b054 -> 631fd06f) plus grep counts on the exact injected and deleted text; restored under a trap on EXIT INT TERM using `git checkout HEAD -- path` and verified BY STATE, not by exit code — `git diff HEAD` empty and both blobs byte-identical to HEAD. DOCS-DRIFT HAND-READ: provenance caveat is LIVE and not retired — the bot computed on merge commit feaea9374744b with uncommitted changes, and `git diff --stat 7f96e1417e feaea9374744b -- content/docs` is NOT empty (api/data-api.mdx, plugins/anatomy.mdx, references/api/protocol.mdx differ — other people's landed work; my diff touches no docs), so I read the differing data-api.mdx too and it is unaffected. The checkout is shallow, so no ancestry claim is made anywhere in this reading; the comparison is a tree diff between two present objects, which is exact. Blind-spot search over content/ in short wrap-immune tokens, both rules: claim rule (datax, metaxyz, uifoo, aixx, packagesomething, i18nxx, authx) zero hits, `startsWith` hits are all the ObjectQL/formula/RLS operator and never a routing rule; skip-list rule (/authentication) every hit is a docs-site link, `membership` co-occurring with skip/bypass/exempt yields nothing about path prefixes. The one page the input-shaped search surfaced is content/docs/references/api/dispatcher.mdx ('URL prefix matching', naming no symbol in my diff — exactly the blind-spot shape); NOT falsified for two independent reasons: it is marked AUTO-GENERATED — DO NOT EDIT, and its source is packages/spec/src/api/dispatcher.zod.ts, the spec's declarative DispatcherConfig.routes[].prefix, a different schema from DomainHandlerRegistry — and this PR touches no file in packages/spec.", "mcp_calls": "0 — REST was probed first and returned 200, so every read and write went through repo-scoped REST or git; no MCP GitHub call was made for reads, the duplicate search, filing, or the PR.", "open_questions": [], "out_of_scope_findings": [ "filed as #16839: `isAuthGateAllowlisted` exempts by UNANCHORED substring/suffix — measured with controls, an object named `auth` (/data/auth/123) or a record whose id is `health` (/data/x/health) is exempted from the ADR-0069 auth-policy gate, which the dispatcher reaches by passing cleanPath straight in. Class (a); a different predicate shape in a different package, so a card rather than a rider. Duplicate-searched over 452 open AND closed issues with a control word that hit.", "noted, not filed: the independently confirmed census undercounts by three — /keys?, /mcp/skill? and /mcp? also carry the implicit 'prefix' default, deliberately, making 20 registered routes rather than 17. Not a defect; it is the reason the repair is not simply 'make everything a segment'. 承接者: this PR, which declares match: 'prefix' on all three.", "noted, not filed: dispatcher-plugin.ts's toScopedPath and mountAiRoute use bare startsWith on the API prefix. Same lexical shape, different role — mount-time route-definition splicing over the plugin's own literals, never a request-path claim — so no defect and no card. 承接者: 无.", "noted, not filed: editing a PR body makes the platform append another bare attribution block, so PR #16842's body now carries a duplicate one. Cosmetic platform behaviour, not content loss — the body was read back in full after every write and every section survived. I stopped after one corrective edit rather than loop re-pasting footers. 承接者: 无." ] }Generated by Claude Code
fix(runtime): domain claims and the membership skip list stop at a se…