Skip to content

Cloud relay chat: give the machine the person's harness login before the send - #36846

Merged
teamleaderleo merged 48 commits into
feat-cmux-nextfrom
lane/relay-chat-harness-login
Nov 6, 2026
Merged

teamleaderleo merged 48 commits into
feat-cmux-nextfrom
lane/relay-chat-harness-login

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

cmux cloud start-chat tries the relay first (#35829), but cloud relay-chat (CloudStartSession.bindRelayChat) sent the prompt without the login step that cloud start-session runs before its start. So a chat started from the CLI reached a guest with no Claude login, and production answered "Claude is not signed in here".

Leo: "we have the daemon, we have the token, we have the thing that can just send it over the authentication. So why are you still getting stuck on this? ... just fix it"

Before relay.chat (the person's palette run) and relay.answer (CLI and MCP runs), the relay path now does what start-session does, in the same order:

  1. It brings up the machine's app link with the session's own connect. A cold machine that no start-session touched therefore gets an owner answer.
  2. It runs the gated, owned passHarnessLogin on the CloudMachineSession the relay already resolved.
  3. Only then does it send.

The account that asked is checked before the link, again after the link and before the login, after the login, and right before the send. The step reuses the existing credential route, and no field or log line holds a token. The decision log line has verdicts only: link state, owner answer, guest kind, and whether the cache was queried.

If anything other than a usable login comes out of these steps, nothing is sent. A CLI or MCP run gets back a typed answer with delivered: false and the start-session command that recovers.

After the steps Code Sent
login copied, the machine's own login, or no login to pass (pi) (relay answer) yes
machine paused or resuming (this entry never resumes) cloud.relay.machine_not_ready no; start-chat runs start-session, which resumes
the link did not come up cloud.relay.not_connected no; start-chat runs start-session, which connects
a teammate's machine (link answer team) cloud.relay.login_team_machine no
owner answer not read (unknown: no link, or the app server could not read it) cloud.relay.login_unverified no
any other Claude skip cloud.relay.login_skipped no
the rules refuse it (a headless run with no login choice, --login machine with none there, Release consent) cloud.relay.login_refused + login_reason; with no choice, the command adds --login none (a bare rerun refuses again), whose chat's Sign in to Claude card signs in no
Claude with no login on the machine, and none cached on this Mac (or the Keychain didn't answer) cloud.relay.login_missing no
the person declined the copy cloud.relay.login_declined no
the copy failed cloud.relay.login_failed, with the copy's fixed text only no
the account changed during the link or login cloud.relay.login_account_changed no
link plus login still running after 15 s (CLI/MCP only) cloud.relay.login_pending no

Ownership has three states end to end. The app server's cloud.machine.connect answers vm_kind from the Worker's own word: owner and team only on those exact values. It answers unknown for an absent or other value, or for a transient owner read, and that answer is never cached. A machine that is gone or out of reach still refuses the connect. The catalog and the generated app contract gain unknown. On the Mac, CloudLinkSession.callerOwnership reads owner, team or unknown, and only owner grants anything. A team machine the caller made (link owner, guest team) goes the owner way: its own working login sends, and none gives login_missing.

Rust cmux cloud start-chat answers any undelivered cloud.relay.login_* as that typed error with its recovery command (exit 75 for login_pending, else 1), and runs nothing more. Any other undelivered answer (machine_not_ready, not_connected, an app or machine without the relay) still falls back to start-session on that machine.

An honest limit: a cold machine whose link comes up and whose owner answer is owner now gets an owned login and a reply over the relay. If the link can't come up or the owner answer can't be read, the run gets the typed recovery above, not a signed-in reply.

The binding moves out of CloudStartSession.swift (over its file cap) into CloudStartSession+RelayChat.swift. The visible composer (CloudStartChat.runHome) and the daemon_closed failure after 102.7 s are owned elsewhere and aren't touched here.

Changelog

Fixed: A chat that cmux cloud start-chat sends over the Cloud relay now connects to the machine and gives it your Claude login first, or says why it can't and sends nothing, instead of reaching a Claude that isn't signed in.

Testing

The regression (test only, bf5eca7) is CloudRelayChatLoginTests/aCLIRelayChatRunsTheHarnessLoginStepFirst. It drives the app's real cloudRelayChat binding and real login step on a fresh running machine. It was red at bf5eca7 in cmux-ci step c74bca804c818c3a70835cda.

At the head 614ec92 (tree 9810f479, one merge of feat-cmux-next):

  • Mac: cmux-ci step c06d69ce0d3c906899c6719c. CloudRelayChatLoginTests, CloudStartSessionTests, CloudStartChatLoginTests, CloudCredentialTransferTests, CloudRelayChatTests and CloudAppLinkOwnerAnswerTests all passed.
  • Linux: run 42153020137. Server connect_owner passed 16/16, cmux-cloud-start-core vm_kind passed 6/6, and cmux-tui cloud_start_chat::tests passed 39/39. The full check-app-platform.sh passed, including the generated contract and the runtime sync.

CloudRelayChatLoginTests binds the real action with fixture stand-ins for the login step and for the relay's HTTPS transport, and records the order of the steps:

  • a cold running machine runs connect, then login (as owner, through the app's own ownership check), then send;
  • a cold machine whose connect fails runs connect only, then not_connected;
  • cold paused, resuming or unknown-status machines connect nothing and send nothing;
  • an account switch while the link waits starts no login and sends nothing;
  • a link that answers past the deadline starts no login (login_pending);
  • each login code and each ownership state is covered, including the team cases;
  • every refusal carries its recovery command;
  • no answer field carries the fixture bearer.

Reviews over the merge-base range found and fixed:

  • the 40 s deadline;
  • a declined copy that still sent;
  • the start-chat fallback sending on a login refusal;
  • a skipped login that still sent;
  • unknown ownership read as team;
  • the generated contract;
  • start-chat --wait on an auto-paused machine;
  • account checks around the link and the send;
  • a cached unknown connect_info.

Two reviews (at 7e77e41 and 14c5ed6) blocked on a claim that CloudCallerOwnership lacks Equatable. A raw-value enum with no associated values gets it synthesized, and the exact trees compiled and passed (answered in this PR's comments).

Review (codex gpt-6.1-sol xhigh): SHIP at 614ec92.

Observed / Not run

Observed: the fleet runs above. Not run: a live Cloud machine. The real ownership and copy calls are stood in for in these tests; their own suites cover them.

Agent session: agent session

🤖 Generated with Claude Code

teamleaderleo and others added 4 commits November 5, 2026 13:51
… step first

Fails on base: bindRelayChat sends with no login step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
… the send

bindRelayChat now runs passHarnessLogin (the start-session login step) before
relay.chat and relay.answer. A login the rules refuse, a Claude machine still
without a login (no cached login on this Mac), or a failed copy sends nothing
and answers undelivered with cloud.relay.login_refused, login_missing or
login_failed. The binding moves to CloudStartSession+RelayChat.swift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…declined copy sends nothing

A CLI or MCP relay chat answers within the action's result deadline: a login
step still running after loginWithin answers cloud.relay.login_pending and
never sends, and the turn gets what is left of answerWithin. A copy the person
declined on a machine with no login answers login_missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
@teamleaderleo teamleaderleo added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 5, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

teamleaderleo and others added 3 commits November 5, 2026 14:27
…-login test's wording check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
teamleaderleo and others added 2 commits November 5, 2026 14:39
…to the link path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…fallback send

cloud.relay.login_* with delivered:false came back to start-chat as a plain
undelivered answer, so it fell back to start-session and the link path, which
sent the prompt to the same signed-out agent. It now answers the app's typed
code, message and recovery command (exit 75 for login_pending, else 1) and runs
nothing more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
teamleaderleo added a commit that referenced this pull request Nov 5, 2026
…ror fields go into its moved relayFields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-af5b6ebb55d7d78810d2
teamleaderleo added a commit that referenced this pull request Nov 5, 2026
…s outcome is not a #36846 refusal)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-af5b6ebb55d7d78810d2
teamleaderleo added a commit that referenced this pull request Nov 5, 2026
… as 1c2d2e1) + #36847 0681c10 + #36846 b0c7beb

The tree is exactly cae-fresh (4bc42c8); the old candidate is only a parent, so the branch moves forward.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Nov 5, 2026
…n+RelayChat.swift (its arch exception's reason and receipt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-af5b6ebb55d7d78810d2
teamleaderleo and others added 5 commits November 5, 2026 15:06
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…n+RelayChat.swift (its arch exception's reason and receipt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-af5b6ebb55d7d78810d2
…ne/cae-composed)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, router-review): BLOCK at 3e90016

BLOCK at 3e90016

Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] [category:security] [finding:F1] [outcome:pending] relayLoginRefusal only rejects .unknown ownership when the Claude decision reaches .callerNotOwner (CloudStartSession+RelayChat.swift:103-116). For agents without credential transfer, such as pi, or when the transfer gate is off, the decision exits before checking ownership and this function returns nil; the relay then sends despite the link reporting unknown (including no app link). This violates the stated “unknown is unverified, never send” rule and can deliver prompts to an unverified or teammate machine. Reject .unknown unconditionally before evaluating the login decision.

@teamleaderleo teamleaderleo removed the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 6, 2026
@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 6, 2026
…ision

relayLoginRefusal returned nil before it read ownership when no login step
decided anything (pi, or the transfer gate off), and refused unknown only on
the Claude caller-not-owner skip: a team or unknown machine, or one with no
link, still got the send (review21 F1 at 3e90016). Now team refuses with
login_team_machine and unknown with login_unverified for every agent; only
an owner goes on to the login checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…chat-harness-login

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextApp/Cloud/CloudStartChat+Login.swift
…login recovers with --login none

review23 at a54d3d1: F1 (blocking) a login step recorded cancelled matched no
refusal and the prompt was sent; it now refuses with cloud.relay.login_cancelled
and the start-session command. F2: machine-login-missing's command kept the
machine choice and was refused again; it now ends in --login none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…FFI pin

cmux-cloud-start-core is an input of the CCmuxAppFFI pin, so a test-only
change there failed "Check the app FFI pin". vm_kind::read is unchanged; the
unknown answer stays covered by the server's connect_owner tests and the
app's CloudAppLinkOwnerAnswerTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
@cursor

cursor Bot commented Nov 6, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

This branch was successfully deployed

1 active and 3 inactive deployments
artifacts — 50170750 Deployed Nov 6, 2026 by teamleaderleo via publish tree-addressed cmux-tui #118848
sentry-dsyms — 50170750 Deployed Nov 6, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #118848
ci-routing-agent — 50170750 Deployed Nov 6, 2026 by teamleaderleo via cmux-tui tree preflight #118848
cmux-next-cloud-journey — 50170750 Deployed Nov 6, 2026 by teamleaderleo via journey #129
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant