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 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage: domain:cli → domain:services · p2 → p1 + target:v18 · one card, boot first · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T17:53Z

    Read fresh: readings 6174630808 and the release 6174679976. From source, the dispatcher bridge answers all three i18n paths on os serve. It registers first, in its Phase 2 start(), and the Hono adapter hands a duplicate path to the first registration. That makes I18nServicePlugin's three kernel:ready mounts the dead owner. This is inferred, not measured.

    Route: one card, domain:services (service-i18n), measure-first, then fix.

    1. Boot first. Use the release's discriminator: objectstack serve on examples/app-showcase, with I18n routes registered: … in the log, and three anonymous GETs.
      • Dispatcher answering: 401 in the dispatcher envelope, carrying X-Request-Id.
      • Plugin answering: 200, with neither header.
    2. On the inferred branch (the dispatcher answers): retire the plugin's three mounts, so each path has one owner. If the seat keeps them instead, it states why, and they get the anonymous floor (ADR-0056 D2, authz row anonymous-deny-i18n).
    3. If the boot shows the plugin answering: the dev stops and reports. That is a live anonymous bundle read on os serve (runtime: the /i18n dispatcher domain answers an anonymous caller — handleI18nRequest makes no shouldDenyAnonymous call, unlike every other dispatcher domain (ADR-0056 D2); with an objectui companion for the Console sign-in page #22432 reopened). The card comes back here for a security regrade, and the dispatcher-side follow-up goes to domain:cli.

    Why priority:p1 + target:v18: both outcomes of step 2 narrow a REST door in some composition.

    • Consider a stack that mounts the plugin and its own HTTP server without the bridge.
    • Retire the mounts: that stack loses the routes.
    • Keep them with the floor: that stack's anonymous 200 becomes 401.
    • By the protocol-first rule (filing-gate.md 〈定级判据〉, 「REST/数据门收窄亦算」), the change rides the major in development. security stays.

    Rides this card (acceptance notes from the release):

  2. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    priority:p1High: required for production / M2
    and removed
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 14, 2026
  3. 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

  4. 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

  5. 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

  6. 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

  7. 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:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority: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