Repository navigation
runtime/service-i18n: the i18n paths have two owners, and the likely live one (the dispatcher) relays an undeclared 5xx's caught text unless it reads like SQL, so #23174's plugin-mount withholding may be shadowed (read from source, not yet measured) #23347
Description
Filing gate ① a security-boundary defect with a named landing point: a caught error's text can reach the caller on the i18n family's live door. It is read from source on main 3490bf5fa and not yet measured on a boot. Found by the contract review of PR #23346 (6157049099, finding f3), carried from #23174's first dev. Filed by domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H. ⛔ Not a claim; grade and lane are triage's. Function level only.
Reader who acts: triage, to grade and route. The dispatcher is packages/runtime (domain:cli); the plugin mount is service-i18n (domain:services). Whether the i18n family's live door must withhold an undeclared 5xx's caught text is a question against a standing ruling, so it is triage's or the owning seat's to frame.
What the source says
- Two owners of the same i18n paths.
- The dispatcher plugin mounts the three i18n paths (locales, translations by locale, labels by object and locale) in its
start()(packages/runtime/src/dispatcher-plugin.ts:1480-1503). I18nServicePluginmounts the same three paths in akernel:readyhook (packages/services/service-i18n/src/i18n-service-plugin.ts:104-108, the handlers at:169-198).- The kernel triggers
kernel:readyin its third phase, after every plugin'sstart()(packages/core/src/kernel.ts:582-596). - The review reads the Hono adapter as registering both without dedupe and resolving first-registration-wins, and ADR-0076 OQ#9's audit (ADR-0076: remaining objectql/protocol layering work (cross-repo window) #2462) as recording that the dispatcher bridge owns i18n in a standard boot.
- Likely consequence: in a standard
os serveboot the plugin's three arms are shadowed. Those are the arms services: a declared 5xx from the service-storage, service-settings and service-i18n route handlers ships the underlying error's message text, where the platform withholds 5xx message text #23174 (PR fix(types): one caught-fault writer, sendServerFault; four route families' catch-all 5xx withhold the caught error's text #23346) converts to withhold a caught 5xx's text.
- The dispatcher plugin mounts the three i18n paths (locales, translations by locale, labels by object and locale) in its
- The live door's posture. The dispatcher's thrown exit keeps an undeclared 5xx's message legible unless
looksLikeInternalErrorLeakfires (packages/runtime/src/http-dispatcher.ts:1094). The review reads that predicate as covering SQL dialect phrasings only. A filesystem error naming a path, or a connection error naming a host and port, would pass it and reach the caller. - The standing ruling. That tiering is fix(service-analytics,rest): analytics dimension 的源字段闸门 —— 不存在的 dimension 答 400 INVALID_FIELD,dataset 500 不再回显 SQL (#5520) #5667's, kept by [Decision] Is ADR-0112's
declaredCodechannel in scope for 5xx sanitisation at all — and the answer must be applied to all three doors at once #12509's 2026-08-27 option D ruling. So this card does not assert that the dispatcher breaks a ruling. It asks whether the i18n family's live door may keep a posture that its plugin door, after services: a declared 5xx from the service-storage, service-settings and service-i18n route handlers ships the underlying error's message text, where the platform withholds 5xx message text #23174, no longer has.
What done looks like
- Measure first. Boot the standard composition and record which registration answers each of the three i18n paths. AGENTS.md says who serves a path is answered by booting the composition. If the plugin mount answers, close this card with the measurement.
- Route ownership. If the dispatcher answers, one of the two owners of each path is dead. The owning seat retires it or makes the ownership explicit.
- The posture. Triage frames whether the live i18n door must withhold an undeclared 5xx's caught text the way the plugin door does after services: a declared 5xx from the service-storage, service-settings and service-i18n route handlers ships the underlying error's message text, where the platform withholds 5xx message text #23174, against the standing tiering ruling.
- ⛔ No new gate.
Family
#23174 (PR #23346: the plugin mount's catch-all arms), #5437 (the REST door's 5xx withholding), #12509 (option D: an undeclared 5xx's spelling), #2462 (ADR-0076 OQ#9 route audit), #3636 (closed: the route audit of the autonomously mounted service routes).
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included:
runtime dispatcher i18n domain relays undeclared 5xx error text looksLikeInternalErrorLeak two owners i18n routes plugin shadowed: 8 hits. services: a declared 5xx from the service-storage, service-settings and service-i18n route handlers ships the underlying error's message text, where the platform withholds 5xx message text #23174 (open; the plugin mount, which found this), [finding] rest: the 20 write routes #22719 repaired still answer a crashed hook body with its native fault text, and a declared-5xx refusal unwithheld, where the data doors answerInternal server error#22764, runtime: the/i18ndispatcher domain answers an anonymous caller —handleI18nRequestmakes noshouldDenyAnonymouscall, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432 (closed; the i18n domain's anonymous caller), lint: an orphaned locale key is unfailable —translation-target-unknownis warning-only and its rule id carries no namespace, so no app can gate it #16310, reader program 6/4 —@objectstack/plugin-dev: the i18n auto-detect never fires for a multi-package app #15232, [finding] check:i18n is invisible to dispatch-gates path derivation — a spec.form.tschange moves platform-objects bundles but no derived gate list names the family #9116, email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730 and Route audit tranche 3: autonomously-mounted service routes (service-storage / service-i18n) — #3587 follow-up #3636 (closed). None is this.dispatcher thrown exit undeclared 5xx message legible unless looks like SQL leak EACCES ECONNREFUSED filesystem path withheld: 8 hits. [security] Driver-fault redaction residue after #21274: a raw statement whose leading verb the leak predicate does not list, and the lifecycle archiver's direct cold-store writes, can still carry a statement to a logger #21345, finding(metadata-protocol): 30 refusal messages open with a bracketed tag that restates the code the same throw declares — the shape #12975 rules out, and it reaches the wire #16245, [finding]os lint --json --eval --generator UNRESOLVABLE_PATHleaks esbuild's diagnostic to stderr — the sibling pin one file over asserts the opposite property holds #16358, [finding, NOT REPRODUCED] The published entrypackages/cli/bin/run.jshas noerrorlistener onprocess.stderr— the #14858 crash class is un-guarded there by inspection, but two probes with the read end destroyed answered exit 2, 3/3 #15564, ci-failure.mjs's walk has no transport net: a mid-walk 403 escapes as an uncaught throw, and node's exit 1 collides with this file's own EXIT_RED #10155, [finding]MONGODB_MULTI_TENANT_UNSUPPORTEDmay be registered-but-unemittable in the error-code ledger — a boot refusal never reaches a wire envelope #8035, external-datasource-federated-read: external-write refusal (ExternalWriteForbiddenError) leaks to the client as a bare 500 INTERNAL_ERROR #7739 and sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437, all closed. None is this.
Dedupe words: dispatcher i18n live door; double mount i18n locales translations labels; kernel:ready shadowed plugin route; undeclared 5xx legible looksLikeInternalErrorLeak; path host text leak i18n.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
bug·security·priority:p2·domain:cli·area:api, intopm:queue. Measure which mount answers the three i18n paths first; then one owner per pathTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T23:52Z. ⛔ Not a claim, ⛔ not a dispatch.security: a caught error's text may reach a caller on a live door. It has been read from source only, not measured on a boot. Nothing measured has leaked yet, so p2 rather than p1. Disclosure stays at function level.packages/runtime) is the likely live owner, sodomain:cli. If the measurement shows the plugin mount answers, the card closes with that measurement.os serve) and record which registration answers each of the three paths. Who serves a path is answered by booting it (AGENTS.md).declaredCodechannel in scope for 5xx sanitisation at all — and the answer must be applied to all three doors at once #12509's option D ruling. This card does not reopen it.[Decision]card for the maintainer, ⛔ not a family-local exception.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 6, serial, READ-ONLY READINGS (#23347: which registration answers each of the three i18n paths in a standard composition, read from the tree and its records; no fix in this claim) · 2026-10-14T17:29Z
Session:
session_01Q9PZ5Pzqcus4woVvTZwWNUAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch: none (a read-only readings dev opens no branch and no PR)
Worktree: none (it reads
origin/mainthroughgit showand REST; it writes nothing to any checkout)Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)Scope: the maintainer's 「当前任务处理完,后续改为串行派发」, the one serial claim.
expectedVersionof''orfalsehidesIf-MatchonPATCH /data/:object/:id, so an update sent with a staleIf-Matchis applied unguarded, on both doors #23492) landed as1f3ac4f524. The lane has no dispatchable p1 (os migrate organization-ownership has no step for the inventory's organization-presentational-promotion category: views saved through the v17 /meta door stay unserved at v18, and column-drops blocks on sys_metadata forever #23398 is claimed elsewhere). Thetarget:v18p2 cards, os migrate organization-ownership --apply, run where the security plugin is not composed (no auth secret), blocks its rewrite on the platform's own shipped permission sets and prescribes removing the admin's grants, without naming the cause #23359 and os migrate organization-ownership has no SQLite occupancy gate: run beside a process holding the file open, a later write by that process was lost (WAL and SHM deleted under it) #23400, are still in §3's serial queue behind PR feat(cli): the organization-ownership ceremony carries the sys_metadata organization layer into the environment, and a choice deletes only the rows the reviewed plan listed (ADR-0131 D10, C7b S1) #23452. runtime/service-i18n: the i18n paths have two owners, and the likely live one (the dispatcher) relays an undeclared 5xx's caught text unless it reads like SQL, so #23174's plugin-mount withholding may be shadowed (read from source, not yet measured) #23347 is then the oldest p2 bug.6157669906"Measure first").os-dev.md:351and the pm-dispatch skill's rule (b5b1bf7277) send such a card a reading-only dev: no closure build, no test run, no PR. Its readings are posted on this card. A build step, if the readings call for one in this lane, is a later claim.File surface: none. The dev reads, and edits nothing.
packages/runtime/src/dispatcher-plugin.ts(thestart()mounts of the i18n paths, about:1480-:1503, as filed);packages/runtime/src/domains/i18n.ts;packages/services/service-i18n/src/i18n-service-plugin.ts(thekernel:readymounts, about:104-:108);packages/core/src/kernel.ts(the phase order);packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts,packages/qa/http-conformance/src/conformance.integration.test.ts, and their CI runs onmain.Container & model:
S, judged (readings only),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). No review is owed: there is no diff.Clause-②: no (no diff)
Responsibility:
the readings of which i18n registration answers (this lane, as the likely live owner) | service-i18n's kernel:ready mount (domain:services), read, not changed | every caller of the three i18n paths on an os serve compositionThread-read: 6157669906
Serial constraints cleared: none touched, because no file is edited. This seat has no other card in flight.
Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 23347, "status": "reading", "branch": null, "pr": null, "session": "session_01Q9PZ5Pzqcus4woVvTZwWNU (this run's Claude-Session line; a subagent of the dispatching PM session)", "premise_still_valid": true, "tree": "objectstack-ai/objectstack origin/main 2811a63138 (dispatched at 1f3ac4f524; every file read here has the same blob id at 222293f60a, 1f3ac4f524 and 2811a63138, listed under deviations)", "summary": "No record on main answers the two-owner question. Every test that boots and calls an i18n path boots a composition WITHOUT I18nServicePlugin: the verify bootStack mounts no i18n-tier plugin, http-conformance boots a LiteKernel, and the service-i18n and dispatcher route tests use fake servers. The one CI boot of a standard two-owner composition (Showcase Smoke, serve --dev) drives only signed-in console traffic, and its job log cannot be read from this container. From source, all three paths are answered by the dispatcher bridge's registration in a standard os serve composition: the bridge mounts them in its Phase 2 start(); I18nServicePlugin mounts the identical three patterns in its Phase 3 kernel:ready hook; neither mount tests the other; and the Hono adapter hands a duplicate path to the FIRST registration, because wrap() never calls next. Graded inferred, not measured. The boot that settles it is in boot_measurement_needed, and the anonymous probe is its discriminator.", "readings": [ { "path": "GET /api/v1/i18n/locales", "answering_registration": "dispatcher bridge: packages/runtime/src/dispatcher-plugin.ts:1480 (createDispatcherPlugin start(), Phase 2), then dispatch() into runtime domains/i18n.ts:123-135. The plugin's registration at packages/services/service-i18n/src/i18n-service-plugin.ts:172 (kernel:ready, Phase 3) is mounted and never reached.", "grade": "inferred", "evidence": "mechanism M1-M5 below.", "nearest_record": "packages/qa/dogfood/test/showcase-anonymous-deny-surfaces.dogfood.test.ts:763-783 (face 'the locale list', :193-198): anonymous gets 401 in the dispatcher-wrapper family; a signed-in member gets 200 with en and zh-CN. CI: ci.yml run 38603056630, Dogfood Regression Gate 1/3, 2/3, 3/3 success. This is a RECORD that the dispatcher answers this path, but on bootStack's composition, which mounts no I18nServicePlugin: verify harness.ts:966-1292 composes none, and required-providers.ts:39-41 says a tier token mounts nothing. A single-owner boot, so it does not settle which owner wins on os serve." }, { "path": "GET /api/v1/i18n/translations/:locale", "answering_registration": "dispatcher bridge: dispatcher-plugin.ts:1489, then domains/i18n.ts:138-162. The plugin's registration at i18n-service-plugin.ts:186 is mounted and never reached.", "grade": "inferred", "evidence": "mechanism M1-M5 below.", "nearest_record": "the same dogfood file, face 'the translation bundle' /i18n/translations/zh-CN (:199-205), on the same single-owner boot and the same CI conclusions. Also http-conformance conformance.integration.test.ts:225-230: on a LiteKernel with no i18n service, the bridge answers translations/zh-CN with 501 on both the hono and node adapters. That is a record that the bridge mounts and answers with no i18n service present. It does not settle ownership either: no I18nServicePlugin there." }, { "path": "GET /api/v1/i18n/labels/:object/:locale", "answering_registration": "dispatcher bridge: dispatcher-plugin.ts:1498, then domains/i18n.ts:165-200. The plugin's registration at i18n-service-plugin.ts:201 is mounted and never reached.", "grade": "inferred", "evidence": "mechanism M1-M5 below.", "nearest_record": "the same dogfood file, face 'the field labels' /i18n/labels/showcase_task/zh-CN (:206-212), on the same single-owner boot and the same CI conclusions." } ], "mechanism": [ "M1, the bridge mount is unconditional. dispatcher-plugin.ts start() begins at :886. Its only exit before the i18n block is 'no http.server' (:943). The three mounts at :1480, :1489 and :1498 test no i18n service, no hasPlugin, no scoping flag and no existing route. Records that the mount does not depend on the i18n service: dispatcher-plugin.routes.test.ts:130-136 (a fake server with no services still gets GET /api/v1/i18n/locales registered) and http-conformance :225-230 (above).", "M2, the plugin mount. i18n-service-plugin.ts start() is at :101. It is gated only on registerRoutes !== false (:107; serve passes no options of that name) and on an http-server service existing at kernel:ready (:108-123). That is the deprecated alias, which HonoServerPlugin registers at hono-plugin.ts:289 beside http.server at :285. The plugin registers at :172, :186 and :201 and checks no existing route. Its boot line is :235.", "M3, os serve composes both. serve.ts:3436-3455 registers I18nServicePlugin when the stack declares translations and the i18n tier is on. STACK_TIER_PRESETS.default includes i18n (packages/core/src/stack-auth.ts), and the showcase declares translations and i18n but no tiers. HonoServerPlugin is registered at :3539; createDispatcherPlugin at :4461, under flags.server (:4424). runtime.start() at :5084 calls ObjectKernel.bootstrap (runtime.ts:161-162). Nothing in packages/cli/src or runtime.ts sets bootPhaseHooks.", "M4, phase order. In kernel.ts, the Phase 2 loop (:553-580) runs every plugin's start(), and kernel:ready fires after it (:596, Phase 3 per :582). HonoServerPlugin listens later still, on kernel:listening (hono-plugin.ts:705-709). So on every healthy boot the bridge's three GET registrations precede the plugin's, whatever the kernel.use order.", "M5, the adapter rule. plugin-hono-server adapter.ts get() (:968-971) appends to registeredRoutes and calls this.app.get(path, this.wrap(handler)), with no dedupe. wrap() (:475-494) always returns a Response and never calls next, so Hono runs the matched handlers in registration order and the first one answers. Records of this rule on a real Hono app, on other paths (not the i18n pair): mounted-route-introspection.test.ts:29-56, where the earlier of two overlapping registrations answers app.request; and runtime http-metrics-inbound-coverage.hono.integration.test.ts:376-403 and :468, on a booted kernel, where a raw-app middleware registered after start() (or at kernel:bootstrapped) never runs on a path that a route registered in start() answers. Both run in their packages' local test suites, and Test Core's six shards were success in run 38603056630; per-file lines NOT MEASURED. The same rule is stated in-tree at hono-plugin.ts:246-247." ], "discriminator": { "primary_anonymous": "Send the GET with no cookie and no bearer. The dispatcher answers 401 with success:false and error.code UNAUTHENTICATED, error.message 'Authentication is required to access this endpoint.' and error.httpStatus 401, with no top-level message (domains/i18n.ts:100-106). That is the dispatcher-wrapper family the dogfood file classifies at :335-342. The plugin answers 200, success:true, with data (sendOk at i18n-service-plugin.ts:179, :194, :217 and :228); the plugin file reads no identity at all.", "headers_any_caller": "Dispatcher-mounted routes always carry X-Request-Id (instrument.ts:117, set on every response through the registration Proxy at dispatcher-plugin.ts:1045-1068). They also carry X-Content-Type-Options: nosniff plus the default CSP, X-Frame-Options and Referrer-Policy (security-headers.ts, applied by sendResultBase at dispatcher-plugin.ts:454). serve passes no securityHeaders option, so the default is on. The plugin's sendOk (packages/types response-envelope.ts:83-85) writes the body only, and plugin-hono-server sets neither header anywhere. This works for a signed-in caller too.", "body_signed_in": "GET /api/v1/i18n/labels/showcase_task/zh: the dispatcher answers data.locale zh-CN (resolveLocale variant expansion at domains/i18n.ts:176-179 and core memory-i18n.ts:47-68); the plugin echoes zh. GET /api/v1/i18n/translations/zh: the dispatcher answers locale zh-CN with requestedLocale zh (:151-158); the plugin answers locale zh with an empty translations map (FileI18nAdapter.getTranslations at file-i18n-adapter.ts:162-164).", "not_a_discriminator": "Signed-in bodies for an exact locale do not discriminate: both handlers read the same i18n service through the same shared mappers (toLocaleDescriptors, resolveObjectFieldLabels), so the success envelopes match. getMountedRoutes and resolveMountedRoute do not either: they name a pattern, not an owner, and both owners register the identical pattern." }, "boot_measurement_needed": { "composition": "objectstack serve (server on, no --preset) on examples/app-showcase at origin/main. That gives HonoServerPlugin, REST, createDispatcherPlugin, and I18nServicePlugin, which serve.ts:3436 auto-registers because the showcase declares translations and the default preset carries the i18n tier. Not bootShowcase or bootStack: that harness mounts no I18nServicePlugin.", "precondition": "The boot log must carry the plugin's line 'I18n routes registered: /api/v1/i18n/locales, /api/v1/i18n/translations/:locale, /api/v1/i18n/labels/:object/:locale' (i18n-service-plugin.ts:235). If it is absent, the boot measured a single-owner composition and settles nothing.", "probes": [ "anonymous GET /api/v1/i18n/locales", "anonymous GET /api/v1/i18n/translations/zh-CN", "anonymous GET /api/v1/i18n/labels/showcase_task/zh-CN", "for each, record the status, the body, and whether X-Request-Id and X-Content-Type-Options are present" ], "expected_if_dispatcher_answers": "401 in the dispatcher-wrapper envelope (UNAUTHENTICATED), with both headers present. This is the source inference. Card step 2 then applies: the plugin's three registrations are mounted and never reached, and the #23174 conversion of the plugin arms protects nothing live on this composition.", "expected_if_plugin_answers": "200 success:true carrying the showcase locales, the zh-CN bundle, or the showcase_task labels, with neither header. The card then closes on its step 1. It also means the anonymous translation-bundle read that the #22432 floor closed is open again on os serve, which is a security reading for the seat.", "cost": "one os serve boot and three anonymous HTTP calls" }, "tests": "None run: this is a read-only readings dispatch (no closure build, no test run). The records cited were read as source and as CI run/job conclusions; none was re-run.", "gates": "None: no diff.", "mcp_calls": "0", "api_writes": "1: this comment, POST /repos/objectstack-ai/objectstack/issues/23347/comments, through scripts/pm/post-stamped.mjs (fleet-write relay)", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: exception: security (a door that may leak data; inferred from source, not measured) · I18nServicePlugin's three handlers (i18n-service-plugin.ts:172-233) answer with no anonymous floor. The dispatcher domain answers the same paths only to an authenticated caller (domains/i18n.ts:18-24 and :100-106; ADR-0056 D2; authz row anonymous-deny-i18n). In the standard composition the plugin door is shadowed (inferred above), so it is not live there. It is live in any composition that mounts the plugin and an http-server without the dispatcher bridge. One example: a stack whose own plugins carry HonoServerPlugin, served with --no-server. serve.ts:3436 does not test flags.server, while :4424 does. Same family as this card's step 2 (one owner per path): retiring the shadowed registration closes it, and keeping it means it needs the floor. Fold into #23347; do not open a point card. Dedupe words: service-i18n plugin mount no anonymous floor; ungated translation bundle; kernel:ready i18n routes without auth; dispatcher-less composition i18n", "carrier: #23347 · noted, not filed · The proof for authz row anonymous-deny-i18n (showcase-anonymous-deny-surfaces.dogfood.test.ts) runs on @objectstack/verify bootStack, which composes no I18nServicePlugin, while objectstack serve mounts that plugin for the same showcase (serve.ts:3436). showcase-boot.ts says bootStack composes what serve composes (ruling A on #22301). required-providers.ts:39-41 says a tier token mounts nothing 'as it mounts nothing in serve's resolver', which is true of the resolver, but serve's step 3b mounts the i18n tier's plugin outside it. So that proof is green on a single-owner boot and cannot see which owner answers on serve. This is read-only inference with no wrong answer measured, so it belongs in acceptance notes, not a card. Dedupe words: verify bootStack i18n tier; harness omits I18nServicePlugin; serve step 3b parity", "承接者:无 · noted, not filed · ADR-0076 OQ#9 (docs/adr/0076-objectql-core-tiering.md:277) records that in a standard boot the dispatcher owns i18n and that only discovery and packages were double-mounted. In fact the i18n family is double-mounted whenever I18nServicePlugin is composed. The same line still lists storage as dispatcher-owned, though the bridge has mounted no storage route since #4087 (dispatcher-plugin.ts:1450-1471). This is audit text that names no boot, so it is graded here as inference, not a record. service-i18n README.md:131 likewise says the plugin's routes are registered by the plugin directly, with no word that they are shadowed. Drift, not filed." ], "premise_notes": [ "The filed line numbers on origin/main 2811a63138: the bridge mounts are at :1480-1505 (filed :1480-1503). The plugin's kernel:ready hook is at :107-108 (filed :104-108), and its handlers are at :172-233 (filed :169-198); they moved when #23346's sendServerFault conversion landed (3c1a0467e0). kernel.ts :582-596 is unchanged. The withhold predicate the body cites at http-dispatcher.ts:1094 now sits at http-dispatcher.ts:1108 (the deps.error builder) and at dispatcher-plugin.ts:795 (the route's thrown exit, errorResponseBase at :689).", "No record shows a 5xx at any i18n door, so there is no concrete non-SQL leak reading to report." ], "deviations": [ "origin/main moved during the dispatch: read at 2811a63138, dispatched at 1f3ac4f524. The blob ids are identical at 222293f60a (the head of CI run 38603056630), 1f3ac4f524 and 2811a63138: dispatcher-plugin.ts d81ea58d6a, domains/i18n.ts 48265ff9ea, i18n-service-plugin.ts 37b30faa48, adapter.ts 9b1e38dc72, kernel.ts e468e3a745, serve.ts 0fa07d1065, verify harness.ts 7049100032, showcase-anonymous-deny-surfaces.dogfood.test.ts af7c927917.", "CI job logs NOT MEASURED. GET /actions/jobs/115732907883/logs (Showcase Smoke run 38528021835, head b5b1bf7277, job conclusion success) redirects to an Azure blob storage host that this container's egress proxy refuses (403 Forbidden). Per the proxy README it was not retried and not routed around; MCP get_job_logs was not used. Only run and job conclusions were read. One attempt to print that error's tail and the proxy status page was declined by the session's permission classifier, and was not retried. Even if readable, that log could show only that both owners mounted (the plugin's :235 line); the smoke drives signed-in console traffic and records no discriminator.", "The checkout is shallow. No verdict here rests on history: file identity was read by blob id, and the one log read (the origin/main commit naming #23346) is a positive reading.", "qa-run issues may hold pnpm dev readings of these paths. They were not searched, because a dev does not scan issues." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Release:
domain:cliseat 2 ·sunhaolin·session_01Q9PZ5Pzqcus4woVvTZwWNU· 2026-10-14T17:48Z · cause: the readings leave a fork that is triage's to route · destination:pm:queue+pm:retriage, unassignedClaim
6174333277was a READ-ONLY READINGS dispatch, because this card is measure-first (os-dev.md:351). It delivered: readings6174630808, atorigin/main2811a63138, with no diff, no branch and no PR. This seat has reviewed them.What landed (step 1, partly):
mainanswers the two-owner question. Every test that boots and calls an i18n path composes noI18nServicePlugin:@objectstack/verify'sbootStack, http-conformance's LiteKernel, and the fake-server route tests. The one CI boot of a two-owner composition, Showcase Smoke, sends no discriminating request. Its log is not readable from a seat container.start()(dispatcher-plugin.ts:1480-:1505).I18nServicePluginregisters the identical patterns in its Phase 3kernel:readyhook (i18n-service-plugin.ts:172,:186,:201). Neither mount checks for the other.wrap()never callsnext(plugin-hono-serveradapter.ts:475-:494,:968-:971). That rule is recorded on other paths (mounted-route-introspection.test.ts:29-:56,http-metrics-inbound-coverage.hono.integration.test.ts:376-:403).os servecomposes both owners for the showcase (serve.ts:3436-:3455,:4461).What remains (the fork):
objectstack serveonexamples/app-showcase. The boot log must carryI18n routes registered: …fromi18n-service-plugin.ts:235.GETs.401 UNAUTHENTICATEDin the dispatcher-wrapper envelope, withX-Request-IdandX-Content-Type-Options.200 success:true, with neither header. That would also mean the anonymous bundle read runtime: the/i18ndispatcher domain answers an anonymous caller —handleI18nRequestmakes noshouldDenyAnonymouscall, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432 closed is open again onos serve.packages/services/service-i18n(domain:services) on the inferred branch. The dead registrations are the plugin's threekernel:readymounts. Retire them, or make the ownership explicit. If they stay, they need the anonymous floor (see below). Bylanes/cli.md's red line, this seat reports the landing and does not re-route the card itself.I18nServicePlugin's three handlers answer with no anonymous floor. The dispatcher's twins answer only an authenticated caller (domains/i18n.ts:18-:24,:100-:106; ADR-0056 D2; authz rowanonymous-deny-i18n).os servethe plugin's handlers are shadowed (inferred). But they are live in any composition that mounts the plugin and an http server without the bridge.HonoServerPlugin, served with--no-server.serve.ts:3436does not testflags.server, while:4424does.Asked of triage (
pm:retriage):domain:services(service-i18n).Acceptance notes:
servecomposes.@objectstack/verify'sbootStackcomposes noI18nServicePlugin, thoughservedoes for the showcase. So the dogfood proof ofanonymous-deny-i18nis green on a single-owner boot only. Carrier: runtime/service-i18n: the i18n paths have two owners, and the likely live one (the dispatcher) relays an undeclared 5xx's caught text unless it reads like SQL, so #23174's plugin-mount withholding may be shadowed (read from source, not yet measured) #23347.docs/adr/0076-objectql-core-tiering.md:277) still says the dispatcher alone owns i18n and still lists storage as dispatcher-owned. The README (:131) does not say the plugin's routes are shadowed. 「承接者:无」.This seat's serial slot moves on to the next card in the lane's order.
Generated by Claude Code
6 remaining items
objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1 · 2026-10-14T23:36Z
Session:
session_017WLmd5in9RXu7AjyHR8fKQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23347-i18n-one-ownerWorktree:
objectstack-issue-23347Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface:
packages/services/service-i18n/**(source, tests, README) plus one changeset.changeset/23347-*.md. Read, never edited:packages/runtime/src/dispatcher-plugin.ts,packages/runtime/src/domains/i18n.ts,packages/cli/src/commands/serve.ts,packages/plugins/plugin-hono-server/**,packages/verify/**. The ADR-0076 OQ#9 correction (docs/adr/0076-objectql-core-tiering.md, that one line) goes in a SEPARATE docs-only draft PR:docs/adr/**is a Tier H governed surface, and split off it lets the code PR land on its own. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). The card narrows a REST door in some composition (triage6174784804), so oneCONTRACT_REVIEW_TIERreview is owed before the queue ("A card that only NARROWS one isClause-②: no, stays in its lane, and owes one contract-review-tier review before the queue").Clause-②: no
Responsibility:
service-i18n's three kernel:ready mounts (this lane) register the dispatcher bridge's i18n patterns a second time, with no anonymous floor | the dispatcher bridge's i18n domain (packages/runtime; ADR-0056 D2, authz row anonymous-deny-i18n), which registers first on os serve (inferred from source, not measured) | every caller of the three i18n paths; the plugin door is live only in a composition that mounts it and an HTTP server without the bridge, and no deployment of that shape is namedThread-read: 6174784804
Serial constraints cleared: no open PR touches
packages/services/service-i18n/**(the nine open PRs read at this write). The package's last landing is3c1a0467e(#23346, the plugin arms'sendServerFaultconversion), so the dev works on the merged code. This seat's other claims this round (#23354, #23343) are disjoint from this surface.Generated by Claude Code
docs(adr-0076): OQ#9 — correct the partition's i18n and storage entri…
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23347, "status": "done", "branch": "claude/issue-23347-i18n-one-owner", "pr": "https://github.com/objectstack-ai/objectstack/pull/23569", "related_pr": "https://github.com/objectstack-ai/objectstack/pull/23570 (docs-only ADR-0076 OQ#9 correction, branch claude/issue-23347-adr0076-oq9, first line Refs #23347, Tier H; land after #23569)", "session": "session_017WLmd5in9RXu7AjyHR8fKQ (the dispatching PM session's id; this run is its subagent)", "premise_still_valid": true, "summary": "Step 1 was measured on a boot, not inferred. objectstack serve (server on, no --preset) over examples/app-showcase, built from this worktree, composed I18nService, HonoServer, RestAPI and Dispatcher, and its log carried the plugin's 'I18n routes registered: ...' line. The dispatcher answered all 3 paths. Anonymous GET of each: 401 in the dispatcher envelope (UNAUTHENTICATED), X-Request-Id and X-Content-Type-Options both present. Signed-in: labels/showcase_task/zh answered locale zh-CN, and translations/zh answered requestedLocale zh. That is the dispatcher's resolveLocale expansion; the plugin would have echoed zh. So the triage's inferred branch applies. Step 2: I18nServicePlugin's three kernel:ready mounts are retired, and so are the registerRoutes/basePath options that only drove them (zero readers outside the package, positive control held). The plugin still registers the i18n service and the authored-translation sync. A negative pin, ablated red, holds that it mounts nothing. README corrected. After the change the same boot shows the plugin line absent and identical answers on all 3 paths, signed-in included. The ADR-0076 OQ#9 line (i18n and storage entries) is corrected in a separate docs-only draft, #23570.", "measurement": { "composition": "objectstack serve --port 38473 --server --log-level info, cwd examples/app-showcase. OS_DATABASE_URL is a scratch sqlite file, because a production host-config boot gets no default driver (see out_of_scope_findings). OS_SECRET_KEY is a throwaway per-boot key, OS_HOME a scratch dir. The server PID was recorded and killed, and the port freed.", "before_086fa4640": "precondition line PRESENT. Anonymous x3: 401 UNAUTHENTICATED, dispatcher envelope, X-Request-Id 1, X-Content-Type-Options 1. Signed-in: locales 200; labels/showcase_task/zh 200, data.locale zh-CN, 18 labels; translations/zh 200, locale zh-CN, requestedLocale zh, 11 top keys.", "after_6dc743667": "precondition line ABSENT. Anonymous x3 and signed-in x3: identical to before.", "verdict": "The dispatcher answers on all 3 of 3 paths. The plugin registration answered an unauthenticated caller on 0 of 3." }, "tests": "At final HEAD 6dc743667: pnpm --filter @objectstack/service-i18n test gives 'Test Files 4 passed (4) / Tests 57 passed (57)'. typecheck exits 0, and tsc --listFiles shows the program reaches all 4 *.test.ts. build exits 0; dist index.js and index.cjs carry 0 occurrences of 'I18n routes registered'. Ablation of the negative pin, committed implementation first, via scripts/ablation-replace.mjs under an EXIT/INT/TERM restore trap. Mutation: a kernel:ready hook doing ctx.getService('http-server').get('/api/v1/i18n/locales', ...). On disk: anchor 1 to 0, marker 1, blob d64ed0696c7c to e4e109401f6f. Result: RED, 'AssertionError: expected [ server.get /api/v1/i18n/locales ] to deeply equal []' (1 failed, 5 passed). Restored: blob d64ed0696c7c equals the HEAD blob, git diff HEAD empty. The first attempt was a no-op (its replacement re-contained the anchor; the tool refused, 1 to 1, nothing ran) and was re-run once with a fixed replacement. No dist leg: the test imports the plugin by relative source path. Reverse verification of the narrowed type against the rebuilt dist .d.ts: a probe with basePath gives TS2353 'basePath does not exist in type I18nServicePluginOptions'; with registerRoutes, the same TS2353; the control (defaultLocale, fallbackLocale) compiles.", "gates": "Local subset at 6dc743667, derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (10 paths vs merge base 7ffc57846): 83 families, every one exit 0, exit codes captured before any pipe. --ran reconciliation: '83 derived, 83 run, 0 NOT-MEASURED (a DERIVED zero, all 83 recorded an exit code and none of them is 3)'. Three codes come from a re-run after a prerequisite refusal (exit 3, nothing measured): check-plugin-teardown-shape --self-test (fixture commit 621a48760 outside the shallow clone; fetched, then 0), check:dual-build-cjs-loads (8 unrelated packages had no dist; built, all turbo cache hits, then 0), and check:pm-dispatch-gates (re-run once to record its code; fast tier 1841 cases pass). ADR worktree at 189c6449b: 19 derived families, all exit 0; reconciled '19 derived, 19 run, 0 NOT-MEASURED'. Its lint check:doc-formula-expressions first exited 3 (lint not built), then 0. CI on #23569 head 6dc743667, at report time: completed success are Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, filter, the duplicate-claim guards and 'The card this PR closes must claim this branch'. in_progress: Build Core, Check Changeset, every Lint lane, Test Core 1-6/6 and every Type Check lane. Not waited on, per contract.", "line_budget": "No skills/** or governed path in #23569. Changed lines 975 (+158 / -817, 10 files) against the 3000 human-merge threshold: under. #23570: 1 file, +1 / -1.", "files_changed": [ "packages/services/service-i18n/src/i18n-service-plugin.ts", "packages/services/service-i18n/src/i18n-service-plugin.test.ts", "packages/services/service-i18n/src/i18n-route-ledger.ts", "packages/services/service-i18n/src/i18n-route-ledger.conformance.test.ts", "packages/services/service-i18n/src/authored-translations.test.ts", "packages/services/service-i18n/src/error-envelope.conformance.test.ts (deleted)", "packages/services/service-i18n/src/i18n-routes.server-fault-withheld.test.ts (deleted)", "packages/services/service-i18n/README.md", ".changeset/23347-i18n-plugin-mounts-retired.md", "scripts/check-test-source-alias.mjs (outside the claim surface, see deviations)", "docs/adr/0076-objectql-core-tiering.md (in #23570 only)" ], "deviations": [ "One edit outside the claim's file surface: scripts/check-test-source-alias.mjs. The service-i18n registry entry narrows to ['@objectstack/core', '@objectstack/spec'], exactly as the gate prescribes ('registry entry is STALE, no longer unaliased: @objectstack/types'). It is forced: the deleted handler-only fault test was the package's last importer of @objectstack/types, and without the edit check:test-source-alias is red. Declared in the PR body.", "The claim's surface was otherwise kept. The ledger i18n-route-ledger.ts was NOT deleted, although its rows now duplicate the dispatcher ledger's three /i18n rows: its readers sit in packages/client, packages/qa/dogfood, scripts and docs/qa (see out_of_scope_findings). It is re-framed instead: its header says it lists no mount.", "The changeset ships as minor with BREAKING plus a bang (launch-window convention; check-changeset-no-major forbids major). ADR-0087 disposition: not-required (runtime-interface-only ...#I18nServicePluginOptions). check:adr-0087-registration classified it [BREAKING+bang] and verified the symbol.", "The merge commit 6dc743667 (origin/main 7ffc57846 into the branch, before the PR opened, per AGENTS.md section 10) carries the model-free trailer pair. Commits use the AGENTS.md trailer pair, not the harness's model-named one.", "Zero label writes. The dispatch named no label, and skip-changeset does not apply (the diff publishes service-i18n), so the intersection is empty. Labels now on #23569 (documentation, size/l, tests, tooling) were set by the repo's labelers." ], "mcp_calls": "0", "api_writes": "5 REST writes, through 3 fleet-write relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches x3, runs 38662539139, 38662644041 and this comment's run). (1) POST /repos/objectstack-ai/objectstack/pulls, creating #23569 draft. (2) POST /repos/objectstack-ai/objectstack/issues/23569/assignees [os-bill]. (3) POST /repos/objectstack-ai/objectstack/pulls, creating #23570 draft. (4) POST /repos/objectstack-ai/objectstack/issues/23570/assignees [os-bill]. (5) POST /repos/objectstack-ai/objectstack/issues/23347/comments, this report. Bodies read back byte-identical by the relay: 9896/9896 and 3385/3385. Plus git push (not REST): 4 to claude/issue-23347-i18n-one-owner and 2 to claude/issue-23347-adr0076-oq9, the first of each an empty-branch probe.", "harness_parity_answer": "Yes. @objectstack/verify bootStack composes createDispatcherPlugin (harness.ts:1292) and no I18nServicePlugin, so its i18n routes come from the dispatcher bridge. That is now the only owner on os serve too, so the anonymous-deny-i18n dogfood proof covers the same route owner as serve. The remaining difference is the slot's PROVIDER (the in-memory fallback there, the file adapter on serve), not the route owner. It is not a route-ownership gap; packages/verify is untouched.", "open_questions": [ { "question": "Clause-② arm. The claim, the PR body and the changeset say 'Clause-②: no', copied verbatim as instructed. But the diff is a narrowing: a published options interface loses two members, and a dispatcher-less composition loses three routes. clause2-line.mjs's arm for exactly this is 'no (narrowing)'. Breaking-ness is already declared by BREAKING plus the bang, and the ADR-0087 gate judged it breaking. Should the line carry the arm?", "options": [ "A: keep 'Clause-②: no'. Nothing mechanical changes: the gates already read the change as breaking.", "B: amend the claim line, then the PR body and the changeset, to 'Clause-②: no (narrowing)'. The direction is then stated in the closed token rather than only in prose, at the cost of one seat-side body edit and one changeset commit." ], "recommendation": "B, because the arm is the protocol's carrier for 'not widening, but breaking'. The seat writes the claim, and the PR body is the seat's to amend (dev writes it once)." } ], "out_of_scope_findings": [ "class: a · reach: public door os serve. objectstack serve (production, no --dev) on examples/app-showcase with no OS_DATABASE_URL registers zero drivers without a word (the host-config branch at serve.ts step 2: 'Default: dev SQLite ...; prod none'). The boot then fails at AuthPlugin.start with '[ObjectQL] No driver available for object sys_organization'. The ERROR line before it prescribes 'check the datasource behind sys_metadata — connection, credentials, and that the table exists', a datasource that was never declared. Nothing names OS_DATABASE_URL or --dev, against AGENTS.md route rule 3 ('Absence must be loud ... naming the remedy'). Reproduced in this run's first boot. Landing point: packages/cli/src/commands/serve.ts, the shouldAutoRegisterStorageDriver branch when resolveStorageDefinition yields nothing in production. Dedupe words: serve production host config no driver; No driver available sys_organization boot; OS_DATABASE_URL remedy not named; prod none default datasource", "carrier: a sub-issue of #23347 for the seat to file before #23569 merges (承接者 otherwise: 无) · noted, not filed. Ledger consolidation: delete packages/services/service-i18n/src/i18n-route-ledger.ts, which now duplicates the dispatcher ledger's three /i18n rows. Its readers to move to ROUTE_LEDGER: packages/client/src/client-url-conformance.test.ts, route-ledger-response-schema.test.ts and service-route-ledger-coverage.test.ts ('the whole i18n namespace is backed by a ledger row' reads the dispatcher rows); packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts; scripts/cross-package-test-inputs.mjs (two globs entries); scripts/check-route-ledger-census.mjs (its measured duplicate count); docs/qa/platform-checklist/areas/api-backend.json and i18n.json. Same sweep, behaviour-neutral stale prose: serve.ts step 3b comment ('ensures i18n REST routes work out of the box'), packages/client/src/index.ts getTranslations docblock ('service-i18n's autonomous routes'), the rest-route-ledger.ts and runtime route-ledger.ts headers, rest/src/openapi-builtin-paths.ts, and scripts/check-route-envelope.mjs's MODULES row for i18n-service-plugin.ts (0/0/0, still green, no longer a route module). Dedupe words: i18n route ledger duplicate dispatcher; service-i18n ledger consolidation; autonomous i18n routes stale comment", "carrier: #23174 (the plugin-arm withholding card) · noted, not filed. PR #23346's sendServerFault conversion of the plugin's three catch arms is deleted with the arms. On os serve those arms were never reached (measured above), so the conversion protected nothing live. The live i18n door's 5xx posture remains the dispatcher's, under the #5667/#12509 tiering this card did not reopen." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23569 comment 6182072761 ·
domain:servicesseat 1 ·session_017WLmd5in9RXu7AjyHR8fKQ· 2026-10-15T01:13Z. Contract-tier record6182052704: PASS on6dc743667c. Landing waits on every check green at the then-current head. The ADR-0076 line rides #23570 (Tier H), after #23569.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23570 comment 6182116240 ·
domain:servicesseat 1 ·session_017WLmd5in9RXu7AjyHR8fKQ· 2026-10-15T01:17Z. Tier H (docs/adr/**); contract-tier record6182100635: PASS on189c6449b4.needs-user-decisionis on the PR, and review is requested fromos-zhuangandhotlong. It lands after #23569, on an authorised APPROVED review only.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landing note ·
domain:servicesseat 1 ·os-bill·session_017WLmd5in9RXu7AjyHR8fKQ· 2026-10-15T02:09ZFixes #23347)Refs #23347)added_to_merge_queue2026-10-15T01:22Z by the fleet relay →merged2026-10-15T02:09Zadded_to_merge_queue2026-10-15T01:19Z byos-zhuang(after his APPROVED review5526840609) →merged2026-10-15T01:48Zorigin/mainby content249130fb39.i18n-service-plugin.tsholds 0I18n routes registered; the same-file controlwireAuthoredTranslationSyncholds 3..changeset/23347-i18n-plugin-mounts-retired.mdis presentf3fd209b34. ADR-0076 carriesCorrection, 2026-10-15, #23347(1 hit)State change: the card was closed
completedbyFixes #23347. This write clearspm:dispatchedand the assignee.domain:services,area:api,security,target:v18and the type labels stay.Generated by Claude Code