Skip to content

fix: disable permessage-deflate in the app-server protocol probe - #112

Open
Ghost-LZW wants to merge 1 commit into
BytePioneer-AI:mainfrom
Ghost-LZW:fix/app-server-probe-permessage-deflate
Open

fix: disable permessage-deflate in the app-server protocol probe#112
Ghost-LZW wants to merge 1 commit into
BytePioneer-AI:mainfrom
Ghost-LZW:fix/app-server-probe-permessage-deflate

Conversation

@Ghost-LZW

Copy link
Copy Markdown

Purpose

codexhost remote start always fails on a host where a stock codex app-server is
already listening:

$ codexhost remote start
codexhost remote: Remote Host protocol probe timed out

codexhost remote status reports the same and exits 1, even though the
installation is "state": "ready", "issues": [] and the app-server is listening
and responding normally.

Root cause

probeWebSocket() in remote-host-lifecycle.ts constructed its ws client with
the library default:

const socket = new WebSocket("ws://localhost/", { createConnection: input.createConnection });

ws enables permessage-deflate by default, so the handshake offers
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits. The stock
codex app-server uses tokio-tungstenite, which supports no extension and aborts
the upgrade without writing an HTTP response at all
, rather than replying 101
with the extension omitted. The client waits out its own 5s timeout, so both the
direct probe and the codex app-server proxy fallback return state: "unknown",
and startRemoteHost() aborts before it can terminate the stock listener and take
over.

createRemoteOfficialAppServerConnection() already documented this exact
constraint and passed perMessageDeflate: falseprobeWebSocket() just missed
the flag.

Change

  • Add APP_SERVER_WEBSOCKET_CLIENT_OPTIONS (maxPayload + perMessageDeflate: false)
    as the single handshake used by every client that talks to an app-server listener.
  • Use it in probeWebSocket() (the fix) and in createRemoteOfficialAppServerConnection()
    (replacing its local literal), so a third call site can't reintroduce this.
  • Add a regression test that binds a listener which, like tokio-tungstenite, destroys
    any upgrade offering an extension, and asserts the probe still classifies it as
    stock-codex.

Validation

Reproduced against a live stock codex app-server listener (codex-cli 0.152.0,
@codexhost/cli 0.4.0, Node v24.14.0, Linux x86_64). Raw handshakes against the
socket, varying only the extensions header:

Handshake Server response
no Sec-WebSocket-Extensions HTTP/1.1 101 Switching Protocols
permessage-deflate; client_max_window_bits connection closed, zero bytes returned
any bogus extension connection closed, zero bytes returned

Same A/B through a real ws client against that listener:

deflate=true : ERR socket hang up
deflate=false: MSG {"error":{"code":-32600,"message":"Invalid request: unknown variant `codexhost/update/status` ...

The deflate=false response is exactly the shape
classifyRemoteHostProbeResponse() expects for the stock-codex conflict branch,
so once the handshake succeeds the classifier works as designed. Patching the
shipped runtime in place turned the reported "state": "unknown" into:

"runtime": { "state": "conflict", "protocol": "stock-codex" }

which is the state startRemoteHost() needs in order to replace the stock listener.

Checks run on this branch:

  • npm run lint — pass
  • npm run typecheck — pass
  • npm run build:typescript — pass
  • npm run format:check (on changed files) — pass
  • packages/host-runtime tests — 28 files, 277 passed / 2 skipped
  • The new test fails without the fix (probe returns unknown) and passes with it.

Full npm run test:typescript has 6 failures in
tools/dev-desktop/run.test.mjs and packages/adapters/claude-code/test/command.test.ts;
I confirmed these are pre-existing on a clean main and unrelated to this change.
npm run test:rust was not run.

The new test is skipped on Windows, which has no unix-socket listener path here.

probeWebSocket() constructed its ws client with the library default, so the
handshake offered 'Sec-WebSocket-Extensions: permessage-deflate'. The stock
codex app-server uses tokio-tungstenite, which supports no extension and
aborts the upgrade without writing an HTTP response rather than replying 101
with the extension omitted. The client then waited out its own 5s timeout, so
both the direct probe and the 'codex app-server proxy' fallback returned
state 'unknown' and 'codexhost remote start' failed with 'Remote Host
protocol probe timed out'.

createRemoteOfficialAppServerConnection() already documented this exact
constraint; hoist its options into APP_SERVER_WEBSOCKET_CLIENT_OPTIONS so
every client that talks to an app-server listener shares one handshake.
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

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