# Network Communication — Theseus

> Theseus has two network surfaces: an inbound HTTP/WebSocket server (axum) and outbound HTTP clients (reqwest → DeepSeek API, CLI → server).

---

## Inbound: axum Server

### HTTP Server

| Aspect | Detail |
|--------|--------|
| Framework | axum 0.7 |
| Bind address | `0.0.0.0:{port}` (default 3080, configurable via `--port`) |
| Runtime | tokio (full features) |
| Middleware stack | CORS → TraceLayer → JSON error middleware → API key auth → route handlers |

### Middleware Pipeline

Request lifecycle (executed top-down):

```
HTTP Request
  → CorsLayer::permissive()           # Any origin (dev convenience)
  → TraceLayer                        # tracing-based request logging
  → json_error_middleware              # Wraps bare errors in ApiResponse JSON
  → api_key_auth_middleware            # Validates x-api-key / Bearer / ?api_key=
  → Route handler (routes/*.rs)
```

**API key auth** (`lib.rs:450-498`):
- Three sources checked in order: `x-api-key` header → `Authorization: Bearer` → `?api_key=` query param
- Skipped entirely if no API key is configured (server is open, logs warning)
- Missing/invalid → 401 JSON response

**CORS** (`lib.rs:842`):
- Default: `CorsLayer::permissive()` (any origin)
- Configurable via `cors_origins` in `theseus.toml` or `THESEUS_CORS_ORIGINS` env var
- Invalid origins fall back to `http://127.0.0.1:3080`

### WebSocket

| Aspect | Detail |
|--------|--------|
| Endpoint | `GET /api/ws` |
| Library | axum `ws` feature (tokio-tungstenite under the hood) |
| Broadcast channel | `tokio::sync::broadcast`, capacity 256 |
| Message format | Plain text: `"{entity}_{action}:{project_id}"` |

**Server-side handler** (`src/routes/ws.rs`):
- `ws_handler` upgrades HTTP → 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 via `tokio::select!`

**Message patterns:**

| Pattern | Trigger | Example |
|---------|---------|---------|
| `graph:{pid}` | Any mutation (node/edge CRUD, import, agent tool call) | `graph:573ffb12-...` |
| `node_created:{pid}` | Node created via REST API | `node_created:573ffb12-...` |
| `node_updated:{pid}` | Node updated | |
| `node_deleted:{pid}` | Node deleted | |
| `edge_created:{pid}` | Edge created | |
| `edge_updated:{pid}` | Edge updated | |
| `edge_deleted:{pid}` | Edge deleted | |
| `projects` | Project created or deleted | `projects` |

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

---

## Outbound: HTTP Clients

### DeepSeek API (One-shot)

Used by: `analysis.rs`, `report.rs`, `chat.rs` (tell/ask)

| Aspect | Detail |
|--------|--------|
| Function | `deepseek_chat()` at `lib.rs:539-599` |
| Client | `reqwest::Client::new()` (per-call, not pooled) |
| Endpoint | `POST https://api.deepseek.com/chat/completions` |
| Auth | `Authorization: Bearer {api_key}` |
| Model | `deepseek-v4-flash` |
| Payload | OpenAI-compatible chat format (system + user messages) |
| Features | Optional `response_format: {type: "json_object"}` for JSON mode |

**Error handling**: All DeepSeek errors (401, 402, 429, network failures) are mapped to `StatusCode::INTERNAL_SERVER_ERROR` — the raw API error is logged server-side but never exposed to the client.

### DeepSeek API (Agent Loop)

Used by: `src/agent/llm.rs` → `LlmClient`

| Aspect | Detail |
|--------|--------|
| Client | `reqwest::Client` (owned by `LlmClient`, reused across turns) |
| Endpoint | `POST https://api.deepseek.com/chat/completions` |
| Model | `deepseek-v4-flash` |
| Features | Tool calling (OpenAI function-calling format), streaming not used |
| Auth | `Authorization: Bearer {api_key}` |

`LlmClient` supports `ChatMessage` types: `system`, `user`, `assistant`, `tool`. Tool definitions are passed as `Vec<ToolDefinition>` and serialized to OpenAI function-calling format.

### CLI → Server

Used by: `src/main.rs` (all non-`serve` commands)

| Aspect | Detail |
|--------|--------|
| Client | `reqwest::Client` via `build_http_client(api_key)` |
| Default server | `http://127.0.0.1:3080` |
| Auth | `x-api-key` header injected by `build_http_client` |
| Content type | JSON (serde_json), multipart for evidence upload |

---

## Protocol Summary

| Direction | Protocol | Port | Auth | Purpose |
|-----------|----------|------|------|---------|
| Inbound | HTTP | 3080 (configurable) | Optional API key | REST API + static files |
| Inbound | WebSocket | 3080 (same port) | API key via query param | Real-time graph updates |
| Outbound | HTTPS | 443 | Bearer token | DeepSeek AI API |
| CLI ↔ Server | HTTP | 3080 | x-api-key header | CLI commands → REST API |

---

## Security Notes

- **No TLS**: The server runs plain HTTP. Use a reverse proxy (nginx, caddy) for HTTPS in production.
- **No rate limiting**: Add at reverse proxy layer.
- **CORS permissive by default**: Tighten `cors_origins` before exposing beyond localhost.
- **API key is optional**: When unset, server is open to anyone who can reach the port.
- **DeepSeek errors are swallowed**: Client never sees raw API error messages (prevents key leakage in error responses).
