Summary
pyproject.toml declares websocket-client>=1.0 (added in #26), but colab ssh relies on WebSocketBadStatusException.resp_body, which websocket-client only added in 1.6.0. On 1.0–1.5, SSH handshake failures lose the server's explanation.
This is low severity: nothing crashes, and a fresh install resolves a recent websocket-client. It only affects environments that already have an older websocket-client, which pip won't upgrade because >=1.0 is satisfied.
Details
src/colab_cli/commands/ssh.py (_connect_websocket):
except websocket.WebSocketBadStatusException as e:
status = getattr(e, "status_code", None)
body = getattr(e, "resp_body", b"") or b""
The getattr fallback prevents a crash, but on websocket-client < 1.6 body is always empty, so a 400 from the runtime comes out as:
[colab] Server rejected pubkey (HTTP 400): . Re-check your key with `ssh-keygen -y -f <key>`.
with the server's reason (e.g. unsupported key type) dropped.
WebSocketBadStatusException.__init__ signature by version (checked in isolated envs):
| websocket-client |
parameters |
| 1.0.0 – 1.5.0 |
message, status_code, status_message, resp_headers |
| 1.6.0 + |
message, status_code, status_message, resp_headers, resp_body |
Reproduce
Run the suite with every direct dependency at its declared minimum:
uv run --isolated --no-sources --resolution lowest-direct \
--with 'jupyter-kernel-client==0.9.0' pytest -q tests/
# 3 failed, 348 passed (websocket-client 1.0.0)
The failures:
tests/test_ssh.py::test_ssh_handshake_400_emits_actionable_message[bytes-body] / [str-body]: TypeError constructing the exception with 1.6-style arguments. This one is test-side only.
tests/test_ssh_wire_contract.py::test_real_status_mapping_via_loopback[400-...unsupported key type]: assert 'unsupported key type' in '...(HTTP 400): . Re-check your key...'. This is the user-visible regression above.
(jupyter-kernel-client is pinned to 0.9.0 here only because its own declared floor is broken on PyPI; see #137 / #138.)
Adding --with 'websocket-client==1.6.0' to the same command gives 351 passed.
Suggested fix
- "websocket-client>=1.0",
+ "websocket-client>=1.6",
Like the jkc floor discussed in #138, a raised floor also makes pip install --upgrade move older environments forward, which a satisfied >=1.0 never does.
Summary
pyproject.tomldeclareswebsocket-client>=1.0(added in #26), butcolab sshrelies onWebSocketBadStatusException.resp_body, which websocket-client only added in 1.6.0. On 1.0–1.5, SSH handshake failures lose the server's explanation.This is low severity: nothing crashes, and a fresh install resolves a recent websocket-client. It only affects environments that already have an older websocket-client, which pip won't upgrade because
>=1.0is satisfied.Details
src/colab_cli/commands/ssh.py(_connect_websocket):The
getattrfallback prevents a crash, but on websocket-client < 1.6bodyis always empty, so a 400 from the runtime comes out as:with the server's reason (e.g.
unsupported key type) dropped.WebSocketBadStatusException.__init__signature by version (checked in isolated envs):message, status_code, status_message, resp_headersmessage, status_code, status_message, resp_headers, resp_bodyReproduce
Run the suite with every direct dependency at its declared minimum:
The failures:
tests/test_ssh.py::test_ssh_handshake_400_emits_actionable_message[bytes-body]/[str-body]:TypeErrorconstructing the exception with 1.6-style arguments. This one is test-side only.tests/test_ssh_wire_contract.py::test_real_status_mapping_via_loopback[400-...unsupported key type]:assert 'unsupported key type' in '...(HTTP 400): . Re-check your key...'. This is the user-visible regression above.(
jupyter-kernel-clientis pinned to 0.9.0 here only because its own declared floor is broken on PyPI; see #137 / #138.)Adding
--with 'websocket-client==1.6.0'to the same command gives 351 passed.Suggested fix
Like the jkc floor discussed in #138, a raised floor also makes
pip install --upgrademove older environments forward, which a satisfied>=1.0never does.