Creates the FastAPI ASGI app served by main._run_daemon (--daemon (legacy alias: --demon)/--web). Wires bearer auth, persistence, event broker, run manager, routers, CORS, error handlers, lifespan, and optional bundled WebUI SPA. Stays thin — all orchestration lives in tools/api/ and tools/run_service/.
Build a FastAPI(title="BreachPilot WebUI API") with lifespan that calls persistence.recover_interrupted() on startup and run_manager.shutdown() on shutdown (app.py:98).
Load/generate bearer token via tools/api/auth.load_or_create_token (app.py:73), enforce loopback host (app.py:70 assert_api_loopback), create BearerAuth.
Create ApiPersistence(reports_dir) and EventBrokerRegistry(reports_dir, buffer_size) (app.py:81, app.py:88).
Create RunManager(persistence, event_registry, config, config_path, callables) (app.py:91).
Configure CORS (loopback-only + api.allowed_origins validated via is_loopback_origin) and install error/middleware (app.py:120, app.py:129).
Configure route modules: system, runs, decisions, events, graph, graph_explorer, optionally users when api.multi_operator (app.py:133).
Optionally serve webui/dist/ SPA at / when api.serve_webui true (app.py:162): mount /assets, add /{full_path:path} catch-all returning index.html with traversal guard, patch app.openapi to hide webui routes.
No other public symbols — app.py is import-only via main._run_daemon / tests.
reports/api_runtime.db via ApiPersistence — runs + decisions (separate from Flow B research.db).
EventBrokerRegistry — per-run JSONL + ring buffer (api.event_buffer_size, default 256) + WebSocket pub/sub.
webui/dist/ — built SPA; served read-only, never mutated.
Lifespan recover_interrupted() re-marks interrupted runs so UI shows them as failed rather than stuck.
tools/api/auth.py (BearerAuth, load_or_create_token, assert_api_loopback, is_loopback_origin)
tools/api/persistence.py (ApiPersistence)
tools/api/event_broker.py (EventBrokerRegistry)
tools/api/run_manager.py (RunManager)
tools/api/routes/* (system, runs, decisions, events, graph, graph_explorer, users)
tools/api/errors.py (install_error_handlers, install_middleware)
tools/config_cli.load_config (fallback when config is None)
fastapi, starlette (StaticFiles, FileResponse, Route, CORSMiddleware), uvicorn (caller)
main.py:771 create_app(config_path, config) inside _run_daemon.
Tests: tests/test_api_*.py import app.create_app to spin an in-process ASGI app.
flowchart TD
A[main._run_daemon] --> B[create_app]
B --> C[load_config + assert_api_loopback + load_or_create_token]
C --> D[ApiPersistence + EventBrokerRegistry + RunManager]
D --> E[install CORS + error handlers]
E --> F[configure route modules]
F --> G{api.serve_webui?}
G -->|yes| H[mount /assets + add SPA catch-all + patch openapi]
G -->|no| I[return app]
H --> I
I --> J[uvicorn.run host:port]
J --> K[lifespan startup: recover_interrupted]
K --> L[serve /api/v1 + /docs + /openapi.json + SPA]
L --> M[lifespan shutdown: run_manager.shutdown]
Copy
Loopback-only bind validated both in main._run_daemon and defensively in create_app.
API routes always win over SPA catch-all (mounted last; /api/v1, /docs, /openapi.json, /assets excluded).
app.openapi is patched to filter webui-only routes so schema stays valid.
callables=None uses _DEFAULT_CALLABLES (direct imports) so prod and tests share the same wiring.
Bearer token required on every /api/v1 request; WS auth also checks token + origin.
CORS allowlist is loopback-only; allow_credentials=True only for those origins.
SPA serving is read-only FileResponse under webui/dist/ with resolved-path traversal guard.
Run: python -m pytest tests/test_api_runs.py tests/test_api_auth.py -v
create_app signature, lifespan, or middleware ordering changes.
A route module is added/removed or multi_operator wiring changes.
SPA serving (mount point, fallback, openapi filter) is altered.
Loopback/CORS/token validation rules change.
docs/api.md — REST + WebSocket API reference
docs/webui.md — SPA build + serve behavior
main.py (docs/components/root/main.md) — daemon boot that calls this factory
tools/api/run_manager.py, tools/api/persistence.py, tools/api/event_broker.py