Repository navigation
Description
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:
quota-gather-map.ts:99+:146—FILE_EXPORT_WINDOW_SECONDS: Record<string, number> = { "5h": 18000, weekly: 604800 }, read aswindowSeconds: FILE_EXPORT_WINDOW_SECONDS[w.name] ?? nullinsideentryFromFileExport. The window names come from the producer's file-export JSON (latest.jsonwindows).??only catchesnull/undefined, and prototype members are neither.quota-gather-map.ts:263-267+:283—TURSO_NATIVE_RECORDS: Record<string, {unit, limit}>, read asconst native = entry.serviceId === "turso" ? TURSO_NATIVE_RECORDS[w.name] : undefinedinsiderecordsFromEntries. 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 ofFILE_EXPORT_WINDOW_SECONDSgetswindowSeconds: null— includingconstructor,__proto__,toString,hasOwnProperty— and the known keys5h/weeklystill map to 18000/604800. -
recordsFromEntries: a Turso window whose name is not an own key ofTURSO_NATIVE_RECORDSdoes not take the native-record branch (it follows the normal pct/label path or is skipped, as an unknown name does today);databases/storage/syncbehavior unchanged. - Tests alongside the existing quota-gather-map tests pin both sites with
constructorand__proto__window names, asserting the published/serialized shapes (no function/objectwindowSeconds, 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
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 30, 2026
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.hasOwnat 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.11 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
pm-stack/aggregator/src/quota-gather-map.tswithObject.hasOwn(entryFromFileExport + recordsFromEntries), add probe-matrix tests (5h/storage controls + constructor/proto/toString at both sites), PR + CI per fleet policy. aggregator(weights): effortWeight's plain-object lookup returns prototype members as weights — 'effort/constructor' counts as sized and string-poisons every weighted summary #2734 stays untouched (its own packet).dezren39 commentedon Oct 31, 2026
🤖 Agent note · parked (CI pending)
Object.hasOwninpm-stack/aggregator/src/quota-gather-map.ts; probe-matrix tests added at both sites; laptopbun test test/quota-gather-map.test.ts26/26 pass. PR fix(aggregator): own-key guards on quota-gather-map lookups (#2781) #3022 @ d086f61.fixed_merged(merge closes 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).dezren39 commentedon Oct 31, 2026
🤖 Agent note · parked
@codex reviewon the PR this shift. Parked 20m on CI + Codex.chatgpt-codex-connector commentedon Oct 31, 2026
To use Codex here, create an environment for this repo.
fix(aggregator): own-key guards on quota-gather-map lookups (#2781) (#…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done
bun test test/quota-gather-map.test.ts— 26/26 pass.