# Theseus — System Architecture

> Attack path graph for penetration testing. Single Rust binary, CLI + server + web UI.

## 1. Single Binary Design

**Cargo.toml** defines both `[lib]` and `[[bin]]` pointing to the same crate:

```
[lib]     → src/lib.rs   (server: axum app, AppState, Database, routes)
[[bin]]   → src/main.rs  (CLI: clap derive, HTTP client, serve command)
```

`main.rs` imports `theseus::types::*` and `theseus::serve()` from the lib. The `serve` subcommand (`src/main.rs:463-468`) delegates directly:

```rust
TopLevel::Serve { port, deepseek_api_key } => {
    theseus::serve(port, deepseek_api_key, api_key).await;
}
```

All other CLI subcommands (project, node, edge, credential, search, import, tell, ask) are **HTTP clients** that talk to a running server at `http://127.0.0.1:3080` via `reqwest`. The binary is both server and client — `serve` starts the server, everything else sends requests.

## 2. Data Flow

```
┌──────────────┐     HTTP/reqwest     ┌───────────────────┐     Mutex<Connection>     ┌─────────┐
│  CLI client   │ ──────────────────→  │  axum server       │ ──────────────────────→  │  SQLite  │
│  (main.rs)    │                      │  (lib.rs + routes/) │                          │ (.db)    │
└──────────────┘                      └────────┬──────────┘                          └─────────┘
                                               │
                                          broadcast::channel(256)
                                               │
                                               ▼
                                      ┌────────────────┐
                                      │  WebSocket      │
                                      │  /api/ws        │
                                      └────────┬───────┘
                                               │
                                               ▼
                                      ┌────────────────┐
                                      │  Cytoscape.js   │
                                      │  (browser)      │
                                      └────────────────┘
```

**CLI → Server**: Every non-`serve` command builds a `reqwest::Client` (`build_http_client` at `main.rs`), attaches `x-api-key` header, and hits the REST API. The CLI itself has no direct DB access.

**Server → DB**: All mutations go through `Database` methods in `src/db.rs`. The `Database` struct wraps a single `Mutex<Connection>` (line 12-14). Every query acquires the lock via `fn c()` (`db.rs:17-19`), which recovers from poisoned mutexes.

**Server → Frontend**: After mutations, routes call `state.tx.send("graph:{pid}")` or entity-specific messages. The broadcast channel (`tokio::sync::broadcast`, capacity 256) fans out to all connected WebSocket clients.

## 3. AppState Structure

Defined at `src/lib.rs:427-432`:

```rust
pub(crate) struct AppState {
    pub(crate) db: &'static Database,           // Box::leak'd — lives for process lifetime
    pub(crate) tx: broadcast::Sender<String>,   // WebSocket broadcast channel
    pub(crate) config: Config,                  // deepseek_api_key, api_key, cors_origins
    pub(crate) api_key: Option<String>,         // separate from config for middleware access
}
```

**`Box::leak` pattern** (`lib.rs:791-793`): The `Database` is heap-allocated then leaked to get a `&'static` reference. This avoids lifetime complexity in axum's `State` extractor — the DB lives until process exit.

**Config loading** (`lib.rs:353-410`): Cascading priority: CLI args → env vars (`THESEUS_API_KEY`, `THESEUS_DEEPSEEK_API_KEY`) → `theseus.toml` in CWD → `~/.config/theseus/config.toml`.

## 4. Request Lifecycle

```
HTTP request
  → CORS layer (CorsLayer)
  → TraceLayer (tracing logging)
  → json_error_middleware (lib.rs:500-520)     ← catches bare error responses, wraps in ApiResponse
  → api_key_auth_middleware (lib.rs:485-498)    ← validates x-api-key / Bearer / ?api_key=
  → route handler (routes/*.rs)
    → resolve_*_id() for short ID support (lib.rs:602-781)
    → Database method (db.rs)
    → state.tx.send() for mutation broadcast
    → Json(ApiResponse<T>)
```

**Middleware order** (built bottom-up in axum, executes top-down):
1. `json_error_middleware` — outer layer, catches unhandled status codes
2. `api_key_auth_middleware` — applied via `route_layer` on the merged API router (`lib.rs:830-833`)
3. Route handlers — each in `routes/*.rs`, each exposes `pub(crate) fn router() -> Router<AppState>`

**API key extraction** (`lib.rs:450-483`): Three sources checked in order: `x-api-key` header → `Authorization: Bearer` → `?api_key=` query param.

## 5. Static Asset Serving

**HTML** — compiled into the binary via `include_str!` at `lib.rs:894-896`:
```rust
async fn index() -> Html<&'static str> {
    Html(include_str!("../static/index.html"))
}
```
Rebuild required after HTML edits. Served at `/`.

**CSS/JS** — served at runtime via `ServeDir` at `lib.rs:872-875`:
```rust
.nest_service("/static", ServeDir::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")))
```
No rebuild needed for CSS/JS changes. 14 JS modules loaded in dependency order (`index.html:354-367`): state → api → graph → projects → nodes → edges → credentials → timeline → search → evidence → import → analysis → daily_report → ws.

## 6. WebSocket Broadcast Pattern

**Server side** (`src/routes/ws.rs:11-36`):
- Endpoint: `GET /api/ws`
- `ws_handler` upgrades HTTP to WebSocket
- `handle_socket` subscribes to `state.tx` broadcast channel
- Forwards every broadcast message as `Message::Text` to the connected client
- Handles client disconnect and close frames

**Message format**: `"{entity}_{action}:{project_id}"` — examples:
- `node_created:{pid}`, `node_updated:{pid}`, `node_deleted:{pid}` (nodes.rs:179,332,380)
- `edge_created:{pid}`, `edge_updated:{pid}`, `edge_deleted:{pid}` (edges.rs:168,197,243)
- `graph:{pid}` (chat.rs:181, import.rs:120, agent/tools.rs:159,221,305,343) — triggers full graph reload
- Agent tool mutations (create_node, create_edge, update_node, delete_node) also broadcast `graph:{pid}` via `ToolContext.tx`

**Client side** (`static/js/ws.js:4-36`):
- Connects to `ws://{host}/api/ws?api_key=...`
- On `graph:{pid}` message → calls `loadGraph()` if it matches current project
- On `projects` message → calls `loadProjects()`
- Auto-reconnect on close (2s delay)

## 7. Database Schema

6 tables in `theseus.db` (`db.rs:25-80`), all with UUID primary keys and RFC3339 timestamps:

| Table | Key relationships | Notes |
|-------|-------------------|-------|
| `projects` | — | Top-level container |
| `nodes` | FK → projects (CASCADE) | x/y position, properties JSON |
| `edges` | FK → projects (CASCADE) | No FK on source/target_node_id |
| `credentials` | FK → projects (CASCADE), FK → nodes (SET NULL) | |
| `evidence_files` | FK → projects (CASCADE), FK → nodes (CASCADE) | |
| `agent_memory` | FK → projects (CASCADE) | LLM conversation history |

WAL mode + foreign keys enabled. Row deserializers use hardcoded column indices — any SELECT reordering requires updating deserializers.

## 8. Route Module Map

15 route modules in `src/routes/`, each merged into the API router at `lib.rs:814-829`:

| Module | Key endpoints |
|--------|--------------|
| `projects.rs` | CRUD + status/timeline/stats/analysis/report/check |
| `nodes.rs` | CRUD + filtering by type/verdict/evidence + leaves + duplicate check |
| `edges.rs` | CRUD + label-based matching |
| `credentials.rs` | CRUD |
| `evidence.rs` | File upload/download (multipart) |
| `import.rs` | Bulk JSON import with label dedup |
| `analysis.rs` | BFS attack path analysis + optional AI suggestions |
| `search.rs` | Cross-project LIKE search |
| `timeline.rs` | Chronological node view with connections |
| `daily_report.rs` | CST-date-filtered Markdown report |
| `chat.rs` | AI conversation (tell/ask) |
| `agent.rs` | LLM agent loop with tools |
| `check.rs` | Deterministic graph health checks |
| `ws.rs` | WebSocket endpoint |
| `report.rs` | AI-generated penetration test report |

## 9. AI Integration

**Shared helper** — `deepseek_chat()` at `lib.rs:539-599`. Stateless HTTP call to DeepSeek API. Used by `analysis.rs`, `report.rs`, `chat.rs` (tell/ask). Model: `deepseek-v4-flash`. Requires `deepseek_api_key` from config.

**Agent subsystem** (`src/agent/`): autonomous LLM loop with tool-calling. Separate from the one-shot `deepseek_chat()` helper — has its own `LlmClient` with streaming/tool-call support.

| File | Role |
|------|------|
| `llm.rs` | `LlmClient` — wraps DeepSeek API with `ChatMessage` types, tool definitions, function-calling |
| `loop.rs` | `AgentRuntime` — turn-based loop (max `max_turns`), builds project context, loads memory, dispatches tools |
| `memory.rs` | `MemoryStore` — per-project conversation persistence via `agent_memory` table, auto-prunes old messages |
| `prompts.rs` | `system_prompt()` + `build_project_context()` — injects graph stats into system prompt |
| `tools.rs` | 13 tool definitions + `ToolContext` (db + project_id + broadcast sender) |

**Tool registry** (`tools.rs:33-48`): `create_node`, `create_edge`, `update_node`, `delete_node`, `list_nodes`, `get_node_detail`, `list_edges`, `create_credential`, `get_analysis`, `get_timeline`, `get_health_checks`, `search_nodes`, `search_global`. All mutation tools broadcast `graph:{pid}` after success.

**Agent route**: `POST /api/agent/run` → `routes/agent.rs` creates `AgentRuntime`, calls `runtime.run()`. Returns `AgentRunResponse` with answer, turn count, tool calls, and created node/edge summaries.

**Agent loop flow**:
```
build_context() → system_prompt() → load memory → LLM call → tool_calls?
  ├─ yes → execute tools → append results → loop (up to max_turns)
  └─ no  → save memory → return answer
```

## Start Here

**`src/lib.rs`** — the single entry point for understanding the server. Contains `AppState`, `serve()`, all middleware, config loading, UUID resolution, and the DeepSeek client. From there, follow `routes/*.rs` for individual endpoints and `db.rs` for data access.
