mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:36:24 +00:00
fix(login): capture the full claude auth URL on a wide tmux window
Testing the web login surfaced a truncated authorization URL (…client_id=9d1c250a-e61b-44d9-88) and a "missing redirect_uri" error: the login session ran at the default 80 columns, so claude clipped the long URL and capture-pane read it back cut off. - Launch the login session with a very wide, tall window (500x50) via new optional width/height on tmux.new_session, so claude prints the URL on one unclipped line. - Harden URL extraction to stop at box-drawing glyphs (U+2500–U+257F) in case the TUI renders the link flush against a border. Tests: assert the wide window is requested, and that extraction keeps a full redirect_uri/PKCE URL intact and strips a trailing box border. Suite green (197). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
This commit is contained in:
@@ -32,8 +32,17 @@ from . import tmux
|
||||
# One well-known session name: ``login_start`` (re)creates it, ``login_submit`` reuses it.
|
||||
LOGIN_SESSION = "handler__login"
|
||||
|
||||
# Any http(s) URL in the pane; we then prefer the OAuth/authorize link among them.
|
||||
_URL_RE = re.compile(r"https?://[^\s\"'<>`|]+")
|
||||
# A very wide, tall detached window so claude prints the (long) authorization URL on a
|
||||
# single unclipped line — at the default 80 columns capture-pane reads it back truncated
|
||||
# (missing redirect_uri/state), which is the whole point of failure otherwise.
|
||||
LOGIN_COLS = 500
|
||||
LOGIN_ROWS = 50
|
||||
|
||||
# Any http(s) URL in the pane; we then prefer the OAuth/authorize link among them. The
|
||||
# character class stops at whitespace, quotes, and — importantly — box-drawing glyphs
|
||||
# (U+2500–U+257F) the TUI may render flush against the link, so a bordered URL isn't
|
||||
# captured with a trailing "│".
|
||||
_URL_RE = re.compile(r"https?://[^\s\"'<>`|─-╿]+")
|
||||
_OAUTH_HINTS = ("oauth", "authorize", "claude.ai", "claude.com", "console.anthropic")
|
||||
_SUCCESS_HINTS = (
|
||||
"login successful",
|
||||
@@ -85,7 +94,9 @@ def start(
|
||||
if tmux.has_session(LOGIN_SESSION):
|
||||
tmux.kill_session(LOGIN_SESSION)
|
||||
|
||||
tmux.new_session(LOGIN_SESSION, cwd=_home(), command=claude, env={})
|
||||
tmux.new_session(
|
||||
LOGIN_SESSION, cwd=_home(), command=claude, env={}, width=LOGIN_COLS, height=LOGIN_ROWS
|
||||
)
|
||||
_sleep(boot_wait) # let claude boot to its prompt
|
||||
|
||||
tmux.send_keys(LOGIN_SESSION, "/login")
|
||||
|
||||
@@ -19,14 +19,29 @@ def session_name(project_id: str, agent_name: str) -> str:
|
||||
return safe
|
||||
|
||||
|
||||
def new_session(name: str, cwd: str, command: str, env: dict[str, str]) -> None:
|
||||
def new_session(
|
||||
name: str,
|
||||
cwd: str,
|
||||
command: str,
|
||||
env: dict[str, str],
|
||||
*,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> None:
|
||||
"""Launch a detached tmux session running ``command`` in ``cwd`` with ``env`` set.
|
||||
|
||||
``tmux -e`` sets session environment, so the ``claude`` process (and therefore its
|
||||
hooks) inherit the agent identity + ``DATABASE_URL``.
|
||||
hooks) inherit the agent identity + ``DATABASE_URL``. ``width``/``height`` size the
|
||||
detached window (``-x``/``-y``); the login flow uses a very wide window so ``claude``
|
||||
prints the full authorization URL on one line instead of clipping it at the default
|
||||
80 columns (which capture-pane would then read back truncated).
|
||||
"""
|
||||
tmux = get_settings().tmux_bin
|
||||
argv = [tmux, "new-session", "-d", "-s", name, "-c", cwd]
|
||||
if width is not None:
|
||||
argv += ["-x", str(width)]
|
||||
if height is not None:
|
||||
argv += ["-y", str(height)]
|
||||
for key, value in env.items():
|
||||
argv += ["-e", f"{key}={value}"]
|
||||
argv.append(command)
|
||||
|
||||
+3
-2
@@ -79,9 +79,10 @@ def fake_tmux(monkeypatch):
|
||||
|
||||
from handler.control import tmux
|
||||
|
||||
def new_session(name, cwd, command, env):
|
||||
def new_session(name, cwd, command, env, *, width=None, height=None):
|
||||
calls["new_session"].append(
|
||||
{"name": name, "cwd": cwd, "command": command, "env": env}
|
||||
{"name": name, "cwd": cwd, "command": command, "env": env,
|
||||
"width": width, "height": height}
|
||||
)
|
||||
live.add(name)
|
||||
|
||||
|
||||
@@ -43,6 +43,24 @@ def test_extract_url_none_when_no_link():
|
||||
assert login._extract_url("no link here") is None
|
||||
|
||||
|
||||
def test_extract_url_stops_at_box_border():
|
||||
# claude may draw the URL inside a rounded box; a "│" flush against the link must not
|
||||
# be captured as part of the URL.
|
||||
assert login._extract_url(f"│{AUTH_URL}│") == AUTH_URL
|
||||
|
||||
|
||||
def test_extract_url_captures_full_long_url_with_redirect_uri():
|
||||
# The real login URL carries redirect_uri + PKCE; on a wide pane it arrives intact and
|
||||
# extraction must not clip it (the truncation-at-80-cols bug was in capture, not here).
|
||||
long_url = (
|
||||
"https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ab-"
|
||||
"0123456789ab&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com"
|
||||
"%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile&"
|
||||
"code_challenge=abcDEF123&code_challenge_method=S256&state=xyz789"
|
||||
)
|
||||
assert login._extract_url(f"Use this URL to sign in:\n{long_url}") == long_url
|
||||
|
||||
|
||||
def test_start_launches_claude_selects_subscription_and_returns_url(
|
||||
env, fake_tmux, no_sleep, monkeypatch
|
||||
):
|
||||
@@ -56,6 +74,9 @@ def test_start_launches_claude_selects_subscription_and_returns_url(
|
||||
assert len(launched) == 1
|
||||
assert launched[0]["name"] == login.LOGIN_SESSION
|
||||
assert launched[0]["command"] == "claude"
|
||||
# A wide window so the long authorization URL isn't clipped at 80 columns.
|
||||
assert launched[0]["width"] == login.LOGIN_COLS
|
||||
assert launched[0]["height"] == login.LOGIN_ROWS
|
||||
# …then /login was sent, followed by a bare Enter selecting the subscription option.
|
||||
sent = [c["keys"] for c in fake_tmux["calls"]["send_keys"]]
|
||||
assert sent[:2] == ["/login", ""]
|
||||
|
||||
Reference in New Issue
Block a user