Merge nucleic/golden-opal-heron-lalz into dev
This commit is contained in:
@@ -30,9 +30,9 @@ import Security
|
||||
actor PhoneCredentialVault {
|
||||
static let shared = PhoneCredentialVault()
|
||||
|
||||
/// The kinds the phone holds — the two rotating OAuth logins, matching
|
||||
/// The kinds the phone holds — the rotating OAuth logins, matching
|
||||
/// `RunnerCredentialVault.mirrorableKinds`. Static keys are never stored on the phone.
|
||||
static let mirrorableKinds: [CredentialKind] = [.claudeOAuth, .codexAuth]
|
||||
static let mirrorableKinds: [CredentialKind] = [.claudeOAuth, .codexAuth, .grokAuth]
|
||||
|
||||
// MARK: - Contents
|
||||
|
||||
@@ -149,6 +149,11 @@ actor PhoneCredentialVault {
|
||||
else { continue }
|
||||
stamp = CodexCredentialFormat.lastRefresh(json)
|
||||
.map(Date.init(timeIntervalSince1970:)) ?? record.updatedAt
|
||||
case .grokAuth:
|
||||
guard GrokCredentialFormat.shouldReplace(candidate: json, current: current)
|
||||
else { continue }
|
||||
stamp = GrokCredentialFormat.expiresAt(json)
|
||||
.map(Date.init(timeIntervalSince1970:)) ?? record.updatedAt
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2592,6 +2592,7 @@ final class RemoteStore: ObservableObject {
|
||||
switch provider {
|
||||
case .claude: kind = .anthropicAPIKey
|
||||
case .codex: kind = .openAIAPIKey
|
||||
case .grok: kind = .xaiAPIKey
|
||||
default: return false
|
||||
}
|
||||
guard !trimmed.isEmpty,
|
||||
|
||||
@@ -86,6 +86,7 @@ struct AgentAccountsSection: View {
|
||||
switch kind {
|
||||
case .claudeOAuth: "Claude"
|
||||
case .codexAuth: "Codex"
|
||||
case .grokAuth: "Grok"
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
@@ -120,7 +121,7 @@ struct AgentAccountsSection: View {
|
||||
.font(.callout)
|
||||
}
|
||||
// The ToS-defensive fallback (REMOTE_AGENT_LOGIN §8): set a Console/API key
|
||||
// instead of a subscription login. Only for the two key-backed providers, and only
|
||||
// instead of a subscription login. Only for the key-backed providers, and only
|
||||
// when the host can take a sealed key from this phone.
|
||||
if apiKeyProviders.contains(status.provider), store.canSubmitAPIKey(toHost: hostID) {
|
||||
Button {
|
||||
@@ -139,19 +140,15 @@ struct AgentAccountsSection: View {
|
||||
// accepts the tombstone verb is reachable — the deletion then propagates from it
|
||||
// to every other member (and this phone clears its own vault copy regardless).
|
||||
if status.authenticated, store.canRevokeCredentials(onHost: hostID) {
|
||||
if status.method == "apiKey" {
|
||||
if status.method == "apiKey", let kind = Self.apiKeyKind(status.provider) {
|
||||
Button(role: .destructive) {
|
||||
deleteTarget = DeleteTarget(
|
||||
kind: status.provider == .codex ? .openAIAPIKey : .anthropicAPIKey,
|
||||
label: "\(name) API key")
|
||||
deleteTarget = DeleteTarget(kind: kind, label: "\(name) API key")
|
||||
} label: {
|
||||
Label("Delete API Key on All Devices…", systemImage: "trash")
|
||||
}
|
||||
} else {
|
||||
} else if status.method != "apiKey", let kind = Self.signInKind(status.provider) {
|
||||
Button(role: .destructive) {
|
||||
deleteTarget = DeleteTarget(
|
||||
kind: status.provider == .codex ? .codexAuth : .claudeOAuth,
|
||||
label: "\(name) sign-in")
|
||||
deleteTarget = DeleteTarget(kind: kind, label: "\(name) sign-in")
|
||||
} label: {
|
||||
Label("Sign Out on All Devices…", systemImage: "trash")
|
||||
}
|
||||
@@ -160,7 +157,29 @@ struct AgentAccountsSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var apiKeyProviders: [AgentLoginProvider] { [.claude, .codex] }
|
||||
/// The mesh credential kind a provider's subscription sign-in lands as, and the one its API
|
||||
/// key lands as. Nil for a provider this build doesn't know — a newer host can advertise one
|
||||
/// (the wire type is raw-string-backed), and revoking the *wrong* kind would sign the user out
|
||||
/// of a provider they didn't touch, so the menu item is simply withheld.
|
||||
private static func signInKind(_ provider: AgentLoginProvider) -> CredentialKind? {
|
||||
switch provider {
|
||||
case .claude: .claudeOAuth
|
||||
case .codex: .codexAuth
|
||||
case .grok: .grokAuth
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func apiKeyKind(_ provider: AgentLoginProvider) -> CredentialKind? {
|
||||
switch provider {
|
||||
case .claude: .anthropicAPIKey
|
||||
case .codex: .openAIAPIKey
|
||||
case .grok: .xaiAPIKey
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
private var apiKeyProviders: [AgentLoginProvider] { [.claude, .codex, .grok] }
|
||||
|
||||
private func detail(for status: WireProviderAuthStatus) -> String {
|
||||
switch (status.installed, status.authenticated) {
|
||||
@@ -187,7 +206,11 @@ private struct APIKeyEntrySheet: View {
|
||||
@State private var failed = false
|
||||
|
||||
private var keyName: String {
|
||||
target.provider == .claude ? "Anthropic API key" : "OpenAI API key"
|
||||
switch target.provider {
|
||||
case .codex: "OpenAI API key"
|
||||
case .grok: "xAI API key"
|
||||
default: "Anthropic API key"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -249,6 +272,7 @@ struct AgentLoginSheet: View {
|
||||
switch store.agentLoginProvider {
|
||||
case .some(.claude): "Claude"
|
||||
case .some(.codex): "Codex"
|
||||
case .some(.grok): "Grok"
|
||||
case .some(let other): other.rawValue.capitalized
|
||||
case .none: "Agent"
|
||||
}
|
||||
@@ -352,8 +376,11 @@ enum AgentAuthErrors {
|
||||
|| lowered.contains("authentication_error")
|
||||
|| lowered.contains("authentication error")
|
||||
|| lowered.contains("not logged in")
|
||||
// Grok's signed-out turn ("Not signed in. To authenticate without a browser…").
|
||||
|| lowered.contains("not signed in")
|
||||
|| lowered.contains("oauth token has expired")
|
||||
|| lowered.contains("please run /login")
|
||||
|| lowered.contains("run `grok login`")
|
||||
|| lowered.contains("invalid api key")
|
||||
|| lowered.contains("credential")
|
||||
&& lowered.contains("expired")
|
||||
|
||||
Reference in New Issue
Block a user