acpmux: _acpmux/session/auth answers a chat's sign-in from its own turns
Leo: “you gotta re-authenticate. You gotta log in again on the modal picker before the user even sends a prompt.”
The composer has to know whether a chat can send before the user sends. This adds the daemon method _acpmux/session/auth {sessionId, refresh?}, which reads a chat's sign-in from evidence and never guesses. It is the second of three PRs. It builds on #26658, which has merged. The feature isn't listed in initialize yet; the third PR lists it, once the route probe, the target form and peers are in.
Reply. {state, source, evidenceAt, detail, harness, host, routeId, login?, fromSessionId?}. state is ready, signedOut, expired or unknown here (routeAuthNeeded, routeUnreachable and peerUnreachable come in the next PR), and source is turn, authCheck or none. login is {kind: "terminal", command} only for signedOut and expired. For a chat in its own account home, the command names that home, for example CLAUDE_CONFIG_DIR=/path claude auth login. detail is one sentence: an agent's error text is cut at its first sentence end (., ! or ?, after any closing quote, before a space, but not after an abbreviation) and to at most 200 characters.
The method reads a chat's log first (Hub::session_log), so after a deferred start it doesn't answer from a summary alone.
A chat's route, home and env are the ones its spawn uses.
- Route:
routes::spawn_routeis the spawn's choice, now shared withhub/route_switch.rsapply_route_for(the same match moved out unchanged): a chat or workspace binding, then the profile's own route (ACPMUX_PROFILE_ROUTE), then none for your own endpoint, then a family or global binding, and only a route that exists and serves the harness. The reply'srouteIdis that route. - Home: the account home follows
apply_route_for's precedence. A chat's own home stays; a route picked for the chat or the profile replaces a pinned home; a family or global route's home applies only when nothing pins one. - Env: the refresh check runs in
route_account::spawn_env, the envhub/spawn.rsbuilds: the harness env with each${...}expanded, then the chat's own session env over it.
Evidence, in order.
- The chat's newest turn. A failed turn with
errorKindauth.signed_outorauth.expired(#26658) gives signedOut or expired. A turn recorded before those kinds existed is read by its text, through the samerecovery::auth_kind. A completed turn gives ready, so a later completed turn clears signedOut. A cancelled turn, or one that failed another way (a connection error,agent.exited), says nothing. - With
refresh, the harness's own check (source authCheck, whatever it answers; a check that couldn't reach its provider says unknown): the profile'sauth.check, orcodex login status, run in the chat's own env (its preset's and chat store's over the profile's,Hub::harness_env), and only when the harness keeps no login in the keychain (harness_health::keychain_sign_inis None). Otherwise the reply stays unknown and says why. A check that fails with a capacity or connection error is unknown (#26658). - Otherwise unknown, never signedOut.
Rules kept.
- The six-hour
harness/healthcache is never evidence. - Nothing reads the keychain.
- A remote WebSocket (Web) connection can't ask for
refresh, because a check runs a program on this machine. {target}is refused as not answered yet (the next PR answers it).- Exactly one of
sessionIdandtargetis accepted, otherwise invalid params. - An unknown
sessionIdreturns the existing session-not-found error (-32002). - A peer's chat isn't sent through the generic forward. It is answered here from the link: peerUnreachable (source peerLink) when the link is down, otherwise unknown. The next PR asks the peer.
Protocol. MUX_SESSION_AUTH = "_acpmux/session/auth" is in rpc.rs, protocol/auth.rs holds the types, and the protocol IR (spec/acpmux-protocol.json) and the generated webview types are regenerated. Of the about 1150 changed lines, 253 are generated (the IR and TS) and 248 are the integration tests.
Tests. Before is base 18af98cc with only the tests added (run 40634054284). After is this head (cmux-ci run 40660028234, on the current base): fmt and clippy -D warnings are clean. acpmux lib passes 1598, including protocol_ir_is_current. session_auth, typed_errors, harness_health, claude_auth_env, hub_integration (511) and the route suites pass, and the webview acpmux-protocol:check passes.
session_auth::session_auth_follows_the_chats_own_newest_turn: no turn gives unknown, never signedOut. With refresh, Claude's keychain-held login stays unknown and says why. A sign-in failure gives signedOut with the login command. A later completed turn gives ready. A connection error gives unknown. Before: FAILED. After: ok.session_auth::session_auth_names_the_chats_own_account_home: before FAILED, after ok.session_auth::session_auth_checks_in_the_chats_own_env: the check passes only in the env the chat's preset sets, so the preset's chat is ready and the plain one signedOut (authCheck). After: ok. It was added after the before run; on base it fails at its first call like the other session_auth tests, because the method doesn't exist.session_auth::session_auth_checks_in_the_chats_own_envalso covers a chat whose own session env carries what the check needs: it is ready, the plain chat signedOut.session_auth::session_auth_names_the_route_the_chat_spawns_on: a profile that pins its own route (ACPMUX_PROFILE_ROUTE) names that route, not the global default, and checks in its account home. After: ok; on base it fails at its first call (no method).hub::session_auth::tests::{each_state_comes_from_its_evidence, no_evidence_and_transport_errors_are_unknown, a_detail_is_one_sentence, a_chat_in_its_own_account_home_signs_in_there}: before, they don't compile (no module); after, ok.
Risk: low. It is a new read-only method that nothing calls yet. apply_route_for now calls the shared routes::spawn_route, the same match moved out; the route suites (routes_rpc, routes_account_home, routes_file_secret) pass. refresh runs only a check that reads no keychain, and never for a Web connection.
No capture: no UI changes; the webview change is generated protocol types for a method no view calls yet.
Agent session: agent session
🤖 Generated with Claude Code