Agent Diagnostic
Pointed agent at: crates/openshell-sandbox/src/
Diagnostic Investigation Path:
Traced Code: Read crates/openshell-sandbox/src/proxy.rs to trace the CONNECT handling and protocol upgrade flow .
Finding: Confirmed that while the proxy correctly evaluates OPA policies for the initial CONNECT request, it fails to detach the L7 hyper parser upon receiving an HTTP 101 Switching Protocols response from the upstream server.
Logic Gap: The current implementation in proxy.rs lacks the hyper::upgrade::on() mechanism required to extract the raw Upgraded I/O object. Consequently, the proxy continues to treat the stream as standard HTTP, causing subsequent binary WebSocket frames from the client to be rejected by the parser and silently dropped.
Verification: Compared against PR #683 and PR #718. The agent confirms that although these fixes exist in the main branch, the openclaw:latest container image still reflects the unpatched state where tokio::io::copy_bidirectional is not invoked for the upgraded stream.
Description
The openshell-sandbox L7 egress proxy fails to relay client-to-upstream bytes after a successful WebSocket upgrade (HTTP 101 Switching Protocols) inside an HTTP CONNECT tunnel.
What happened: When an HTTP client inside the sandbox uses CONNECT to tunnel a WebSocket connection through the L7 proxy (10.200.0.1:3128) to an external upstream, the CONNECT handshake succeeds (200 Connection Established), the WebSocket upgrade request reaches upstream,
the 101 Switching Protocols response is returned to the client, but any binary WebSocket frames the client sends after the 101 are silently dropped at the proxy. The TCP connection remains nominally open and the upstream never receives the frames. After the client's
WebSocket handshake-timeout, the connection eventually closes with close code 1006.
What I expected: Per RFC 7231 §4.3.6, after CONNECT returns 2xx, the proxy should act as an opaque Layer-4 pipeline relaying raw TCP bytes bidirectionally, regardless of payload protocol. Subsequent WebSocket frames after the 101 should pass through transparently.
Suspected root cause: The hyper-based L7 proxy in crates/openshell-sandbox/src/proxy.rs does not invoke hyper::upgrade::on() to detach the HTTP parser when the upstream returns 101. The hyper state machine continues parsing subsequent binary frames as HTTP headers,
silently faulting and dropping the bytes. The fix is to detect the 101 status, extract the upgraded I/O, and switch to tokio::io::copy_bidirectional for opaque relay.
Impact: Blocks AI agents (openclaw) from attaching to remote Chrome DevTools Protocol endpoints through the L7 proxy. Also breaks any other WebSocket-based integration (Slack Socket Mode, Discord gateway, WhatsApp Web, etc.) when accessed through the proxy.
Note: This appears to match what an earlier search turned up as Issue #652 — if that issue exists in this repo, please dedupe / link.
Reproduction Steps
Minimal Node.js reproducer (run from inside the sandbox). Targets a generic WebSocket-capable upstream — works against any HTTP server that supports Upgrade: websocket. I used Chrome DevTools Protocol (port 9222) for my testing but the bug is protocol-agnostic.
// File: /tmp/cdp-ws-bug-repro.mjsimport{connect}from'net';constPROXY_HOST='10.200.0.1';// OpenShell L7 proxyconstPROXY_PORT=3128;constTARGET_HOST='<upstream-with-websocket-server>';constTARGET_PORT=9222;// any port; bug not port-specificconstsock=connect(PROXY_PORT,PROXY_HOST,()=>{sock.write(`CONNECT ${TARGET_HOST}:${TARGET_PORT} HTTP/1.1\r\n`+`Host: ${TARGET_HOST}:${TARGET_PORT}\r\n\r\n`);});letphase='connect';sock.on('data',(chunk)=>{conststr=chunk.toString();if(phase==='connect'&&str.includes('200 Connection Established')){phase='tunneled';// Send WebSocket upgrade request through the tunnelsock.write(`GET /devtools/browser/X HTTP/1.1\r\n`+`Host: localhost:9222\r\n`+`Upgrade: websocket\r\n`+`Connection: Upgrade\r\n`+`Sec-WebSocket-Version: 13\r\n`+`Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==\r\n\r\n`);}elseif(phase==='tunneled'&&str.includes('101')){console.log('Got 101 Switching Protocols, sending WS frame...');setTimeout(()=>{// Build a masked text WS frame: opcode=0x81 (FIN+TEXT), len=4, mask=0, payload='test'constframe=Buffer.concat([Buffer.from([0x81,0x84,0,0,0,0]),Buffer.from('test')]);sock.write(frame);console.log('Sent 10-byte WS frame. Waiting 5s for response...');},100);}else{console.log('Server bytes:',chunk.length,JSON.stringify(str.substring(0,80)));}});setTimeout(()=>{console.log('Test complete (timed out). Expected: server response. Actual: no bytes received after the 101.');process.exit(0);},5000);
Steps:
1.StartaWebSocket-capableserverreachablefromthesandbox(Chromewith--remote-debugging-port=9222works;anyechoserverworkstoo)2.Addtheupstream host:porttothesandbox's network policy with minimal config: access: full and no protocol: / tls: fields
3.node/tmp/cdp-ws-bug-repro.mjs4. Observe: "Got 101 Switching Protocols"prints(proxycorrectlyforwardsthe101)5. Observe: "Sent 10-byte WS frame"prints(relaylogsshow10bytessentintothetunnel)6. Observe: Testtimesoutat5secondswithNOresponsefromupstream7.Server-sidepacketcaptureconfirmstheWSframeneverarrivesatupstream
Expected: WSframeshouldarriveatupstream,upstream's response frame should come back through the tunnel.
Actual: Bytesafterthe101aredroppedattheproxy.Upstreamneverseesthem.
### Environment-**OS:**Windows11+WSL2(Ubuntu22.04inWSL)-**Docker:**Dockerversion29.2.1,builda5c7197-**OpenShellCLI:**openshell0.0.16-**Sandboximage:**`ghcr.io/nvidia/openshell-community/sandboxes/openclaw:latest`-**Clusterimage:**`ghcr.io/nvidia/openshell/cluster:0.0.16`-**agent-sandbox-controller:**v0.1.0(per`kubectl get pods`insidetheclustercontainer)-**Topology:**WSL2→Docker→`openshell-cluster-nemoclaw`containerrunningk3s→`catherine`pod(thesandbox)
### Logs```shell## Sandbox-side relay byte counts (showing the drop)Setup: Node.js TCP relay inside the sandbox at 127.0.0.1:9222, which CONNECTs through the L7 proxy to an upstream WebSocket-capable server. Relay logs bytes flowing in each direction.[19:54:47.103Z] upstream CONNECT response: HTTP/1.1 200 Connection Established[19:54:47.103Z] client→upstream 276 bytes (total 276) # WS upgrade request (GET + Upgrade headers)[19:54:47.121Z] upstream→client 211 bytes (total 211) # HTTP/1.1 101 Switching Protocols + headers[19:54:47.125Z] client→upstream 46 bytes (total 322) # WebSocket frame (CDP command, ~10 byte header + 36 byte payload)[19:54:51.093Z] client end # client times out after 4 sec, gives upThe 4-second gap between sending the 46-byte WS frame and the client closing — with ZERO bytes coming back from upstream — is the bug. Server-side packet capture on the upstream confirms the 46 bytes never arrived.## Cross-check: binary content DOES tunnel correctly without an intermediate 101Same proxy, same CONNECT pattern, but using HTTP requests only (no WS upgrade):CONNECT 192.168.4.77:19222 HTTP/1.1Host: 192.168.4.77:19222\r\n[client sends 11 bytes of random binary garbage][client sends valid HTTP GET /json/version][upstream returns 556 bytes — HTTP/1.1 200 OK + JSON body — successfully delivered to client]So the tunnel relays arbitrary binary content correctly when the upstream response is a normal HTTP transaction. The bug is specifically triggered by the proxy seeing a 101 Switching Protocols response — after that point, subsequent client→upstream bytes are dropped.## Gateway log entries showing failed CDP attach attempts (downstream symptom)[ws] ⇄ res ✗ browser.request 1515ms errorCode=INVALID_REQUEST errorMessage=Remote CDP for profile "openclaw" is not reachable at http://192.168.4.77:19222.[ws] ⇄ res ✗ browser.request 753ms errorCode=INVALID_REQUEST errorMessage=Browser attachOnly is enabled and CDP websocket for profile "openclaw" is not reachable.These are openclaw's own error messages after `isChromeCdpReady()` failsbecausetheWebSockethealth-checkcommandsentpost-101nevergetsaresponse.
## ProcessverificationTheproxydoingtheinspectionis/opt/openshell/bin/openshell-sandboxrunningasPID1inthesandboxcontainer:
$sshopenshell-catherine'ps -ef --forest'|head-3root100May14 ? 00:10:09/opt/openshell/bin/openshell-sandboxsandbox5810May14 ? 00:00:00sleepinfinity
sandbox 626431006:17 ? 00:00:00openclaw(agentruntimeunderit)Agent-First Checklist
Agent Diagnostic
Pointed agent at: crates/openshell-sandbox/src/
Diagnostic Investigation Path:
Traced Code: Read crates/openshell-sandbox/src/proxy.rs to trace the CONNECT handling and protocol upgrade flow .
Finding: Confirmed that while the proxy correctly evaluates OPA policies for the initial CONNECT request, it fails to detach the L7 hyper parser upon receiving an HTTP 101 Switching Protocols response from the upstream server.
Logic Gap: The current implementation in proxy.rs lacks the hyper::upgrade::on() mechanism required to extract the raw Upgraded I/O object. Consequently, the proxy continues to treat the stream as standard HTTP, causing subsequent binary WebSocket frames from the client to be rejected by the parser and silently dropped.
Verification: Compared against PR #683 and PR #718. The agent confirms that although these fixes exist in the main branch, the openclaw:latest container image still reflects the unpatched state where tokio::io::copy_bidirectional is not invoked for the upgraded stream.
Description
The openshell-sandbox L7 egress proxy fails to relay client-to-upstream bytes after a successful WebSocket upgrade (HTTP 101 Switching Protocols) inside an HTTP CONNECT tunnel.
What happened: When an HTTP client inside the sandbox uses CONNECT to tunnel a WebSocket connection through the L7 proxy (
10.200.0.1:3128) to an external upstream, the CONNECT handshake succeeds (200 Connection Established), the WebSocket upgrade request reaches upstream,the 101 Switching Protocols response is returned to the client, but any binary WebSocket frames the client sends after the 101 are silently dropped at the proxy. The TCP connection remains nominally open and the upstream never receives the frames. After the client's
WebSocket handshake-timeout, the connection eventually closes with close code 1006.
What I expected: Per RFC 7231 §4.3.6, after CONNECT returns 2xx, the proxy should act as an opaque Layer-4 pipeline relaying raw TCP bytes bidirectionally, regardless of payload protocol. Subsequent WebSocket frames after the 101 should pass through transparently.
Suspected root cause: The hyper-based L7 proxy in
crates/openshell-sandbox/src/proxy.rsdoes not invokehyper::upgrade::on()to detach the HTTP parser when the upstream returns 101. The hyper state machine continues parsing subsequent binary frames as HTTP headers,silently faulting and dropping the bytes. The fix is to detect the 101 status, extract the upgraded I/O, and switch to
tokio::io::copy_bidirectionalfor opaque relay.Impact: Blocks AI agents (openclaw) from attaching to remote Chrome DevTools Protocol endpoints through the L7 proxy. Also breaks any other WebSocket-based integration (Slack Socket Mode, Discord gateway, WhatsApp Web, etc.) when accessed through the proxy.
Note: This appears to match what an earlier search turned up as Issue #652 — if that issue exists in this repo, please dedupe / link.
Reproduction Steps
Minimal Node.js reproducer (run from inside the sandbox). Targets a generic WebSocket-capable upstream — works against any HTTP server that supports
Upgrade: websocket. I used Chrome DevTools Protocol (port 9222) for my testing but the bug is protocol-agnostic.Agent-First Checklist
debug-openshell-cluster,debug-inference,openshell-cli)