Repository navigation
Description
Problem
isFleetFeed (pm-stack/aggregator/src/feed.ts:207-219) never validates
the optional saas section at all — it checks only version,
generatedAt, quota.pools being an array, and attention being an
array. But saas is the section that gates the live source's whole
usage branch, and the services it carries are served verbatim into the
attention-flag builder and the /quota service cards, which
dereference every entry unguarded. One malformed entry in a feed that
passes the gate crashes the entire Usage & billing page — inside
usage() itself, before rendering starts.
Evidence (verified this run, main 5a34fa70)
feed.ts:42-43:FleetFeed.saas?: SaasService[](shape at
model.ts:1121-1147: stringid/name/plan,metrics: UsageMetric[],conflicts: string[],paygEstimateobject-or-null,
numericcadenceDays, etc.).feed.ts:207-219:isFleetFeedcontains nosaascheck of any
kind — not array-ness, not entry shape.sources/live.ts:808-815:usage()'s feed branch is taken whenever
feed.saasis truthy with a non-zerolength, and the section is
passed through verbatim:services: feed.saas— no filter or
per-entry guard, unlike the registry / projects / snapshots
consumers in the same file, which validate or filter first.sources/live.ts:832:usage()calls
buildAttentionFlags(feed.quota, partial)while building its return
value;derive.ts:823-848dereferencessvc.metricsand
svc.paygEstimate.highper entry — a null entry or an entry missing
metricsthrows here, insideusage(), so every route that awaits
usage()fails.render.ts:1808-1809,1703-1720:renderQuotafilters
usage.servicesbys.groupand each surviving card dereferences
svc.metrics.filter(...),svc.conflicts.length,
svc.billedUsd.toFixed(2),svc.paygEstimate.low|high.toFixed(2)—
an entry with a validgroupbut missingconflictsthrows in the
render even though the flag builder passed it.server.ts:486-487(/quota),:492(/quota/service/:id— also
usage.services.find((s) => s.id ...)),:656-665
(/api/quota.json): all awaitsource.usage()with no catch, so
the throw becomes a 500 for the whole hub page and both JSON twins.
Laptop Bun probe (~/git/fleet/probe-2026-10-29-laneA-feedpayg/
probe2.ts, real feed.ts + derive.ts + render.ts from main
5a34fa70, md5-identical, Bun 1.4.2; usage view assembled exactly as
live.ts:811-815 assembles it, flags built exactly as live.ts:832):
saas |
isFleetFeed |
feed branch taken (live.ts:808) |
buildAttentionFlags (in usage()) |
renderQuota |
|---|---|---|---|---|
| valid entry (control) | true |
yes | OK | OK — card renders |
[null] |
true |
yes | throws TypeError: null is not an object (evaluating 'svc.metrics') |
throws on s.group |
"oops" (non-array string) |
true |
yes (.length 4) |
throws on svc.metrics (iterates chars) |
throws usage.services.filter is not a function |
{...} (non-array object) |
true |
no — .length undefined, usage() silently falls back to direct extraction and the feed section is ignored |
n/a on the live path | n/a on the live path |
entry missing metrics |
true |
yes | throws on svc.metrics |
throws on svc.metrics.filter |
entry missing conflicts (valid group) |
true |
yes | OK | throws TypeError: undefined is not an object (evaluating 'svc.conflicts.length') |
[valid, null] mixed |
true |
yes | throws on svc.metrics |
throws — the valid card never renders |
Why it matters
saas is the hub's core section (§2.3): the service cards are the
page, and this section is also the switch that selects the feed path
for the entire usage view — so a single drifted or hand-edited
collector entry doesn't just drop one card, it takes down /quota,
/quota/service/:id and /api/quota.json (and every other usage()
consumer), or — in the non-array-object case — silently discards the
feed's hub sections in favour of a stale direct read with no signal.
This is the same failure class as #2427 (attention), #2429 (quota
pools), #2439 (chargesAttention), #2445 (charges), #2446
(subscriptions) and #2450 (payg) at the same gate — the fix should
stay consistent with whatever layer those pin. This issue is the
saas field specifically, with its own consumers
(buildAttentionFlags + the service cards) and its branch-switch role.
Acceptance criteria
- A feed passing
isFleetFeedcan no longer crashusage()or
/quotaviasaas: either the gate validatessaas(array of
entries with stringid/name/plan, arraymetrics/conflicts,
well-formedpaygEstimatewhen non-null), or the live source
filters/defaults malformed entries before the view is built — the
layer pinned consistently with the
aggregator(feed): isFleetFeed never checks attention entries — one null entry crashes /attention + /today, entries without a string sessionId silently collapse into one #2427/aggregator(feed): isFleetFeed checks only that quota.pools is an array — burn/choices, pool entries and windows unvalidated, one malformed pool crashes /quota #2429/aggregator(feed): isFleetFeed never checks chargesAttention — one malformed entry crashes usage() and takes down /quota + the quota JSON twins #2439/aggregator(feed): isFleetFeed never checks charges — one malformed row crashes /quota, a non-array object silently empties the charges strip #2445/aggregator(feed): isFleetFeed never checks subscriptions — one null row crashes /quota #2446/aggregator(feed): isFleetFeed never checks payg — one malformed line crashes usage() and /quota #2450 rulings. - Malformed entries are dropped individually; well-formed entries in
the same array still render and still contribute attention flags. - A non-array
saasvalue no longer silently switches the whole
usage view to the fallback path without any provenance/diagnostic
signal. - Regression tests cover:
[null]entry, non-arraysaas(string
and object), entry missingmetrics, entry missingconflicts,
[valid, null]mixed — each rendering the valid remainder, never
throwing; a well-formed feed unchanged, and the collector still
accepts its own emitted feed.
Dedup
gh searches (open + closed) for payg, isFleetFeed, saas feed
and isFleetFeed never checks returned no covering issue for the feed
saas gate. #1119 is a contract-test request for the
/api/quota.json saas/payg schema — related, but it does not track
this verified crash at the feed gate. Siblings at the same gate:
#2427 (attention), #2429 (quota pools), #2439 (chargesAttention),
#2445 (charges), #2446 (subscriptions), #2450 (payg — filed this run)
— each a distinct field and consumer.
Activity
developing-today commentedon Oct 29, 2026
CODE-1252
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
31 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
web-pushdependency in the fresh worktree, resolved bybun install).bunx tsc --noEmitshows one error in test/overview.test.ts (HealthViewconflicts) that is identical on clean origin/main — pre-existing, not from PR fix(aggregator): sanitize malformed SaaS feed rows #4192. Codex review of the previous head reported no findings.dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up (resume)
2a2394c87(matches checkpoint), MERGEABLE/CLEAN; branch already contains origin/main merge75d3fe5e8; full aggregator suite on laptop 1418 pass / 0 fail (prior checkpoint); Codex code review completed on2a2394c87with no findings; regression tests intest/saas-feed-gate.test.tscover null rows, non-array saas, and rows missing metrics. The Actionstestcheck never registered on this head (only CodeRabbit) — merging under the standing verified-done merge authority rather than parking again on a stalled check.dezren39 commentedon Nov 3, 2026
🤖 Agent note · done
541c35541dabe552db540ba87cfce37b9b778b38). The sanitizer now drops malformed SaaS feed rows (null rows, non-arraysaas, rows missing metrics/conflicts) at the feed boundary with a drop diagnostic instead of crashingusage()//quotaor silently switching the whole view to fallback; regression coverage inpm-stack/aggregator/test/saas-feed-gate.test.ts(7 tests). Verification: full aggregator suite on laptop 1418 pass / 0 fail on head2a2394c87after merging origin/main; Codex review completed on that head with no findings; the Actionstestcheck never registered on the head (infra stall — only CodeRabbit ran), merge proceeded under the standing verified-done authority.541c35541