Skip to content

Commit 2e2b83b

Browse files
committed
docs(agents): document hardening pass architecture conventions
Capture singleton lifecycle, settings threading, isolation invariants, driver capabilities, schemas layout, bench harness, and pyright gate.
1 parent e64432d commit 2e2b83b

1 file changed

Lines changed: 81 additions & 25 deletions

File tree

AGENTS.md

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,52 +9,95 @@
99

1010
```
1111
app/
12-
main.py # create_app() factory; module-level `app` for uvicorn
13-
config.py # Settings (pydantic-settings); single env source of truth
14-
schemas.py # wire Pydantic models + OpenAPI examples
15-
exceptions.py # domain exceptions (e.g. RequestIdCollisionError)
12+
main.py # create_app() factory; module-level `app` for uvicorn
13+
constants.py # SERVICE_NAME and other shared literals
14+
config.py # Settings + nested SentrySettings; single env source of truth
15+
exceptions.py # domain exceptions (e.g. RequestIdCollisionError)
1616
api/
17-
deps.py # FastAPI Depends: settings, engine, executor, ScrapeService
18-
errors.py # 422 + 500 handlers → scrape error envelope
19-
openapi.py # route OpenAPI metadata
20-
routes/ # thin HTTP handlers (health, scrape)
17+
deps.py # FastAPI Depends: settings, engine, executor, ScrapeService
18+
errors.py # 422 + 500 handlers → scrape error envelope
19+
openapi.py # route OpenAPI metadata (configure_openapi at app creation)
20+
openapi_examples.py # OpenAPI examples built from Pydantic model instances
21+
routes/ # thin HTTP handlers (health, scrape)
2122
domain/
22-
scrape_service.py # validation, threadpool orchestration, status mapping
23+
scrape_service.py # validation, threadpool orchestration, status mapping
2324
engine/
24-
orchestrator.py # ScraperEngine.execute
25-
session.py # ScrapeSession lifecycle
26-
request_tier.py # HTTP/curl_cffi path
27-
browser_tier.py # Chromium path
28-
strategies.py # NavigationMode resolution, driver helpers
29-
envelope.py # success/error builders, UTF-8 HTML normalization
30-
infra/ # telemetry, progress, metadata, xhr, runtime cleanup, sentry
31-
security/ # UrlGuard SSRF guardrails
25+
orchestrator.py # ScraperEngine.execute
26+
session.py # ScrapeSession lifecycle
27+
request_tier.py # HTTP/curl_cffi path
28+
browser_tier.py # Chromium path
29+
strategies.py # NavigationMode resolution, driver helpers
30+
driver_capabilities.py # DriverProtocol + call_if_available adapter
31+
envelope.py # success/error builders, UTF-8 HTML normalization
32+
schemas/
33+
enums.py # ExecutionMode, NavigationMode, ErrorCategory, ...
34+
request.py # ScrapeRequest and validators
35+
response.py # ScrapeSuccess, ScrapeError, HealthResponse, ...
36+
infra/ # telemetry, progress, metadata, xhr, runtime cleanup, sentry
37+
security/ # UrlGuard SSRF guardrails
38+
scripts/
39+
bench_scrape.py # TestClient wall-time bench for POST /scrape (request tier)
3240
tests/
33-
support/http.py # TestClient + dependency_overrides helper
41+
api/ # HTTP contract + request schema tests
42+
domain/ # (reserved)
43+
engine/ # ScraperEngine unit tests
44+
infra/ # challenge, metadata, xhr unit tests
45+
security/ # UrlGuard tests
46+
support/
47+
http.py # TestClient + dependency_overrides helper
48+
fakes.py # shared FakeDriver, FakeRequest, ...
49+
test_bench_regression.py # lightweight guard that bench script completes
50+
test_engine_isolation.py # singleton + per-request isolation regression tests
3451
```
3552

3653
Layer rules:
3754

3855
| Layer | May import | Must not import |
3956
| --- | --- | --- |
40-
| `api/routes` | `domain`, `api/deps`, `schemas` | `engine` internals, Botasaurus |
41-
| `domain` | `engine`, `security`, `schemas`, `infra` | FastAPI, Botasaurus |
42-
| `engine` | `infra`, `security`, `schemas`, `config` | FastAPI |
43-
| `infra` | Botasaurus, CDP | FastAPI, routes |
57+
| `api/routes` | `domain`, `api/deps`, `schemas.*` | `engine` internals, Botasaurus |
58+
| `domain` | `engine`, `security`, `schemas.*`, `infra` | FastAPI, Botasaurus |
59+
| `engine` | `infra`, `security`, `schemas.*`, `config` | FastAPI |
60+
| `infra` | Botasaurus, CDP (lazy at use sites) | FastAPI, routes |
4461

4562
Conventions:
4663

4764
- Use `create_app()` in tests; override deps via `app.dependency_overrides`, not module globals.
65+
- Use `TestClient(app)` as a context manager so lifespan runs (singleton engine/executor).
4866
- Config: add env vars to `Settings` in `config.py`; call `reset_settings_cache()` in tests that patch env.
49-
- Wire types stay in `schemas.py`; domain logic stays out of route handlers and Pydantic shells.
67+
- Wire types live in `app/schemas/` submodules; import directly (`from app.schemas.request import ScrapeRequest`). No long-lived re-export shim.
68+
- Domain logic stays out of route handlers and Pydantic shells.
5069
- Typed exceptions over string-matching (`RequestIdCollisionError`, not `RuntimeError` message checks).
5170
- `NavigationMode` end-to-end in engine code; no raw strategy strings outside enum conversion boundaries.
71+
- Optional Botasaurus driver methods go through `driver_capabilities.call_if_available` only; do not ad-hoc `getattr(driver, ...)`.
72+
- OpenAPI route examples come from `openapi_examples.py` model instances, not hand-typed dicts.
73+
- Botasaurus/CDP imports are lazy inside tier entrypoints (`run_request_tier`, `run_browser_tier`, XhrCollector methods), not at app import time.
74+
75+
## Singleton + Settings Threading
76+
77+
- **One** `ScraperEngine` and **one** `ThreadPoolExecutor` are created in `create_app()` lifespan and stored on `app.state`.
78+
- `get_engine` / `get_executor` / `SettingsDep` read from `request.app.state` (not per-request construction).
79+
- Lifespan shutdown calls `executor.shutdown(wait=False, cancel_futures=True)` on the real pool instance.
80+
- Do **not** freeze settings at import time in schemas or OpenAPI modules. `configure_openapi(settings)` runs during `create_app()`; `clamp_wait_timeout_seconds` reads live `get_settings()`.
81+
- `ScraperEngine` and tier functions require an injected `Settings` parameter; no `get_settings()` fallback in the hot path.
82+
83+
## Isolation Invariants
84+
85+
| Resource | Lifetime | Rule |
86+
| --- | --- | --- |
87+
| `ScraperEngine` | process (app.state) | shared |
88+
| `ThreadPoolExecutor` | process (app.state) | shared, sized by `SCRAPE_MAX_WORKERS` |
89+
| `_active_request_ids` | in-process memory | shared; collision guard |
90+
| runtime dir `/tmp/scrape/<request_id>` | per request | isolated; deleted in `finally` |
91+
| browser profile | per request | isolated; no reuse |
92+
| Botasaurus Driver | per request | isolated; closed in `finally` |
93+
94+
Multi-worker uvicorn breaks in-process collision detection unless request ids are sticky to a worker. Default to single-worker for isolation semantics.
5295

5396
## Contract (Do Not Break)
5497

5598
- Endpoints: `GET /health`, `POST /scrape`.
5699
- `openapi.yaml` is generated from `app.openapi()` via `make openapi`. Do not hand-edit. `make openapi-verify` is part of `make check`. Spectral (`make spectral`) lints the snapshot; do not add a post-processor that mutates the dump.
57-
- Wire types live in `app/schemas.py`. Engine imports them. Routes serialize via `ScrapeService.serialize()` / `json_response()`.
100+
- Wire types live in `app/schemas/`. Engine imports them. Routes serialize via `ScrapeService.serialize()` / `json_response()`.
58101
- OpenAPI `info.version` is `2.0.0`. Schema names: `ScrapeSuccess` (200) and `ScrapeError` (400/403/422/502/504). No `ScrapeResponse` alias.
59102
- Success `/scrape` fields: `url`, `final_url`, `status_code`, `headers`, `html`, `metadata_error`, `xhr_responses`, `diagnostics`.
60103
- When `html` is present, document `headers` `content-type` is `text/html; charset=utf-8` and `html` is UTF-8-normalized.
@@ -92,14 +135,27 @@ Conventions:
92135
- keep `/usr/bin/google-chrome` symlink to Chromium for compatibility
93136
- If browser install logic changes, re-verify binary path and Botasaurus startup.
94137

138+
## Performance
139+
140+
- Baseline bench: `PYTHONPATH=. .venv/bin/python3 scripts/bench_scrape.py --runs 10` (TestClient, `execution_mode=request`).
141+
- Record p50 wall time when changing hot paths; avoid regressions vs prior baseline.
142+
- On low-RAM hosts (Docker `--memory=768m`, 1–2 vCPU), tune `SCRAPE_MAX_WORKERS` to `1` or `2`; higher values increase queue wait and swap without improving wall time.
143+
- Browser tier skips XHR harvest before Cloudflare bypass; one consolidated `collect_page_state` pass runs after bypass.
144+
145+
## Types
146+
147+
- `make typecheck` runs `pyright app tests` and is part of `make check`.
148+
- Engine driver seams use `DriverProtocol` in `driver_capabilities.py`; cast vendor `Driver` at construction when needed.
149+
- Test-only pyright relaxations live in `pyproject.toml` `[[tool.pyright.executionEnvironments]]` for `tests/`.
150+
95151
## Safety
96152

97153
- Keep SSRF guardrails: localhost/domain checks and blocked IP classes (loopback/private/link-local/multicast/reserved/unspecified).
98154
- Do not weaken URL validation without explicit request plus docs/tests updates.
99155

100156
## Done Criteria
101157

102-
- Run `make check` before finish.
158+
- Run `make check` before finish (lint, test, typecheck, openapi-verify).
103159
- When Pydantic models or route response metadata change, run `make openapi` and commit the snapshot with the code change.
104160
- When API contract, Docker behavior, or error semantics change, also run `make smoke`.
105161
- `make smoke` must cover build, boot, `/health`, `/scrape` happy path, strategy override, retry path, isolation check, localhost guardrail.

0 commit comments

Comments
 (0)