Repository navigation
Description
Problem
bin/sync-models.mjs's policy validation does not actually validate two things it is there to validate, and the failure modes are a silent provider wipe and an unnamed crash.
1. Family-name validation is a plain-object lookup — prototype names pass
loadPolicy (bin/sync-models.mjs:180-192, check at :185):
if (!policy.families?.[family]) {
throw new Error(`sync-models.policy.json: ${name} keeps unknown family "${family}"`);
}policy.families[family] walks the prototype chain. A keep entry of constructor, toString, or hasOwnProperty finds an inherited member (truthy), so validation passes for a family that does not exist in the policy. allowedModelIds (:202-218) then builds new RegExp(policy.families[family]) from that inherited value:
constructor/toString/hasOwnProperty: the resulting regex (the function's source text) matches no real model id, so the allowed set is empty. The sync's removal pass (:503-515) treats an empty allowed set as authoritative: it removes every existing model of that provider ("not in its policy") and adds none. One typo'dkeepentry that happens to be anObject.prototypemember name silently wipes a provider's model list on the next sync, instead of failing at startup like a real typo does.__proto__:policy.families['__proto__']isObject.prototype;new RegExp(Object.prototype)stringifies to the character-class pattern[object Object], which matches most model ids — and thenmatch[1]isundefined(see below), crashing the sync.
A genuinely unknown family (gemini-flah) correctly throws the naming error above, so the validator's intent is clear — it just doesn't own-key check.
2. A family pattern with no participating capture group crashes unnamed
allowedModelIds (:208-210):
const match = id.match(new RegExp(policy.families[family]));
if (!match) continue;
const version = match[1].replace(/-/g, '.').split('.').map(Number);Nothing checks that the family's pattern captures a version in group 1. A pattern with no capture group (^gemini-\d+-flash$), or one whose group is optional and does not participate for a matching id (^gemini-(\d+)?flash$ vs gemini-flash), yields match[1] === undefined and the sync dies with:
TypeError: Cannot read properties of undefined (reading 'replace')
— naming neither the family, the provider, nor the model id.
Evidence (verified this run)
Probe on the laptop (~/git/fleet/probe-2026-10-30-laneA-syncpolicy/probe.mjs), using the same verbatim source-extraction method as bin/sync-models.test.mjs (#704) against the real bin/sync-models.mjs (md5 89c914c87a4b4eb444795ee00e4118a9, identical on sandbox main da42bfe7 and the laptop clone):
CONTROL known family: [ 'gemini-3-flash' ]
CASE keep=constructor: loadPolicy PASSED (no throw) | allowed=[]
CASE keep=__proto__: loadPolicy PASSED (no throw) | allowedModelIds THREW: TypeError: Cannot read properties of undefined (reading 'replace')
CASE keep=toString: loadPolicy PASSED (no throw) | allowed=[]
CASE keep=hasOwnProperty: loadPolicy PASSED (no throw) | allowed=[]
CASE keep=gemini-flah (typo): loadPolicy threw: sync-models.policy.json: p keeps unknown family "gemini-flah"
CASE groupless regex THREW: TypeError: Cannot read properties of undefined (reading 'replace')
CASE optional group THREW: TypeError: Cannot read properties of undefined (reading 'replace')
The shipped bin/sync-models.policy.json is not affected today (all five families are own keys with a participating group 1) — this is about the next policy edit landing wrong.
Why it matters
The policy file is hand-edited (families are added as new model lines appear). The validator exists precisely to catch a bad edit at startup with the provider and family named. Two classes of bad edit currently bypass it: one converts the sync into a provider-wiping run, the other into a stack trace with no pointer to the offending entry.
Acceptance criteria
-
loadPolicyrejects akeepentry that is not an own key ofpolicy.families(e.g. viaObject.hasOwn), soconstructor,__proto__,toString,hasOwnPropertyproduce the same named "keeps unknown family" error as any other typo. - A family pattern that can match without a participating capture group 1 is rejected at load time (named family + provider), or
allowedModelIdstreats a match with no group 1 as a non-match / named error — never an unnamedTypeErrorfrommatch[1].replace. - Tests in
bin/sync-models.test.mjsstyle cover: prototype-named keep entries, a group-less family pattern, and an optional-group pattern against a non-participating id; the shipped policy still loads and the known-family control still selects the newest version. - No behavior change for the current shipped policy.
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 Phase 1 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 the read site — the #2787 embodiment, closed today in id/web); a prototype spelling must fail at startup with this issue's existing naming error, exactly like a genuinely unknown family. This phase lands first of the design's three phases because it is the only instance that destroys data (the provider model wipe). Its item 2 (group-less pattern crash) stays in this issue as filed.dezren39 commentedon Oct 30, 2026
Agent note — triage verdict: CONFIRMED (queue task 3962)
Re-verified independently this shift:
b706a837:loadPolicyinbin/sync-models.mjsstill validates with!policy.families?.[family](prototype-chain lookup, no own-key check), andallowedModelIdsstill dereferencesmatch[1].replace(...)with no group-1 participation check — both exactly as the issue quotes.bin/sync-models.test.mjs): all 8 cases reproduced —constructor/toString/hasOwnPropertypass validation withallowed=[](the provider-wipe setup),__proto__passes then throws the unnamedTypeError, thegemini-flahtypo control throws the named error, and group-less + optional-group patterns throw the unnamedTypeError. Shipped policy is not affected today (all five families are own keys with participating group 1), as the issue states.Follow-up: fix enqueued as queue task 4024 (small_fix, dedupe
repo-health:v1:fix:developing-today/code:issue:2790) carrying the issue's acceptance criteria. This issue stays open until that fix lands.3 remaining items
dezren39 commentedon Nov 1, 2026
Agent note — picked up (queue task 4024)
queue/4024-2790-sync-models-policy-guardsdezren39 commentedon Nov 1, 2026
Agent note — handed off (queue task 4024)
1082585defb68f6e6f5ae868aa922ebbf8dcfb1fnode --test bin/sync-models.test.mjs— 13/13 pass; syntax checks andgit diff --checkpass.dezren39 commentedon Nov 1, 2026
Agent note — done (queue task 4024)
pr_opened; CI remains pending and can proceed without holding a claim.Merge pull request #3489 from developing-today/queue/4024-2790-sync-m…
dezren39 commentedon Nov 3, 2026
agent-note:v1
state: done (merged)
Agent: queue worker repo-worker-code (lane bf419306-c4ef-470f-a553-beabf93239de, OpenChamber scheduled lane)
Task: 7969 — triage #2790 (loop-back coverage pass 2026-11-03)
Outcome: fixed and merged — the existing PR #3489 was updated and merged rather than re-implementing.
e89332c188a306380cd5f9306ef382b5b0e9f767onmain(2026-11-03T13:48:56Z), through the merge queue.mainbefore touching anything:loadPolicystill used!policy.families?.[family](prototype-chain lookup) andallowedModelIdsstill dereferencedmatch[1].replaceunguarded. The earlier queue task (4024) had opened PR fix(sync-models): validate own policy families (#2790) #3489 on 2026-11-01 and completed aspr_openedwith CI pending; this triage resumed it from that checkpoint instead of starting over.mainin (07f0f1dfd). One conflict, in the test file only (this PR's appended tests vs. newer inline-comment-removal and argv-validation tests) — both kept.node --test bin/sync-models.test.mjs— 23/23 pass, including prototype-family rejections (constructor,__proto__,toString,hasOwnProperty), group-less and non-participating-group patterns, and the shipped-policy control.Object.hasOwn(policy.families || {}, family)with a truthiness check inloadPolicy, andif (match[1] === undefined) continue;inallowedModelIds. No shipped-policy or removal behavior change.files/designs/code-own-key-lookups.md); Phase 1 is now landed, so the design owner can tick it.Labels:
task/triageremoved (no live task). Issue closed as completed by the merge.developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 30 (priority/low). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03