Skip to content

feat: capture and decode WebSocket frames for OpenAI /v1/responses#35

Draft
tito wants to merge 4 commits intomainfrom
mathieu/feat-decoding-websocket
Draft

feat: capture and decode WebSocket frames for OpenAI /v1/responses#35
tito wants to merge 4 commits intomainfrom
mathieu/feat-decoding-websocket

Conversation

@tito
Copy link
Copy Markdown
Contributor

@tito tito commented Apr 2, 2026

Summary

  • Adds end-to-end WebSocket MITM capture for permessage-deflate compressed WebSocket connections (used by OpenAI /v1/responses and the codex CLI)
  • 101 Switching Protocols responses are now visible in the activity log with full request/response headers
  • Captured frames are stored as WS_REQ / WS_RESP transactions with decompressed JSON payloads
  • Conversation assembler no longer crashes on WebSocket rows

Changes

Core WebSocket capture (sniffer.go, mitm_hook.go):

  • Fire HTTPRoundTripHook on 101 responses so upgrades appear in the activity log
  • Rewrite Sec-Websocket-Extensions on WebSocket upgrades to force client_no_context_takeover; server_no_context_takeover, making each frame independently decompressible
  • Add GlobalWebSocketFrameHook / WebSocketFrameInfo for per-frame callbacks
  • Fix in-place XOR masking bug: copy payload before unmasking so forwarded frames stay correctly masked on the wire

Decompression (program.go):

  • Wire WebSocket frame hook to store frames as WS_REQ / WS_RESP transactions
  • Decompress permessage-deflate payloads using the gorilla-style 9-byte tail (\x00\x00\xff\xff\x01\x00\x00\xff\xff) required by Go's compress/flate

Assembler fixes (conversation_assembler.go):

  • Scan response_content_type and duration_ms as nullable (*string, *int64) — both are NULL for WS rows, which previously caused the assembler to skip every WS transaction

Other:

  • Add -silent-allow flag for test matrix runner
  • Enable sniffing.websocket: true and mitm.alpn: http/1.1 in greyproxy.yml
  • Add make local target building to ./greyproxy (path used by scripts/test-matrix/run.sh)
  • docs/websocket-support.md: full history of issues encountered and fixes applied

Test plan

  • Run scripts/test-matrix/run.sh --isolate with codex CLI and verify WS_REQ/WS_RESP transactions appear in the activity log with readable JSON payloads
  • Confirm no decompress failures in the log
  • Confirm assembler processes WS rows without WARN lines
  • Confirm regular HTTP/SSE traffic (Anthropic, OpenAI REST) still works correctly

tito added 4 commits April 1, 2026 18:12
…assembly

Add OpenAI Responses API dissector that parses /v1/responses endpoint
traffic (not /v1/chat/completions). Handles the heterogeneous input[]
format with role-based (developer/user) and type-based (function_call,
function_call_output, reasoning) items. Session ID extracted from
prompt_cache_key field.

Make ClassifyThread provider-aware: Anthropic uses system prompt length
thresholds, OpenAI uses tool list heuristics (task/question/todowrite
presence indicates main conversation vs subagent).

Fix cross-provider contamination in loadTransactionsForSessions: the
LIKE query could match transactions that merely mention a session ID
in tool results. Now filters post-extraction by dissector-extracted
SessionID.

Add remapOpenAISubagents to link OpenCode subagent sessions to their
parent via task_id references in function_call_output content.

Remove the Linked Subagents blue box from conversation detail UI.
Add docs/llm-api-comparison.md documenting Anthropic Messages API vs
OpenAI Responses API wire formats as observed through proxy traffic.

Add toolIconCategory template func that normalizes tool names across
providers (PascalCase vs lowercase) and add icons for WebFetch,
Skill, AskUserQuestion/question, TodoWrite, and apply_patch.
When the assembler processes transactions incrementally, subagent sessions
(with their own prompt_cache_key) may have been processed in a previous
batch. The parent session's task_id references then can't find them for
remapping. Fix by scanning loaded entries for task_id references and
loading those additional sessions before groupBySession runs.
Adds end-to-end WebSocket MITM support for the permessage-deflate
compressed WebSocket protocol used by OpenAI's /v1/responses endpoint
(and codex CLI).

Changes:
- sniffer.go: fire HTTPRoundTripHook on 101 Switching Protocols so
  upgrades are visible in the activity log
- sniffer.go: rewrite Sec-Websocket-Extensions on WS upgrades to force
  client_no_context_takeover + server_no_context_takeover, making each
  frame independently decompressible
- sniffer.go: add GlobalWebSocketFrameHook and WebSocketFrameInfo type;
  fix in-place XOR masking bug (copy before unmask so forwarded frames
  stay correctly masked)
- mitm_hook.go: re-export MitmWebSocketFrameInfo and
  SetGlobalMitmWebSocketFrameHook for use outside gostx/internal
- program.go: wire WebSocket frame hook to store frames as WS_REQ /
  WS_RESP transactions; decompress permessage-deflate payloads using
  gorilla-style 9-byte tail (\x00\x00\xff\xff\x01\x00\x00\xff\xff)
- program.go: add -silent-allow flag to auto-allow all traffic on start
- main.go: wire -silent-allow flag
- conversation_assembler.go: scan response_content_type and duration_ms
  as nullable (*string, *int64) so WS rows don't crash the assembler
- greyproxy.yml: enable sniffing.websocket and mitm.alpn for both
  HTTP and SOCKS5 services
- Makefile: add `make local` target building to ./greyproxy (used by
  scripts/test-matrix/run.sh)
- docs/websocket-support.md: document all issues encountered and fixes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant