Skip to content

fix(runtime)!: the /i18n dispatcher domain refuses an anonymous caller, with the console pin moved past the sign-in companion (#22432) - #22496

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-22432-i18n-anonymous-deny
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-22432-i18n-anonymous-deny

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22432
Clause-②: no (narrowing)

Round 2: the pin bump's citation re-measure is in this PR. The seat widened the claim to it in surface extension 6083306429 on #22432, and the previous bump (PR 22412) set the precedent for doing it in one PR. Each of the 55 packages/spec records that asserted the old pin f0268ad78 is re-measured at 47b1f0bb7174, and check:objectui-pin-citations now exits 0. No record's read point died or changed meaning. The diff now reaches non-test packages/spec/src/**, so a contract-tier review is owed before the ACCEPT (by face, contract-review.md).

What changed

packages/runtime/src/domains/i18n.ts#handleI18nRequest now opens with the shared anonymous-deny decision, shouldDenyAnonymous from @objectstack/core (ADR-0056 D2). It is the handler's first statement, in the hoisted form the analytics and security domains use:

  • It runs before the i18n provider is looked up. An anonymous caller gets 401 whether or not a provider is installed, never the 501 an empty slot answers.
  • It runs before any face reads its parameters. An anonymous request that leaves out its locale gets 401, never the 400 the face answers.
  • Every face of the domain answers the dispatcher-wrapper 401 UNAUTHENTICATED and serves nothing of the bundle. A signed-in caller, an API-key caller and an internal system context are served exactly as before. A CORS preflight is unchanged.
  • No new error code and no spec key, so Clause-②: no. No second gate at the dispatcher mount: dispatcher-plugin.ts is untouched.

The console pin moves past the objectui companion in the same PR, through the repo's bump procedure, so the refusal never ships on a Console that reads the domain before sign-in.

This executes ruling 6074960686 on #22146, item 1, verbatim:

  1. The /i18n dispatcher domain gains the domain-level anonymous refusal every other dispatcher domain has (ADR-0056 D2; 401), with an objectui companion: the Console serves its sign-in page from the built-in packs and loads the application's translations after sign-in. The dev measures first whether the Console re-fetches translations once signed in today, and sizes the companion from that reading; the two land together so no signed-in user sees raw keys.

Readings (class level: faces, caller classes, statuses and codes)

A real boot of this branch's build: pnpm dev -- --fresh (the showcase, with I18nServicePlugin auto-registered), at 5b97d08ae. The faces are the locale list, the translation bundle and the field labels.

face caller after
each of the three faces anonymous 401 UNAUTHENTICATED, dispatcher-wrapper envelope, no data
each of the three faces signed-in admin, bearer 200, served
each of the three faces signed-in admin, cookie 200, served
control: the metadata read of a showcase object anonymous 401

The anonymous body is the dispatcher's wrapped envelope, so the gated dispatcher handler is the one answering on this composition. I18nServicePlugin's own mounts of the same paths register later, at kernel:ready, and the first registration wins. The "before" column comes from the ablation below: on the booted showcase, with the gate removed, each anonymous face answered 200.

The Console at the new pin, in a browser (triage's pin)

Chromium (/opt/pw-browsers/chromium, Playwright 1.63.0) against the same boot, served the bumped Console (/_console/, built from objectui 47b1f0bb7174 by pnpm objectui:build). Each run signs in through the Console's own sign-in form, then opens the showcase app and its task list. It was run twice, once in en and once in zh-CN.

run /api/v1/i18n requests signed out signed in raw keys on screen
en 0 6, all 200, each carrying the session (bearer and cookie) none on the sign-in page, after sign-in, the app page or the task list
zh-CN 0 6, all 200, each carrying the session none on the same four screens
  • In zh-CN the task list shows the showcase's own translated object, column and status labels after sign-in. The built-in packs carry no showcase labels, so the post-sign-in load is what put them there.
  • Raw-key scan: every dotted identifier in the visible text whose first segment names a namespace of the Console's built-in pack or the application bundle. Zero hits on all eight screens. Screenshots were reviewed by eye too.
  • From the same browser with no session: an anonymous read of the domain answered 401 UNAUTHENTICATED with no data.

The pin bump (H2)

Range f0268ad78485..47b1f0bb7174: 14 objectui commits, the companion (objectui#12034, PR objectui#12042) is its tip. Written by the procedure, with no hand edits:

  1. scripts/bump-objectui.sh 47b1f0bb7174 --no-commit: .objectui-sha and .changeset/console-47b1f0bb7174.md. The range walked completely: 13 releasing changesets, 0 breaking, so @objectstack/console takes patch.
  2. pnpm objectui:build: the Console built at the pin against this tree. The single-zod canary and the "carries THIS tree's spec" check passed. No objectui commit in the range needed a framework change that is not on main.
  3. node scripts/gen-sdui-manifest-node.mjs: scripts/sdui-manifest.record.json re-recorded at the pin. sdui.manifest.json did not move by a byte (107 components, same sha256).
  4. pnpm gen:sdui-lockstep, with OBJECTUI_ROOT set to the pinned build worktree (its HEAD is the pin): packages/sdui-parser/objectui-lockstep.json re-recorded. check:sdui-lockstep then read the two parser copies byte-identical over 214 grammar lines and agreed on all 25 codes and the containment predicate.

check:sdui-manifest, check:sdui-lockstep and check:console-sha are green at the new pin. The one red is check:objectui-pin-citations, the gap at the top.

The pin citations, re-measured at 47b1f0bb7174

The gate's own procedure, record by record. Each cited objectui file was read at the new pin in a dedicated objectui worktree (the shared checkout's HEAD was not moved), its line numbers re-derived there, and the anchor and the sha moved together. The hop f0268ad78485..47b1f0bb7174 touches 129 paths over 14 commits and deletes or renames none. Two files a record cites changed on it, each for objectui#11865 (a picker drawn with the shared Select):

  • ObjectGrid.tsx, +19/-11: one import line, and the grouped pager's rows-per-page picker redrawn;
  • ListView.tsx, +85/-26: its "Color by field" and rows-per-page pickers redrawn.

Every cited line in those two files moved with its text byte-identical. Every other cited file is byte-identical across the hop.

file records moved (byte-identical, re-pointed) byte-identical files only died or changed meaning
src/ui/component.zod.ts 27 4: the keyboardNavigation read ObjectGrid.tsx:5519 → :5520 (two records); the empty-state message 6529-6530 → 6537-6538 and its draw 6546-6547 → 6554-6555; inlineEditable 1824 → 1825; the case 'tree' arm ListView.tsx:3853-3872 → 3913-3932, :3867 → :3927 23 0
src/ui/component.test.ts 6 0 6 0
src/kernel/functional-completeness.ts 4 4: the case 'calendar' / 'gantt' / 'timeline' / 'map' arms, each byte-identical and moved by 60; the renderers, resolveTimelineDateBinding and resolveListMapConfig unmoved 0 0
src/data/api-methods-batch-conformance.test.ts 1 1: the grid's selection block ObjectGrid.tsx:4867-4894 → 4868-4895, its hash-object still c88443302d40… 0 0
src/ui/view.zod.ts 2 0 2 (the FormField.span record, and its describe, whose sha alone changes) 0
src/ui/dataset.zod.ts, src/ui/action.zod.ts, src/ui/action-outcome-messages.test.ts 1 each 0 1 each 0
the six src/migrations/entries/semantic/18.*-unit-in-key.ts 6 n/a: corpus counts re-counted, see below 0
src/migrations/registry.ts 6 written by pnpm --filter @objectstack/spec gen:migration-registry
content/docs/references/ui/view.mdx written by pnpm --filter @objectstack/spec check:generated --fix, which regenerated only the one artifact it proved stale (check:docs)
  • The counts the records carry re-read the same: the keyboardNavigation method still finds 15 hit lines against 3 for the control. The kanban counts read 2 / 2 / 11, and objectui still registers no kanban-ui block. The ElementDataSourceGate shells read 0 / 3 / 3 / 3 / 4.
  • The six migration entries: their corpus counts were re-taken with git grep -o -F (the method reproduces every f0268ad78485 number).
    • The corpus is now 8281 tracked files.
    • Every token an entry counts as zero still reads zero: none of them occurs on a line the hop adds or removes. For three entries, a full count of every token at both pins finds no zero that turned non-zero.
    • The controls moved with the corpus: objectstack 17956 → 17980, @objectstack/spec 7522 → 7523, useState 2622 → 2630, timeout 1658 → 1674, window 4430 → 4449, metrics 404 → 455. period, interval, TTL, tenant, RuntimeConfig, resourceLimits, Span and SpanSchema are unchanged.
  • --verify-anchors at 47b1f0bb7: exit 0. "55 asserting objectui pin citation(s) match .objectui-sha (47b1f0bb7), 144 historical citation(s) recorded and not checked, across 1932 spec source(s). 13 anchor content assertion(s) verified against objectui at 47b1f0bb7; 502 file:line anchor(s) seen."
  • .changeset/objectui-pin-citations-47b1f0bb7174.md: @objectstack/spec patch, in the precedent's shape.
  • ⛔ No schema shape, key, describe text (beyond the span describe's sha) or behaviour changes.

Pins

  • Runtime unit pin, per face: packages/runtime/src/domains/i18n-anonymous-deny.test.ts (52 cases).
    • Anonymous gets 401 in both shapes the dispatcher produces (an unresolved context and the guest envelope). The test asserts code, status and the message, and that no bundle key and no data ride on the body.
    • The provider is never consulted: neither the slot lookup nor any provider method runs.
    • A missing locale and an empty slot still get 401. An unknown sub-path and a write verb get 401 too, so a face added later arrives behind the floor.
    • Signed-in control: a member is served exactly the body as before, a system context passes, an empty slot still answers a member 501, a member's missing locale still gets 400, and a CORS preflight stays outside the floor.
  • Booted proof: showcase-anonymous-deny-surfaces.dogfood.test.ts gains the three mounted faces. Each is driven anonymously (401, dispatcher-wrapper family, no bundle key) and by a signed-in member (200, the showcase's own zh-CN bundle and labels). Each anonymous body is classified into exactly one envelope family. The file claims the new matrix row.

Translation-flip sweep

Every pin and prose line that held "the domain answers without a session", all in this PR:

  • Six runtime suites drove the domain with no identity, as the smallest context that compiled. They now carry a signed-in caller: AUTHED_CALLER, a SIGNED_IN context, or the dispatcher's resolution seam stubbed the way the analytics cases already do. Only identity moved, and every expectation is unchanged. The suites: http-dispatcher (20 calls), domain-handler-registry (2), http-dispatcher.multi-tenant-concurrency (1), i18n-success-envelope.conformance (6), i18n-supported-locales (2) and error-envelope.conformance (1).
  • http-dispatcher.multi-tenant-concurrency.test.ts called the domain "anonymous-reachable (no shouldDenyAnonymous gate)". Each tenant kernel now carries a tagged auth slot that signs the request in. The probe is still the i18n read behind the floor, and the suite still parks request A inside its own identity resolution.
  • The anonymity half of each flipped pin carries weight in the two new pins above: status, code, nothing served.
  • content/docs/permissions/system-context.mdx, row 51 (the anonymous-deny seams on the domain dispatchers) now names handleI18nRequest. The page's census counts were regenerated by pnpm gen:system-context-census (the new floor reads isSystem).
  • Searched and found nothing to flip: content/docs/**, skills/**, @objectstack/client, the route ledgers (the domain's rows are sdk, not public), and the CLI. http-conformance authenticates every request with a stub session, so its 501 probe is unchanged.

Ledgers (H4)

  • New enforced row anonymous-deny-i18n. It covers a GATE_PIN key on the domain's shouldDenyAnonymous call (i18n:domains/i18n.ts:anonymous-gate) and the dispatcher-domain key dispatcher-domain:route-ledger.ts:/i18n, the pairing the analytics row makes. Its cited proof is the booted file above.
  • authz-ledger-population.baseline.ts: the /i18n key leaves. MAX goes from 31 to 30, with a dated note. The population pin lists it as classified.
  • Probe census, re-derived from deriveProbeFileCensus() on the merged ref:
    • PROBE_TABLE moves from 19 / 14 / 18 to 20 / 15 / 20, and MATRIX_HEADER_PROBE_CLAIM from 19 to 20. A new PROBE_FILE_CENSUS row covers domains/i18n.ts: gate pin, population 1, blind spot 0, controls shouldDenyAnonymous( 1 and handleI18nRequest 3.
    • The census's handler-name class now admits digits. Read letters-only, handleI18nRequest counted as a population of 0 under a reach of 1. No other row's count moves (re-derived).
    • The dispatcher-domain (route-ledger.ts) row note now reads 21 domains, 7 classified, 14 baselined, and 9 domain files no probe names. It had read 5 / 16 and 11 files since before /analytics was classified.
  • The matrix header figures are re-measured from the rows: 20 probes over 15 files; 39 ledger keys, 9 classified and 30 baselined; 44 of 54 rows carry no covers (10 rows, 20 keys); 38 of 47 enforced rows are in-resolver; 7 of the 20 keys are gate pins.

Reverse verification (from the committed fix)

The mutation replaced the gate's condition with a never-true comparison against a planted literal, through scripts/ablation-replace.mjs (anchor hit x1, blob changed), inside a script carrying its own EXIT/INT/TERM restore.

  • Source-resolved legs:
    • The runtime unit pin went 32 failed / 20 passed: every anonymous case red, every signed-in control and the preflight case green.
    • authz-conformance.test.ts and authz-probe-blind-spot.test.ts went 7 failed / 85 passed, naming STALE covers … i18n:domains/i18n.ts:anonymous-gate, DEAD PROBE … domains/i18n.ts, and the census control.
  • Dist-resolved leg:
    • @objectstack/runtime was rebuilt, and ablation-dist-preflight found the marker in 2 built files.
    • showcase-anonymous-deny-surfaces went 7 failed / 69 passed: the 3 anonymous faces (each read expected 200 to be 401), their 3 envelope classifications and the shared code-and-message case. All 3 member controls stayed green.
  • Restore: blob equal to HEAD, git diff HEAD empty. Rebuilt; the marker is absent from all 6 built files, and the tree is clean.
  • Direction: red as predicted, in every leg.

Tests and gates

  • @objectstack/runtime at 5b97d08ae: the full suite gave 349 files, 5681 passed, 19 skipped. typecheck exit 0: tsc --noEmit plus check:test-typecheck, whose program holds 7 of the 7 touched test files (counted with --listFiles).

  • @objectstack/dogfood at 5b97d08ae: typecheck exit 0, with 6 of the 6 touched files in the program. The WHOLE suite, through the verify lock: 236 files (234 passed, 1 skipped, 1 failed), 1872 tests (1861 passed, 9 skipped, 2 failed), exit 1. The 2 failures are both in external-import-destructive-remedy.dogfood.test.ts, each Test timed out in 5000ms, while unlocked gate scripts ran beside it on the shared box. That file never touches the /i18n domain. Re-run alone through the lock at the same head: 4 of 4 passed, exit 0.

  • pnpm lint (the full run) exit 0 at 5b97d08ae.

  • node scripts/pm/dispatch-gates.mjs --commands derives 107 commands at 5b97d08ae (the dispatch's 82 plus the docs and scripts families this diff reaches). All 107 ran at 5b97d08ae, each exit code captured before any pipe. 106 exited 0. The one red is check:objectui-pin-citations (exit 1), the gap at the top. --ran reconciles 107 derived, 107 run, 0 NOT-MEASURED (a derived zero: every line carries its exit code).

  • Round 2, at e7c00d422, after merging main (e148ca984) in, without conflicts or a generated-artifact deferral:

    • dispatch-gates --commands derives 128 commands; all 128 exit 0, check:objectui-pin-citations included. --ran reconciles 128 derived, 128 run, 0 NOT-MEASURED. Two of them, check:console-sha and check:console-injection, skipped for want of a Console dist in the fresh worktree: NOT MEASURED locally this round. Both were measured green in round 1 at the same pin, and CI's Console Pin Gate is green on e7c00d422.
    • @objectstack/spec: test 630 files / 18798 passed; typecheck exit 0; check:generated reports all 15 artifacts up to date.
    • @objectstack/runtime: the full suite 349 files / 5680 passed / 19 skipped (re-run because the merge touched packages/runtime/src).
    • Dogfood: the authz pair plus the booted proof, 3 files / 168 passed. The WHOLE suite, through the verify lock (re-run because the merge changed dogfood inputs: the verify harness now boots what serve composes): 236 files (235 passed, 1 skipped), 1871 tests (1862 passed, 9 skipped), exit 0.
    • Typecheck: @objectstack/runtime and @objectstack/dogfood both exit 0. A first pass red on both with TS7016 "Could not find a declaration file", because it ran while an unlocked gate (check:type-check-debt, its --re-measure) rebuilt the workspace dist/. Re-run through the lock after that: exit 0.
    • pnpm lint (the full run) exit 0 at e7c00d422.
  • Round 3, at d4357cac5, after merging main (ee8751d41) in (merge 6656a940d), no new behaviour:

    • One text conflict, in content/docs/permissions/system-context.mdx, on the census line both sides had moved. main (fix(service-storage,plugin-audit,plugin-security)!: the attachment and comment parent gates judge a controlled_by_parent parent through its master #22513) brought row 9b and its own +1 to the counts; this branch kept its row-51 anchor packages/runtime/src/domains/i18n.ts#handleI18nRequest. The counts were then re-derived by pnpm gen:system-context-census: 123 reads in 58 files, living in 105 symbols. registry.ts text-merged, and gen:migration-registry rewrote it byte-identical.
    • main's 4e9fe9ff6 (PROTOCOL_VERSION 17 → 18, feat(spec)!: PROTOCOL_VERSION 17 → 18 in an ordinary PR — regenerated spec-changes.json and upgrade guide, ^18 handshakes, pre-mode lockstep exception (#22085 Q1 → B) #22215) began rendering the 18.* semantic entries into packages/spec/spec-changes.json and docs/protocol-upgrade-guide.md. Six of them carry this PR's re-measured pin readings, so both artifacts read stale on the merged tree: check:spec-changes, check:upgrade-guide and check:generated each exit 1. They were regenerated by gen:spec-changes and gen:upgrade-guide in d4357cac5: +12/-12 and +6/-6, the six entries' rationale text only.
    • dispatch-gates --commands derives 128 commands, and all 128 exit 0 at d4357cac5. --ran reconciles 128 derived, 128 run, 0 NOT-MEASURED. check:console-sha and check:console-injection again skipped for want of a Console dist, so they are NOT MEASURED locally.
    • @objectstack/spec at d4357cac5: test 631 files / 18833 passed. @objectstack/runtime at 6656a940d: 349 files / 5680 passed / 19 skipped. Dogfood at 6656a940d: the authz pair plus the booted proof, 3 files / 168 passed. The WHOLE dogfood suite: 237 files (236 passed, 1 skipped), 1881 tests (1872 passed, 9 skipped), exit 0. The typecheck of spec, runtime and dogfood exits 0. d4357cac5 touches only the two generated files, and neither the runtime suite nor the dogfood suite reads them.
    • The contract face did not move: git diff over packages/spec, with the generated registry.ts and spec-changes.json excluded, hashes 921af3c8abc9… both at e7c00d422 (against e148ca984) and at d4357cac5 (against ee8751d41); the seat re-computed both. The contract review 6086141672 therefore still covers this PR's contract face.
  • Round 4, at a252adcef, after merging main (f782f1764, which carries ce78ff7bc, the ci: the required Temporal Conformance job pulls postgres:16 and mysql:8.0 from Docker Hub unauthenticated, and Docker Hub's pull rate limit now fails it before any test runs, so the merge queue ejects every pull request #22541 CI fix) and regenerating spec-changes.json and the upgrade guide through their generators:

    • CI on a252adcef: 34 success and 1 roster skip (Packed-tarball smoke), Temporal Conformance included.
    • The contract face is identical at all three reviewed heads (e7c00d422, d4357cac5, a252adcef), each diffed against its own merge base over packages/spec with the generated registry.ts and spec-changes.json excluded. The seat measured it three ways: --full-index e66dc13a8567…; with the index lines dropped, 926808406aae…; with --abbrev=10, 48d27a3badea…. The round-3 figure 921af3c8… is the same diff under a shorter blob abbreviation.
    • Against main 86bf9ed7d5, a plain three-way text merge of spec-changes.json and the upgrade guide (as the merge queue does it) has no conflict: this PR's hunks and main's are disjoint.

File surface beyond the claim, declared

  • content/docs/permissions/system-context.mdx: one anchor on row 51, plus the census counts its generator rewrote. check-system-context-census is red without it, because the floor reads ExecutionContext.isSystem. I read it as the claim's docs bullet (a hand-written page stating which domains hold the anonymous floor). The seat accepted that reading and declared the page on [PM seat] domain:devx @ objectstack — ⏳ vacant #6023 (6083343996).

  • The six runtime suites under "Translation-flip sweep" sit beside the handler under test, and the dispatch's sweep clause ordered them flipped.

  • packages/spec (the 15 files whose records cited the old pin; anchors and shas only), the generated content/docs/references/ui/view.mdx, and .changeset/objectui-pin-citations-47b1f0bb7174.md: the seat widened the claim to the pin bump's citation re-measure in 6083306429, and declared it to domain:spec on [PM seat] domain:spec · seat 2 — 🟢 marchtian · session_016njDy8ozy9B9Ns5Y8kAWEK #18549 and [PM seat] domain:spec — 🟢 os-project-manager · session_01S3aAf11JjbW1mSGL1EhfFj #6017.

  • packages/spec/spec-changes.json and docs/protocol-upgrade-guide.md (round 3): generator output only. Both were re-derived on the merged tree from the six claimed 18.*-unit-in-key.ts entries, once main's protocol-18 bump began rendering those entries. No source edit.

Acceptance notes

  • The objectui starter gap, recorded, not fixed here (pointer 6077215779 on the card, seat reading 6078635289). objectui's examples/console-starter loads translations with a bare fetch and signs in in place. Once this lands, a starter user sees untranslated application labels after sign-in until a reload. No raw key is shown: the readers fall back to the authored literal. objectui's seat owns any starter card.
  • The cloud row: cloud's objectui pin must carry 47b1f0bb7174 no later than its framework pin carries this refusal. That row on objectstack-ai/cloud#2709 belongs to the landing seat.
  • Stale figures, corrected here because the touched rows own them: the matrix header still read 52 rows, 8 rows with covers, 17 keys and 45 enforced after PR 22446 (53 / 9 / 18 / 46 measured at the base). The census's dispatcher-domain note had read 5 / 16 and 11 files since /analytics.
  • Observation, not filed: I18nServicePlugin.registerI18nRoutes mounts the same three paths with no anonymous floor of its own. In every in-repo composition (os serve, os dev, DevPlugin) the dispatcher's gated mounts register first and answer; this boot measured that. A host that composes I18nServicePlugin on an HTTP server without the runtime dispatcher plugin was not measured. It is listed in the card report for the seat, with the ADR-0138 everything-else re-measure as its natural carrier.
  • Documentation drift, not touched: the anonymous-deny.ts docblock in @objectstack/core still says "the five runtime domains", and the table header in packages/runtime/src/endpoint-policy.ts is in the same state. Neither names /packages, /analytics or now /i18n. Carrier: none.
  • QA checklist, not extended: access-security.anonymous-deny-surfaces does not gain an i18n variant, and the i18n.json item's server-truth step reads the three faces without saying it signs in first. A runner who reads them anonymously now gets 401. Carrier: none.
  • Disclosure: function level throughout, as the ruling requires.
  • The changeset is minor with the BREAKING banner, the launch-window convention for a door narrowing. ADR-0087 disposition: not-required (no-migration-prescription).

Generated by Claude Code

claude added 4 commits October 9, 2026 12:00
… first

handleI18nRequest opens with the shared anonymous-deny floor
(shouldDenyAnonymous, ADR-0056 D2) ahead of the provider probe and every
route, in the hoisted form the analytics and security domains use.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
The anonymous-deny-i18n row covers the domain's gate pin and its
dispatcher-domain key, which leaves the shrink-only population baseline
(31 to 30). The probe census and the matrix header are re-derived on the
merged ref, the booted showcase proof drives every mounted face, the
system-context page anchors the new elevation read, and the changeset
declares the narrowing.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
Six runtime suites drove the /i18n domain with no identity, as the
smallest context that compiled. The domain stands on the anonymous-deny
floor now, so they carry a session (or the dispatcher's resolution seam
is stubbed, as the analytics cases already do); only identity moves and
every expectation is unchanged. The multi-tenant suite's probe comment
no longer calls the domain anonymous-reachable.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
…gn-in translations companion)

Written by the pin-update procedure, no hand edits:
scripts/bump-objectui.sh 47b1f0bb7174 --no-commit (the pin and the
@objectstack/console changeset), pnpm objectui:build, then
node scripts/gen-sdui-manifest-node.mjs (the manifest record; the
manifest bytes did not move) and pnpm gen:sdui-lockstep against the
pinned objectui worktree (the lockstep record).

objectui@47b1f0bb71748a7d16f36edecc50059367d2e35a

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/runtime, @objectstack/sdui-parser, @objectstack/spec, touching 15 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json, packages/spec/spec-changes.json, packages/spec/src/kernel/functional-completeness.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via DatasetMeasureSchema (symbol, a top-level const))
  • content/docs/permissions/system-context.mdx (via handleI18nRequest (symbol, a top-level function))
  • content/docs/protocol/objectui/layout-dsl.mdx (via ComponentPropsMap (symbol, a top-level const object))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PageTabsProps (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-3.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-5.mdx (via ComponentPropsMap (symbol, a top-level const object))
  • content/docs/releases/v17/17-7.mdx (via ComponentPropsMap (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/sdui-parser/objectui-lockstep.json, packages/spec/spec-changes.json, packages/spec/src/kernel/functional-completeness.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f782f1764410dcf7ac80108c3526eab4043032d7 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bbdfbc77b2abc5309ab5c33b1c3c9344bf0298b8 — the merge of head a252adcef18e6e99d9b51f13af2d746bf6de6d07 into base f782f1764410dcf7ac80108c3526eab4043032d7, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bbdfbc77b2abc5309ab5c33b1c3c9344bf0298b8 && git checkout bbdfbc77b2abc5309ab5c33b1c3c9344bf0298b8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f782f1764410dcf7ac80108c3526eab4043032d7 a252adcef18e6e99d9b51f13af2d746bf6de6d07 && git checkout -B drift-repro f782f1764410dcf7ac80108c3526eab4043032d7 && git merge --no-ff a252adcef18e6e99d9b51f13af2d746bf6de6d07

node scripts/docs-audit/affected-docs.mjs --json f782f1764410dcf7ac80108c3526eab4043032d7

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f782f1764410dcf7ac80108c3526eab4043032d7 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Every record that asserted the old pin f0268ad78 is re-read at the new
pin by the gate's procedure: the cited objectui file read at 47b1f0bb7,
its line numbers re-derived there, and the anchor and the sha moved
together. Nine records cite ObjectGrid.tsx or ListView.tsx, whose cited
lines moved with their text byte-identical; the other 34 cite only
files byte-identical across the hop. The six migration entries'
corpus counts are re-taken at the new pin; registry.ts is regenerated
by gen:migration-registry.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
…ibe's new pin sha

Written by pnpm --filter @objectstack/spec check:generated --fix, which
regenerated only the one artifact it proved stale.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e7c00d42284f0c7865c5bc1597bf855a2b1e8a95
Local-runs: none

Inputs, and nothing else: card #22432 (its body and all ten comments, 6076094879 through 6085903598), PR #22496 (its body, its 37-file list, and the net diff against main at the merge-base e148ca984, read from the fetched head ref), and the 42 check-runs on the head. Nothing was checked out, built, run or re-run.

① Derived judgments

  1. The /i18n accept-set narrowing in @objectstack/runtime (packages/runtime/src/domains/i18n.ts#handleI18nRequest) — RIGHT. shouldDenyAnonymous({ userId, isSystem, method }) is the handler's first statement, ahead of deps.getService(…) and ahead of every face's parameter read, in the hoisted form domains/analytics.ts:137-143 uses; the refusal is deps.error(ANONYMOUS_DENY_MESSAGE, 401, { code: 'UNAUTHENTICATED' }), the dispatcher-wrapper family. No path is passed, so the control-plane exemption cannot apply (the pathless contract in packages/core/src/security/anonymous-deny.ts), and ALLOW_ROUTES in auth-gate.ts names no /i18n route. OPTIONS passes the predicate and then meets the unchanged m !== 'GET' arm (handled: false), so a CORS preflight is untouched. The guest envelope (assembleExecutionContextOrGuest, packages/core/src/security/assemble-execution-context.ts:397-400) carries no userId, and an unresolved context carries none, so both anonymous shapes the dispatcher produces are denied. This executes ruling 6074960686 item 1 under ADR-0056 D2 exactly as the card's first acceptance line asks: every face, decided once at domain level.
  2. One side-effect of the hoisting — RIGHT, and declared. An anonymous non-GET verb or an unknown sub-path, which previously fell through as handled: false, now answers 401 (pinned in the new unit file as "domain-wide, not per route"). A signed-in caller on those paths is unchanged. That is the "a face added later cannot arrive ungated" property the ruling's per-domain wording asks for.
  3. Public surface of @objectstack/runtime — unchanged. handleI18nRequest is not on packages/runtime/src/index.ts; the _context → context rename changes no type. No new error code and no spec key: Clause-②: no (narrowing) is the right reading.
  4. The console pin .objectui-sha f0268ad78485 → 47b1f0bb7174 — RIGHT. origin/main (4e9fe9ff6) and the merge-base both still carry f0268ad78485, so the move is forward, past the companion objectui#12034 (the tip of the range in .changeset/console-47b1f0bb7174.md). The spec seat's ordering note (6083810549) is answered by 6083858677: chore(objectui): bump the console pin past the fix for objectstack-ai/objectui#12056 (the designer's repeater row writes dataSource). #11509's v18 retirement must ship with it #22476 is blocked behind this PR and bumps further afterwards. The refusal lands on no pin that predates the companion, as triage 6077783090 required.
  5. Generated bump outputs — RIGHT. scripts/sdui-manifest.record.json (objectuiSha, modulesRoot) and packages/sdui-parser/objectui-lockstep.json (rev, revDate, recordedAgainstPin) carry the new pin and nothing else; sdui.manifest.json is not in the diff, consistent with the PR's "same sha256". objectui-lockstep.json is outside @objectstack/sdui-parser's published files (dist, README.md, CHANGELOG.md), so no changeset is owed for it. The file surface matches the precedent bump bf492c854 (PR chore(objectui): bump the console pin to f0268ad78485 (carries objectui#11880) #22412) one for one. CI Console Pin Gate is green on this head.
  6. packages/spec/src/**, 15 files — the governed face that makes this review owed — RIGHT. Read hunk by hunk: every change is inside a comment block or a JSDoc, plus three classes of shipped string — the FormField.span describe in view.zod.ts:3376 (sha f0268ad78485 → 47b1f0bb7174, no other character), and the six 18.*-unit-in-key.ts migration description strings (pin sha and corpus counts), with registry.ts regenerated to match. No key, default, enum member, describe wording, refinement or export moves; the dev's round-2 check:api-surface reads "unchanged", and CI Type Check · source gates (the job that carried the check:objectui-pin-citations red at 5b97d08ae, per 6083306429) is green at this head. The asserting spelling .objectui-sha = f0268ad78 is gone from every record (the 101 f0268ad78 lines that remain are the dated "At f0268ad78, re-read there" form the gate does not check); the 55 asserting citations now read 47b1f0bb7 (1 + 4 + 6×1 + 6 + 1 + 1 + 6 + 27 + 1 + 2 across the 15 files). The nine re-pointed anchors (ObjectGrid.tsx 5519→5520, 1824→1825, 6529-6530→6537-6538, 6546-6547→6554-6555, 4867-4894→4868-4895; ListView.tsx 3853-3872→3913-3932, 3867→3927) are re-pointed after a re-read with the text byte-identical, which is the path the gate's own verifyAssertion text permits (it forbids re-pointing a read point that moved file or died, and none did). functional-completeness.ts cites its four ListView arms by name, not by line, so its "moved by 60" prose leaves no numeric anchor stale.
  7. packages/qa/dogfood ledgers (a private package) — RIGHT. New enforced row anonymous-deny-i18n covering i18n:domains/i18n.ts:anonymous-gate and dispatcher-domain:route-ledger.ts:/i18n, the /analytics pairing; a GATE_PIN probe on domains/i18n.ts; the /i18n key leaves the shrink-only baseline (LEDGER_POPULATION_BASELINE_MAX 31 → 30, dated); PROBE_TABLE 20 / 15 / 20 and MATRIX_HEADER_PROBE_CLAIM 20; the handler-name class widened to [A-Za-z0-9]+ (no other handler name carries a digit, so no other row moves). The header figures reconcile: 54 rows, 10 with covers (20 keys), 44 without, 47 enforced, 7 gate pins, each the base's measured figure plus this one row. The booted proof showcase-anonymous-deny-surfaces.dogfood.test.ts drives the three mounted faces anonymously (401, the dispatcher-wrapper family only, no translations / locales / labels / 任务 / 标题 on the wire) and as a member (200, the showcase's own zh-CN bundle and labels), and claims the row. CI Dogfood Regression Gate (1/3, 2/3, 3/3) is green. This satisfies the card's third acceptance line: the domain is no longer unclassified.
  8. The runtime unit pin domains/i18n-anonymous-deny.test.ts — RIGHT. Five faces × two anonymous shapes × (plain, locale left out, empty slot), the domain-wide sub-path and verb cases, the preflight case, and the signed-in controls (member 200 served, system passes, member 501 on an empty slot, member 400 on a missing locale), through the real apiErrorResponse builder so error.code is read where the wire carries it. With the per-face unit pin and the booted proof, the card's "reject-path test asserts code and status" line is met.
  9. The translation-flip sweep, six runtime suites — RIGHT. Only the identity moved (AUTHED_CALLER(), SIGNED_IN, the timedResolveExecutionContext stub the /analytics cases already use, signInDispatchCaller, and a tagged auth slot per tenant kernel in the multi-tenant suite); every status, body and handled expectation is unchanged.
  10. content/docs/permissions/system-context.mdx and content/docs/references/ui/view.mdx — RIGHT (prose face, outside this tier's three faces; noted because the diff touches them). Row 51 gains domains/i18n.ts#handleI18nRequest, and the generator's census counts move 121 → 122 reads, 57 → 58 files, 103 → 104 symbols; the seat declared the page on [PM seat] domain:devx @ objectstack — ⏳ vacant #6023 (6083306429). view.mdx is the regenerated describe. CI Lint & Repo Gates and Build Docs are green.
  11. Check-runs on the head: 42, all completed, none failed. 39 success, 3 skipped: the 17:32Z re-triggered PR-Automation run's Auto Label and Check PR Size (both success on the 15:23Z run of the same head) and Packed-tarball smoke (opt-in). A list read at 17:34Z still showed Check Changeset 113943652453 as in_progress; the run itself had concluded success at 17:33:25Z, and the 17:39Z re-read shows all 42 settled.

② Semver level

  • @objectstack/runtime — minor with the BREAKING banner, .changeset/22432-i18n-anonymous-deny.md — RIGHT. AGENTS.md §3: (narrowing) is BREAKING; scripts/check-changeset-no-major.mjs is the launch-window guard under which a breaking change ships as minor and major is refused. The changeset carries exactly one ADR-0087 marker, not-required (no-migration-prescription), on a fact pattern that fits it (nothing authorable is removed or renamed; what narrows is which callers are served), the consumer prescription for an own client (send the session, bearer or API key; render pre-sign-in strings from the client), and the Clause-②: no (narrowing) line the gate reads. CI Check Changeset is green.
  • @objectstack/console — patch, .changeset/console-47b1f0bb7174.md — RIGHT: 13 releasing changesets in the range, 0 breaking, written by scripts/bump-objectui.sh.
  • @objectstack/spec — patch, .changeset/objectui-pin-citations-47b1f0bb7174.md — RIGHT: shipped text only (the describe's sha, the migration descriptions' sha and counts), Clause-②: no, in the precedent's shape (.changeset/objectui-pin-citations-f0268ad78485.md).
  • Nothing else publishes: @objectstack/dogfood is private, and the lockstep JSON ships in no package. No skip-changeset is claimed and none would be right.
  • The Clause-②: line reads no (narrowing) on the PR body, on the claim (6080345533) and in the runtime changeset — consistent with a diff that adds no key to any published payload and narrows an accept set.

③ Boundary flags

Round-1 os-dev-report 6083216104:

  • open_questions[0], who re-measures the 55 citations and where — ANSWERED by the seat with option A (6083306429) and executed in round 2; check:objectui-pin-citations and --verify-anchors exit 0 (6085903598), CI green. Closed.
  • deviations — the packages/spec fence (resolved by the surface extension); system-context.mdx beyond the claim (the seat accepted it and declared the page on [PM seat] domain:devx @ objectstack — ⏳ vacant #6023); the six-suite identity flip (judged in ①9: identity only); the bump committed by the dev from the script's unmodified outputs (the claim forbade hand edits, not a dev-authored commit of generated output; the three pin gates are green at the pin); gen:sdui-lockstep run with OBJECTUI_ROOT at the build worktree (the lockstep JSON records rev = the pin, and check:sdui-lockstep reads recorded-against 47b1f0bb7174 = live pin); one small vitest run outside the verify lock, the usage wall and the attribution reminder (process notes with no bearing on the diff); the stale matrix figures corrected inside the claimed files (judged in ①7). All closed.
  • out_of_scope_findings:
    • I18nServicePlugin.registerI18nRoutes mounts the same three paths with no floor of its own (packages/services/service-i18n/src/i18n-service-plugin.ts:162-197, at kernel:ready) — confirmed by reading. It is a different surface from the ruling's "/i18n dispatcher domain"; on the reference composition the booted proof shows the gated mount answering (401, wrapper family). The dev's carrier, the ADR-0138 everything-else re-measure on design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146, is the right one. ESCALATED to the seat: confirm that carrier in the ACCEPT. Not a blocker here.
    • The anonymous-deny.ts docblock ("the five runtime domains") and the endpoint-policy.ts table header omit /packages, /analytics and now /i18n — pre-existing comment drift in @objectstack/core and runtime. ANSWERED: no contract moves; not blocking; a chore carrier is the seat's call.
    • The QA checklist's access-security.anonymous-deny-surfaces has no i18n variant, and the i18n.json server-truth step reads the faces without signing in — docs/qa/** is domain:devx. ANSWERED: outside the claim, not blocking. ESCALATED as a devx carrier so a runner does not read the new 401 as a defect.
    • objectui console-starter keeps untranslated labels after an in-place sign-in (6077215779) — triage moved the card to pm:queue with no Blocked-by: on a starter companion; the seat's deferral (6078635289) read that as land-with-the-gap-recorded, no objection arrived, the PR records it in its Acceptance notes, and the ruling's floor (no raw key on screen) holds there. Closed; objectui's seat owns any starter card.

Round-2 os-dev-report 6085903598:

  • open_questions — none.
  • deviations — the re-measure method (byte-identical files held, the two changed files located by exact block match and hash-object: the gate's own permitted path, judged in ①6); wrapped prose splitting one backticked span (visible in the diff, cosmetic; the gate's self-test covers both wrap positions and the precedent carries the same); the typecheck first pass red while an unlocked gate rebuilt dist/ (re-run green; the CI Type Check jobs are green); the pgrep self-kill and the attribution reminder (process). All closed.
  • out_of_scope_findings — 421 anchors carry no content assertion: informational and pre-existing. Closed.
  • pr_body_replacements — carried into the PR body (the Round-2 top note, the re-measure section, the Round-2 tests). Closed.

PR body, Acceptance notes:

  • The cloud row on objectstack-ai/cloud#2709 (cloud's objectui pin must carry 47b1f0bb7174 no later than its framework pin carries this refusal) belongs to the landing seat per triage 6076094879 and the deferral 6078635289. ESCALATED: attach it before landing, or record that it could not be.
  • Disclosure at function level — holds in the PR body, the changeset and the tests (faces, functions, caller classes, statuses and codes).

Cross-lane on the card: 6083810549 (spec seat 2: ordering hazard and shortcut) — ANSWERED by 6083858677 (devx seat: #22476 waits and bumps past both; not a request to widen) and by origin/main still at f0268ad78485. Closed.

Implemented-by: claude/issue-22432-i18n-anonymous-deny
Reviewed-by: session_01BmsuLyUeuG5CNpZFMH1jzS

VERDICT: PASS


Generated by Claude Code

system-context.mdx: both sides' prose kept (main's row 9b from #22513,
this branch's row-51 anchor packages/runtime/src/domains/i18n.ts#handleI18nRequest);
counts re-derived by pnpm gen:system-context-census.
registry.ts: re-derived by pnpm --filter @objectstack/spec gen:migration-registry
(byte-identical to the text merge).

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
…e merged tree

Main's 4e9fe9f (PROTOCOL_VERSION 17 -> 18, #22215) began rendering the
18.* semantic entries into both artifacts. Six of them carry this branch's
re-measured objectui pin readings, so the merged tree's copies were stale.
Generators only: pnpm --filter @objectstack/spec gen:spec-changes and
gen:upgrade-guide. No source edit.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits October 9, 2026 22:57
…e merged tree (main f782f17)

The os-regen text merge kept this branch's stale copy of main's
flow-value-slot-template-dialect-refused replacement text (list[0]); main's
source entry reads items[0] since 40a6ee5 (#22524). Generators only:
pnpm --filter @objectstack/spec gen:spec-changes and gen:upgrade-guide.

Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 23:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 23:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit b53b949 Oct 10, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22432-i18n-anonymous-deny branch October 10, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants