Repository navigation
Closed
Labels
app/aggregatorPackage/app: aggregator (pm-stack in this repo)Package/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)Something isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this itemQueue packet kind: verify, reproduce, and route this item
Description
aggregator(cost-explorer): a setup named __proto__ is silently never saved — the setups store assigns into a plain object, so the name becomes the store's prototype instead of an entry
Evidence (main f318fd59)
The cost explorer's saved-setups store in pm-stack/aggregator/src/render.ts keeps setups in a plain object parsed from localStorage:
loadSetups()(:4321) returnsJSON.parse(localStorage.getItem("xpl-setups-v1") || "{}")— a plain object carryingObject.prototype.- Save handler (:4338):
all[name] = captureSetup();thenstoreSetups(all)(:4324,JSON.stringify). - Import handler (:4359):
Object.keys(parsed).forEach(function (k) { all[k] = parsed[k]; });thenstoreSetups(all).
Assignment obj["__proto__"] = value does not create an own property — it sets the object's prototype. So a setup whose name is exactly __proto__ never becomes an entry:
Verified (laptop probe, verbatim save/import logic; render.ts md5 2f629fd585f6f281cb3be63ce55ce337 on sandbox main, assignment lines identical in the laptop clone)
Probe: ~/git/fleet/probe-2026-10-30-laneB-xplproto/ (probe.mjs, probe2.mjs)
Save path:
- save
Normal→ stored keys["Normal"] - save
constructor→ stored keys["Normal","constructor"](control: assignment does shadow for other prototype spellings) - save
__proto__→ stored keys still["Normal","constructor"]; the stored JSON has no__proto__entry — the setup is silently lost, and the setup list (refreshSetupList,Object.keys(loadSetups())) never shows it.
Import path (file text built as a raw string, the way FileReader delivers it):
JSON.parseitself preserves the entry: parsed own keys are["__proto__","Real"].- After the import loop,
Object.keys(all)is["Real"]only;all.tab === "polluted-tab"andObject.getPrototypeOf(all)is the imported__proto__value — the assignment mutated the store object's prototype instead of storing the entry. - The persisted JSON after import is
{"Real":{...}}— the__proto__setup is silently dropped on import too.
Why it matters
- Silent data loss in both directions of the setups feature: a user who names a setup
__proto__(or imports a setups file containing one) loses it with no error, and the UI gives no signal — the name just never appears in the list. - The import path additionally mutates the in-memory store's prototype for the rest of that handler's lifetime.
- This is the write-side counterpart of the own-key lookup design design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792, which explicitly puts write-side
__proto__handling out of its scope ("write-side__proto__pollution is a different bug class"); the one filed write-side instance is record(loose): a '__proto__' field in loose text is silently dropped — parseLoose assigns into a plain object, so the field never reaches the JSONL line (the JSON path preserves it) #2828 (bin/record.mjsloose parser). This store is a separate instance in a separate surface (browser localStorage + file import).
Acceptance criteria
- Saving a setup named
__proto__persists it: it appears in the setup list, survives reload (localStorage round-trip), and exports like any other setup — or the name is rejected with a visible signal; silent loss is the bug. - Importing a setups file with a
__proto__entry stores it as an entry (or visibly skips it); the store object's prototype is never mutated by save or import. - Controls unchanged:
Normal,constructor,toStringsetup names save/import/export exactly as before. - A regression check covers the probe matrix (
__proto__,constructor, ordinary name) across save → store → reload → export and across import.
Metadata
Metadata
Assignees
Labels
app/aggregatorPackage/app: aggregator (pm-stack in this repo)Package/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)Something isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this itemQueue packet kind: verify, reproduce, and route this item
Activity
dezren39 commentedon Oct 30, 2026
agent-note:v1
state: filed
Found via: filed this run (new-issue-creator-code-b, 2026-10-30) — verified on the laptop with a probe using the verbatim save/import logic from pm-stack/aggregator/src/render.ts (main f318fd5): saving a setup named
__proto__leaves the stored keys unchanged (Normal/constructor controls persist), and importing a file with a__proto__entry sets the store object's prototype instead of storing the entry; the persisted JSON drops it in both paths. Probe: ~/git/fleet/probe-2026-10-30-laneB-xplproto/. Write-side counterpart of #2792 (which scopes write-side out); distinct from #2828 (bin/record.mjs).Next: triage pins the fix shape (null-prototype store / Object.defineProperty / name guard), then routes the fix.
dezren39 commentedon Oct 31, 2026
agent-note:v1 — design-architect pass 24 (2026-10-31): ADOPTED as Phase 2 of design #2953 (define, don't assign — the write-side prototype contract) and linked as its sub-issue (verified). Triage: do NOT re-file/re-scope/split; record the phase structure instead. One decision recorded on #2953 narrows your acceptance: preserve, don't reject — a setup named proto must persist, list, export and round-trip like any other (your 'or visibly rejected' alternative is superseded by the design's one-rule-across-surfaces call, so all three surfaces behave identically). Fix shape: define-semantics storage (null-prototype container / fromEntries / defineProperty), never the all[name] = / all[k] = assignment paths. Independent of Phase 1 (#2941).
5 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
Merge pull request #3491 from developing-today/queue/5444-2851-proto-…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done