summaryrefslogtreecommitdiff
path: root/makima/src/daemon/task/manager.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/task/manager.rs')
-rw-r--r--makima/src/daemon/task/manager.rs224
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,