Commit bc296ef
authored
fix(backend): keep Developer API conversation reads under one shared ceiling (#11682)
Follow-up to the Developer API read hardening from #8713 (landed in
af3ab1e).
## What this fixes
Splitting conversation reads into per-route policies gave each route its
own Redis bucket:
| | Before the split | On `main` today |
|---|---|---|
| `GET /v1/dev/user/conversations` | `dev:conversations_read` 60/hr |
`dev:conversations_read` 60/hr |
| `GET /v1/dev/user/conversations/{id}` | *same* 60/hr bucket |
`dev:conversation_detail_read` **separate** 60/hr |
| **Aggregate per key** | **60/hr** | **120/hr** |
Each policy is an independent counter
(`rl:{policy}:{prefix}:{uid}:{app_id}:{key_id}`), so adding a policy
adds budget rather than subdividing it.
## Honest scoping — this is small
I don't want to oversell it. The practical exposure delta is minor:
- Detail reads return **one** conversation. The list endpoint already
allows up to 100 per request (`backend/routers/developer.py:1241`), so
60 list req/hr ≈ 6,000 records/hr vs. the detail path's 60 records/hr —
roughly **1%** on top.
- The vector from the original incident (`GET
/v1/dev/user/conversations?limit=20` at ~1,300 req/hr) is the **list**
endpoint, and it is unchanged at 60/hr. That cap still does its job.
So this is not a live vulnerability, and "120 vs 60" counts requests,
which overstates it.
**The reason to fix it is structural:** nothing bounds the policy set.
Every future `dev:conversation_*_read` policy silently raises the
aggregate again, and no test or comment flags that. This caps it once,
in a way that keeps working as policies are added.
## Approach
Add `dev:conversation_reads_total` (60/hr). Every conversation read
charges it *before* its per-route budget:
- Aggregate returns to the pre-split 60/hr — no client that worked
before is affected.
- List / detail / transcript stay independently tunable underneath the
ceiling. This also answers the review question on #8743 about why detail
was given the same value as list: the per-route numbers are headroom,
the ceiling is the real limit.
- Transcript reads still charge their stricter 25/hr bucket on top.
**Tradeoff:** one extra Redis round trip on the two conversation read
routes. The transcript sub-budget already charges two buckets on a
single request, so the pattern isn't new — but it is a real cost on a
read path, and worth a maintainer's call.
## Tests
`test_conversation_reads_share_an_aggregate_ceiling` drives both routes
in alternation, so neither per-route bucket can be what stops the
caller, then asserts the aggregate is. It fails on the current wiring
with:
```
AssertionError: 120 != 60
```
Also added `dev:conversation_reads_total` to the existing policy-wiring
assertions, and updated the ordered policy list in
`test_dependency_async_boundaries.py` (the shared ceiling is charged
first, and still routes through `critical_executor`).
Verified per-file the way `backend/test.sh` runs in CI:
`test_rate_limiting.py`, `test_dependency_async_boundaries.py`,
`test_dev_api_conversations_poison.py`,
`test_dev_api_folder_filters.py`, `test_dev_api_lock_bypass.py` all
pass. `black --line-length 120 --skip-string-normalization` clean.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/BasedHardware/omi/pull/11682?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---
No existing class in `.github/failure-classes/` describes this mode —
subdividing a shared quota into per-route policies *adds* budget rather
than partitioning it. Declaring `none` rather than minting a class,
since reviewers asked for a declaration and not a registry change; happy
to switch to `new` and add the definition if maintainers would rather
have the class tracked.
Failure-Class: none4 files changed
Lines changed: 88 additions & 2 deletions
File tree
- backend
- tests/unit
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
339 | 339 | | |
340 | 340 | | |
341 | 341 | | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
342 | 359 | | |
343 | 360 | | |
344 | 361 | | |
345 | 362 | | |
346 | 363 | | |
347 | | - | |
| 364 | + | |
348 | 365 | | |
349 | 366 | | |
350 | 367 | | |
| |||
353 | 370 | | |
354 | 371 | | |
355 | 372 | | |
356 | | - | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
357 | 376 | | |
358 | 377 | | |
359 | 378 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
282 | 282 | | |
283 | 283 | | |
284 | 284 | | |
| 285 | + | |
| 286 | + | |
285 | 287 | | |
| 288 | + | |
286 | 289 | | |
287 | 290 | | |
288 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
489 | 489 | | |
490 | 490 | | |
491 | 491 | | |
| 492 | + | |
492 | 493 | | |
493 | 494 | | |
494 | 495 | | |
| |||
561 | 562 | | |
562 | 563 | | |
563 | 564 | | |
| 565 | + | |
564 | 566 | | |
565 | 567 | | |
566 | 568 | | |
| |||
592 | 594 | | |
593 | 595 | | |
594 | 596 | | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
595 | 653 | | |
596 | 654 | | |
597 | 655 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
121 | 127 | | |
122 | 128 | | |
123 | 129 | | |
| |||
0 commit comments