This document walks through almost every project-owned file and explains what it is, why it exists, and how it connects to neighbors. It is written for someone seeing a full-stack Python + React repo for the first time—assume you know basic programming (variables, functions) but not necessarily HTTP, async, or build tools.
How to use it
- Read §1–2 once for orientation.
- Skim the directory sections; drill into a file when you are debugging or changing that area.
- When you see jargon (ORM, WebSocket, …), there is a short explanation inline or in §3.
Related: PROJECT_GUIDE.md is the shorter “packages + folders” tour. CAPABILITIES.md maps features to modules. ARCHITECTURE_ESSAY.md is a full narrative of how the app works.
Imagine you paste https://example.com into the UI and press Run.
- Browser loads the React app from Vite (
frontend). App.tsxsendsPOST /api/operationswith the URL (and maybe a billing session id).main.pyvalidates the URL withurl_guard, inserts a row in SQLite viamodels+database, startsasyncio.create_task(execute_operation(...)), returns the new operation id immediately.runner.execute_operation(background task) builds a StrandsAgentinvecna_agent, streams LLM + tool events, pushes JSON messages throughhub, and persists blobs onOperation.- The browser opens
WebSocket /ws/operations/{id};main.pyreplays stored events if any, thenhubqueues live events until “done” or “error”. serialize.pyturns noisy Strands events into smaller JSON + ahuman_linestring for the timeline.- When the agent finishes,
runnerwrites final costs, findings, summary;billingmay update aBillingSessionrow.
You do not need to memorize this—come back when something “should have happened” and trace forward or backward.
| Term | One sentence |
|---|---|
| Module | A .py or .ts file that other files import. |
| Package / folder | A directory treated as a namespace (e.g. app.services). |
__init__.py |
Marks a Python directory as importable; can be empty. |
| ORM (SQLAlchemy) | Describe tables as Python classes; generate SQL for you. |
Async / async def |
Functions that can await I/O without blocking the whole server. |
| WebSocket | Long-lived TCP connection; server pushes messages anytime. |
| REST / HTTP API | GET/POST URLs returning JSON—request/response per call. |
| ORM session | A unit of work: load rows, change them, commit() to disk. |
| Environment variables | Key/value config (often in .env) read at startup. |
| LiteLLM | Library that speaks many LLM providers with a similar Python API. |
| Strands | Agent framework: model + tools in a loop, streaming events. |
| SSRF | “Server-Side Request Forgery”—tricking your server into hitting internal IPs; url_guard blocks obvious cases. |
Human-facing quick start: how to install Python deps, run Uvicorn, run Vite, where health checks live, pricing env vars. It is the first file collaborators open.
Tells Git not to commit generated or secret files: virtualenv (.venv/), node_modules/, local SQLite vecna.db, .env, Python __pycache__, build output (frontend/dist/). If .env were committed, your API keys would leak—this file helps prevent that.
Shorter conceptual guide: stack glossary, dependency tables, folder map.
This file—deep per-file notes.
Pin / list Python libraries to install with pip install -r requirements.txt. Each line is a distribution name (sometimes with a minimum version). See PROJECT_GUIDE.md for a column-by-column explanation. Nothing runs just because it is listed—you must pip install.
Secrets and local config: API keys, model id, CORS, pricing. Copied from .env.example. The backend loads it via python-dotenv + Pydantic (config.py). Never commit real keys.
Template without secrets: safe to commit. Shows variable names and comments so someone new knows what to fill in.
SQLite database file on disk. Tables operations and billing_sessions are created/migrated when the app starts. You can inspect it with DB Browser for SQLite or sqlite3 CLI—useful when learning what columns exist.
Often empty; marks app as a Python package so you can from app.config import Settings.
FastAPI application definition and almost all HTTP/WebSocket routes.
lifespan: On startup, creates DB tables and runsschema_migrate; on shutdown, disposes the DB engine.Settings: Loaded once at import time fromconfig.- CORS middleware: Browsers block cross-origin requests unless the server allows your frontend origin—
CORS_ORIGINSfrom settings lists allowed origins. - Pydantic models (
StartOperationBody,OperationSummary, …): Validate JSON bodies and document the API shape. - Routes (high level):
POST /api/billing/sessions— create a billing session id (stored client-side).GET /api/billing/sessions/{id}— read cumulative session totals.POST /api/operations— validate URL, createOperation, spawn background taskexecute_operation, return id. Usesops_rate_limitfromrate_limit.POST /api/operations/{id}/cancel—cancel_taskin runner.GET /api/config— non-secret hints for the UI (model id, pricing visibility).GET /api/operations— paginated list for history.GET /api/operations/{id}— full detail for one operation (events, findings, costs).GET /api/operations/{id}/report.json—build_structured_reportexport.WebSocket /ws/operations/{id}— replay + live stream viahub.POST /api/demo/force-error— demo of retry/error UI without spending tokens.GET /api/health/deep— diagnostics (DB, keys, config warnings).GET /api/metrics— Prometheus-style text metrics.GET /health— minimal liveness check.
Study tip: When you add a feature, ask “is it HTTP (REST) or streaming (WebSocket)?”—that tells you which section of main.py to extend.
Central configuration object (Settings):
- Loads
.envfrom thebackend/directory even if you start Uvicorn from elsewhere (path logic at top). load_dotenv(..., override=True)so values in.envwin over empty shell exports.- Imports
agent.vecna_litellmearly so LiteLLM is patched before other code importslitellm. - Fields map to environment variables (e.g.
VECNA_HIDE_COSTS→vecna_hide_costs). model_validator export_keys_to_environ: Copies keys intoos.environbecause LiteLLM and Strands sometimes read keys from the environment directly (especially OpenRouter + OpenAI-compatible paths).
Why it matters: One place to see “what can be configured” without reading the whole codebase.
Minimal SQLAlchemy async setup:
Base: Declarative base class; models inherit from it.make_engine(settings): Createscreate_async_engine(settings.database_url)— default URL usesaiosqlitedriver + local filevecna.db.make_session_factory: Returns a factory that producesAsyncSessionobjects forasync with session_factory() as session:.
Student note: “Engine” ≈ connection pool; “session” ≈ one transaction-ish scope for queries.
SQLAlchemy ORM models (tables):
BillingSession: One row per “operator session” (browser session id)—stores rolled-up token totals, costs, scan/tool fees across many operations.Operation: One row per scan: URL, status (pending/running/completed/failed/…), token counts, JSON blobs forevents_json(stream log) andfindings_json, foreign keysession_idto billing.
Column names align with what the frontend and runner expect—changing them requires migrations + UI updates.
SQLite’s create_all does not add columns to existing tables. This file runs ALTER TABLE ... ADD COLUMN for columns added after the first deploy. Called from lifespan in main.py.
Lesson: In production you might use Alembic; here the project keeps migrations tiny and explicit.
Safety gate for user-supplied URLs: rejects loopback, private IP ranges, metadata hostnames, etc., to reduce SSRF risk when your server fetches those URLs during recon.
Returns (True, "") or (False, reason_string); main.py turns failures into HTTP 400.
In-memory pub/sub for live operations:
OperationEventHubholdsoperation_id → list of asyncio.Queue.register: Subscriber (WebSocket handler) gets a queue;publishpushes a dict to every queue for that operation id.unregister: WebSocket disconnect cleanup.
Why queues? Multiple browser tabs could subscribe; each gets its own queue. Nothing is persisted here—persistence is Operation.events_json in the DB.
Package marker + short docstring; may re-export nothing—just makes app.services.runner importable.
execute_operation orchestrates a single operation:
- Sets
set_findings_bucketso tools can append findings. - Marks operation
running, logs LLM query telemetry. build_agent,attach_operation_runtime(context for tool threads).async for raw in agent.stream_async(prompt): Consumes Strands stream;event_to_jsonablesanitizes;hub.publishsends to WebSockets;_persistperiodically writes DB.- On
AgentResult, merges usage metrics, publishes credit update, findings delta. - Retry loop: On retryable errors,
classify_agent_error, sleep with backoff, rebuild agent, retry (capped). - Success path:
billable_for_completed_scan,record_operation_for_session, final_persist,hub.publishdone. - Cancel path:
asyncio.CancelledErrorfromcancel_task.
Also register_task / cancel_task / _deregister_task for cancellation.
Study tip: Trace a single event from hub.publish to App.tsx WebSocket handler—you’ll understand streaming.
Outbound HTTP for recon tools (not the LLM):
- Timeouts, User-Agent, per-request id header, structured logging.
detect_gateway_from_model_id: Maps LiteLLM model id prefix (openrouter/, …) to a short label for telemetry (LLM gateway, separate from tool HTTP).recon_client(): Context manager yielding anhttpxclient—tools should use this instead of rawhttpx.getso behavior is consistent.
Token → USD estimates:
- Static
_PREFIX_COSTStable for models LiteLLM might not price. usage_from_metrics: Converts StrandsEventLoopMetricsintoUsageSummary.calculate_cost_from_tokens,format_cost, etc.
Separation: cost.py is LLM money; pricing.py adds platform scan/tool fees.
Platform billing on top of token cost:
- Default tool family map: DNS / network / paths tools.
BillableBreakdown,billable_for_completed_scan,compute_tool_fees(legacy flat vs tiered JSON).tool_pricing_public_snapshotforGET /api/config(no secrets).
Billing session persistence: create session row, record_operation_for_session after a successful op, get_session_snapshot for API responses. Aggregates per-model usage JSON.
Structured logging (logger.info with JSON) for operations, tool lifecycle, LLM retries, unknown model cost—helps operators grep logs or ship to a log aggregator.
Exception classification for LLM/agent failures: maps exceptions to error_type strings (rate_limit, timeout, …), decides retryability and user-facing messages. Used by runner retry loop.
Context variables (contextvars) so sync tool code running on a worker thread still knows:
- Current
operation_id events_log- Main
asyncioloop andsession_factory
schedule_tool_emit: Thread-safe way to push “tool started/finished” events to hub + DB from tools.
Why: Strands runs tools in threads; normal global variables would be unsafe—contextvars propagate per-async-task.
In-memory per-IP limits for POST /api/operations (sliding windows). Env VECNA_OPS_PER_MINUTE, VECNA_OPS_PER_HOUR. Raises HTTP 429 when exceeded.
Converts raw Strands stream dicts into JSON-safe dicts and adds human_line—a single readable sentence for the UI timeline. Drops huge/noisy keys, handles AgentResult, parses nested “reasoning” / “tool use” chunks.
Coupling: Frontend streamMerge.ts assumes certain human_line patterns—change carefully.
build_structured_report(op) builds the JSON for GET .../report.json: grouped findings, telemetry block, schema version string.
Package marker.
Wires the Strands Agent:
SYSTEM_PROMPT: Instructions (passive recon, which tools, output format).- Key helpers
_openrouter_key,_openai_key,_claude_direct_api_key, … — read settings + env. _client_args_for_model: Returns provider-specific kwargs for LiteLLM (e.g. OpenRouter base URL).build_agent(settings): Validates keys for the chosen model prefix, constructsVecnaLiteLLMModel, registers tools fromtools.py, returnsAgent.user_message_for_target(url): User message string passed to the agent.
Error messages here are what developers see if keys are missing.
Monkey-patches LiteLLM’s completion / acompletion so openrouter/ models always get API key + base URL (workaround for Strands/LiteLLM edge cases). Also defines VecnaLiteLLMModel subclass that merges client_args into each request so auth wins over defaults.
Imported side-effect first from config.py—unusual pattern, but ensures patches apply before any litellm call.
Three @tool functions the LLM can invoke:
resolve_dns_and_subdomains— DNS + crt.sh passive discovery.analyze_http_security_headers— GET + header analysis.probe_common_paths— fixed path list GETs.
Implementation functions _resolve_dns_impl, etc., call run_tool_with_hooks from tool_hooks. Uses recon_client, set_findings_bucket / _merge_findings for the findings panel.
run_tool_with_hooks: Before/after wrapper—emits lifecycle events, telemetry, timings. Keeps tools.py focused on recon logic.
Pytest fixture bootstrap: sets DATABASE_URL to in-memory SQLite so tests do not touch your real vecna.db.
test_api.py, test_billing.py, test_cost.py, test_pricing.py, test_errors.py, test_http_client.py, test_tool_hooks.py
Each file targets one area: HTTP routes, billing math, cost estimates, pricing rules, error classification, HTTP client gateway detection, tool hook behavior.
How to learn: Read a test name + assertion—it documents expected behavior often better than comments.
npm manifest: scripts (dev, build, lint), dependencies (React, Vite, Tailwind, Radix, …). npm install reads this and writes package-lock.json (exact resolved versions).
Lockfile—commit it so everyone gets the same dependency tree.
Vite configuration:
- Plugins: React, Tailwind v4.
@alias →src/.- Dev server proxy:
/apiand/ws→127.0.0.1:8000so the browser uses same origin (no CORS issues in dev).
TypeScript compiler options split for app vs Vite config files—standard Vite + React setup.
Single HTML shell: div#root where React mounts; script entry /src/main.tsx.
React entrypoint: imports global CSS, renders <App /> inside StrictMode (extra dev checks).
The entire UI in one component (common in small apps):
- State for URL input, operation id, WebSocket messages, findings, history list, billing session id in
localStorage(BILLING_SESSION_KEY). - Fetches
GET /api/config,POST /api/operations, opens WebSocket, handles message types (stream,done,retry, …). - Uses
appendStreamEntry/foldStreamEntriesfromstreamMerge.tsto build a readable timeline. - UI components from
components/ui/*.
Study tip: Search for WebSocket and walk through onmessage—that is the live feed.
Styles—Tailwind layers + any custom rules for layout/branding.
Tiny helpers—typically cn() merges class names (Tailwind-friendly).
apiUrl / wsUrl — prepend optional VITE_API_ORIGIN so the same app works behind the Vite proxy (browser) and when opened from file:// (Electron + built dist).
Merges noisy stream lines for display: combines chunked tool args and text deltas, collapses duplicate human_lines, handles “cumulative vs incremental” provider quirks. Documented in file header comments.
Reusable UI primitives (Button, Card, Input, ScrollArea, Badge) built with Radix primitives + Tailwind. Shadcn-style pattern—consistent look, accessible focus/keyboard behavior.
Static assets served as-is; index.html references favicon.
Declares Electron and scripts: npm start (load Vite dev URL), npm run start:dist (load built frontend/dist).
Opens a BrowserWindow: by default http://localhost:5173 (run Vite + backend first). Can load ../frontend/dist/index.html when ELECTRON_USE_DEV_SERVER=0; then build the frontend with VITE_API_ORIGIN=http://127.0.0.1:8000 so fetch/WebSocket hit the API (see electron/README.md).
Step-by-step for dev vs built UI and CORS.
- Trace a request: From
App.tsxfetch(apiUrl('/api/operations'))→main.pystart_operation→runner.execute_operation→ firsthub.publish. - Change copy only: Edit
SYSTEM_PROMPTinvecna_agent.py, rerun a scan, observe different report tone. - Add a log line: In
telemetry.py, add onelogger.infoin a function you understand; watch the Uvicorn console. - Run one test:
cd backend && pytest tests/test_http_client.py -q(or anytest_*.py) and read that file alongside the module it imports.
node_modules/,.venv/— third-party code; use docs sites, not line-by-line reading.frontend/dist/— build output; regenerated bynpm run build..git/— Git internals; learn Git separately.
If you add new first-party files, append a short subsection here so the next reader (or future you) stays oriented.