Skip to content

aggregator(quota-gather-map): plain-object lookups return prototype members as window metadata — a window named 'constructor'/'__proto__' gets a fabricated windowSeconds, and a Turso window with such a name fabricates a snapshot record with no unit/limit #2781

Closed

Description

@dezren39

aggregator(quota-gather-map): plain-object lookups return prototype members as window metadata — a window named constructor/__proto__/toString gets a fabricated windowSeconds, and a Turso window with such a name fabricates a snapshot record with no unit/limit

Evidence (main dccfec44)

Two lookup tables in pm-stack/aggregator/src/quota-gather-map.ts are plain object literals indexed by a window name that comes from an external document, with no own-key check:

  1. quota-gather-map.ts:99 + :146 — FILE_EXPORT_WINDOW_SECONDS: Record<string, number> = { "5h": 18000, weekly: 604800 }, read as windowSeconds: FILE_EXPORT_WINDOW_SECONDS[w.name] ?? null inside entryFromFileExport. The window names come from the producer's file-export JSON (latest.json windows). ?? only catches null/undefined, and prototype members are neither.
  2. quota-gather-map.ts:263-267 + :283 — TURSO_NATIVE_RECORDS: Record<string, {unit, limit}>, read as const native = entry.serviceId === "turso" ? TURSO_NATIVE_RECORDS[w.name] : undefined inside recordsFromEntries. Any truthy hit takes the native-record branch. Window names here come from quota-current entries (themselves built from producer documents).

Same pattern previously filed/fixed as #2734 (aggregator weights) and #2757 (pm-dispatch priority) — different file, different tables, different published artifacts.

Verified repro (laptop probe, real module, md5-matched to main)

Probe: laptop ~/git/fleet/probe-2026-10-30-laneA-gatherproto/ (bun run probe.ts, module md5 e6634146c5e436a18ae130fdfc7f48c0 = sandbox main copy).

entryFromFileExport with windows 5h (control), constructor, __proto__, toString:

FE window "5h"          windowSeconds typeof= number   value= 18000
FE window "constructor" windowSeconds typeof= function value= function Object() { [native code] }
FE window "__proto__"   windowSeconds typeof= object   value= [object Object]
FE window "toString"    windowSeconds typeof= function value= function toString() { [native code] }
FE JSON published: [{"name":"5h","windowSeconds":18000},{"name":"constructor"},{"name":"__proto__","windowSeconds":{}},{"name":"toString"}]

So in the published quota-current document a constructor/toString window silently loses its windowSeconds field (functions are dropped by JSON.stringify; the contract at model.ts:1245 says number | null), and a __proto__ window publishes windowSeconds: {} — a wrong-typed value where consumers expect a number or null.

recordsFromEntries with a Turso entry carrying windows storage (control), constructor (value 42), __proto__ (value 7):

REC storage     unit= gb        limit= 5         value= 2
REC constructor unit= undefined limit= undefined value= 42
REC __proto__   unit= undefined limit= undefined value= 7
REC JSON: [..., {"metricId":"constructor","value":42,"source":"api",...}, {"metricId":"__proto__","value":7,...}]

The prototype hit is treated as a native-unit window: a snapshot record is fabricated whose unit and limit are undefined (dropped in JSON), violating the SnapshotRecord contract (model.ts:1291-1304, unit is a required union member). An unknown window name should fall through to the normal pct path (or be skipped), never invent a native record.

Why it matters

Both inputs are producer documents the aggregator does not control (the file-export convention is explicitly "a producer on the laptop writes latest.json"; quota windows are provider-shaped). One oddly-named window — a provider adding a window literally named e.g. toString, or a corrupt/hand-edited export — silently corrupts the published quota-current document and the append-only snapshot history: wrong-typed/missing windowSeconds, and unit-less snapshot records that downstream rollups cannot interpret. The module's stated philosophy is fail-closed at this boundary; these two lookups fail open via the prototype chain.

Acceptance criteria

  • entryFromFileExport: a window whose name is not an own key of FILE_EXPORT_WINDOW_SECONDS gets windowSeconds: null — including constructor, __proto__, toString, hasOwnProperty — and the known keys 5h/weekly still map to 18000/604800.
  • recordsFromEntries: a Turso window whose name is not an own key of TURSO_NATIVE_RECORDS does not take the native-record branch (it follows the normal pct/label path or is skipped, as an unknown name does today); databases/storage/sync behavior unchanged.
  • Tests alongside the existing quota-gather-map tests pin both sites with constructor and __proto__ window names, asserting the published/serialized shapes (no function/object windowSeconds, no unit-less record).
  • No other behavior change for ordinary window names.

Dedup: #2571 covers entryFromFileExport/entryFromResult percent range checks (different mechanism); #2553/#2568/#2582 are range checks in sibling mappers; #2734/#2757 are the same prototype-lookup pattern in different files/tables. No open or closed issue covers these two tables.

Activity

  1. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/lowNice to have; do when convenient
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-30 10:35 CDT
    • Found via: filed this run
    • Task: Filed after verifying both plain-object lookup sites in pm-stack/aggregator/src/quota-gather-map.ts with the real module on the laptop (probe ~/git/fleet/probe-2026-10-30-laneA-gatherproto/, md5-matched to main dccfec4): prototype-member window names fabricate windowSeconds in entryFromFileExport and unit-less native snapshot records in recordsFromEntries.
    • Expectation: effort/xs — triage pins the own-key guard shape for both tables; fix + tests follow via the queue packet.
    • Links: packet packet-2781-triage.json (hidden_files, goal keep-the-four-repos-healthy)
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — design-architect pass 21 (2026-10-30): this issue is ADOPTED as part of Phase 2 of design #2792 ("own keys only — a plain object is not a map for untrusted keys", doc files/designs/code-own-key-lookups.md). It is linked as a sub-issue of #2792. Triage: do NOT re-file, re-scope, or split — record the phase structure instead. Phase decisions recorded on #2792: the fix is the own-key rule (Object.hasOwn at both read sites — the #2787 embodiment, closed today in id/web); a prototype spelling must behave exactly like a genuinely unknown window name (windowSeconds: null, no native-record branch). Phase 2 pairs this issue with #2734 (same app, same fix idiom, shared probe matrix as tests). Also settled on #2792: this family is NOT in #2576's scope — that design validates document contents; lookup semantics are a different mechanism.

  4. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 30, 2026
  5. 11 remaining items

  6. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked (CI pending)

  8. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  10. chatgpt-codex-connector commented on Oct 31, 2026

    @chatgpt-codex-connector
  11. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  12. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions