diff options
Diffstat (limited to 'makima/src/daemon/task/manager.rs')
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 224 |
1 files changed, 153 insertions, 71 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index addcd71..df5e167 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -117,6 +117,7 @@ fn get_auth_flow_storage() -> &'static std::sync::Mutex<Option<std::sync::mpsc:: } /// Send an auth code to the pending OAuth flow. +/// Deprecated: The new setup-token flow outputs tokens directly, so this is no longer needed. pub fn send_auth_code(code: &str) -> bool { let storage = get_auth_flow_storage(); if let Ok(mut guard) = storage.lock() { @@ -127,16 +128,68 @@ pub fn send_auth_code(code: &str) -> bool { } } } - tracing::warn!("No pending auth flow to send code to"); + tracing::warn!("No pending auth flow to send code to (this is expected with the new token-based flow)"); false } +/// Extract an OAuth token from a line of setup-token output. +/// Looks for tokens matching the `sk-ant-oat01-` prefix format. +fn extract_oauth_token(line: &str) -> Option<String> { + let trimmed = line.trim(); + if trimmed.starts_with("sk-ant-oat01-") { + Some(trimmed.to_string()) + } else { + None + } +} + +/// Save an OAuth token to the ~/.makima directory for later use by spawned Claude processes. +fn save_oauth_token(token: &str) -> std::io::Result<()> { + let makima_dir = dirs::home_dir() + .unwrap_or_default() + .join(".makima"); + std::fs::create_dir_all(&makima_dir)?; + let token_path = makima_dir.join("claude_oauth_token"); + std::fs::write(&token_path, token)?; + // Set restrictive permissions on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&token_path, std::fs::Permissions::from_mode(0o600))?; + } + tracing::info!(path = %token_path.display(), "Saved OAuth token to disk"); + Ok(()) +} + +/// Load a previously saved OAuth token from ~/.makima/claude_oauth_token. +/// Returns None if no token file exists or is empty. +pub fn load_oauth_token() -> Option<String> { + let token_path = dirs::home_dir()? + .join(".makima") + .join("claude_oauth_token"); + std::fs::read_to_string(&token_path).ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) +} + +/// Result of the OAuth login flow initiated by `get_oauth_login_url`. +/// Contains the URL for the user to visit, plus a receiver for when the token is saved. +struct OAuthFlowResult { + /// The OAuth login URL the user should visit. + login_url: String, + /// Receiver that will yield the saved token once authentication completes. + token_rx: tokio::sync::oneshot::Receiver<String>, +} + /// Spawn `claude setup-token` to initiate OAuth flow and capture the login URL. /// This spawns the process in a PTY (required by Ink) and reads output until we find a URL. -/// The process continues running in the background waiting for auth completion. -async fn get_oauth_login_url(claude_command: &str) -> Option<String> { +/// +/// The new `claude setup-token` flow outputs a token directly (sk-ant-oat01-...) after +/// the user completes browser authentication, so no code submission is needed. +/// The token is automatically detected, saved to disk, and reported via the token_rx channel. +async fn get_oauth_login_url(claude_command: &str) -> Option<OAuthFlowResult> { use portable_pty::{native_pty_system, CommandBuilder, PtySize}; - use std::io::{Read, Write}; + use std::io::Read; tracing::info!("Spawning claude setup-token in PTY to get OAuth login URL"); @@ -173,7 +226,7 @@ async fn get_oauth_login_url(claude_command: &str) -> Option<String> { } }; - // Get the reader and writer from the master side + // Get the reader from the master side let mut reader = match pair.master.try_clone_reader() { Ok(reader) => reader, Err(e) => { @@ -182,7 +235,8 @@ async fn get_oauth_login_url(claude_command: &str) -> Option<String> { } }; - let mut writer = match pair.master.take_writer() { + // Take the writer - we keep it alive but don't need to write auth codes anymore + let _writer = match pair.master.take_writer() { Ok(writer) => writer, Err(e) => { tracing::error!(error = %e, "Failed to take PTY writer"); @@ -191,22 +245,25 @@ async fn get_oauth_login_url(claude_command: &str) -> Option<String> { }; // Create channels for communication - let (code_tx, code_rx) = std::sync::mpsc::channel::<String>(); let (url_tx, url_rx) = std::sync::mpsc::channel::<String>(); + let (token_tx, token_rx) = tokio::sync::oneshot::channel::<String>(); - // Store the code sender globally so it can be used when AUTH_CODE message arrives + // Also store a legacy code sender for backward compatibility (in case old server sends SubmitAuthCode) { + let (code_tx, _code_rx) = std::sync::mpsc::channel::<String>(); let storage = get_auth_flow_storage(); if let Ok(mut guard) = storage.lock() { *guard = Some(code_tx); } } - // Spawn reader thread - reads PTY output and sends URL when found + // Spawn reader thread - reads PTY output, sends URL when found, and watches for token let reader_handle = std::thread::spawn(move || { let mut buffer = [0u8; 4096]; let mut accumulated = String::new(); let mut url_sent = false; + let mut token_saved = false; + let mut token_tx = Some(token_tx); let mut read_count = 0; tracing::info!("setup-token reader thread started"); @@ -241,6 +298,22 @@ async fn get_oauth_login_url(claude_command: &str) -> Option<String> { } } + // Look for OAuth token in output (new setup-token format) + if !token_saved { + if let Some(token) = extract_oauth_token(&clean_line) { + tracing::info!("Found OAuth token in setup-token output"); + if let Err(e) = save_oauth_token(&token) { + tracing::error!(error = %e, "Failed to save OAuth token"); + } else { + tracing::info!("OAuth token saved successfully"); + } + if let Some(tx) = token_tx.take() { + let _ = tx.send(token); + } + token_saved = true; + } + } + // Check for success/failure messages if clean_line.contains("successfully") || clean_line.contains("authenticated") || clean_line.contains("Success") { tracing::info!("Authentication appears successful!"); @@ -256,39 +329,12 @@ async fn get_oauth_login_url(claude_command: &str) -> Option<String> { } } } - tracing::info!("setup-token reader thread ended"); + tracing::info!("setup-token reader thread ended (token_saved={})", token_saved); }); - // Spawn writer thread - waits for auth code and writes it to PTY + // Spawn cleanup thread - waits for reader to finish and cleans up the child process std::thread::spawn(move || { - tracing::info!("setup-token writer thread started, waiting for auth code (10 min timeout)"); - - // Wait for auth code from frontend (with long timeout - user needs time to authenticate) - match code_rx.recv_timeout(std::time::Duration::from_secs(600)) { - Ok(code) => { - tracing::info!(code_len = code.len(), "Received auth code from frontend, writing to PTY"); - // Write code followed by carriage return (Enter key in raw terminal mode) - let code_with_enter = format!("{}\r", code); - if let Err(e) = writer.write_all(code_with_enter.as_bytes()) { - tracing::error!(error = %e, "Failed to write auth code to PTY"); - } else if let Err(e) = writer.flush() { - tracing::error!(error = %e, "Failed to flush PTY writer"); - } else { - tracing::info!("Auth code written to setup-token PTY successfully"); - // Give Ink a moment to process, then send another Enter in case first was buffered - std::thread::sleep(std::time::Duration::from_millis(100)); - let _ = writer.write_all(b"\r"); - let _ = writer.flush(); - tracing::info!("Sent additional Enter keypress"); - } - } - Err(e) => { - tracing::info!(error = %e, "Auth code receive ended (timeout or channel closed)"); - } - } - - // Wait for reader thread to finish - tracing::debug!("Waiting for reader thread to finish..."); + tracing::debug!("setup-token cleanup thread: waiting for reader thread to finish..."); let _ = reader_handle.join(); // Wait for child to fully exit @@ -301,11 +347,17 @@ async fn get_oauth_login_url(claude_command: &str) -> Option<String> { tracing::error!(error = %e, "Failed to wait for setup-token process"); } } + + // Keep _writer alive until here so PTY stays open + drop(_writer); }); // Wait for URL with timeout match url_rx.recv_timeout(std::time::Duration::from_secs(30)) { - Ok(url) => Some(url), + Ok(login_url) => Some(OAuthFlowResult { + login_url, + token_rx, + }), Err(e) => { tracing::error!(error = %e, "Timed out waiting for OAuth login URL"); None @@ -1894,15 +1946,60 @@ impl TaskManager { // Spawn in a task so it doesn't block command handling tokio::spawn(async move { match get_oauth_login_url(&claude_command).await { - Some(login_url) => { - tracing::info!(request_id = %request_id, login_url = %login_url, "Got OAuth login URL for reauth"); + Some(flow_result) => { + tracing::info!(request_id = %request_id, login_url = %flow_result.login_url, "Got OAuth login URL for reauth"); + // Send url_ready status immediately let msg = DaemonMessage::ReauthStatus { request_id, status: "url_ready".to_string(), - login_url: Some(login_url), + login_url: Some(flow_result.login_url), error: None, + token_saved: false, }; let _ = ws_tx.send(msg).await; + + // Now wait for the token to be detected and saved (up to 10 minutes) + let ws_tx_token = ws_tx.clone(); + tokio::spawn(async move { + match tokio::time::timeout( + std::time::Duration::from_secs(600), + flow_result.token_rx, + ).await { + Ok(Ok(_token)) => { + tracing::info!(request_id = %request_id, "OAuth token received and saved, reporting completion"); + let msg = DaemonMessage::ReauthStatus { + request_id, + status: "completed".to_string(), + login_url: None, + error: None, + token_saved: true, + }; + let _ = ws_tx_token.send(msg).await; + } + Ok(Err(_)) => { + tracing::warn!(request_id = %request_id, "Token channel closed without receiving token"); + let msg = DaemonMessage::ReauthStatus { + request_id, + status: "failed".to_string(), + login_url: None, + error: Some("setup-token process ended without producing a token".to_string()), + token_saved: false, + }; + let _ = ws_tx_token.send(msg).await; + } + Err(_) => { + tracing::warn!(request_id = %request_id, "Timed out waiting for OAuth token (10 min)"); + let msg = DaemonMessage::ReauthStatus { + request_id, + status: "failed".to_string(), + login_url: None, + error: Some("Timed out waiting for authentication to complete".to_string()), + token_saved: false, + }; + let _ = ws_tx_token.send(msg).await; + } + } + }); } None => { tracing::error!(request_id = %request_id, "Failed to get OAuth login URL for reauth"); @@ -1911,40 +2008,25 @@ impl TaskManager { status: "failed".to_string(), login_url: None, error: Some("Failed to get OAuth login URL from setup-token".to_string()), + token_saved: false, }; let _ = ws_tx.send(msg).await; } } }); } - DaemonCommand::SubmitAuthCode { request_id, code } => { - tracing::info!(request_id = %request_id, "Received auth code submission from server"); - let ws_tx = self.ws_tx.clone(); - - if send_auth_code(&code) { - tracing::info!(request_id = %request_id, "Auth code forwarded to setup-token for reauth"); - // Wait a short time then report completion - // (the setup-token process takes a moment to complete) - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - let msg = DaemonMessage::ReauthStatus { - request_id, - status: "completed".to_string(), - login_url: None, - error: None, - }; - let _ = ws_tx.send(msg).await; - }); - } else { - tracing::warn!(request_id = %request_id, "No pending auth flow to receive code for reauth"); - let msg = DaemonMessage::ReauthStatus { - request_id, - status: "failed".to_string(), - login_url: None, - error: Some("No pending auth flow to receive the code. Try triggering reauth again.".to_string()), - }; - let _ = self.ws_tx.send(msg).await; - } + DaemonCommand::SubmitAuthCode { request_id, code: _ } => { + // Deprecated: The new setup-token flow outputs tokens directly. + // This handler is kept for backward compatibility but is a no-op. + tracing::info!(request_id = %request_id, "Received auth code submission (deprecated - new flow auto-detects token)"); + let msg = DaemonMessage::ReauthStatus { + request_id, + status: "completed".to_string(), + login_url: None, + error: None, + token_saved: load_oauth_token().is_some(), + }; + let _ = self.ws_tx.send(msg).await; } DaemonCommand::ApplyPatchToWorktree { target_task_id, |
