fix: cap network and memory blast radius on shared infra - #358
Closed
lfiaschi wants to merge 1 commit into
Closed
Conversation
Three reliability/security fixes surfaced during a principal-engineer
review of the server. Each is small and independently verifiable.
1. infra/github.py — every outbound GitHub call now carries a 15s read /
5s connect timeout. Previously every helper used a bare
`httpx.AsyncClient()` which falls back to httpx's ~5-minute default
connect timeout. A slow github.com would pin the FastAPI event loop
for minutes, starving other async traffic on the same Modal container.
Added a regression test that scans the module source for the buggy
`httpx.AsyncClient()` form.
2. api/rate_limit.py — the per-IP sliding-window limiter had two issues:
(a) every admitted request ran `sum(len(v) for v in self._requests.values())`
to decide whether to purge, an O(n) scan that scales with the number
of tracked IPs and also fires unpredictably (only on exact mod-100
totals);
(b) nothing bounded the number of distinct IPs we track, so a botnet
rotating source addresses could grow the dict without limit between
purges.
Replaced the sum with a cheap per-instance counter and added a hard
`_MAX_TRACKED_KEYS` cap that purges eagerly and rejects with 429 when
the limiter is saturated. Two tests cover periodic purge and saturation
admit/reject behavior.
3. tests/test_infra/test_cache.py — replaced `time.sleep` with a fake
monotonic clock fixture. The old tests relied on 10–20 ms sleeps that
can flake under CI load and add ~100 ms per run for no real coverage.
Added a small eviction test that asserts max-size is never exceeded
under a tight insertion loop.
All 939 server tests pass. Lint, format, mypy clean on the touched files.
Contributor
Author
|
Closing as part of the 2026-08-11 open-PR consolidation. This automated review-sweep PR overlaps heavily with the retained merge queue (#449, #448, #447, #446, #405, then #438, #443, #375). Unique fixes not covered by the retained set are catalogued in #451 for a follow-up best-of PR. The branch is preserved, so nothing is lost. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three small reliability/security fixes surfaced during a principal-engineer review of the server. Each fix is independently verifiable.
infra/github.py— every outbound GitHub call now has a finite timeout (15 s read / 5 s connect). Previously every helper used a barehttpx.AsyncClient(), which falls back to httpx's ~5-minute default. A slowgithub.comwould pin the FastAPI event loop and starve other async traffic on the same Modal container.api/rate_limit.py— replaced an O(n)sum(len(v) for ...)purge trigger with a per-instance counter and added a hard_MAX_TRACKED_KEYScap so a rotating-IP flood can no longer grow the dict without bound. Limiter rejects with 429 once saturated and nothing is purgeable.tests/test_infra/test_cache.py— replacedtime.sleepwith a fake-monotonic-clock fixture so the suite is deterministic and ~100 ms faster per run.All 939 server tests pass. Lint, format, mypy clean on the touched files.
Review notes (for context)
This PR is one slice of a wider review. The full assessment (system map, top-15 leverage list, test plan, etc.) is below. Only the highest-confidence, smallest-blast-radius fixes are in this PR — bigger items are flagged for follow-up.
High-level summary
The codebase is a uv workspace monorepo with four components: a Typer-based CLI (
client/), a FastAPI server on Modal (server/), a shared domain model package (shared/), and a React 19 + Vite frontend. The split is principled and the shared package keeps duplication low. The biggest risks are concentrated in a few large files (server/src/decision_hub/infra/database.py~3.5k lines,client/src/dhub/cli/registry.py~1.9k lines,server/src/decision_hub/api/registry_routes.py~1.3k lines) and in implicit assumptions about external services — most notably the GitHub timeouts fixed here. Test coverage is broad but skewed toward route-level mocked tests; integration coverage ofpublish_pipelineandrepo_utilsis thin.Top high-leverage changes (10 items, ranked)
httpx.AsyncClient()calls ininfra/github.pytime.monotonicclient/src/dhub/cli/registry.py(1912 LOC) into per-command submodules; share a singlehttpx.Clientfactory with retry/backoff for 429/5xxdomain/publish_pipeline.execute_publish()covering the happy path and rollback on upload failureapi/registry_routes.pyfor IDOR / enumeration via differing 403 vs 404 responses on org-scoped paths (delete_skill_version, audit-log, eval-run lookups)find_version-then-insert_versionpattern withINSERT ... ON CONFLICTorSELECT ... FOR UPDATEAbortControllertofrontend/src/api/client.tsso stale fetches don't update unmounted componentsfrom loguru import loggerout of domain functions (e.g.gauntlet.py:1346); domain layer should return values, not logpytest-timeoutand per-test 10 s budget to bound CI hang riskWhy only items 1–3 here
Items 4–10 are higher effort or have broader review surface (UX changes, schema/transaction shifts, route-level error-shape changes). They deserve their own PRs with focused reviewers. Items 1–3 share a tight theme — bound the blast radius of network calls, memory growth, and test wall-clock — and are mechanical enough to land safely together.
Skipped findings (rejected after verification)
A few findings from the initial review were rejected on closer reading:
set()— thelen >= max_sizecheck is already inside the lock atcache.py:60–62. Not a real race.SkillDetailPage.tsx:699array index as key — the surroundingexpandedIndexstate is itself an index, so the index-as-key is intentional. Switching tocheck_namewould actually break expand/collapse if names duplicate._find_credential_hits/_find_suspicious_lines/_find_prompt_injection_hitsingauntlet.py— the three functions return dicts with different schemas ({source,label,line}vs{file,label,line}vs{pattern,label,context}); collapsing them would force an artificial uniform shape and obscure intent.Open questions / assumptions
_MAX_TRACKED_KEYS = 10_000is sized for a single Modal container; revisit if container concurrency changes materially.Test plan
make test-server(excludes slow LLM tests) — 939 passed, 34 deselecteduvx ruff checkanduvx ruff format --checkon the five changed files — cleanuvx mypy server/src/decision_hub/infra/github.py server/src/decision_hub/api/rate_limit.py— no issueshttpx.AsyncClient()(no kwargs) creeps back intoinfra/github.pyhttps://claude.ai/code/session_012LMuzsUtWEUAfEBXW5yDyL
Generated by Claude Code