acpmux: session/auth reads the route probe and answers a target

Draft
TE
8d ago
+656-74

Leo: “you gotta re-authenticate. You gotta log in again on the modal picker before the user even sends a prompt.”

This is the third of four PRs for _acpmux/session/auth. It builds on #26699, which has merged. It adds two things: the bound route's probe as evidence, and the target form for a chat that hasn't started. The fourth PR, #26845 (stacked on this one), asks peers and lists the sessionAuth feature, so initialize doesn't list it yet. Together with the next PR, this was one PR. That PR got a SHIP from the reviewer and was split here to stay under 800 lines.

Evidence, in order:

  1. Peer link. As in #26699, a peer's chat is answered from its link. A down link gives peerUnreachable; otherwise the answer is unknown. A target on a peer (target.host) is invalid params ("not answered yet"). The next PR asks the peer.
  2. Bound route probe. The route is the one a spawn would pick (routes::spawn_route, from #26699).
    • A probe is evidence only for the harness family whose endpoint and key it tests (route_probe::probes_for).
      • A relay route (local-coderouter, or cmux-router with no secret) mints a key per family, so its probe speaks only for the exact family it probed as.
      • Any other route sends its own secret to every family. Its health URL speaks for all families, and its base URL for the families on that endpoint (the Anthropic one for Claude, the OpenAI one for the rest).
    • With refresh, a fresh probe runs. Otherwise the last answer is used if it is no more than 5 minutes old and came from the route as it is now.
    • route_probe.rs keeps each route's last answer with the route file it probed. Every read and every probe drops answers older than 5 minutes, and the oldest once more than 64 routes are kept.
    • A probe never runs for a route whose secret or a header is a keychain: reference.
    • auth_needed gives routeAuthNeeded, with login {kind: "route", routeId}. unreachable gives routeUnreachable.
    • rate_limited and unknown fall through and are never treated as a sign-in state.
  3. Newest turn. A chat uses its own newest turn, as in #26699. A target {harness, routeId?} uses the newest turn of any chat on this daemon with the same harness, the same route and the same account home, and sets fromSessionId to that chat. Another account home never leaks in.
    • Home and env are resolved as spawn resolves them: the family's and the profile's defaults env under the profile's env (Hub::spawn_profile). A home those pin beats a family or global route's home, but not the home of a route picked for the chat or the profile.
    • A missing routeId means the default route, as at spawn.
    • A routeId that is gone, or that doesn't serve the harness, is invalid params, as a spawn would refuse it. A sibling chat whose own route a spawn would refuse is skipped.
    • An unknown harness is invalid params.
  4. Harness check. With refresh, the harness's own check that reads no keychain, as in #26699, run over the defaults' env.
  5. Otherwise unknown.

Tests. After is this head, on cmux-ci run 40669532019: fmt and clippy -D warnings are clean; acpmux lib (1630), session_auth (7), hub_integration (514; prompt_supersede::a_prompt_queued_before_a_restart_is_still_replaced is flaky, see below), the route suites, typed_errors, harness_health, claude_auth_env and no_keychain pass.

Before, each new test failed on the tree without its fix:

  • Base 18af98cc with only the tests added (run 40634054284):
    • session_auth_for_a_target_reads_only_its_own_account: a target with no chats gives unknown. A chat in another account home that is signed out doesn't leak into the target. A sibling's signedOut is picked up, with fromSessionId. An unknown harness is invalid params.
    • session_auth_reads_the_routes_probe_and_a_rate_limit_falls_through (local HTTP 401, 429 and 200 endpoints):
      • With no probe yet, the answer is unknown.
      • With refresh, the answer is routeAuthNeeded with a route login, and that answer is reused without refresh.
      • Once the endpoint answers 200, a refresh probes again.
      • A 429 falls through.
      • A profile's own route beats the global default.
      • A route for another family is refused.
  • Run 40667053893: session_auth_reads_the_home_the_defaults_pin, where a home the defaults env pins is named for both a chat and a target.
  • Run 40665976369: route_probe::tests::a_probe_speaks_for_the_family_whose_endpoint_it_tests, for relay routes and for OpenCode and Pi.
  • Unit tests, after ok:
    • route_probe::tests::{kept_answers_are_recent_and_bounded, a_kept_answer_is_for_the_route_as_probed}
    • hub::session_auth::tests::{a_rate_limited_probe_falls_through, a_target_keeps_the_home_its_profile_pins, a_route_is_taken_as_a_spawn_takes_it}

Risk: medium-low. Everything is read-only. A fresh probe runs only with refresh, never on a keychain route, and never for a Web connection.

No capture: no UI changes; this is a daemon method no view calls yet.

Agent session: agent session

🤖 Generated with Claude Code