Repository navigation
acpmux: run an agent CLI's own sign-in on a Cloud machine for the chat's sign-in card - #30188
Conversation
… sign-in card _acpmux/agent_login/start|finish|cancel|api_key run `claude auth login --claudeai` or `codex login --device-auth` (or save an API key) on the machine acpmux runs on, so a Cloud machine signs in with the person's own account instead of a copy of the Mac's login. Refused on a team machine; model-plane env never reaches the CLI; codes and keys are never logged or answered. Advertised as the `agent_login` feature and `agent-login-v1`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…lues without references Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
… signal; hold a finishing sign-in through a new start Review follow-ups: PR_SET_PDEATHSIG follows the spawning thread, which a block_in_place worker can lose mid-sign-in; a sessionId that resolves nowhere would have signed this machine in instead; the start that replaces a sign-in now holds its lock through the stop. The Bedrock/Vertex/Foundry switches join the removed model-plane env. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
There was a problem hiding this comment.
9 issues found across 15 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmux-tui/crates/acpmux/src/server/agent_login.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/server/agent_login.rs:80">
P2: `cli_for` only accepts profiles keyed exactly `claude`/`codex`, and rejects Claude profiles with launch arguments. A custom-named family profile therefore falls back to `which(...)`, losing the chat’s configured executable and credential-store environment, so the card can authenticate a different CLI/store. Resolve the selected family profile with the same resolver used for session startup.</violation>
<violation number="2" location="cmux-tui/crates/acpmux/src/server/agent_login.rs:90">
P1: `cli_for` drops the agent family's/defaults environment, so a configured `CODEX_HOME` or `CLAUDE_CONFIG_DIR` is ignored and login writes credentials to the fallback store instead of the store used by the running harness. Merge `cfg.defaults_for(agent.name()).env` with profile env, preserving profile values as the higher-precedence layer.</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/server/requests.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/server/requests.rs:908">
P2: `initialize` advertises `agent_login` in `features` but omits all four sign-in methods from `_meta.acpmux.extensions`. Clients that gate callable extensions on that list will not expose or invoke the sign-in card; add `MUX_AGENT_LOGIN_START`, `MUX_AGENT_LOGIN_FINISH`, `MUX_AGENT_LOGIN_CANCEL`, and `MUX_AGENT_LOGIN_API_KEY` to the advertised extensions.</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/agent_login.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/agent_login.rs:74">
P2: `MODEL_PLANE_ENV` omits provider credential variables such as AWS and Google/Vertex credentials. `Cli::command` reapplies profile env after the daemon scrub, so those values can reach `claude auth login`; remove the provider credential namespaces too.</violation>
<violation number="2" location="cmux-tui/crates/acpmux/src/agent_login.rs:85">
P1: The sign-in CLI still inherits Cloud's team-plane environment because this scrub list omits `CMUX_CLAUDE_TEAM_API_KEY` and the related `CMUX_CLAUDE_TEAM_BASE_URL`, `CMUX_CODEROUTER_URL`, and `CMUX_VM_ID` values. Add those model-plane variables before spawning; otherwise own sign-in violates the stated isolation and exposes the team credential to the CLI and its children.</violation>
<violation number="3" location="cmux-tui/crates/acpmux/src/agent_login.rs:222">
P1: `Machine::shared` treats an `owner` marker as private without checking `/srv/team`. `start` and `save_api_key` can therefore authorize own credentials on a team machine; reject a present or unreadable team marker too.</violation>
<violation number="4" location="cmux-tui/crates/acpmux/src/agent_login.rs:453">
P2: `kill_group` is a no-op on non-Unix targets. Cancel, expiry, and URL timeout then kill only the leader, allowing CLI descendants to outlive the sign-in; use a platform process-group or job equivalent.</violation>
<violation number="5" location="cmux-tui/crates/acpmux/src/agent_login.rs:583">
P2: Concurrent starts both spawn before either inserts its entry. The later insert kills the first process, but the first call still returns its invalid login ID; reserve or serialize the per-agent slot before spawning.</violation>
</file>
<file name="cmux-tui/crates/acpmux/src/agent_login/parse.rs">
<violation number="1" location="cmux-tui/crates/acpmux/src/agent_login/parse.rs:63">
P2: `strip_terminal` treats every `ESC` as an OSC terminator, even when it is not followed by `\\`. A pipe read can end on that byte, so `offer` may expose an unfinished OSC-8 target; set `closed` only for BEL or `ESC\\`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| }; | ||
| let model = cfg.defaults_for(agent.name()).model.unwrap_or_default(); | ||
| let expand = |v: &str| crate::hub::expand_env_value(v, &cwd, &home, &model); | ||
| let env: std::collections::BTreeMap<String, String> = profile |
There was a problem hiding this comment.
P1: cli_for drops the agent family's/defaults environment, so a configured CODEX_HOME or CLAUDE_CONFIG_DIR is ignored and login writes credentials to the fallback store instead of the store used by the running harness. Merge cfg.defaults_for(agent.name()).env with profile env, preserving profile values as the higher-precedence layer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/agent_login.rs, line 90:
<comment>`cli_for` drops the agent family's/defaults environment, so a configured `CODEX_HOME` or `CLAUDE_CONFIG_DIR` is ignored and login writes credentials to the fallback store instead of the store used by the running harness. Merge `cfg.defaults_for(agent.name()).env` with profile env, preserving profile values as the higher-precedence layer.</comment>
<file context>
@@ -0,0 +1,203 @@
+ };
+ let model = cfg.defaults_for(agent.name()).model.unwrap_or_default();
+ let expand = |v: &str| crate::hub::expand_env_value(v, &cwd, &home, &model);
+ let env: std::collections::BTreeMap<String, String> = profile
+ .env
+ .iter()
</file context>
| "ANTHROPIC_CUSTOM_HEADERS", | ||
| "CLAUDE_CODE_USE_BEDROCK", | ||
| "CLAUDE_CODE_USE_VERTEX", | ||
| "CLAUDE_CODE_USE_FOUNDRY", |
There was a problem hiding this comment.
P1: The sign-in CLI still inherits Cloud's team-plane environment because this scrub list omits CMUX_CLAUDE_TEAM_API_KEY and the related CMUX_CLAUDE_TEAM_BASE_URL, CMUX_CODEROUTER_URL, and CMUX_VM_ID values. Add those model-plane variables before spawning; otherwise own sign-in violates the stated isolation and exposes the team credential to the CLI and its children.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/agent_login.rs, line 85:
<comment>The sign-in CLI still inherits Cloud's team-plane environment because this scrub list omits `CMUX_CLAUDE_TEAM_API_KEY` and the related `CMUX_CLAUDE_TEAM_BASE_URL`, `CMUX_CODEROUTER_URL`, and `CMUX_VM_ID` values. Add those model-plane variables before spawning; otherwise own sign-in violates the stated isolation and exposes the team credential to the CLI and its children.</comment>
<file context>
@@ -0,0 +1,743 @@
+ "ANTHROPIC_CUSTOM_HEADERS",
+ "CLAUDE_CODE_USE_BEDROCK",
+ "CLAUDE_CODE_USE_VERTEX",
+ "CLAUDE_CODE_USE_FOUNDRY",
+];
+
</file context>
| "CLAUDE_CODE_USE_FOUNDRY", | |
| "CLAUDE_CODE_USE_FOUNDRY", | |
| "CMUX_CLAUDE_TEAM_BASE_URL", | |
| "CMUX_CLAUDE_TEAM_API_KEY", | |
| "CMUX_CODEROUTER_URL", | |
| "CMUX_VM_ID", |
| /// On a team machine (module docs): `etc/cmux/vm-kind` exists and is not | ||
| /// `owner`, or cannot be read. | ||
| pub fn shared(&self) -> bool { | ||
| let kind = self.root.join("etc/cmux/vm-kind"); |
There was a problem hiding this comment.
P1: Machine::shared treats an owner marker as private without checking /srv/team. start and save_api_key can therefore authorize own credentials on a team machine; reject a present or unreadable team marker too.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/agent_login.rs, line 222:
<comment>`Machine::shared` treats an `owner` marker as private without checking `/srv/team`. `start` and `save_api_key` can therefore authorize own credentials on a team machine; reject a present or unreadable team marker too.</comment>
<file context>
@@ -0,0 +1,743 @@
+ /// On a team machine (module docs): `etc/cmux/vm-kind` exists and is not
+ /// `owner`, or cannot be read.
+ pub fn shared(&self) -> bool {
+ let kind = self.root.join("etc/cmux/vm-kind");
+ match std::fs::symlink_metadata(&kind) {
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
</file context>
| let kind = self.root.join("etc/cmux/vm-kind"); | |
| let team = self.root.join("srv/team"); | |
| match std::fs::symlink_metadata(&team) { | |
| Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} | |
| _ => return true, | |
| } | |
| let kind = self.root.join("etc/cmux/vm-kind"); |
| "personInstance": hub.person.instance(), | ||
| "extensions": extensions, "operations": crate::hub::HANDOFF_OPERATIONS.iter().chain(crate::hub::PERMISSION_GROUP_OPERATIONS.iter()).chain(super::fork_through::FORK_OPERATIONS.iter()).collect::<Vec<_>>(), "handoff": {"maxCapsuleBytes": crate::hub::MAX_CAPSULE_BYTES}, | ||
| "features": ["promptAccepted", "turnIds", "eventPaging", "eventKinds", "eventStream", "cancelRequest", "messageSuperseded", "turnErrorText", "permissionGroups", "trustGate", "chiefBuiltinPresets", "personChallenge", "queueEdit", "steerInterrupt", "personHolds", "sessionTransfer", "approverDecisions", "toolInventory", "promptWithdraw", "processes", "worktreeCare", "harnessInventory", super::inventory_watch::FEATURE, crate::harness_health::INVENTORY_REFRESH_FEATURE, crate::harness_health::UPDATE_PLAN_FEATURE, crate::harness_health::UPDATE_PREPARE_FEATURE, "surfaces", crate::hub::FILE_PUT_FEATURE, crate::hub::file_uploads::CHUNKS_FEATURE, crate::hub::MEDIA_READ_FEATURE, crate::hub::SUBAGENT_TRANSCRIPT_FEATURE, crate::hub::NATIVE_HISTORY_FEATURE, crate::attach::FEATURE, crate::prompt_uploads::FEATURE, crate::hub::file_uploads::RESUME_FEATURE, super::upload_frames::FEATURE, crate::peer_states::STATE_EVENTS_FEATURE, crate::hub::session_auth::FEATURE, crate::hub::cancel::PERSON_STOP_FEATURE, crate::hub::stop_if_idle::FEATURE, SESSION_RESUME_FEATURE, crate::watch_delta::FEATURE], "trustGate": true}} | ||
| "features": ["promptAccepted", "turnIds", "eventPaging", "eventKinds", "eventStream", "cancelRequest", "messageSuperseded", "turnErrorText", "permissionGroups", "trustGate", "chiefBuiltinPresets", "personChallenge", "queueEdit", "steerInterrupt", "personHolds", "sessionTransfer", "approverDecisions", "toolInventory", "promptWithdraw", "processes", "worktreeCare", "harnessInventory", super::inventory_watch::FEATURE, crate::harness_health::INVENTORY_REFRESH_FEATURE, crate::harness_health::UPDATE_PLAN_FEATURE, crate::harness_health::UPDATE_PREPARE_FEATURE, "surfaces", crate::hub::FILE_PUT_FEATURE, crate::hub::file_uploads::CHUNKS_FEATURE, crate::hub::MEDIA_READ_FEATURE, crate::hub::SUBAGENT_TRANSCRIPT_FEATURE, crate::hub::NATIVE_HISTORY_FEATURE, crate::attach::FEATURE, crate::prompt_uploads::FEATURE, crate::hub::file_uploads::RESUME_FEATURE, super::upload_frames::FEATURE, crate::peer_states::STATE_EVENTS_FEATURE, crate::hub::session_auth::FEATURE, crate::agent_login::FEATURE, crate::hub::cancel::PERSON_STOP_FEATURE, crate::hub::stop_if_idle::FEATURE, SESSION_RESUME_FEATURE, crate::watch_delta::FEATURE], "trustGate": true}} |
There was a problem hiding this comment.
P2: initialize advertises agent_login in features but omits all four sign-in methods from _meta.acpmux.extensions. Clients that gate callable extensions on that list will not expose or invoke the sign-in card; add MUX_AGENT_LOGIN_START, MUX_AGENT_LOGIN_FINISH, MUX_AGENT_LOGIN_CANCEL, and MUX_AGENT_LOGIN_API_KEY to the advertised extensions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/requests.rs, line 908:
<comment>`initialize` advertises `agent_login` in `features` but omits all four sign-in methods from `_meta.acpmux.extensions`. Clients that gate callable extensions on that list will not expose or invoke the sign-in card; add `MUX_AGENT_LOGIN_START`, `MUX_AGENT_LOGIN_FINISH`, `MUX_AGENT_LOGIN_CANCEL`, and `MUX_AGENT_LOGIN_API_KEY` to the advertised extensions.</comment>
<file context>
@@ -905,7 +905,7 @@ async fn dispatch_request(
"personInstance": hub.person.instance(),
"extensions": extensions, "operations": crate::hub::HANDOFF_OPERATIONS.iter().chain(crate::hub::PERMISSION_GROUP_OPERATIONS.iter()).chain(super::fork_through::FORK_OPERATIONS.iter()).collect::<Vec<_>>(), "handoff": {"maxCapsuleBytes": crate::hub::MAX_CAPSULE_BYTES},
- "features": ["promptAccepted", "turnIds", "eventPaging", "eventKinds", "eventStream", "cancelRequest", "messageSuperseded", "turnErrorText", "permissionGroups", "trustGate", "chiefBuiltinPresets", "personChallenge", "queueEdit", "steerInterrupt", "personHolds", "sessionTransfer", "approverDecisions", "toolInventory", "promptWithdraw", "processes", "worktreeCare", "harnessInventory", super::inventory_watch::FEATURE, crate::harness_health::INVENTORY_REFRESH_FEATURE, crate::harness_health::UPDATE_PLAN_FEATURE, crate::harness_health::UPDATE_PREPARE_FEATURE, "surfaces", crate::hub::FILE_PUT_FEATURE, crate::hub::file_uploads::CHUNKS_FEATURE, crate::hub::MEDIA_READ_FEATURE, crate::hub::SUBAGENT_TRANSCRIPT_FEATURE, crate::hub::NATIVE_HISTORY_FEATURE, crate::attach::FEATURE, crate::prompt_uploads::FEATURE, crate::hub::file_uploads::RESUME_FEATURE, super::upload_frames::FEATURE, crate::peer_states::STATE_EVENTS_FEATURE, crate::hub::session_auth::FEATURE, crate::hub::cancel::PERSON_STOP_FEATURE, crate::hub::stop_if_idle::FEATURE, SESSION_RESUME_FEATURE, crate::watch_delta::FEATURE], "trustGate": true}}
+ "features": ["promptAccepted", "turnIds", "eventPaging", "eventKinds", "eventStream", "cancelRequest", "messageSuperseded", "turnErrorText", "permissionGroups", "trustGate", "chiefBuiltinPresets", "personChallenge", "queueEdit", "steerInterrupt", "personHolds", "sessionTransfer", "approverDecisions", "toolInventory", "promptWithdraw", "processes", "worktreeCare", "harnessInventory", super::inventory_watch::FEATURE, crate::harness_health::INVENTORY_REFRESH_FEATURE, crate::harness_health::UPDATE_PLAN_FEATURE, crate::harness_health::UPDATE_PREPARE_FEATURE, "surfaces", crate::hub::FILE_PUT_FEATURE, crate::hub::file_uploads::CHUNKS_FEATURE, crate::hub::MEDIA_READ_FEATURE, crate::hub::SUBAGENT_TRANSCRIPT_FEATURE, crate::hub::NATIVE_HISTORY_FEATURE, crate::attach::FEATURE, crate::prompt_uploads::FEATURE, crate::hub::file_uploads::RESUME_FEATURE, super::upload_frames::FEATURE, crate::peer_states::STATE_EVENTS_FEATURE, crate::hub::session_auth::FEATURE, crate::agent_login::FEATURE, crate::hub::cancel::PERSON_STOP_FEATURE, crate::hub::stop_if_idle::FEATURE, SESSION_RESUME_FEATURE, crate::watch_delta::FEATURE], "trustGate": true}}
});
// Large messages go compressed from here on (`frame_compress.rs`).
</file context>
| break; | ||
| } | ||
| if o == '\u{1b}' { | ||
| if chars.peek() == Some(&'\\') { |
There was a problem hiding this comment.
P2: strip_terminal treats every ESC as an OSC terminator, even when it is not followed by \\. A pipe read can end on that byte, so offer may expose an unfinished OSC-8 target; set closed only for BEL or ESC\\.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/agent_login/parse.rs, line 63:
<comment>`strip_terminal` treats every `ESC` as an OSC terminator, even when it is not followed by `\\`. A pipe read can end on that byte, so `offer` may expose an unfinished OSC-8 target; set `closed` only for BEL or `ESC\\`.</comment>
<file context>
@@ -0,0 +1,154 @@
+ break;
+ }
+ if o == '\u{1b}' {
+ if chars.peek() == Some(&'\\') {
+ chars.next();
+ }
</file context>
| pub(crate) fn cli_for(cfg: &Config, agent: Agent) -> Option<Cli> { | ||
| let cwd = std::env::temp_dir(); | ||
| let home = dirs::home_dir().unwrap_or_default(); | ||
| let profile = cfg.harnesses.get(agent.name()).filter(|p| match agent { |
There was a problem hiding this comment.
P2: cli_for only accepts profiles keyed exactly claude/codex, and rejects Claude profiles with launch arguments. A custom-named family profile therefore falls back to which(...), losing the chat’s configured executable and credential-store environment, so the card can authenticate a different CLI/store. Resolve the selected family profile with the same resolver used for session startup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/agent_login.rs, line 80:
<comment>`cli_for` only accepts profiles keyed exactly `claude`/`codex`, and rejects Claude profiles with launch arguments. A custom-named family profile therefore falls back to `which(...)`, losing the chat’s configured executable and credential-store environment, so the card can authenticate a different CLI/store. Resolve the selected family profile with the same resolver used for session startup.</comment>
<file context>
@@ -0,0 +1,203 @@
+pub(crate) fn cli_for(cfg: &Config, agent: Agent) -> Option<Cli> {
+ let cwd = std::env::temp_dir();
+ let home = dirs::home_dir().unwrap_or_default();
+ let profile = cfg.harnesses.get(agent.name()).filter(|p| match agent {
+ Agent::Claude => p.kind == HarnessKind::ClaudeStdio && p.argv.len() == 1,
+ Agent::Codex => crate::codex_app_server::is_profile_launch(&p.argv),
</file context>
| store, | ||
| running: Arc::new(tokio::sync::Mutex::new(Running { child, stdin, _folder: folder })), | ||
| }; | ||
| if let Some(old) = lock_table().insert(agent, entry) { |
There was a problem hiding this comment.
P2: Concurrent starts both spawn before either inserts its entry. The later insert kills the first process, but the first call still returns its invalid login ID; reserve or serialize the per-agent slot before spawning.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/agent_login.rs, line 583:
<comment>Concurrent starts both spawn before either inserts its entry. The later insert kills the first process, but the first call still returns its invalid login ID; reserve or serialize the per-agent slot before spawning.</comment>
<file context>
@@ -0,0 +1,743 @@
+ store,
+ running: Arc::new(tokio::sync::Mutex::new(Running { child, stdin, _folder: folder })),
+ };
+ if let Some(old) = lock_table().insert(agent, entry) {
+ // A start that raced this one: the later one wins.
+ kill_group(old.group);
</file context>
| } | ||
|
|
||
| #[cfg(not(unix))] | ||
| fn kill_group(_group: Option<u32>) {} |
There was a problem hiding this comment.
P2: kill_group is a no-op on non-Unix targets. Cancel, expiry, and URL timeout then kill only the leader, allowing CLI descendants to outlive the sign-in; use a platform process-group or job equivalent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/agent_login.rs, line 453:
<comment>`kill_group` is a no-op on non-Unix targets. Cancel, expiry, and URL timeout then kill only the leader, allowing CLI descendants to outlive the sign-in; use a platform process-group or job equivalent.</comment>
<file context>
@@ -0,0 +1,743 @@
+}
+
+#[cfg(not(unix))]
+fn kill_group(_group: Option<u32>) {}
+
+/// Takes the sign-in `id` (any agent) out of the table and stops its CLI.
</file context>
|
|
||
| /// The model-plane values the sign-in never sees: they would receive the | ||
| /// person's sign-in or outrank it. | ||
| pub const MODEL_PLANE_ENV: &[&str] = &[ |
There was a problem hiding this comment.
P2: MODEL_PLANE_ENV omits provider credential variables such as AWS and Google/Vertex credentials. Cli::command reapplies profile env after the daemon scrub, so those values can reach claude auth login; remove the provider credential namespaces too.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/agent_login.rs, line 74:
<comment>`MODEL_PLANE_ENV` omits provider credential variables such as AWS and Google/Vertex credentials. `Cli::command` reapplies profile env after the daemon scrub, so those values can reach `claude auth login`; remove the provider credential namespaces too.</comment>
<file context>
@@ -0,0 +1,743 @@
+
+/// The model-plane values the sign-in never sees: they would receive the
+/// person's sign-in or outrank it.
+pub const MODEL_PLANE_ENV: &[&str] = &[
+ "ANTHROPIC_API_KEY",
+ "ANTHROPIC_AUTH_TOKEN",
</file context>
|
Review (subagent opus-5.5 medium): SHIP at 87f6af8 The independent review found no blocking finding. The fixes for its follow-ups landed at this head and were checked again:
Follow-ups: #30225 (a saved Claude key after a switch to team mode;
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base cfb0745; does not block Shipped in:
|
|
Found 9 test failures on Blacksmith runners: Failures
|
…session acpmux's agent_login (#30188) forwards a call that names a chat to the machine the chat lives on, and refuses a caller that is not the owner's (remote_refused) or a chat it does not have (unknown_session). The card names the chat on start and cancel too, words both reasons, and the host allowlist takes the four methods as optionally session scoped with sessionId on each. The daemon relay names the tab's session on every step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…he user's own machine (#30214) * cmux NEXT Cloud: a failed sign-in card signs the agent in on its own machine A Cloud chat whose Claude or Codex has no login on its machine offers Sign in to Claude / Codex (the agent's own login, run there through acpmux _acpmux/agent_login/*) and Use an API key, inline in the failed turn's card. Never copies this Mac's login. Once signed in, the failed prompt goes again on the person's click (a session/prompt gesture ticket); nothing is sent without one. The pane's host allowlist and the shared policy gain the four methods; the wire log keeps no code or key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in card: a failed API key asks for the key again on Retry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in card: design-token spacing, test types Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in card: format Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in: only the person's click starts a login; the page opens on its own click Review (router-review, BLOCK at b02e46f): start took no gesture, so a pane script could start a login and finish it into the chat. The host now gates _acpmux/agent_login/start like api_key. The Sign in click's gesture goes to the start, so Open sign-in page leads the card and opens the page on its own click. Also: a card that goes away ends every step in flight, and a start reply with a page this Mac will not open cancels that login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in card: format Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in reaches Cloud chats: the daemon relays agent_login Review (router-review, BLOCK at 61faf41): Cloud chats talk to their machine through the session daemon (RemoteAcpmuxWire), which neither advertised agent_login nor relayed its methods. cmux-tui-core gains agent-session-login (agent-session-login-v1): each step goes to acpmux's _acpmux/agent_login/<op> on the tab's own session, owner only (never a teammate, guest or observation), acpmux's refusal back as {refused:{reason}}, finish waiting up to 15 minutes for Codex's approval. The Swift attach client and the remote wire relay the four frames, and the wire's handshake names the agent_login feature where the daemon serves it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * agent-session-login: fix a closure lifetime and an async autoclosure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in: every step names its chat; remote_refused and unknown_session acpmux's agent_login (#30188) forwards a call that names a chat to the machine the chat lives on, and refuses a caller that is not the owner's (remote_refused) or a chat it does not have (unknown_session). The card names the chat on start and cancel too, words both reasons, and the host allowlist takes the four methods as optionally session scoped with sessionId on each. The daemon relay names the tab's session on every step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Remote wire: a sign-in step must name this chat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * agent-session-login: rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in card: text field focus shows by its border, floating promises voided, fmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * Cloud sign-in: Retry after a wrong code starts a new login, card cleanup stops a pending resend, step deadlines match acpmux's acpmux ends a login on any refused step, a wrong Claude code too, so Retry now starts a fresh one. A resend checks that its card is still up after the prompt's attachments load. Start and API-key steps wait past acpmux's own waits (20 s and 60 s) in the Swift client and the daemon relay. The handshake test reads an absent feature list as false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * daemon capabilities export: agent-session-login-v1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 * AgentSessionAttachClient: call takes a timeout again (the base revert #30791 dropped it; login steps use it) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…r Codex is not signed in (#31517) Leo, 2026-10-26: "if user picks a model they don't have api key, we offer them to add credits or add their own api key" #27058 shipped the Add Credits half (the router's plan and credit refusals). The key half waited on key plumbing (manaflow-ai/cmuxterm-hq#5411); #30188 has since given acpmux `_acpmux/agent_login/api_key`, which saves a key where the agent's own CLI reads it (Claude's settings.json env.ANTHROPIC_API_KEY, `codex login --with-api-key`) and restarts the chat on it. Only Cloud chats offered it. A chat on this Mac whose built-in `claude` or `codex` fails a turn for want of a sign-in now shows Use an API key after Sign in again; Save stores the key and Send again resends the failed prompt on the person's click. Routes (claude-cr, claude-sr) set their own key and endpoint, so they get no key field. Tests: bun test test/cloud-signin-card.test.tsx test/access-offer.test.tsx test/signed-out-rail.test.tsx -> 30 pass. Agent-Session: https://cmux.com/deeplink/agent?id=aid-60c63e855fe3c933a547 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo, 2026-11-01, on building the one-click sign-in for Cloud machines now: "yes those are important".
In production cmux must not copy the Mac's Claude or Codex login to a Cloud machine (it may be against the agents' terms), and CodeRouter never serves production users. So the first Cloud chat on a machine with no login needs a sign-in that runs the agent's own login on that machine. This PR adds the acpmux side (Rust only); the transcript card is a separate webview PR built against this contract.
Contract (as built)
ACP extension methods on acpmux's JSON-RPC:
_acpmux/agent_login/start {agent: "claude"|"codex", sessionId?}->{loginId, url, userCode?, needsCode, expiresAtMs}claude auth login --claudeai,needsCode: true. codex: runscodex login --device-auth,needsCode: false,userCodeset.httpsURL on an allowed host (claude.com, claude.ai, platform.claude.com, console.anthropic.com, auth.anthropic.com, auth.openai.com): no user part, no port, ASCII only, OSC-8 and ANSI stripped first. The read is bounded (20 s, 64 KiB kept; the rest is drained so the CLI never blocks).busywhile that one's finish is writing the login). The CLI runs in its own process group in a private 0700 temp folder, getskill_on_drop, hasPR_SET_PDEATHSIGon Linux, and is killed 15 minutes after it starts._acpmux/agent_login/finish {loginId, code?, sessionId?}->{signedIn, restarted}code + "\n"to stdin (one line of printable ASCII), then waits up to 120 s. codex: waits for the device flow until the sign-in expires.agent-auth.shreads it, never by value:<CLAUDE_CONFIG_DIR or ~/.claude>/.credentials.jsonhasclaudeAiOauth;<CODEX_HOME or ~/.codex>/auth.jsonhas tokens or a key. On macOS, Claude's exit 0 counts by itself, because Claude keeps its login in the keychain there and acpmux never reads it._acpmux/agent_login/cancel {loginId}->{}(does nothing when no such sign-in is running)._acpmux/agent_login/api_key {agent, key, sessionId?}->{saved: true, restarted}. claude: setsenv.ANTHROPIC_API_KEYin the Claude home'ssettings.json, keeping every other key; the write is 0600 and atomic, and a file that is not a JSON object is left as it is (failed). codex: runscodex login --with-api-keywith the key on stdin.After a sign-in or saved key:
/etc/cmux/agent-auth.shexists),~/.config/cmux/agent-auth/<agent>is set toown\n(0600, folder 0700), so the image's launcher uses the person's login and drops CodeRouter.sessionId, that chat's agent restarts throughHub::restart_harnesses(the same path as_acpmux/sessions/restart_harness). If the chat is between turns it restarts now (restarted: true). If it is in a turn, it restarts when the turn ends (restarted: false); the turn is never cut.Errors: JSON-RPC error with
data.reasonone ofunsupported_agent,shared_machine,busy,cli_missing,no_url,bad_code(a code or key that is malformed or that the CLI refused),expired,failed,unknown_login. One addition to the brief:remote_refused, for a caller that may not run a sign-in (below).shared_machinewhen/etc/cmux/vm-kindexists and is notowner, or cannot be read (fail closed, ascmux_agent_own_login_allowed).vm_boundary::owner). Never a Web page, a paired device, or a peer forwarding for one. Nothing the caller sends names a program, path or env.claude/codexharness profile (with Codex's-c/-pglobals andCODEX_PATH), else the one on the login PATH. The env is acpmux's, minusANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URL,CLAUDE_CODE_OAUTH_TOKEN,OPENAI_API_KEY,OPENAI_BASE_URL,CODEX_API_KEY,ANTHROPIC_CUSTOM_HEADERS,DISPLAYandWAYLAND_DISPLAY. It addsBROWSER=trueandCMUX_CLAUDE_AUTH=own/CMUX_CODEX_AUTH=own, so the image's/usr/local/bin/claude/codexlaunchers add no CodeRouter config to the login run.Zeroizingbuffers.agent_loginininitialize's_meta.acpmux.features, and capabilityagent-login-v1(in_meta.acpmux.capabilitiesand_acpmux/status).Routing: the card names its chat on every call
sessionIdis optional on every method. A request that names a chat living on a peer is forwarded whole to that peer by the existingpeer_forward.rspath. So a pane connected to the Mac's acpmux reaches the Cloud machine's acpmux by passing the Cloud chat'ssessionIdtostart,finish,cancelandapi_key. Without it, the sign-in runs on the daemon the pane is connected to.What the card PR must add (not in this PR)
The app pane's host allowlist is default-deny. The card PR must add the four methods to
AcpmuxPaneMethods.requests, their params toknownParams(agent,sessionId,loginId,code,key), the methods tooptionallySessionScoped, and a gesture rule forstart/finish/api_key. It must make the same change incmux-tui/crates/cmux-agent-pane-policy/policy.json.the_allowlist_is_what_the_page_sendsrequires that table to match what the page sends, so the allowlist ships with the card.How a Cloud machine gets this acpmux
The VM's acpmux is its cmux-tui binary,
/home/cmux/.cmux/bin/cmux-tui. Once this lands, thecmux-tui artifactsworkflow publishes the tree build, includingcmux-tui-x86_64-unknown-linux-musl. Every DEV, nightly or release app built from a tree that contains this commit bundles that daemon (scripts/cmux-next/pin-cmux-tui.shtree mode).The app's automatic machine update then installs it on the machine:
CloudMachineUpdateTargetrunscmux link machine update --to-bundled --if-older. That writes the pinned Linux build beside~/.cmux/bin/cmux-tui, swaps it in and restarts only the daemon, so chats and terminals keep running and a failed update rolls back. Machines too old for that get the same build through the Cloud API exec instead (CloudLegacyMachineUpdate). A local source build with no payload manifest cannot update machines.Tests
agent_login/tests.rs):claude auth loginoutput, both plain pipes and under a terminal (OSC-8 and colours)vm-kindshared-machine detection, and refusal before anything runscli_missingown\n, 0600/0700settings.jsonmerge: keys kept, 0600, no temp file left, non-object refused and left untouchedtests/agent_login.rs), with stand-inclaudeandcodexscripts on the harness profiles. The scripts print what the real CLIs print, fail if any model-plane value orDISPLAYreaches them, and read the code or key from stdin. Over a real connection it covers:initializeadvertises the feature and capabilityunsupported_agentbad_code, after which the login is gone:unknown_login)shared_machineon a team machinecmux-ci test:cargo test -p acpmux --lib -- agent_login capabilities protocol,cargo test -p acpmux --test agent_login,cargo clippy -p acpmux --all-targets -- -D warningsandcargo test -p cmux-agent-pane-policy.Risk
The change touches credentials. Mitigations:
It adds no Swift and no new trust path. Peer forwarding is the existing session-forwarding path, and the receiving daemon applies its own origin check.
Agent session: agent session
🤖 Generated with Claude Code