Back to Omniroute

Traffic Inspector

docs/frameworks/TRAFFIC_INSPECTOR.md

3.8.4930.7 KB
Original Source

Traffic Inspector

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.


§1 Overview

What makes Traffic Inspector unique

FeaturemitmwebCharlesFiddlerOmniRoute Traffic Inspector
Web-based
Open-sourcepartial
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 splitpartial
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)

Architecture in one paragraph

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.


§2 Capture modes

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".

Mode 1 — AgentBridge (default, always on)

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"

Mode 2 — Custom Hosts (DNS redirect)

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:

  • Monitor api.openai.com from Python scripts
  • Debug my-internal-llm.company.com
  • Capture traffic from mobile devices on the same network (via ARP spoofing — advanced)

Mode 3 — HTTP_PROXY listener (port 8080)

Source: 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"

bash
# 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.

Mode 4 — System-wide proxy (advanced, opt-in)

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:

  • macOS: networksetup -setwebproxy / -setsecurewebproxy
  • Linux: gsettings set org.gnome.system.proxy + /etc/environment
  • Windows: netsh 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:

  • Auto-disable timer (default 30 min, configurable via INSPECTOR_SYSTEM_PROXY_GUARD_MINUTES)
  • Previous system proxy state is saved in DB and restored on revert
  • Dashboard shows "Reverting system proxy" prompt if user navigates away while active
  • UI shows ⚠ Advanced badge + explicit confirmation checkbox

Mode 5 — TPROXY transparent decrypt (Linux, root, opt-in)

Source: 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).

Capture mode comparison

ModeSetupSudo?ReachNotes
1. AgentBridgeAutomaticOnce (cert+hosts)9 IDE agentsDefault on
2. Custom HostsPer-host inputYes (hosts file)Any app using that hostPersisted in DB
3. HTTP_PROXYexport HTTPS_PROXY=...NoApps respecting envPort 8080, no TLS decrypt by default
4. System-wideToggle + confirmYesAll apps on machineAuto-disable in 30 min
5. TPROXY decryptToggle (Linux + native addon)Yes (root + CA install)Any host on the target portDecrypts arbitrary hosts; off by default — see MITM-TPROXY-DECRYPT.md

§3 UI

3.1 Layout

┌─ 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 ║                                       │
└══════════════════════════════════╝══════════════════════════════════════╝

3.2 Request list (left panel)

  • Virtualized (useVirtualList + ResizeObserver): handles 1000 items without freezing
  • Auto-scroll with toggle to pause while inspecting
  • Color-coded status: green (2xx), yellow (3xx), red (4xx/5xx), gray (in-flight)
  • Agent emoji: 🔵 Antigravity, 🟢 Copilot, 🟠 Kiro, 🟣 Codex, 🔷 Cursor, 🟤 Zed, 🟡 Claude Code, ⚫ Open Code, 🌐 custom host
  • Context color bar: 1px left border colored by contextKey (SHA-256 of system prompt) — visually groups related conversations
  • Lazy body: only the selected request's body is materialized in the detail tabs (avoids rendering 1000 × 1MB bodies)

3.3 Detail pane — 7 tabs

TabContentNotes
ConversationMulti-turn chat bubbles (system/user/assistant + tool_use/tool_result)Normalized from any provider format; only shown for detectedKind === "llm"
HeadersRequest + response header tablesSensitive headers (Authorization, Cookie, api-key) masked by default; "Show secrets" toggle
RequestRaw body, JSON tree view, model field badgePretty-printed JSON or raw text
ResponseRaw body or SSE event list; toggle "Raw ↔ Merged"SSE merger reconstructs final message from delta events
TimingWaterfall: proxy overhead vs upstream latencyTotal, TTFB, and size
LLM DetailsProvider, model, messages count, tokens in/out, cost estimate, mapped targetOnly shown for LLM requests
StatsRecharts: latency timeline, token bar chart, tool call scatterOnly shown when a recorded session is loaded

3.4 Toolbar controls

ControlAction
⎉ PauseStops rendering new requests; "X new" badge accumulates
🗑 ClearClears the UI list (server buffer is not affected)
⬇ Export .harDownloads current filtered list as HAR file
● Record sessionStarts a named recording session
Profile selectorLLM only / Custom hosts / All
Host filterSubstring match on host field
Agent filterDropdown: All / per-agent
Status filterAll / 2xx / 3xx / 4xx / 5xx / error
Source filterAll / agent-bridge / custom-host / http-proxy / system-proxy / tproxy
Live filterShow only in-flight (open) requests — liveOnly toggle (see §4.6)

3.5 Resizable panels

  • List and detail pane separated by a drag handle
  • List width: min 280px, max 720px, persisted in localStorage (inspector.listWidth)
  • Collapsible to a 48px rail (icon-only); click a row in the rail to expand

§4 LLM-aware features

4.1 Kind detector (src/mitm/inspector/kindDetector.ts)

Classifies each request as "llm", "app", or "unknown" using 4 signals:

  1. Host registry — ~18 known LLM API hostnames (OpenAI, Anthropic, Gemini, Groq, Mistral, Together, Fireworks, Cohere, Perplexity, Hugging Face, OpenRouter, xAI, Moonshot, etc.)
  2. Path patterns/v1/chat/completions, /v1/messages, /generateContent, /v1/responses, etc.
  3. Body shape — detects messages[] (OpenAI/Claude), contents[] (Gemini), prompt, input fields
  4. User-agent hintscodex, claude, gemini, antigravity, kiro, copilot, cursor in UA string

Custom hosts added via Mode 2 inherit their kind from the form input (defaults to "custom").

4.2 SSE merger (src/mitm/inspector/sseMerger.ts)

MIT port from chouzz/llm-interceptor

Reconstructs the final assistant message from raw SSE delta events:

  • Anthropic: accumulates content_block_delta by index; handles text_delta, input_json_delta (tool calls), thinking_delta
  • OpenAI: accumulates choices[i].delta.content and tool_calls by index
  • Gemini: accumulates candidates[i].content.parts
  • Unknown: returns raw events as-is

The Response tab shows a toggle: "Raw events ↔ Merged".

4.3 Conversation normalizer (src/mitm/inspector/conversationNormalizer.ts)

MIT port from chouzz/llm-interceptor

Converts OpenAI, Anthropic, and Gemini message formats to a single NormalizedConversation before rendering:

ts
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.

4.4 Context key colorization (src/mitm/inspector/contextKey.ts)

  • Computes SHA-256 of the system prompt (first role:system message, or system field, or Gemini systemInstruction)
  • Returns a 12-character hex prefix ("a3f9c2...")
  • Frontend maps the key to a deterministic HSL color for the left-border bar
  • Filtro "same context": clicking the ctx #a3f chip adds a filter to show only requests with the same fingerprint

This makes it easy to visually distinguish different "personas" or tasks running in the same agent session.

4.5 LLM metadata extraction

For LLM requests, the LLM Details tab extracts:

ts
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
}

4.6 Live in-flight request filter

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:

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).

4.7 Process attribution (Linux)

On Linux, each intercepted request can be attributed to the originating local process. Two optional fields are added to InterceptedRequest:

ts
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:

  1. Reading /proc/net/tcp and /proc/net/tcp6 to find the socket inode for the port (parseProcNetTcpForInode, a pure fixture-testable parser).
  2. Scanning /proc/<pid>/fd/ for a symlink to socket:[<inode>].
  3. Reading the process name from /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).


§5 Sessions

5.1 Recording a session

  1. Click "● Record session" in the toolbar → enter a name (optional)
  2. Live tail continues normally; a red pulsing indicator shows ◉ REC · <name> · 00:42 · 23 reqs
  3. Click "⏹ Stop" → the session snapshot is saved to inspector_sessions + inspector_session_requests

5.2 Viewing a recorded session

The Sessions dropdown in the toolbar lists saved sessions. Selecting one:

  • Loads the session's snapshot (frozen state)
  • A banner shows: Viewing recorded session "<name>" — [Back to live]
  • The Stats tab becomes available with Recharts aggregates

5.3 Export formats

Each session can be exported as:

FormatUse
HAR (HTTP Archive 1.2)Compatible with Chrome DevTools, Charles, Fiddler — import for offline analysis
JSONLOne 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.


§6 Security

Traffic Inspector shows all intercepted HTTPS traffic, including authorization headers and request bodies. The following controls are in place:

ControlDetails
LOCAL_ONLYAll routes and the WebSocket endpoint are loopback-only (enforced in routeGuard.ts before auth)
Secret maskingmaskSecrets() applied to all headers and bodies before TrafficBuffer.push() — enabled by default (INSPECTOR_MASK_SECRETS=true)
Body size capBodies > INSPECTOR_MAX_BODY_KB (default 1024 KB) are truncated with "(truncated for performance)" notice
Sensitive header maskingauthorization, cookie, api-key, x-api-key, proxy-authorizationBearer *** in Headers tab; "Show secrets" toggle
CSPStrict Content Security Policy on Traffic Inspector pages to prevent XSS via injected response bodies
No persistence by defaultThe TrafficBuffer is in-memory and lost on server restart. Sessions are persisted only when explicitly recorded

Hard Rules applied

RuleApplication
#12 sanitizeErrorMessageAll HTTP error responses from Traffic Inspector routes are sanitized
#15 + #17 isLocalOnlyPath()/api/tools/traffic-inspector/ is LOCAL_ONLY + SPAWN_CAPABLE (system proxy commands)

Known limitations

  • System-wide proxy mode affects all applications on the machine, including VPN clients and SSO. Always use with the auto-disable timer. Do not use on shared machines.
  • CONNECT tunnel HTTPS: Mode 3 (HTTP_PROXY) captures only tunnel metadata for HTTPS destinations unless TLS interception is enabled. This is by design — transparent capture without the AgentBridge cert being trusted would break TLS verification for those apps.
  • Hardcoded strings in some components: Some UI components (F7/F8) have a small number of hardcoded strings not yet covered by i18n keys. These are documented as a Known Limitation in the i18n gap report; they will be migrated in a follow-up pass. Affected strings are UI decorative labels that don't require translation for functional use.

§7 Troubleshooting

WebSocket disconnection

If the live tail shows "Disconnected":

  1. Check the server is still running: GET /api/tools/traffic-inspector/capture-modes
  2. Reload the page — the WebSocket reconnects and receives a fresh snapshot
  3. If the server was restarted, the in-memory buffer was cleared — old entries are gone unless a session was recorded

Port 8080 conflict

If HTTP_PROXY mode fails to start:

bash
lsof -i :8080    # find the process

Change the port:

bash
# .env
INSPECTOR_HTTP_PROXY_PORT=8888

System proxy not reverted

If OmniRoute crashes while system-wide proxy mode is active:

macOS:

bash
networksetup -setwebproxystate Wi-Fi off
networksetup -setsecurewebproxystate Wi-Fi off

Linux (GNOME):

bash
gsettings set org.gnome.system.proxy mode 'none'

Windows:

cmd
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.

Buffer full

When the buffer reaches INSPECTOR_BUFFER_SIZE (default 1000), new entries rotate out the oldest. If important requests are being lost:

  • Increase INSPECTOR_BUFFER_SIZE (e.g., 5000) — trades memory for retention
  • Record a session to persist the relevant window to DB

§8 API reference

All routes are LOCAL_ONLY (loopback-only) and SPAWN_CAPABLE (system proxy commands). See src/server/authz/routeGuard.ts.

Base path: /api/tools/traffic-inspector/

Request management

MethodPathDescription
GET/requestsList requests (filterable: ?profile=llm&host=&agent=&status=&source=&sessionId=)
GET/requests/{id}Single request details
DELETE/requestsClear the in-memory buffer
POST/requests/{id}/replayRe-execute the same request through OmniRoute router
PUT/requests/{id}/annotationSave or update a note on a request

WebSocket

MethodPathDescription
GET/wsLive WebSocket stream. Sends snapshot on connect, then new/update/clear events

Export

MethodPathDescription
GET/export.harExport current filtered list as HAR 1.2

Custom hosts

MethodPathDescription
GET/hostsList custom hosts
POST/hostsAdd host (auto-edits /etc/hosts)
DELETE/hosts/{host}Remove host
PATCH/hosts/{host}Toggle enabled

Capture modes

MethodPathDescription
GET/capture-modesState of the AgentBridge / custom-hosts / HTTP_PROXY / system-proxy modes + the tls-intercept toggle
POST/capture-modes/http-proxyStart/stop HTTP_PROXY listener ({action: "start"|"stop"})
POST/capture-modes/system-proxyApply/revert system-wide proxy ({action: "apply"|"revert"})
POST/capture-modes/tls-interceptToggle 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/. See docs/security/MITM-TPROXY-DECRYPT.md.

Sessions

MethodPathDescription
POST/sessionsStart recording ({name?: string})
PATCH/sessions/{id}Stop or rename ({action: "stop"|"rename", name?: string})
GET/sessionsList all saved sessions
GET/sessions/{id}Session snapshot (all requests)
DELETE/sessions/{id}Delete session
GET/sessions/{id}/export.harExport session as HAR 1.2

Internal ingest (D4 fallback)

MethodPathDescription
POST/internal/ingestAccepts intercepted request from server.cjs passthrough path; requires INSPECTOR_INTERNAL_INGEST_TOKEN header

Full OpenAPI schemas: docs/openapi.yaml → tag Traffic Inspector.