Skip to content

feat(agent-proxy): broker credentials over WebSocket connections - #356

Open
saifsmailbox98 wants to merge 3 commits into
mainfrom
saif/age2-52-add-websocket-support-for-agent-proxy
Open

feat(agent-proxy): broker credentials over WebSocket connections#356
saifsmailbox98 wants to merge 3 commits into
mainfrom
saif/age2-52-add-websocket-support-for-agent-proxy

Conversation

@saifsmailbox98

@saifsmailbox98 saifsmailbox98 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description 📣

The proxy MITMs every CONNECT and strips Upgrade as a hop-by-hop header, so a WebSocket handshake never survived it. Real-time APIs failed outright rather than being brokered, which ruled out anything on Slack Socket Mode, the Discord gateway, or a streaming MCP transport.

Two things now work.

Handshake brokering. The upgrade is an ordinary HTTP request, so header rewrites and path / query / header substitution apply to it exactly as they do anywhere else. The upgrade headers are preserved across the hop-by-hop strip, the upstream is dialled on a connection the proxy owns (a hijacked WebSocket can't be driven through http.Transport's pooled round tripper), and after 101 the proxy owns both sockets. A non-101 response is relayed normally so the agent sees the real rejection instead of a proxy error.

Frame substitution. A new websocket substitution surface, for services that expect the credential in the first message rather than in the handshake. Frames are parsed on the way out and re-masked with a fresh key per RFC 6455 §5.3 — reusing the client's key would leak the XOR relationship between the placeholder and the real value. Binary, fragmented, compressed and oversized (>1MB) frames are forwarded byte-for-byte, so an unsupported shape degrades to passthrough rather than corrupting the stream. Substitution runs client-to-upstream only; nothing is ever rewritten on the way back to the agent.

Three things worth calling out, each of which was a bug before it was a decision:

  • permessage-deflate is removed from the upgrade offer where the websocket surface is in use. A compressed frame carries RSV1 and is never substituted, and most clients offer compression by default, so leaving the offer in place made the surface silently do nothing. Only affects services that use that surface.
  • The hijacked connection's deadlines are cleared. Go leaves them in place across Hijack, so the tunnel's 60s ReadTimeout killed every WebSocket after a minute.
  • serveTunnel now reports whether the inner handler hijacked. Serve returns the moment the one-shot listener closes on hijack, so handleConnect's deferred close was tearing the socket out from under a live pipe. Nothing had ever hijacked inside a tunnel before, so the invariant was untested.

upstreamTLSConfig reads the TLS settings off the transport rather than hardcoding system roots, so a WebSocket verifies its upstream the same way the plain HTTP path does.

Companion PR: Infisical/infisical#7581

Type ✨

  • Bug fix
  • New feature
  • Improvement
  • Breaking change
  • Documentation

Tests 🛠️

# Here's some code block to paste some code snippets

The proxy MITMs every CONNECT and strips Upgrade as a hop-by-hop header, so a
WebSocket handshake never survived it: real-time APIs failed outright rather
than being brokered.

- Preserve the upgrade headers across the hop-by-hop strip, dial the upstream on
  a connection the proxy owns, and take over both sockets after 101. A non-101
  response is relayed normally so the agent sees the real rejection.
- Add a 'websocket' substitution surface for services that expect the credential
  in the first message rather than the handshake. Frames are parsed on the way
  out and re-masked with a fresh key; binary, fragmented, compressed and
  oversized frames are forwarded unchanged.
- Decline permessage-deflate where that surface is in use, since a compressed
  frame is unreadable and the substitution would silently never fire.
- Clear the hijacked connection's deadlines and let the inner handler own the
  conn, so a long-lived WebSocket is not killed by the tunnel read timeout or
  closed underneath by handleConnect.
@linear

linear Bot commented Aug 7, 2026

Copy link
Copy Markdown

AGE2-52

@infisical-review-police

Copy link
Copy Markdown

💬 Discussion in Slack: #pr-review-cli-356-feat-agent-proxy-broker-credentials-over-websocket-connections

Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel.

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds credential-brokered WebSocket upgrades and client-frame substitution to the agent proxy, including raw upstream dialing, handshake-header preservation, compression negotiation changes, connection ownership, and frame remasking.

  • Refactors request preparation so HTTP and WebSocket forwarding share service matching and credential injection.
  • Adds bidirectional WebSocket piping with substitution for complete, uncompressed text frames.
  • Adds unit and integration coverage for handshake forwarding, response relay, frame shapes, remasking, TLS, and connection lifecycle.

Confidence Score: 4/5

The active-stream timeout defect should be fixed before merging because a valid one-way WebSocket can be disconnected while traffic is still flowing.

Each pipe direction maintains its own read deadline, while completion of either copier closes both sockets, so ten minutes of inactivity from the client terminates an otherwise active upstream stream; substitution audit records also omit the credential labels already prepared for them.

Files Needing Attention: packages/agentproxy/websocket.go, packages/agentproxy/rewrite.go

Important Files Changed

Filename Overview
packages/agentproxy/proxy.go Refactors upstream preparation and transfers tunnel ownership after a nested WebSocket hijack; no independent defect was found here.
packages/agentproxy/rewrite.go Adds WebSocket substitution selection and safe audit labels, but those labels are not consumed by the new close activity record.
packages/agentproxy/websocket.go Implements handshake and frame brokering; independent per-direction deadlines can terminate an active one-way stream, and substitution audit metadata is omitted.
packages/agentproxy/websocket_test.go Provides broad protocol coverage but does not test active one-way traffic across the idle timeout or credential metadata in close logs.

Reviews (1): Last reviewed commit: "feat(agent-proxy): broker credentials ov..." | Re-trigger Greptile

Comment thread packages/agentproxy/websocket.go Outdated
Comment thread packages/agentproxy/websocket.go Outdated
Comment thread packages/agentproxy/websocket.go Outdated
Comment thread packages/agentproxy/websocket.go Outdated
@veria-ai

veria-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

Comments here should earn their place by explaining why, per the rule in the
infisical repo's CLAUDE.md. Drops the leading clauses that restated a function's
signature (nothing in this file is exported, so godoc's name-first form buys
nothing) and one change-history note in a test, keeping the constraints and
reasons that are not visible from the code.
- Redact on the return path. The agent chooses where its placeholder goes, so it
  could plant one in a field the service echoes (a correlation id, an error
  message) and read the real credential out of the reply. Inbound text frames now
  swap the value back to the placeholder, which also stops a service that quotes
  the credential in an error from leaking it by accident.
- Share the idle deadline across both directions. It was per-direction, and
  either side expiring closed the connection, so a subscribe-mostly stream that
  received for ten minutes while sending nothing was torn down mid-stream.
- Bound the upstream dial. wsResponseTimeout only starts once TCP is up, so a
  dial at a black-holed address pinned a connection slot for as long as the OS
  took to give up.
- Name the brokered credentials on the close record, and only once a frame was
  actually rewritten. The labels were built and then never logged, which left
  audit consumers unable to tell which credential a connection carried.
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