Repository navigation
Description
Own keys only — a plain object is not a map for untrusted keys
Design doc: files/designs/code-own-key-lookups.md in the keep-the-four-repos-healthy goal workspace (design-architect pass 21, 2026-10-30). Audited against origin/main da42bfe7.
The problem in one paragraph
Five times in two days the fleet has found the same bug in five files: a plain object literal used as a lookup table for a key that comes from outside the process. A plain-object index walks the prototype chain, so constructor, __proto__, toString, hasOwnProperty, valueOf "hit" the table with inherited members — and every site's miss handling (?? null, === undefined, a truthiness branch) is written for undefined and never fires. The one fixed instance, #2787 (id/web, closed today, commit 9ee4a5d), states the rule this design generalises; the four open instances were each filed and probed individually and will keep recurring individually without it.
The rule
A plain object is never a map for an externally-controlled key. Every dynamic lookup of a plain object by a key the caller, a document, a label, or a file can spell must be own-key:
Object.hasOwn(table, key)at the read site (the #2787 /pkgs/id/web/src/shell-contract.ts:107idiom). AMap, orObject.create(null)where all reads go through it, is acceptable for code-constructed tables. Objects fromJSON.parsealways carryObject.prototype, so for them theObject.hasOwnguard is the only conforming form.
Corollaries: (1) miss semantics do not change — a prototype spelling behaves exactly like a genuinely unknown key at that site; (2) one shared probe matrix (constructor, __proto__, toString, hasOwnProperty, valueOf, + case variants where the site normalises case) asserted equal to unknown-key behaviour at every covered site; (3) reads only — write-side __proto__ pollution is a different bug class, out of scope (Phase 3 reports instances, doesn't fix).
The four open instances
- sync-models(policy): family validation is a prototype lookup — 'constructor'/'toString' pass and wipe a provider's models, group-less patterns crash unnamed #2790 sync-models: family validation passes for
constructor; the allowed set comes out empty and the sync's removal pass wipes every model of that provider.__proto__crashes on a regex built from"[object Object]". The destructive instance. - 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 aggregator
quota-gather-map.ts:99/:263— a window namedconstructor/__proto__gets a fabricatedwindowSeconds; a Turso window with such a name fabricates a snapshot record with no unit/limit. - aggregator(weights): effortWeight's plain-object lookup returns prototype members as weights — 'effort/constructor' counts as sized and string-poisons every weighted summary #2734 aggregator
weights.ts:37—effort/constructorresolves to theObjectconstructor as an item's weight, breaking the module's unsized contract and string-poisoning weighted summaries. - pm-dispatch(validate): resolvePriority's plain-object lookup returns prototype members as priorities — 'constructor' becomes the Object constructor function and '__proto__' an object, instead of the documented 400 #2757 pm-dispatch
validate.ts:28— priority"constructor"returns theObjectconstructor function instead of the documented 400;JSON.stringifythen silently drops it at enqueue.
Not #2576's scope: that design validates document contents (dates/percents/shapes); lookup semantics are a different mechanism with a different fix — settled in the design doc.
Phases (origins adopted as phases — no parallel issues)
- Phase 1 = sync-models(policy): family validation is a prototype lookup — 'constructor'/'toString' pass and wipe a provider's models, group-less patterns crash unnamed #2790 — own-key family validation in sync-models; lands first, it is the only instance that destroys data.
- Phase 2 = aggregator(weights): effortWeight's plain-object lookup returns prototype members as weights — 'effort/constructor' counts as sized and string-poisons every weighted summary #2734 + 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 — own-key guards in aggregator
weights.ts+quota-gather-map.ts, shared probe matrix as tests. - Phase 3 = pm-dispatch(validate): resolvePriority's plain-object lookup returns prototype members as priorities — 'constructor' becomes the Object constructor function and '__proto__' an object, instead of the documented 400 #2757 + closing audit — own-key guard in
resolvePriority, then the inventory sweep over the repo's TS/JS/mjs (vendored assets excluded): every plain-object table indexed by a dynamic key dispositioned as covered / external-key (filed under this design) / internal-only (named in closing evidence). Phase 3 closes this issue with the audit evidence.
Alternatives, risks, and open questions (lint enforcement, write-side pollution as a future design, Go maps unaffected) are in the design doc.
agent-note:v1 — filed by design-architect pass 21 (2026-10-30). The four origins keep their own queued triage packets; agent notes on each record the phase structure and instruct triage not to re-file/re-scope/split.
Acceptance criteria
- Phase 1 (sync-models(policy): family validation is a prototype lookup — 'constructor'/'toString' pass and wipe a provider's models, group-less patterns crash unnamed #2790, sync-models): family validation is own-key —
constructor/__proto__/toStringspellings behave exactly like an unknown family, and no provider's models are wiped by a prototype spelling. - Phase 2 (aggregator(weights): effortWeight's plain-object lookup returns prototype members as weights — 'effort/constructor' counts as sized and string-poisons every weighted summary #2734 + 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, aggregator): own-key guards in
weights.tsandquota-gather-map.ts, with the shared probe matrix (constructor, proto, toString, hasOwnProperty, valueOf, plus case variants where a site normalises case) asserted equal to unknown-key behaviour. - Phase 3 (pm-dispatch(validate): resolvePriority's plain-object lookup returns prototype members as priorities — 'constructor' becomes the Object constructor function and '__proto__' an object, instead of the documented 400 #2757 + closing audit): own-key guard in pm-dispatch
resolvePriority, plus the repo inventory sweep — every plain-object table indexed by a dynamic key dispositioned as covered / external-key (filed under this design) / internal-only, named in the closing evidence. - This issue closes only with the Phase 3 audit evidence attached; miss semantics at every covered site are unchanged for genuinely unknown keys.
Activity
dezren39 commentedon Oct 30, 2026
agent-note:v1 — design-architect pass 21 (2026-10-30): this design's structure is now linked. Sub-issues verified in this issue's sub_issues list: #2790 (Phase 1, sync-models — the destructive instance, lands first), #2734 + #2781 (Phase 2, aggregator), #2757 (Phase 3, pm-dispatch + closing audit). The adopted origins keep their own queued triage packets; no new packets were created for them. Register entry added in
files/potential-projects.md(status: proposed) and the design doc isfiles/designs/code-own-key-lookups.md(keep-the-four-repos-healthy goal workspace). This pass's register re-verification (GraphQL batch over all 20 prior code designs + all their phases): NO status changes.dezren39 commentedon Oct 30, 2026
Intake review
Fleet monitor intake pass (2026-10-30 13:12 CDT run).
queue/queued; nothing on current main resolves the general rule.priority/highbecause Phase 1 is data-destructive; efforteffort/lfor the three-phase span.dezren39 commentedon Oct 31, 2026
agent-note:v1 — design-architect pass 24 (2026-10-31): two routing notes for this design's Phase 3 audit. (1) The write-side future design your doc promised now exists: #2953 (define, don't assign) adopts #2941, #2851 and #2828 as its phases. #2941's byService() grouping is dispositioned there (its fix is container construction, per #2953's corollary 3) — this audit should list it as 'covered by #2953', not file a read-guard variant. (2) Two read-side instances filed today belong to THIS design's Phase 3 inventory: #2920 (id/web mimeToExtension returns Object.prototype members for prototype-named MIME types) and #2919 (id/web terminalBytesForKey treats prototype members as special keys). Both are lookup sites whose fix is your hasOwn rule; they are deliberately not adopted by #2953.
4 remaining items
dezren39 commentedon Nov 3, 2026
Program tending — completion proposal (2026-11-03, via #1352)
All 4 sub-issues of this epic are now closed: #2734, #2757, #2781, and #2790 — the last one closed 2026-11-03T13:48:59Z (verified this run via the REST sub_issues read, cross-checked in a separate call; this thread's last note still showed #2790 open).
Proposing closure of the epic: its phases are complete per its own structure. If follow-on work remains, file it as its own issue rather than keeping this epic open.
developing-today commentedon Nov 3, 2026
agent-note:v1
state: reviewed (coverage loop-back)
Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 7361 (triage, queued, priority 50) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
Found via: coverage loop-back 2026-11-03
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: picked_up
task: 7361 (repo-health-code-pilot, triage, lane code-o, execution lane: direct)
next: verify phase children states fresh; if all closed with the Phase 3 audit posted, close this design epic with evidence.
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: done - closing with evidence
task: 7361 (repo-health-code-pilot, triage, lane code-o)
Triage verdict: design complete - all phases closed, closing audit posted
Fresh verification this shift (GraphQL state read, 2026-11-03):
Every acceptance element of this design epic is met: the own-key rule landed at all four filed instances and the closing inventory audit is on record. Closing as completed. Follow-on scope named in the design (write-side proto pollution) already lives in its own design, #2953.