Repository navigation
Conversation
769c90b to
5489e3e
Compare
|
Thanks for the fix! A PR merged just before this one touched \CHANGELOG.md, \ Everything else looks good — CI passed on all test and lint checks. |
…s it `_find_free_port()` bound a socket, read the port and closed it again, so between picking the port and the callback server binding it, any process on the machine could claim it. The browser's redirect then landed on a port nothing was listening on and the flow failed with a connection refused (`devagent auth` / any MCP OAuth login on a busy machine). The flow now binds the socket once and passes it to the server, which adopts it instead of binding a port of its own: - `_bind_callback_socket()` binds loopback on an ephemeral port and keeps the socket -- the port is never free in between. - `_start_callback_server(sock)` builds the `HTTPServer` with `bind_and_activate=False`, assigns the already-bound socket and calls `server_activate()` to listen on it. The redirect URI is built from `sock.getsockname()`, so the address the browser is sent to is the socket the server is serving on. - the socket is closed when the flow ends (`server_close()` in the `finally`), so the port is not left bound for the rest of the process. Measured on this host, with the old helper and the new one side by side: ``` helper: _find_free_port (bind, read, close), port=64311 another process can take the port: True <- the window this fixes helper: _bind_callback_socket (socket held open), port=64310 another process can take the port: False ``` 7 tests in `tests/test_pkce_callback_port.py`, all offline (loopback only): another socket cannot take the port; the adopted socket actually serves the callback (code and error arms); closing the server releases the port; the redirect URI the flow advertises is the port it is listening on (the end-to-end arm, standing in for the browser); and the port is released when the flow returns. Full suite on this host: green apart from `test_phase17.py::TestFixtureProject:: test_data_store_tests_pass_despite_bug`, a pre-existing Windows GBK-locale failure that also fails on unmodified main. ruff clean. Fixes knight22-21#61
…ound The flow test that checks the callback port is free once `pkce_authorize` returns makes a real request to the server first, and the fake browser never closed its end. On Linux the server side of that connection keeps the port in `FIN_WAIT_2`, so the probe's `bind()` fails: first with `EADDRINUSE` (red on both Linux legs, green on Windows, which allows the rebind), and still failing with `SO_REUSEADDR` set, because `SO_REUSEADDR` forgives a connection in `TIME_WAIT`, not one that was never closed. The test now asserts the fact it is about: once the flow returns, nothing is listening on the callback port, checked by connecting and expecting a refusal. A refused connect needs no listener and does not depend on the old connection's state, on either OS. The bind probe stays for the held case, where a live listener must make it fail -- checked: it returns False with the server up, so the assertion is not vacuous. The fake browser closes the response it reads, standing in for a browser closing the redirect page, so the server is not left in `FIN_WAIT_2` either. Follow-up to b3b6b52 (the original fix) and 769c90b (a first attempt that added `SO_REUSEADDR` and did not turn the Linux legs green). b3b6b52's body also said "7 tests"; the file has six. Local: 6 passed; probe control: False while listening, True after `server_close` and with a lingering connection and no listener.
5489e3e to
2e21951
Compare
|
Rebased onto Re-running the test file turned up something in my own entry: it claimed "7 tests" for Local after the rebase: 6 passed in |
Problem
_find_free_port()bound a socket, read the port, and closed it again. Between that close and the callback server binding the port, any process on the machine could claim it — the browser's redirect then landed on a port nothing was listening on and the flow failed with a connection refused.devagent auth(and any MCP OAuth login) is exposed on a machine with normal socket churn.Measured on this host, old helper and new one side by side:
Change
The socket is bound once and handed to the server, so the port is never free in between (issue option A — no retry loop needed):
_bind_callback_socket()binds loopback on an ephemeral port and keeps the socket._start_callback_server(sock)builds theHTTPServerwithbind_and_activate=False, assigns the already-bound socket, and callsserver_activate()to listen on it. The redirect URI is built fromsock.getsockname(), so the address the browser is sent to is the socket the server serves on.server_close()in thefinally) — previously the listening socket stayed bound for the rest of the process._find_free_port()had no other caller and is removed.Tests
7 tests in
tests/test_pkce_callback_port.py, all offline (loopback only, no browser):errorarms;The failure is
test_phase17.py::TestFixtureProject::test_data_store_tests_pass_despite_bug, a pre-existing Windows GBK-localeUnicodeDecodeErrorthat fails on unmodifiedmaintoo. ruff clean.Fixes #61.