Skip to content

spec(i18n-resolver): resolveBundleLocale's exact fast path and pickData's exact read answer an Object.prototype member name (constructor) over a plain-object bundle #30619

Closed

Description

@objectstack-fleet

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Filing gate: ① a defect at named locations, found by the contract review of PR #30601 (card #30538, comment 6665489168, should-fix S6); pre-existing on main, not introduced by that PR. ⛔ Not a claim.

Business reason. resolveBundleLocale(bundle, requested) (packages/spec/src/system/i18n-resolver.ts, the fast path if (bundle[requested] !== undefined) return requested;, :526 at PR #30601's head) and pickData (:593, const exact = bundle[locale]) index a plain object with the requested code. A requested code that is an Object.prototype member name (constructor, toString, __proto__) is found on every plain-object bundle, so the resolver answers that name as a locale and pickData hands back a function instead of undefined. The list-based doors (core's resolveLocale, which after #30601 is resolveLocaleAmong over a key list) are not reached this way; a door that passes a raw request header or query value to resolveBundleLocale is.

Wanted

  • Both reads use an own-property check (Object.hasOwn(bundle, requested)), so an inherited member is never a bundle key.
  • Pins: resolveBundleLocale({ en: {} }, 'constructor') answers what an unknown code answers (the same fallback as 'xx'), and pickData returns undefined for it.
  • Measure which callers pass an unvalidated request value, and say it in the PR.

Dedupe: repo:objectstack-ai/objectstack is:issue is:open resolveBundleLocale prototype → 2 hits (#30538, the alias step; #6015, the triage seat post). New.

Suggested-grade: p3 · a malformed locale code yields a wrong answer, not data exposure; no list-based door reaches it.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p3 · domain:spec · 2026-11-05T18:55Z

    ⛔ Not a claim.

    • A malformed locale code such as __proto__ reads an Object.prototype member in resolveBundleLocale / pickData. It gives a wrong answer and exposes no data. p3 as suggested.

    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

    Claim: PM loop round 8 (p3)
    Session: session_01L6bhgTqqeHSwZYJmApbDCa
    Account: os-sales
    Branch: claude/issue-30619-bundle-own-key
    Worktree: objectstack-issue-30619
    Domain: domain:spec
    Seat: domain:spec#3
    File surface:

    • resolveBundleLocale's exact fast path (:526) and pickData's exact read (:594) in packages/spec/src/system/i18n-resolver.ts. Both become own-property reads.
    • New pins in packages/spec/src/system/i18n-resolver.test.ts.
    • A @objectstack/spec patch changeset, plus any generated artifact the change feeds, regenerated and never hand-edited.
    • ⛔ No signature, export or schema change, and no edit to a caller outside packages/spec.
      Container & model: S, mode:subagent, model: TIER_DEFAULT, effort: medium
      Clause-②: no (expected; no accept set moves, and only an inherited member name stops answering as a bundle key. The dev measures.)
      Metadata-protocol: none (expected; the dev confirms with check-changeset-no-major)
      Responsibility: a locale code that is an Object.prototype member name (constructor, toString, __proto__) is answered as a registered locale, and pickData returns a function as bundle data | both reads index a plain-object bundle with no own-property check | every door that passes a raw request value to resolveBundleLocale. The dev measures which of the seven non-test callers on main do (engine-validation-messages.ts:204, reader-locale-labels.ts:128, activity-summary-message.ts:278, email-service.ts:1512, hook-refusal-catalogue.ts:113, engine.ts:1462, notification-text.ts:353)
      Thread-read: 6666526394
      Premise: holds at origin/main b8929f48. i18n-resolver.ts :526 reads if (bundle[requested] !== undefined) return requested; and :594 reads const exact = bundle[locale];. Neither reads an own property.
      Serial constraints cleared:
    • Of the 36 open PRs (file lists read 21:11Z), none touches i18n-resolver.ts, i18n-resolver.test.ts or platform-locales.ts.
    • domain:spec#2 routed this card to this seat in its alternation message at 21:04Z. Its platform locale table: script-subtag codes (zh-Hant-TW, zh-Hans-CN, zh-Hant-HK) are not read aliases, so a two-script bundle answers them with the first registered zh-* key #30618 adds cases to i18n-resolver.test.ts and does not touch i18n-resolver.ts, so whichever lands second merges main.

    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa
    os-dev-report

    {
      "issue": 30619,
      "status": "done",
      "branch": "claude/issue-30619-bundle-own-key",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30678",
      "head": "a160083be",
      "session": "session_01L6bhgTqqeHSwZYJmApbDCa",
      "premise_still_valid": true,
      "summary": "resolveBundleLocale's exact fast path and pickData's exact read in packages/spec/src/system/i18n-resolver.ts now use Object.prototype.hasOwnProperty.call. The spec tsconfig targets lib ES2020, so Object.hasOwn is not available; this matches the file's existing own-key reads. The !== undefined meaning is kept. The rest of resolveBundleLocale (resolveLocaleAmong over Object.keys) and pickData's last read (bundle[resolved], keyed by an own key) needed no change. A new describe block at the end of i18n-resolver.test.ts pins three cases: prototype-name codes answer as 'xx' does, both in resolveBundleLocale and through resolveViewLabel (pickData is not exported); an inherited-only key is no locale; an own constructor/__proto__ key (JSON.parse, Object.create(null)) still answers. Caller trace: 5 of the 7 outside callers pass an unvalidated ExecutionContext.locale (Accept-Language top tag or REST ?locale=). Each ends with ?? requested, so its answer was the same before the fix. email-service and notification-text pass validated locales. No user-visible sentence changed before or after; the fix is latent-contract hardening.",
      "tests": "head a160083be: vitest src/system/i18n-resolver.test.ts -> Tests 500 passed (500), lock VERDICT command-exit 0. Full spec suite at aa1ad0bc8 -> Test Files 817 passed (817), Tests 25782 passed | 1 todo, VERDICT command-exit 0; the later commit only rewrites one fixture line, and the file rerun passes. Ablation with scripts/ablation-replace.mjs on the committed fix (tests import source, no dist leg). Leg A, fast path reverted: anchor x1->x0, blob 405d8b76->d21038ff, 4 failed | 496 passed (the 3 prototype-name resolveBundleLocale pins and the inherited-key pin). Leg B, pickData read reverted: anchor x1->x0, blob 405d8b76->eb268592, 1 failed | 499 passed (the inherited-key pin). In both legs the restore blob equals the HEAD blob 405d8b76 and git diff HEAD is empty. Direction note: the prototype-name reader pins stay green under leg B, because no exported reader reads a property of Object, Function.prototype.toString or Object.prototype.",
      "gates": "Local, head a160083be. pnpm --filter @objectstack/spec build && typecheck: VERDICT command-exit 0. check:generated: exit 0, all 14 artifacts up to date. A first run flagged a new TS2740 in the test fixture; the fixture was rewritten and the ledger left untouched. eslint --no-inline-config on both changed .ts files: exit 0. check-changeset-no-major --base b3d2f4bdb --event (body declarations): exit 0, LEVEL AXIS a non-protocol change at patch. dispatch-gates --commands --repo objectstack-ai/objectstack: 78 commands, run with exit codes captured before any pipe. 75 exited 0 first time. check:doc-formula-expressions, check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (PREREQUISITE NOT MET, unbuilt dists). After a turbo build of formula, lint and objectql (cache hit) and a full pnpm build (118/118 tasks, VERDICT 0), all three exit 0. dispatch-gates --ran: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. CI check-runs on PR #30678: in_progress at report time, not awaited.",
      "line_budget": "63 changed lines (+60/-3) across 3 files vs the human-merge threshold 3000: under. .changeset 10/0, i18n-resolver.test.ts 41/0, i18n-resolver.ts 9/3",
      "files_changed": [
        ".changeset/30619-bundle-locale-own-key.md",
        "packages/spec/src/system/i18n-resolver.test.ts",
        "packages/spec/src/system/i18n-resolver.ts"
      ],
      "deviations": [
        "Pin for pickData goes through the exported reader resolveViewLabel, as the dispatch allowed. Prototype-name reader pins cannot go red under a pickData-only ablation, because no exported reader observes the returned function. An inherited-key pin (Object.create(bundle)) was added as the ablation-sensitive pin for the pickData read.",
        "PR body carries the harness-requested HTML attribution comment on line 1, per the dispatch; os-dev.md exempts that line.",
        "check:generated first run went red on test-typecheck (TS2740 from a constructor write typed as Function in the new test); fixed in commit a160083be, the ledger was not touched.",
        "No merge of origin/main: it moved 6 commits since base b3d2f4bdb, none touching i18n-resolver.ts, its test or platform-locales.ts, and #30618 had not landed."
      ],
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0. No GitHub MCP calls; GitHub reads went through REST curl",
      "api_writes": "relay writes: 2. (1) fleet-write dispatch pr_create -> PR #30678, run 42118747375, success, body read back identical (11122 bytes). (2) post-stamped comment on #30619 (this report). git push is not a REST write.",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none. Noted, not filed. resolveBundleLocale({ en: {}, fr: undefined }, 'fr') answers 'fr', because step 1 of resolveLocaleAmong finds the key in Object.keys even though the fast path skips its undefined value. Pre-existing and unchanged; recorded in the PR's Acceptance notes.",
        "carrier: none. Noted, not filed. ExecutionContext.locale is the raw Accept-Language top tag or the raw REST ?locale= string with no tag-shape validation (packages/core/src/security/assemble-execution-context.ts:394, packages/runtime/src/security/resolve-execution-context.ts:271). No wrong answer is reachable after this PR, so this is an observation only, in the Acceptance notes."
      ]
    }
  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa
    os-dev-report

    {
      "issue": 30619,
      "status": "done",
      "branch": "claude/issue-30619-bundle-own-key",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30678",
      "head": "fdba0dbd7",
      "session": "session_01L6bhgTqqeHSwZYJmApbDCa",
      "premise_still_valid": true,
      "summary": "Round 1, from contract review 6671371228 (PASS, should-fix S1, adopted). The round-0 census missed the inside caller builtinSystemFieldLabel. It returns entry[resolved] directly, and translateObject assigns any truthy answer as the field label, so before this PR a /meta object read under a constructor or __proto__ locale served the Object constructor or Object.prototype as the label of every injected system field still at its English default. That is the one user-visible wrong answer, and the round-0 fast-path fix already removes it, so no source change was needed (i18n-resolver.ts blob 405d8b76 is unchanged). Round 1 adds a translateObject pin (created_at under 'constructor' and '__proto__' equals what 'xx' gives, a string, 'Created At'). The changeset now names the system-field label. Body r1, which corrects the census (7 outside callers plus 4 inside: pickData, builtinSystemFieldLabel, validation-message, operation-message), the impact sentence, Tests and a round-1 Acceptance note, is written to body-r1.md and dry-run only. The report's earlier 'latent-contract hardening' claim is withdrawn.",
      "tests": "head fdba0dbd7: vitest src/system/i18n-resolver.test.ts -> Tests 501 passed (501), VERDICT command-exit 0. spec build && typecheck && test -> Test Files 817 passed (817), Tests 25783 passed | 1 todo (25784), VERDICT command-exit 0. Ablation leg A rerun on fdba0dbd7 (fast path reverted, scripts/ablation-replace.mjs): anchor x1->x0, blob 405d8b76->d21038ff, Tests 5 failed | 496 passed. Red: the 3 prototype-name resolveBundleLocale pins, the inherited-key pin, and the new translateObject system-field pin. Restore blob 405d8b76 equals the HEAD blob, git diff HEAD empty. Leg B was not rerun: i18n-resolver.ts is unchanged since round 0 (1 failed | 499, the inherited-key pin).",
      "gates": "Local, head fdba0dbd7. spec build/typecheck: VERDICT 0. check:generated: exit 0, all 14 up to date, check:test-typecheck green with the new pin. eslint --no-inline-config on both changed .ts: exit 0. check-changeset-no-major --base b3d2f4bdb --event (body r1): exit 0, LEVEL AXIS a non-protocol change at patch. dispatch-gates --commands: 78 commands, the same list as round 0, run with exit codes captured before any pipe. 75 exited 0 first time. doc-formula-expressions, dual-build-cjs-loads and lean-entry-closure exited 3 (fresh worktree, no dists). After a full pnpm build (118/118, VERDICT 0) all three exit 0. --ran: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derivation printed a STALE TREE note (15 commits behind origin/main, 8 derivation-input files changed upstream); no merge was taken because origin/main moved nothing on i18n-resolver.ts, its test or platform-locales.ts. CI on fdba0dbd7: not awaited.",
      "line_budget": "Net vs base b3d2f4bdb: 3 files, i18n-resolver.ts 9/3 (unchanged in r1), i18n-resolver.test.ts 51/0 (+10 in r1), changeset 10/0 with one sentence extended. Under the human-merge threshold 3000.",
      "files_changed": [
        ".changeset/30619-bundle-locale-own-key.md",
        "packages/spec/src/system/i18n-resolver.test.ts",
        "packages/spec/src/system/i18n-resolver.ts"
      ],
      "deviations": [
        "Pin for pickData goes through the exported reader resolveViewLabel, as the dispatch allowed. Prototype-name reader pins cannot go red under a pickData-only ablation, because no exported reader observes the returned function. An inherited-key pin (Object.create(bundle)) was added as the ablation-sensitive pin for the pickData read.",
        "PR body carries the harness-requested HTML attribution comment on line 1, per the dispatch; os-dev.md exempts that line.",
        "check:generated first run went red on test-typecheck (TS2740 from a constructor write typed as Function in the new test); fixed in commit a160083be, the ledger was not touched.",
        "No merge of origin/main: it moved 6 commits since base b3d2f4bdb, none touching i18n-resolver.ts, its test or platform-locales.ts, and #30618 had not landed.",
        "Round-0 summary and PR-body claim 'no user-visible sentence changed / latent-contract hardening' was wrong (missed builtinSystemFieldLabel); corrected in body-r1.md (not sent; the seat sends it) and the changeset.",
        "body-r1.md first line is the seat signature, as the coordinator specified; the dry-run shows post-stamped keeps it in the stored body, above the attribution comment.",
        "No merge of origin/main despite the dispatch-gates STALE TREE note: origin/main moved no commit on the three named files, per round-1 instruction 4 and AGENTS.md section 10."
      ],
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0. No GitHub MCP calls; reads went through REST curl",
      "api_writes": "relay writes: 3 cumulative. Round 0: pr_create PR #30678 and comment 6670828385. Round 1: this r1 report comment via post-stamped. Body r1 was dry-run only (post-stamped --body=30678 --dry-run, exit 0, nothing written).",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none. Noted, not filed. resolveBundleLocale({ en: {}, fr: undefined }, 'fr') answers 'fr', because step 1 of resolveLocaleAmong finds the key in Object.keys even though the fast path skips its undefined value. Pre-existing and unchanged; recorded in the PR's Acceptance notes.",
        "carrier: none. Noted, not filed. ExecutionContext.locale is the raw Accept-Language top tag or the raw REST ?locale= string with no tag-shape validation (packages/core/src/security/assemble-execution-context.ts:394, packages/runtime/src/security/resolve-execution-context.ts:271). No wrong answer is reachable after this PR, so this is an observation only, in the Acceptance notes."
      ],
      "round": 1,
      "body_r1": "/tmp/claude-0/-home-claude-objectstack/6bdf5ba9-50dc-5ba0-b3c0-b830fe25dfc8/scratchpad/issue-30619/body-r1.md"
    }
  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

    ACCEPT → PR #30678 comment 6672254140 · head fdba0dbd7563d1da25a725806ed887270211b89f. Contract review PASS twice (6671371228 on a160083be, one should-fix landed in round 1; delta 6672224800). Handed to the merge queue with pr_ready + automerge_enable at 2026-11-05T23:31Z.


    Generated by Claude Code

  8. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

    Landed: PR #30678 merged at f2ea1931.

    • resolveBundleLocale's exact fast path and pickData's exact read now accept only a bundle's own keys, so constructor, toString, __proto__ and the other Object.prototype names no longer answer as a registered locale.
    • The one user-visible effect is that an injected system field's label is no longer lost on a /meta object read under such a locale code.
    • No signature, export or schema moved.
    Probe on origin/main after the merge Reading
    git merge-base --is-ancestor f2ea1931 origin/main (tip 447226b22) ancestor
    git grep "hasOwnProperty.call(bundle" in packages/spec/src/system/i18n-resolver.ts 2 lines, :528 (fast path) and :600 (pickData)

    The merge queue landed it at 2026-11-05T23:55:19Z, after seat 2's #30618 (PR #30681). The shared test file merged clean. Its Fixes line closed the card as completed. This seat removes pm:dispatched in the same stroke. Carried: nothing.


    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

Labels

bugSomething isn't workingdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions