docs/frameworks/TRAFFIC_INSPECTOR.md
Traffic Inspector is OmniRoute's built-in HTTPS traffic debugger — a Charles Proxy / mitmweb / HTTP Toolkit-like tool that is LLM-aware and agent-aware. It lives at /dashboard/tools/traffic-inspector and receives live traffic from up to 5 simultaneous capture sources.
Dashboard location: /dashboard/tools/traffic-inspector
Sidebar group: Tools (after AgentBridge)
See also: AGENTBRIDGE.md — AgentBridge is capture mode 1.
| Feature | mitmweb | Charles | Fiddler | OmniRoute Traffic Inspector |
|---|---|---|---|---|
| Web-based | ✓ | ✗ | ✗ | ✓ |
| Open-source | ✓ | ✗ | partial | ✓ |
| Agent-aware (knows if request is from Antigravity/Copilot/etc.) | ✗ | ✗ | ✗ | ✓ |
| LLM-aware (parses OpenAI/Anthropic/Gemini shape, tokens, model) | ✗ | ✗ | ✗ | ✓ |
| Model mapping visible (gemini-3-flash → claude-sonnet-4.7) | ✗ | ✗ | ✗ | ✓ |
| Proxy/upstream latency split | partial | ✗ | ✗ | ✓ |
| Integrated with OmniRoute routing, fallback, cost | ✗ | ✗ | ✗ | ✓ |
| System-wide proxy debug (any app on the machine) | ✓ | ✓ | ✓ | ✓ |
| Custom host capture (per-host DNS redirect) | ✓ | ✓ | ✓ | ✓ |
| HTTP_PROXY env mode | ✓ | ✓ | ✓ | ✓ |
| Conversation view (multi-turn bubbles, tool_use/tool_result) | ✗ | ✗ | ✗ | ✓ |
| SSE stream merger (reconstruct from delta events) | ✗ | ✗ | ✗ | ✓ |
| Session recording (named, exportable .har/.jsonl) | ✗ | ✓ | ✓ | ✓ |
The TrafficBuffer (src/mitm/inspector/buffer.ts) is a shared in-memory ring buffer (default 1000 entries, configurable via INSPECTOR_BUFFER_SIZE). All capture sources write to it via push(). The buffer classifies each entry using kindDetector.ts (determines if it's an LLM request), computes a contextKey (SHA-256 fingerprint of the system prompt), and broadcasts to all WebSocket subscribers via globalTrafficBuffer.subscribe(). The dashboard connects via GET /api/tools/traffic-inspector/ws and receives a snapshot on connect, followed by new/update/clear events.
Traffic Inspector supports 5 simultaneous capture sources. Each is independently toggleable. The source field on every InterceptedRequest (src/mitm/inspector/types.ts) is one of "agent-bridge", "custom-host", "http-proxy", "system-proxy", or "tproxy".
Source: AgentBridge handlers (src/mitm/handlers/base.ts)
Mechanism: Every intercept() call in MitmHandlerBase calls hookBufferStart() before forwarding and hookBufferUpdate() on completion. Zero extra config — works as soon as AgentBridge is running.
Reach: The 9 IDE agents configured in AgentBridge
Note: source field in InterceptedRequest = "agent-bridge"
Source: User-defined host list (inspector_custom_hosts table)
Mechanism: Adding a host via the UI adds 127.0.0.1 <host> to /etc/hosts (requires sudo). The existing AgentBridge MITM server (port 443) generates a SNI cert dynamically for the new host.
Reach: Any application using the added host — no app config change needed
Note: source = "custom-host"
Example use cases:
api.openai.com from Python scriptsmy-internal-llm.company.comSource: Applications using HTTP_PROXY/HTTPS_PROXY environment variables
Mechanism: Secondary listener at port 8080 (src/mitm/inspector/httpProxyServer.ts) that acts as a standard explicit HTTP/HTTPS proxy. Accepts CONNECT tunnels (HTTPS) and direct HTTP requests.
Reach: Any application that respects HTTP_PROXY env — no DNS change, no sudo
Note: source = "http-proxy"
# Quick capture for a single command:
HTTPS_PROXY=http://127.0.0.1:8080 curl https://api.openai.com/v1/models
# Persistent capture in a shell session:
export HTTP_PROXY=http://127.0.0.1:8080
export HTTPS_PROXY=http://127.0.0.1:8080
TLS limitation: HTTPS CONNECT tunnels are captured as metadata only (host, port, timing) — TLS body is not decrypted by default. Enable "Decrypt HTTPS in proxy mode" toggle (opt-in, requires AgentBridge cert to be trusted) for full body inspection.
Port conflict: If port 8080 is in use, AgentBridge returns a 409 with a structured error. Change the port via INSPECTOR_HTTP_PROXY_PORT env var.
Source: OS-level proxy settings (applies to all apps on the machine) Mechanism: Uses OS APIs to redirect all HTTP/HTTPS traffic through the HTTP_PROXY listener:
networksetup -setwebproxy / -setsecurewebproxygsettings set org.gnome.system.proxy + /etc/environmentnetsh winhttp set proxy 127.0.0.1:8080
Reach: Every application on the machine that respects system proxy settings
Note: source = "system-proxy"Safety mechanisms:
INSPECTOR_SYSTEM_PROXY_GUARD_MINUTES)⚠ Advanced badge + explicit confirmation checkboxSource: Kernel TPROXY + policy routing (src/mitm/tproxy/)
Mechanism: Marks new local outbound TCP connections to a target port (default 443) in mangle OUTPUT, an ip rule reroutes the marked packets to local delivery, and mangle PREROUTING's TPROXY target hands them to a transparent (IP_TRANSPARENT) listener (default port 8443). The listener terminates TLS with a leaf certificate issued per SNI hostname on demand by a dynamic CA, captures the decrypted exchange, and forwards the request re-encrypted to the original destination.
Reach: Arbitrary destination hosts on the target port — no /etc/hosts spoof, no HTTP_PROXY env, no system-wide proxy mutation. The intercepted process needs no config change, but must trust the dynamic CA.
Note: source = "tproxy"
Requirements: Linux only (IP_TRANSPARENT is Linux-only), the CAP_NET_ADMIN capability (root), and a native N-API addon that must be built with a C toolchain (npm run build:native:tproxy). When unavailable, the dashboard toggle is disabled with the tooltip "TPROXY decrypt requires Linux + root + the native addon". The firewall rules apply/revert transactionally (a crash never leaves a mangle rule behind) and flush on reboot. An SO_MARK-based anti-loop keeps the proxy's own re-encrypted forward from being re-intercepted.
This is a substantial subsystem with its own dedicated operator guide — see docs/security/MITM-TPROXY-DECRYPT.md for the full firewall recipe, the per-SNI dynamic CA + trust-store installer, the local-only route, anti-loop details, and the configuration schema. The toggle is driven by GET / POST / DELETE /api/tools/agent-bridge/tproxy (note: the route lives under the AgentBridge prefix, not the Traffic Inspector prefix).
| Mode | Setup | Sudo? | Reach | Notes |
|---|---|---|---|---|
| 1. AgentBridge | Automatic | Once (cert+hosts) | 9 IDE agents | Default on |
| 2. Custom Hosts | Per-host input | Yes (hosts file) | Any app using that host | Persisted in DB |
| 3. HTTP_PROXY | export HTTPS_PROXY=... | No | Apps respecting env | Port 8080, no TLS decrypt by default |
| 4. System-wide | Toggle + confirm | Yes | All apps on machine | Auto-disable in 30 min |
| 5. TPROXY decrypt | Toggle (Linux + native addon) | Yes (root + CA install) | Any host on the target port | Decrypts arbitrary hosts; off by default — see MITM-TPROXY-DECRYPT.md |
┌─ Traffic Inspector ─────────────────────────────────────────────────────┐
│ ┌─ Capture sources toolbar ─────────────────────────────────────────┐ │
│ │ [✓ AgentBridge] [✓ Custom hosts (3)] [○ HTTP_PROXY] [○ System]│ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─ Filter/control bar ──────────────────────────────────────────────┐ │
│ │ Profile: (●) LLM only (○) Custom (○) All │ │
│ │ [⎉ Pause] [🗑 Clear] [⬇ .har] [● REC session] ● live 482/1k │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
├══◀▶══════════════════════════════╬══════════════════════════════════════╤╡
│ REQUEST LIST (resizable) ║ DETAIL PANE ▲ │
│ ────────────────────────────── │ ║ [Conversation][Headers][Request] │ │
│ ▎ 14:32 POST 200 12k AG openai ║ [Response][Timing][LLM][Stats] │ │
│ ▎ 14:31 POST 200 8k CP openai ║ ▼ │
│ ▎ 14:31 POST 503 ⚠ KR ... ║ │
│ ▎ 14:30 GET 200 3k 🌐 custom ║ │
└══════════════════════════════════╝══════════════════════════════════════╝
useVirtualList + ResizeObserver): handles 1000 items without freezingcontextKey (SHA-256 of system prompt) — visually groups related conversations| Tab | Content | Notes |
|---|---|---|
| Conversation | Multi-turn chat bubbles (system/user/assistant + tool_use/tool_result) | Normalized from any provider format; only shown for detectedKind === "llm" |
| Headers | Request + response header tables | Sensitive headers (Authorization, Cookie, api-key) masked by default; "Show secrets" toggle |
| Request | Raw body, JSON tree view, model field badge | Pretty-printed JSON or raw text |
| Response | Raw body or SSE event list; toggle "Raw ↔ Merged" | SSE merger reconstructs final message from delta events |
| Timing | Waterfall: proxy overhead vs upstream latency | Total, TTFB, and size |
| LLM Details | Provider, model, messages count, tokens in/out, cost estimate, mapped target | Only shown for LLM requests |
| Stats | Recharts: latency timeline, token bar chart, tool call scatter | Only shown when a recorded session is loaded |
| Control | Action |
|---|---|
| ⎉ Pause | Stops rendering new requests; "X new" badge accumulates |
| 🗑 Clear | Clears the UI list (server buffer is not affected) |
| ⬇ Export .har | Downloads current filtered list as HAR file |
| ● Record session | Starts a named recording session |
| Profile selector | LLM only / Custom hosts / All |
| Host filter | Substring match on host field |
| Agent filter | Dropdown: All / per-agent |
| Status filter | All / 2xx / 3xx / 4xx / 5xx / error |
| Source filter | All / agent-bridge / custom-host / http-proxy / system-proxy / tproxy |
| Live filter | Show only in-flight (open) requests — liveOnly toggle (see §4.6) |
localStorage (inspector.listWidth)src/mitm/inspector/kindDetector.ts)Classifies each request as "llm", "app", or "unknown" using 4 signals:
/v1/chat/completions, /v1/messages, /generateContent, /v1/responses, etc.messages[] (OpenAI/Claude), contents[] (Gemini), prompt, input fieldscodex, claude, gemini, antigravity, kiro, copilot, cursor in UA stringCustom hosts added via Mode 2 inherit their kind from the form input (defaults to "custom").
src/mitm/inspector/sseMerger.ts)MIT port from chouzz/llm-interceptor
Reconstructs the final assistant message from raw SSE delta events:
content_block_delta by index; handles text_delta, input_json_delta (tool calls), thinking_deltachoices[i].delta.content and tool_calls by indexcandidates[i].content.partsThe Response tab shows a toggle: "Raw events ↔ Merged".
src/mitm/inspector/conversationNormalizer.ts)MIT port from chouzz/llm-interceptor
Converts OpenAI, Anthropic, and Gemini message formats to a single NormalizedConversation before rendering:
interface NormalizedConversation {
request: NormalizedTurn[]; // messages / contents / prompt from request body
response: NormalizedTurn[]; // assistant response (merged via sseMerger)
contextKey: string | null; // SHA-256 system-prompt fingerprint
}
Block types: text, tool_use, tool_result. The Conversation tab uses this shape regardless of provider.
src/mitm/inspector/contextKey.ts)SHA-256 of the system prompt (first role:system message, or system field, or Gemini systemInstruction)"a3f9c2...")ctx #a3f chip adds a filter to show only requests with the same fingerprintThis makes it easy to visually distinguish different "personas" or tasks running in the same agent session.
For LLM requests, the LLM Details tab extracts:
interface LlmMetadata {
provider: string | null; // "openai" | "anthropic" | "gemini" | ...
apiKind: string | null; // "chat.completions" | "messages" | "embeddings" | ...
model: string | null; // from request body or response
messages: number; // turn count
tokensIn: number | null; // usage.prompt_tokens / usage.input_tokens
tokensOut: number | null; // usage.completion_tokens / usage.output_tokens
streamed: boolean; // true if SSE response
mappedTo: string | null; // x-omniroute-mapped header
costEstimateUsd: number | null; // estimated cost based on OmniRoute pricing
}
The request status field is number | "in-flight" | "error" — an entry is
pushed as "in-flight" the moment the request starts and updated in place
when the response (or error) arrives. The toolbar's "Live" toggle
(liveOnly, i18n key trafficInspector.liveOnly) restricts the list to entries
whose status === "in-flight", letting you watch open connections in real time.
The filter is a pure, client-side predicate in
src/lib/inspector/matchesTrafficFilter.ts:
if (f.liveOnly && req.status !== "in-flight") return false;
The toggle state lives in useTrafficFilters (the inspector dashboard hooks) and
combines with the other filters (profile, host, agent, source, status, context).
On Linux, each intercepted request can be attributed to the originating local
process. Two optional fields are added to InterceptedRequest:
pid?: number; // originating process id (Linux only)
processName?: string; // originating process name (Linux only)
src/mitm/inspector/processAttribution.ts maps the connection's client
ephemeral port to a PID + name by:
/proc/net/tcp and /proc/net/tcp6 to find the socket inode for the
port (parseProcNetTcpForInode, a pure fixture-testable parser)./proc/<pid>/fd/ for a symlink to socket:[<inode>]./proc/<pid>/comm.A 1-second TTL cache bounds the procfs scan cost under load. Attribution is
best-effort — any failure resolves to null and never blocks capture. On
macOS/Windows the function returns null (stub; lsof/GetExtendedTcpTable
support is a follow-up).
◉ REC · <name> · 00:42 · 23 reqsinspector_sessions + inspector_session_requestsThe Sessions dropdown in the toolbar lists saved sessions. Selecting one:
Viewing recorded session "<name>" — [Back to live]Each session can be exported as:
| Format | Use |
|---|---|
| HAR (HTTP Archive 1.2) | Compatible with Chrome DevTools, Charles, Fiddler — import for offline analysis |
| JSONL | One InterceptedRequest per line — compatible with llm-interceptor format |
Export via GET /api/tools/traffic-inspector/sessions/{id}/export.har or the ⬇ button in the Sessions dropdown.
Traffic Inspector shows all intercepted HTTPS traffic, including authorization headers and request bodies. The following controls are in place:
| Control | Details |
|---|---|
| LOCAL_ONLY | All routes and the WebSocket endpoint are loopback-only (enforced in routeGuard.ts before auth) |
| Secret masking | maskSecrets() applied to all headers and bodies before TrafficBuffer.push() — enabled by default (INSPECTOR_MASK_SECRETS=true) |
| Body size cap | Bodies > INSPECTOR_MAX_BODY_KB (default 1024 KB) are truncated with "(truncated for performance)" notice |
| Sensitive header masking | authorization, cookie, api-key, x-api-key, proxy-authorization → Bearer *** in Headers tab; "Show secrets" toggle |
| CSP | Strict Content Security Policy on Traffic Inspector pages to prevent XSS via injected response bodies |
| No persistence by default | The TrafficBuffer is in-memory and lost on server restart. Sessions are persisted only when explicitly recorded |
| Rule | Application |
|---|---|
#12 sanitizeErrorMessage | All HTTP error responses from Traffic Inspector routes are sanitized |
#15 + #17 isLocalOnlyPath() | /api/tools/traffic-inspector/ is LOCAL_ONLY + SPAWN_CAPABLE (system proxy commands) |
If the live tail shows "Disconnected":
GET /api/tools/traffic-inspector/capture-modesIf HTTP_PROXY mode fails to start:
lsof -i :8080 # find the process
Change the port:
# .env
INSPECTOR_HTTP_PROXY_PORT=8888
If OmniRoute crashes while system-wide proxy mode is active:
macOS:
networksetup -setwebproxystate Wi-Fi off
networksetup -setsecurewebproxystate Wi-Fi off
Linux (GNOME):
gsettings set org.gnome.system.proxy mode 'none'
Windows:
netsh winhttp reset proxy
The dashboard will also offer "Revert system proxy" on next load if it detects the DB state indicates proxy was active.
When the buffer reaches INSPECTOR_BUFFER_SIZE (default 1000), new entries rotate out the oldest. If important requests are being lost:
INSPECTOR_BUFFER_SIZE (e.g., 5000) — trades memory for retentionAll routes are LOCAL_ONLY (loopback-only) and SPAWN_CAPABLE (system proxy commands). See src/server/authz/routeGuard.ts.
Base path: /api/tools/traffic-inspector/
| Method | Path | Description |
|---|---|---|
| GET | /requests | List requests (filterable: ?profile=llm&host=&agent=&status=&source=&sessionId=) |
| GET | /requests/{id} | Single request details |
| DELETE | /requests | Clear the in-memory buffer |
| POST | /requests/{id}/replay | Re-execute the same request through OmniRoute router |
| PUT | /requests/{id}/annotation | Save or update a note on a request |
| Method | Path | Description |
|---|---|---|
| GET | /ws | Live WebSocket stream. Sends snapshot on connect, then new/update/clear events |
| Method | Path | Description |
|---|---|---|
| GET | /export.har | Export current filtered list as HAR 1.2 |
| Method | Path | Description |
|---|---|---|
| GET | /hosts | List custom hosts |
| POST | /hosts | Add host (auto-edits /etc/hosts) |
| DELETE | /hosts/{host} | Remove host |
| PATCH | /hosts/{host} | Toggle enabled |
| Method | Path | Description |
|---|---|---|
| GET | /capture-modes | State of the AgentBridge / custom-hosts / HTTP_PROXY / system-proxy modes + the tls-intercept toggle |
| POST | /capture-modes/http-proxy | Start/stop HTTP_PROXY listener ({action: "start"|"stop"}) |
| POST | /capture-modes/system-proxy | Apply/revert system-wide proxy ({action: "apply"|"revert"}) |
| POST | /capture-modes/tls-intercept | Toggle HTTPS body decryption in proxy mode ({enabled: boolean}) |
TPROXY decrypt (capture mode 5) is driven by a separate route under the AgentBridge prefix —
GET / POST / DELETE /api/tools/agent-bridge/tproxy— not under/api/tools/traffic-inspector/. Seedocs/security/MITM-TPROXY-DECRYPT.md.
| Method | Path | Description |
|---|---|---|
| POST | /sessions | Start recording ({name?: string}) |
| PATCH | /sessions/{id} | Stop or rename ({action: "stop"|"rename", name?: string}) |
| GET | /sessions | List all saved sessions |
| GET | /sessions/{id} | Session snapshot (all requests) |
| DELETE | /sessions/{id} | Delete session |
| GET | /sessions/{id}/export.har | Export session as HAR 1.2 |
| Method | Path | Description |
|---|---|---|
| POST | /internal/ingest | Accepts intercepted request from server.cjs passthrough path; requires INSPECTOR_INTERNAL_INGEST_TOKEN header |
Full OpenAPI schemas: docs/openapi.yaml → tag Traffic Inspector.