Skip to content

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

Closed

Description

@objectstack-fleet

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

What done looks like

  1. 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.
  2. 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.
  3. 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.
  4. ⛔ 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:

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

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · security · priority:p2 · domain:cli · area:api, into pm:queue. Measure which mount answers the three i18n paths first; then one owner per path

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T23:52Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    and removed on Oct 13, 2026
  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    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_01Q9PZ5Pzqcus4woVvTZwWNU
    Account: sunhaolin (the seat's linked user as GET /user answers 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/main through git show and REST; it writes nothing to any checkout)
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    Scope: the maintainer's 「当前任务处理完,后续改为串行派发」, the one serial claim.

    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 composition
    Thread-read: 6157669906
    Serial constraints cleared: none touched, because no file is edited. This seat has no other card in flight.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: domain:cli seat 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, unassigned

    Claim 6174333277 was a READ-ONLY READINGS dispatch, because this card is measure-first (os-dev.md:351). It delivered: readings 6174630808, at origin/main 2811a63138, with no diff, no branch and no PR. This seat has reviewed them.

    What landed (step 1, partly):

    • No record on main answers the two-owner question. Every test that boots and calls an i18n path composes no I18nServicePlugin: @objectstack/verify's bootStack, 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.
    • Inferred from source, for all three paths: the dispatcher bridge answers.
      • It registers in its Phase 2 start() (dispatcher-plugin.ts:1480-:1505).
      • I18nServicePlugin registers the identical patterns in its Phase 3 kernel:ready hook (i18n-service-plugin.ts:172, :186, :201). Neither mount checks for the other.
      • The Hono adapter hands a duplicate path to the first registration: wrap() never calls next (plugin-hono-server adapter.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 serve composes both owners for the showcase (serve.ts:3436-:3455, :4461).
      • The grade is inferred, not measured.

    What remains (the fork):

    1. The boot that settles step 1. It is cheap, but it needs a build and a boot, which a readings dispatch may not do.
    2. Step 2's fix lands in packages/services/service-i18n (domain:services) on the inferred branch. The dead registrations are the plugin's three kernel:ready mounts. Retire them, or make the ownership explicit. If they stay, they need the anonymous floor (see below). By lanes/cli.md's red line, this seat reports the landing and does not re-route the card itself.
    3. Folded here, not filed separately (the readings' class-b finding, the security exception, inferred): 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 row anonymous-deny-i18n).
      • On os serve the plugin's handlers are shadowed (inferred). But they are live in any composition that mounts the plugin and an http server without the bridge.
      • For example: a stack carrying its own HonoServerPlugin, served with --no-server. serve.ts:3436 does not test flags.server, while :4424 does.
      • Retiring the shadowed mounts closes it; keeping them means adding the floor. It is step 2's own fix, so it rides this card.

    Asked of triage (pm:retriage):

    • Route this card, or split it: the boot measurement, and the plugin-mount retirement or floor, to domain:services (service-i18n).
    • Or keep the measurement here as a separate boot dispatch.
    • If the boot shows the plugin answering, the dispatcher-side follow-up is this lane's, and it is also a security reading.

    Acceptance notes:

    This seat's serial slot moves on to the next card in the lane's order.


    Generated by Claude Code

  6. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 14, 2026
  7. 6 remaining items

  8. added and removed
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 14, 2026
  9. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-14T23:36Z
    Session: session_017WLmd5in9RXu7AjyHR8fKQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23347-i18n-one-owner
    Worktree: objectstack-issue-23347
    Domain: domain:services
    Seat: 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 (triage 6174784804), so one CONTRACT_REVIEW_TIER review is owed before the queue ("A card that only NARROWS one is Clause-②: 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 named
    Thread-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 is 3c1a0467e (#23346, the plugin arms' sendServerFault conversion), 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

  10. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  11. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23569 comment 6182072761 · domain:services seat 1 · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T01:13Z. Contract-tier record 6182052704: PASS on 6dc743667c. 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

  12. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23570 comment 6182116240 · domain:services seat 1 · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T01:17Z. Tier H (docs/adr/**); contract-tier record 6182100635: PASS on 189c6449b4. needs-user-decision is on the PR, and review is requested from os-zhuang and hotlong. It lands after #23569, on an authorised APPROVED review only.


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing note · domain:services seat 1 · os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T02:09Z

    Reading PR #23569 (code, Fixes #23347) PR #23570 (ADR-0076 OQ#9 line, Refs #23347)
    Queue membership (timeline) added_to_merge_queue 2026-10-15T01:22Z by the fleet relay → merged 2026-10-15T02:09Z added_to_merge_queue 2026-10-15T01:19Z by os-zhuang (after his APPROVED review 5526840609) → merged 2026-10-15T01:48Z
    origin/main by content 249130fb39. i18n-service-plugin.ts holds 0 I18n routes registered; the same-file control wireAuthoredTranslationSync holds 3. .changeset/23347-i18n-plugin-mounts-retired.md is present f3fd209b34. ADR-0076 carries Correction, 2026-10-15, #23347 (1 hit)

    State change: the card was closed completed by Fixes #23347. This write clears pm:dispatched and the assignee. domain:services, area:api, security, target:v18 and the type labels stay.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2securitytarget:v18

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions