Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
/// account, else after one confirm that runs `setup-token` once in a visible local terminal. Every
/// later machine of the account then gets it with no prompt.
///
/// DEV and dogfood only (``passesHarnessLogin(isDebugBuild:setting:)``; Leo: "IN THE ACTUAL
/// DEV and dogfood only (``passesHarnessLogin(isDogfoodBuild:setting:)``; Leo: "IN THE ACTUAL
/// PRODUCTION APP IT MAY BE AGAINST TOS"). The relay alternative, which keeps the token on the Mac,
/// is scoped in cmux#29766.
///
Expand All @@ -24,13 +24,14 @@ extension CloudStartChat {
/// The one switch for every Mac-to-machine login transfer: this pass, the turn card's recovery
/// and the new-workspace setup, Claude's `setup-token` and the Codex broker alike. The
/// `cmux.cloud.passHarnessLogin` default wins when set (`-bool NO` makes a DEV build behave as
/// production); unset, it is on in a Debug build and off in release, where the machine signs in
/// on its own instead.
nonisolated static func passesHarnessLogin(isDebugBuild: Bool, setting: Bool?) -> Bool { setting ?? isDebugBuild }
/// production); unset, it is on in a dogfood build (``DevTools/isEnabled``: a Debug compile or a
/// NIGHTLY bundle, the NEXT nightly included) and off in release, where the machine signs in on
/// its own instead.
nonisolated static func passesHarnessLogin(isDogfoodBuild: Bool, setting: Bool?) -> Bool { setting ?? isDogfoodBuild }

/// This process's answer to ``passesHarnessLogin(isDebugBuild:setting:)``.
/// This process's answer to ``passesHarnessLogin(isDogfoodBuild:setting:)``.
nonisolated static var passesHarnessLoginHere: Bool {
passesHarnessLogin(isDebugBuild: DevTools.isDebugBuild,
passesHarnessLogin(isDogfoodBuild: DevTools.isEnabled,
setting: UserDefaults.standard.object(forKey: passHarnessLoginKey) as? Bool)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ extension CloudHandlers {
let cloud = context.services.cloud, services = context.services
let userID = cloud.auth.user?.id, teamID = cloud.auth.teamID
let availability = CloudWorkspaceSetupWizard.CredentialAvailability.resolve(
teamID: cloud.auth.teamID, supportsFileRoute: AgentTabStore.cloudCredentialTransferIsAvailable(linkSource: cloud.configuration.linkSource, isSignedIn: true))
teamID: cloud.auth.teamID, supportsFileRoute: AgentTabStore.cloudCredentialTransferIsAvailable(linkSource: cloud.configuration.linkSource, isSignedIn: true, passesLogin: true),
passesLogin: CloudStartChat.passesHarnessLoginHere)
let present: (CmuxDialogSpec) async -> CmuxDialogAnswer = { spec in
let scope: CmuxDialogScope = services.windows.active?.window.map(CmuxDialogScope.window) ?? .app
return await CmuxDialogCenter.shared.present(spec, in: scope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,15 @@ public struct CloudWorkspaceSetupWizard {
case available
case teamMachine
case unsupportedCloudRoute
/// This build does not copy the Mac's login (the shipping app): the person signs in on the
/// machine, from its chat's sign-in.
case signInOnMachine

/// Shared team machines never receive personal credentials through this flow.
public static func resolve(teamID: String?, supportsFileRoute: Bool) -> CredentialAvailability {
/// `passesLogin`: this build copies the Mac's login at all (DEV and dogfood).
public static func resolve(teamID: String?, supportsFileRoute: Bool, passesLogin: Bool = true) -> CredentialAvailability {
guard teamID == nil else { return .teamMachine }
guard passesLogin else { return .signInOnMachine }
return supportsFileRoute ? .available : .unsupportedCloudRoute
}
}
Expand Down Expand Up @@ -306,6 +311,8 @@ private enum CloudWorkspaceSetupWizardStrings {
String(localized: "cloud.workspaceSetup.credentialsTeamUnavailable", defaultValue: "Personal provider credentials stay off shared team VMs because they share a home folder.", table: "Cloud", bundle: .module)
case .unsupportedCloudRoute:
String(localized: "cloud.workspaceSetup.credentialsRouteUnavailable", defaultValue: "Provider credential setup is unavailable with this Cloud connection.", table: "Cloud", bundle: .module)
case .signInOnMachine:
String(localized: "cloud.workspaceSetup.credentialsSignInOnMachine", defaultValue: "Sign in to Claude or Codex on the VM itself: its chat offers a one-click sign-in.", table: "Cloud", bundle: .module)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,29 @@
"zh-Hans": {"stringUnit": {"state": "translated","value": "此云端连接不支持设置提供商凭据。"}},
"zh-Hant": {"stringUnit": {"state": "translated","value": "此雲端連線不支援設定供應商憑證。"}}
}},
"cloud.workspaceSetup.credentialsSignInOnMachine": {"localizations": {
"ar": {"stringUnit": {"state": "translated","value": "سجّل الدخول إلى Claude أو Codex على الآلة الافتراضية نفسها: توفّر محادثتها تسجيل دخول بنقرة واحدة."}},
"bs": {"stringUnit": {"state": "translated","value": "Prijavite se u Claude ili Codex na samoj virtuelnoj mašini: njen chat nudi prijavu jednim klikom."}},
"da": {"stringUnit": {"state": "translated","value": "Log ind på Claude eller Codex på selve VM'en: dens chat tilbyder login med ét klik."}},
"de": {"stringUnit": {"state": "translated","value": "Melde dich auf der VM selbst bei Claude oder Codex an: Ihr Chat bietet eine Anmeldung mit einem Klick."}},
"en": {"stringUnit": {"state": "translated","value": "Sign in to Claude or Codex on the VM itself: its chat offers a one-click sign-in."}},
"es": {"stringUnit": {"state": "translated","value": "Inicia sesión en Claude o Codex en la propia VM: su chat ofrece un inicio de sesión con un clic."}},
"fr": {"stringUnit": {"state": "translated","value": "Connectez-vous à Claude ou Codex sur la VM elle-même : son chat propose une connexion en un clic."}},
"it": {"stringUnit": {"state": "translated","value": "Accedi a Claude o Codex direttamente sulla VM: la sua chat offre l’accesso con un clic."}},
"ja": {"stringUnit": {"state": "translated","value": "VM 上で Claude または Codex にサインインしてください。チャットからワンクリックでサインインできます。"}},
"km": {"stringUnit": {"state": "translated","value": "ចូលគណនី Claude ឬ Codex នៅលើ VM ផ្ទាល់៖ ការជជែករបស់វាផ្តល់ការចូលគណនីដោយចុចតែម្តង។"}},
"ko": {"stringUnit": {"state": "translated","value": "VM에서 직접 Claude 또는 Codex에 로그인하세요. 해당 채팅에서 원클릭 로그인을 제공합니다."}},
"nb": {"stringUnit": {"state": "translated","value": "Logg på Claude eller Codex på selve VM-en: chatten tilbyr pålogging med ett klikk."}},
"pl": {"stringUnit": {"state": "translated","value": "Zaloguj się do Claude lub Codex na samej maszynie wirtualnej: jej czat oferuje logowanie jednym kliknięciem."}},
"pt-BR": {"stringUnit": {"state": "translated","value": "Entre no Claude ou no Codex na própria VM: o chat dela oferece login com um clique."}},
"ru": {"stringUnit": {"state": "translated","value": "Войдите в Claude или Codex на самой виртуальной машине: в её чате есть вход в один клик."}},
"th": {"stringUnit": {"state": "translated","value": "ลงชื่อเข้าใช้ Claude หรือ Codex บน VM โดยตรง: แชทของ VM มีการลงชื่อเข้าใช้ด้วยคลิกเดียว"}},
"tr": {"stringUnit": {"state": "translated","value": "Claude veya Codex'e doğrudan VM üzerinde giriş yapın: sohbeti tek tıkla giriş sunar."}},
"uk": {"stringUnit": {"state": "translated","value": "Увійдіть у Claude або Codex на самій віртуальній машині: її чат пропонує вхід одним клацанням."}},
"vi": {"stringUnit": {"state": "translated","value": "Đăng nhập Claude hoặc Codex ngay trên VM: cuộc trò chuyện của VM có đăng nhập bằng một lần nhấp."}},
"zh-Hans": {"stringUnit": {"state": "translated","value": "请在虚拟机上直接登录 Claude 或 Codex:其聊天提供一键登录。"}},
"zh-Hant": {"stringUnit": {"state": "translated","value": "請在虛擬機上直接登入 Claude 或 Codex:其聊天提供一鍵登入。"}}
}},
"cloud.workspaceSetup.credentialsTeamUnavailable": {"localizations": {
"ar": {"stringUnit": {"state": "translated","value": "تبقى بيانات اعتماد المزوّد الشخصية خارج الآلات الافتراضية المشتركة للفريق لأنها تشترك في مجلد رئيسي واحد."}},
"bs": {"stringUnit": {"state": "translated","value": "Lične vjerodajnice provajdera ostaju izvan zajedničkih timskih virtuelnih mašina jer dijele početni direktorij."}},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxNextActions
@testable import CmuxNextApp
import CmuxNextCloudCredentials
import Testing
Expand All @@ -6,14 +7,23 @@ import Testing
struct CloudStartChatLoginTests {
typealias Step = CloudStartChat.LoginStep

@Test func theSettingWinsElseOnlyDebugBuildsPassALogin() {
#expect(CloudStartChat.passesHarnessLogin(isDebugBuild: true, setting: nil))
#expect(!CloudStartChat.passesHarnessLogin(isDebugBuild: false, setting: nil))
#expect(CloudStartChat.passesHarnessLogin(isDebugBuild: false, setting: true))
#expect(CloudStartChat.passesHarnessLogin(isDebugBuild: true, setting: true))
@Test func theSettingWinsElseOnlyDogfoodBuildsPassALogin() {
#expect(CloudStartChat.passesHarnessLogin(isDogfoodBuild: true, setting: nil))
#expect(!CloudStartChat.passesHarnessLogin(isDogfoodBuild: false, setting: nil))
#expect(CloudStartChat.passesHarnessLogin(isDogfoodBuild: false, setting: true))
#expect(CloudStartChat.passesHarnessLogin(isDogfoodBuild: true, setting: true))
// `defaults write ... cmux.cloud.passHarnessLogin -bool NO`: a DEV build behaves as production.
#expect(!CloudStartChat.passesHarnessLogin(isDebugBuild: true, setting: false))
#expect(!CloudStartChat.passesHarnessLogin(isDebugBuild: false, setting: false))
#expect(!CloudStartChat.passesHarnessLogin(isDogfoodBuild: true, setting: false))
#expect(!CloudStartChat.passesHarnessLogin(isDogfoodBuild: false, setting: false))
}

/// The NEXT nightly is dogfood (only the team uses NEXT Cloud), so it passes the login by
/// default like a DEV build; the shipping app does not.
@Test(arguments: [("com.cmuxterm.app.nightly.cmux-next", false, true), ("com.cmuxterm.app.debug.next", true, true),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This fixture does not test the shipped NEXT DEV build: com.cmuxterm.app.debug.next is a Release artifact, but isDebugBuild: true makes DevTools.isAvailable return true for any bundle. Pass false here so the test verifies the bundle-specific dogfood rule and catches regressions that disable login transfer for NEXT DEV.

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 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudStartChatLoginTests.swift, line 22:

<comment>This fixture does not test the shipped NEXT DEV build: `com.cmuxterm.app.debug.next` is a Release artifact, but `isDebugBuild: true` makes `DevTools.isAvailable` return true for any bundle. Pass `false` here so the test verifies the bundle-specific dogfood rule and catches regressions that disable login transfer for NEXT DEV.</comment>

<file context>
@@ -6,14 +7,23 @@ import Testing
+
+    /// The NEXT nightly is dogfood (only the team uses NEXT Cloud), so it passes the login by
+    /// default like a DEV build; the shipping app does not.
+    @Test(arguments: [("com.cmuxterm.app.nightly.cmux-next", false, true), ("com.cmuxterm.app.debug.next", true, true),
+                      ("com.cmuxterm.app", false, false), ("com.cmuxterm.app.rc", false, false)])
+    func dogfoodBundlesPassALoginByDefault(bundleID: String, isDebugBuild: Bool, passes: Bool) {
</file context>
Suggested change
@Test(arguments: [("com.cmuxterm.app.nightly.cmux-next", false, true), ("com.cmuxterm.app.debug.next", true, true),
@Test(arguments: [("com.cmuxterm.app.nightly.cmux-next", false, true), ("com.cmuxterm.app.debug.next", false, true),

("com.cmuxterm.app", false, false), ("com.cmuxterm.app.rc", false, false)])
func dogfoodBundlesPassALoginByDefault(bundleID: String, isDebugBuild: Bool, passes: Bool) {
let dogfood = DevTools.isAvailable(bundleID: bundleID, isDebugBuild: isDebugBuild)
#expect(CloudStartChat.passesHarnessLogin(isDogfoodBuild: dogfood, setting: nil) == passes)
}

@Test func harnessFamiliesMapToTheirProvider() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ struct CloudWorkspaceSetupWizardTests {
#expect(CloudWorkspaceSetupWizard.CredentialAvailability.resolve(teamID: teamID, supportsFileRoute: supportsFileRoute) == expected)
}

/// A build that does not copy the Mac's login points a personal VM at its own sign-in, not at
/// the connection; a team VM keeps its own note.
@Test func aBuildThatDoesNotPassTheLoginSaysToSignInOnTheMachine() {
typealias A = CloudWorkspaceSetupWizard.CredentialAvailability
#expect(A.resolve(teamID: nil, supportsFileRoute: true, passesLogin: false) == .signInOnMachine)
#expect(A.resolve(teamID: "team-1", supportsFileRoute: true, passesLogin: false) == .teamMachine)
}

@MainActor
@Test(arguments: [true, false])
func accountOrTeamChangeDuringSetupRefusesTheCreationPlan(changesTeam: Bool) async throws {
Expand Down
Loading