Skip to content

aggregator(token-adapter): file-export adapter's fail-closed window check throws on a null window element and accepts a non-string window name, instead of returning the retained unparseable outcome #2551

Closed

Description

@dezren39

aggregator(token-adapter): the file-export adapter's fail-closed window check throws on a null window element — and accepts a non-string window name — instead of returning the retained unparseable outcome

Summary

pm-stack/aggregator/src/token-adapter.ts is the browser-token adapter framework core (#1153). Its contract is fail-closed: any dashboard/export payload whose shape drifts produces an AdapterOutcome with a failure status and the RETAINED previous value — never a fabricated zero, and (for collectAdapter) never a throw from a parse step, because parse errors are caught and classified unparseable (token-adapter.ts:164-221).

collectFileExportAdapter (token-adapter.ts:229) implements the same discipline for file-export documents, with an explicit fail-closed window validation at :270-283:

for (const w of doc.windows) {
  if (!w.name || typeof w.usedPct !== "number" || !Number.isFinite(w.usedPct)) {
    return retainedOutcome("unparseable", ...);
  }
}

Two holes in that check:

  1. A null (or undefined) window element throws. w.name dereferences the element before any object check, so a success document with windows: [null] throws TypeError: null is not an object (evaluating 'w.name') straight out of collectFileExportAdapter. Unlike collectAdapter, this function has no try/catch around its validation/mapping, so the throw escapes the framework entirely: no retained previous value, no unparseable classification, and any future caller that trusts the AdapterOutcome contract crashes (or must add its own catch, defeating the framework's purpose).
  2. A non-string window name passes. !w.name is a truthiness check only, so { name: 42, usedPct: 1 } validates and is handed to entryFromFileExport (:285), which stamps a numeric-named window into the published quota-current entry as a successful ok reading — exactly the "partial/fabricated value published instead of retained" outcome the module header says must never happen.

The existing test suite pins the neighboring case — a window missing usedPct is unparseable with the previous entry retained (test/token-adapter.test.ts, "a drifted success document (window without usedPct) is unparseable, previous retained") — but no test covers a null element or a non-string name.

Evidence (verified this run, main 3891731)

Probe: laptop Bun 1.4.2, the real src/token-adapter.ts (verbatim copy of the aggregator src/ tree, md5 334689a9912d2bb86a08ea26b0df8ac4, matching main 3891731) at ~/git/fleet/probe-2026-10-29-laneB-tokenadapter/probe.ts, calling collectFileExportAdapter with a healthy meta file-export doc shape (mirroring the test fixture) and varying only windows:

  • Control [{ name: "5h", usedPct: 12.5 }] → outcome status ok.
  • Drift [{ name: "5h" }] (no usedPct) → outcome status unparseable, detail "file-export drift: window 5h lacks a finite usedPct" (the test-pinned behavior).
  • [null] → THREW TypeError: null is not an object (evaluating 'w.name') — no outcome at all.
  • [undefined] → THREW TypeError: undefined is not an object (evaluating 'w.name').
  • ["5h"] (string element) → outcome unparseable (benign control: property reads on a string do not throw).
  • [{ name: 42, usedPct: 1 }] → outcome status ok — a numeric window name is published as a successful reading.

File:line (main 3891731):

  • pm-stack/aggregator/src/token-adapter.ts:270-283 — the window validation loop: no element object check, name checked by truthiness only.
  • pm-stack/aggregator/src/token-adapter.ts:285 — entryFromFileExport(doc, ...) runs on whatever passed the loop.
  • pm-stack/aggregator/src/token-adapter.ts:11-19 (module header) and :255-269 — the stated contract: shape drift is unparseable, the previous value is retained, never a partial/fabricated publish.

Why it matters

This framework is the template every per-provider quota adapter builds on (#1159/#1161/#1163/#1165/#1301 family), and its whole value is that callers can trust the outcome contract: failures arrive as classified outcomes with the last good value retained. A producer whose export gains one null window (a serializer edge, a hand-edited latest.json, a half-written document read mid-replace) currently breaks that contract at the framework's own validation step — the one place written specifically to convert drift into unparseable. The numeric-name hole is the same check under-validating in the other direction: malformed data stamped ok and published as measured quota data.

Acceptance criteria

  • Triage pins the validation shape (expected: a window element that is not an object, or whose name is not a non-empty string, is drift → retainedOutcome("unparseable", ...) with the previous value retained, matching the existing missing-usedPct behavior — or triage pins a different shape; throwing out of collectFileExportAdapter for a parseable-but-malformed document is the one ruled-out option).
  • Regression tests in test/token-adapter.test.ts: windows: [null], windows: [undefined], and windows: [{ name: 42, usedPct: 1 }] each return status unparseable with the previous entry retained and do not throw.
  • No behavior change for well-formed documents or the already-pinned drift cases: the existing token-adapter.test.ts expectations pass unchanged.

Filed by new-issue-creator-code-b (lane B), run 2026-10-29 22:50 CDT. Dedup: gh searches for collectFileExportAdapter / "token-adapter windows" / file-export drift returned no covering issue; #1153 is the parent framework feature (in-review), not this bug; #2332/#2416 are quota-gather-map/quota-current window handling in different files; the meta-exporter window issues (#1566 closed, #2460) are the producer-side .mjs parser, not this aggregator-side adapter.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    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/aggregatorPackage/app: aggregator (pm-stack in this repo)
    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 job (new-issue-creator-code-b, lane B)
    • Session / ID: cron new-issue-creator-code-b, run 2026-10-29 22:50 CDT
    • Found via: filed this run
    • Task: Triage the collectFileExportAdapter window-validation gap: pin the element/name validation shape, then route a small_fix packet
    • Expectation: effort/xs triage; done = validation shape pinned and fix packet routed
    • Links: probe ~/git/fleet/probe-2026-10-29-laneB-tokenadapter/ (laptop); evidence in issue body
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. 2 remaining items

  5. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: done
    task: 3788 (repo-health-code-pilot, lane D)
    stage: triage complete
    did: findings + pin posted (comment 6556076269); follow-up small_fix enqueued as queue task 5363 (packet: hidden_files/packets/task3788-2551-smallfix.json). Issue stays open for the fix task.
    resume: task 5363 implements the element/name validation per the pin.

  7. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — queue task 5363 (lane C) — picked up for implementation

    Implementing the pin from triage task 3788: in collectFileExportAdapter's window loop, reject non-object window elements as unparseable-retained instead of throwing on w.name, and require a non-empty string name instead of truthiness. Regression tests for [null], [undefined], [{name: 42, usedPct: 1}] in the token-adapter tests.

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

    @dezren39
    MemberAuthor

    agent-note:v1 — queue task 5363 (lane C) — done: fixed and merged

    PR #3454 merged as d61aec488485f828190ad09e13f0832a6c905e19 (head 6855d0805).

    • collectFileExportAdapter window loop now rejects a non-object element via isRecord as unparseable-retained (no more TypeError escaping the AdapterOutcome contract) and requires a non-empty string name instead of truthiness; finite-usedPct check unchanged.
    • Tests: bun test test/token-adapter.test.ts — 15 pass, 0 fail, including new [null], [undefined], and [{name: 42, usedPct: 1}] cases, each unparseable with the previous entry retained. Codex review on 6855d0805: completed, no findings.
    • Noticed while typechecking: src/feed.ts does not parse on current main (missing closing brace from 4d7f643) — filed separately as aggregator(feed): feed.ts does not parse — isUnmatchedCharge is missing its closing brace (merged in 4d7f643e3, #2445) #3452; not part of this fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/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