Skip to content

sync-models(policy): family validation is a prototype lookup — 'constructor'/'toString' pass and wipe a provider's models, group-less patterns crash unnamed #2790

Closed

Description

@dezren39

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'd keep entry that happens to be an Object.prototype member 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__'] is Object.prototype; new RegExp(Object.prototype) stringifies to the character-class pattern [object Object], which matches most model ids — and then match[1] is undefined (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

  • loadPolicy rejects a keep entry that is not an own key of policy.families (e.g. via Object.hasOwn), so constructor, __proto__, toString, hasOwnProperty produce 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 allowedModelIds treats a match with no group 1 as a non-match / named error — never an unnamed TypeError from match[1].replace.
  • Tests in bin/sync-models.test.mjs style 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    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
    app/sync-modelsbin/sync-models.mjs model catalog sync
    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 (creator lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-30 12:05 CDT
    • Found via: filed this run
    • Task: Triage the sync-models policy-validation gap: prototype-named keep entries bypass loadPolicy and yield an empty allowed set (provider wipe) or an unnamed TypeError; group-less family patterns crash the same way.
    • Expectation: effort/xs — done when loadPolicy own-key-validates family names, group-1 participation is checked (load-time or at use, named), and extraction-style tests cover all three cases with no change for the shipped policy.
    • Links: evidence in the issue body; probe on the laptop at ~/git/fleet/probe-2026-10-30-laneA-syncpolicy/
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    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.hasOwn at 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.

  4. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    Agent note — triage verdict: CONFIRMED (queue task 3962)

    Re-verified independently this shift:

    • Code, current main b706a837: loadPolicy in bin/sync-models.mjs still validates with !policy.families?.[family] (prototype-chain lookup, no own-key check), and allowedModelIds still dereferences match[1].replace(...) with no group-1 participation check — both exactly as the issue quotes.
    • Behavior, laptop probe re-run (source-extraction method, same as bin/sync-models.test.mjs): all 8 cases reproduced — constructor/toString/hasOwnProperty pass validation with allowed=[] (the provider-wipe setup), __proto__ passes then throws the unnamed TypeError, the gemini-flah typo control throws the named error, and group-less + optional-group patterns throw the unnamed TypeError. 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.

  5. 3 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Agent note — picked up (queue task 4024)

  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Agent note — handed off (queue task 4024)

  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Agent note — done (queue task 4024)

  9. added a commit that references this issue on Nov 3, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    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.

    • Merge: PR fix(sync-models): validate own policy families (#2790) #3489 → merge commit e89332c188a306380cd5f9306ef382b5b0e9f767 on main (2026-11-03T13:48:56Z), through the merge queue.
    • Was the bug still live? Yes, verified on current main before touching anything: loadPolicy still used !policy.families?.[family] (prototype-chain lookup) and allowedModelIds still dereferenced match[1].replace unguarded. The earlier queue task (4024) had opened PR fix(sync-models): validate own policy families (#2790) #3489 on 2026-11-01 and completed as pr_opened with CI pending; this triage resumed it from that checkpoint instead of starting over.
    • Branch update: the PR branch was 1711 commits behind; merged main in (07f0f1dfd). One conflict, in the test file only (this PR's appended tests vs. newer inline-comment-removal and argv-validation tests) — both kept.
    • Targeted tests: 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.
    • Diff at merge: exactly the two guards — Object.hasOwn(policy.families || {}, family) with a truthiness check in loadPolicy, and if (match[1] === undefined) continue; in allowedModelIds. No shipped-policy or removal behavior change.
    • Design: this issue was adopted as Phase 1 of design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792 (files/designs/code-own-key-lookups.md); Phase 1 is now landed, so the design owner can tick it.

    Labels: task/triage removed (no live task). Issue closed as completed by the merge.

  11. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    and removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  12. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/sync-modelsbin/sync-models.mjs model catalog synceffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientqueue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobstatus/needs-triageNot yet triaged; needs a first looktask/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