summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-03-02 15:18:31 +0000
committerGitHub <noreply@github.com>2026-03-02 15:18:31 +0000
commit78cb861412850889424ae7d5ae5cd952a2b90295 (patch)
tree7a6eb0693457886dbe0eea84c0c1489724791f79 /makima/src/server
parent2bc1cd4717b587cd2b8ffccd723b62f888e61aa8 (diff)
downloadsoryu-78cb861412850889424ae7d5ae5cd952a2b90295.tar.gz
soryu-78cb861412850889424ae7d5ae5cd952a2b90295.zip
feat: move daemon reauth to daemons page, add contract-backed directive steps, rename Mesh to Exec (#84)
* feat: soryu-co/soryu - makima: Rename Mesh to Exec in navigation * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Add contract-backed steps to directive flow * WIP: heartbeat checkpoint
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/mesh.rs284
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs31
-rw-r--r--makima/src/server/mod.rs3
-rw-r--r--makima/src/server/state.rs60
4 files changed, 377 insertions, 1 deletions
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index c840676..0e72bdf 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -20,7 +20,7 @@ use crate::db::models::{
use crate::db::repository::{self, RepositoryError};
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
-use crate::server::state::{DaemonCommand, SharedState, TaskUpdateNotification};
+use crate::server::state::{DaemonCommand, DaemonReauthStatus, SharedState, TaskUpdateNotification};
// =============================================================================
// Authentication Types
@@ -4283,3 +4283,285 @@ pub async fn restart_daemon(
})
.into_response()
}
+
+// =============================================================================
+// Daemon Reauthorization
+// =============================================================================
+
+/// Response from the trigger reauth endpoint.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct TriggerReauthResponse {
+ /// Whether the reauth command was sent successfully.
+ pub success: bool,
+ /// The daemon ID that received the reauth command.
+ #[serde(rename = "daemonId")]
+ pub daemon_id: Uuid,
+ /// Unique request ID for tracking this reauth flow.
+ #[serde(rename = "requestId")]
+ pub request_id: Uuid,
+}
+
+/// Request body for submitting an auth code.
+#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
+pub struct SubmitAuthCodeRequest {
+ /// The auth code obtained from the OAuth login flow.
+ pub code: String,
+ /// The request ID from the trigger reauth response.
+ #[serde(rename = "requestId")]
+ pub request_id: Uuid,
+}
+
+/// Response from the submit auth code endpoint.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct SubmitAuthCodeResponse {
+ /// Whether the auth code was sent to the daemon successfully.
+ pub success: bool,
+}
+
+/// Response from the reauth status polling endpoint.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct ReauthStatusResponse {
+ /// Current status of the reauth flow: "pending", "url_ready", "completed", "failed"
+ pub status: String,
+ /// OAuth login URL (present when status is "url_ready")
+ #[serde(rename = "loginUrl", skip_serializing_if = "Option::is_none")]
+ pub login_url: Option<String>,
+ /// Error message (present when status is "failed")
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option<String>,
+}
+
+/// Trigger OAuth re-authentication on a daemon.
+///
+/// Sends a reauth command to the specified daemon, which will spawn `claude setup-token`
+/// and return the OAuth login URL via a status update.
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/daemons/{id}/reauth",
+ params(
+ ("id" = Uuid, Path, description = "Daemon ID")
+ ),
+ responses(
+ (status = 200, description = "Reauth command sent", body = TriggerReauthResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Daemon not found or not connected", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn trigger_daemon_reauth(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify the daemon exists and belongs to this owner
+ match repository::get_daemon_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Daemon not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ // Check if daemon is connected
+ if !state.is_daemon_connected(id) {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new(
+ "DAEMON_NOT_CONNECTED",
+ "Daemon is not currently connected",
+ )),
+ )
+ .into_response();
+ }
+
+ // Generate a unique request ID for this reauth flow
+ let request_id = Uuid::new_v4();
+
+ // Initialize the status as "pending"
+ state.set_daemon_reauth_status(id, request_id, "pending".to_string(), None, None);
+
+ // Send reauth command to daemon
+ let command = DaemonCommand::TriggerReauth { request_id };
+ if let Err(e) = state.send_daemon_command(id, command).await {
+ tracing::error!("Failed to send reauth command to daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DAEMON_ERROR", e)),
+ )
+ .into_response();
+ }
+
+ tracing::info!(
+ daemon_id = %id,
+ request_id = %request_id,
+ owner_id = %auth.owner_id,
+ "Reauth command sent to daemon"
+ );
+
+ Json(TriggerReauthResponse {
+ success: true,
+ daemon_id: id,
+ request_id,
+ })
+ .into_response()
+}
+
+/// Submit an OAuth auth code to a daemon's pending reauth flow.
+///
+/// Sends the auth code to the daemon, which will forward it to the `claude setup-token` process.
+#[utoipa::path(
+ post,
+ path = "/api/v1/mesh/daemons/{id}/reauth/code",
+ params(
+ ("id" = Uuid, Path, description = "Daemon ID")
+ ),
+ request_body = SubmitAuthCodeRequest,
+ responses(
+ (status = 200, description = "Auth code submitted", body = SubmitAuthCodeResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Daemon not found or not connected", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn submit_daemon_auth_code(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(body): Json<SubmitAuthCodeRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify the daemon exists and belongs to this owner
+ match repository::get_daemon_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Daemon not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ // Check if daemon is connected
+ if !state.is_daemon_connected(id) {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new(
+ "DAEMON_NOT_CONNECTED",
+ "Daemon is not currently connected",
+ )),
+ )
+ .into_response();
+ }
+
+ // Send auth code command to daemon
+ let command = DaemonCommand::SubmitAuthCode {
+ request_id: body.request_id,
+ code: body.code,
+ };
+ if let Err(e) = state.send_daemon_command(id, command).await {
+ tracing::error!("Failed to send auth code to daemon {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DAEMON_ERROR", e)),
+ )
+ .into_response();
+ }
+
+ tracing::info!(
+ daemon_id = %id,
+ request_id = %body.request_id,
+ owner_id = %auth.owner_id,
+ "Auth code submitted to daemon"
+ );
+
+ Json(SubmitAuthCodeResponse { success: true }).into_response()
+}
+
+/// Get the status of a daemon reauth request.
+///
+/// Used by the frontend to poll for reauth status updates.
+#[utoipa::path(
+ get,
+ path = "/api/v1/mesh/daemons/{id}/reauth/{request_id}/status",
+ params(
+ ("id" = Uuid, Path, description = "Daemon ID"),
+ ("request_id" = Uuid, Path, description = "Reauth request ID")
+ ),
+ responses(
+ (status = 200, description = "Reauth status", body = ReauthStatusResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Reauth request not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn get_daemon_reauth_status(
+ State(state): State<SharedState>,
+ Authenticated(_auth): Authenticated,
+ Path((id, request_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ match state.get_daemon_reauth_status(id, request_id) {
+ Some(status) => Json(ReauthStatusResponse {
+ status: status.status,
+ login_url: status.login_url,
+ error: status.error,
+ })
+ .into_response(),
+ None => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Reauth request not found")),
+ )
+ .into_response(),
+ }
+}
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index 743a1ca..30439a4 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -350,6 +350,18 @@ pub enum DaemonMessage {
/// Hostname of the daemon requiring auth
hostname: Option<String>,
},
+ /// Reauth status update (response to TriggerReauth/SubmitAuthCode commands)
+ ReauthStatus {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ /// Status: "url_ready", "completed", "failed"
+ status: String,
+ /// OAuth login URL (present when status is "url_ready")
+ #[serde(rename = "loginUrl")]
+ login_url: Option<String>,
+ /// Error message (present when status is "failed")
+ error: Option<String>,
+ },
/// Response to RetryCompletionAction command
CompletionActionResult {
#[serde(rename = "taskId")]
@@ -1622,6 +1634,25 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
"OAuth login URL available - user should open this in browser"
);
}
+ Ok(DaemonMessage::ReauthStatus { request_id, status, login_url, error }) => {
+ tracing::info!(
+ daemon_id = %daemon_uuid,
+ request_id = %request_id,
+ status = %status,
+ login_url = ?login_url,
+ error = ?error,
+ "Daemon reauth status update"
+ );
+
+ // Store the reauth status for polling by the frontend
+ state.set_daemon_reauth_status(
+ daemon_uuid,
+ request_id,
+ status.clone(),
+ login_url.clone(),
+ error.clone(),
+ );
+ }
Ok(DaemonMessage::DaemonDirectories { working_directory, home_directory, worktrees_directory }) => {
tracing::info!(
daemon_id = %daemon_uuid,
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index e0f8e7d..b84b90e 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -98,6 +98,9 @@ pub fn make_router(state: SharedState) -> Router {
.route("/mesh/daemons/directories", get(mesh::get_daemon_directories))
.route("/mesh/daemons/{id}", get(mesh::get_daemon))
.route("/mesh/daemons/{id}/restart", post(mesh::restart_daemon))
+ .route("/mesh/daemons/{id}/reauth", post(mesh::trigger_daemon_reauth))
+ .route("/mesh/daemons/{id}/reauth/code", post(mesh::submit_daemon_auth_code))
+ .route("/mesh/daemons/{id}/reauth/{request_id}/status", get(mesh::get_daemon_reauth_status))
// Merge endpoints for orchestrators
.route("/mesh/tasks/{id}/branches", get(mesh_merge::list_branches))
.route("/mesh/tasks/{id}/merge/start", post(mesh_merge::merge_start))
diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs
index 15fec6b..5c5e24f 100644
--- a/makima/src/server/state.rs
+++ b/makima/src/server/state.rs
@@ -521,6 +521,19 @@ pub enum DaemonCommand {
/// Restart the daemon process
RestartDaemon,
+ /// Trigger OAuth re-authentication on this daemon
+ TriggerReauth {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ },
+
+ /// Submit auth code for pending reauth
+ SubmitAuthCode {
+ #[serde(rename = "requestId")]
+ request_id: Uuid,
+ code: String,
+ },
+
/// Apply a patch to a task's worktree (for cross-daemon merge).
/// Sent by server when routing MergePatchToSupervisor to the supervisor's daemon.
ApplyPatchToWorktree {
@@ -562,6 +575,15 @@ pub struct DaemonConnectionInfo {
pub worktrees_directory: Option<String>,
}
+/// Status of a daemon reauth request (stored in state for polling).
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct DaemonReauthStatus {
+ pub request_id: Uuid,
+ pub status: String,
+ pub login_url: Option<String>,
+ pub error: Option<String>,
+}
+
/// Configuration paths for ML models (used for lazy loading).
#[derive(Clone)]
pub struct ModelConfig {
@@ -616,6 +638,8 @@ pub struct AppState {
pub pending_worktree_info: DashMap<Uuid, oneshot::Sender<WorktreeInfoResponse>>,
/// Lazily-loaded TTS engine (initialized on first Speak connection)
pub tts_engine: OnceCell<Box<dyn TtsEngine>>,
+ /// Daemon reauth status storage (keyed by (daemon_id, request_id))
+ pub daemon_reauth_status: DashMap<(Uuid, Uuid), DaemonReauthStatus>,
}
impl AppState {
@@ -694,6 +718,7 @@ impl AppState {
jwt_verifier,
pending_worktree_info: DashMap::new(),
tts_engine: OnceCell::new(),
+ daemon_reauth_status: DashMap::new(),
}
}
@@ -1201,6 +1226,41 @@ impl AppState {
}
// =========================================================================
+ // Daemon Reauth Status
+ // =========================================================================
+
+ /// Store a daemon reauth status update (from daemon's ReauthStatus message).
+ pub fn set_daemon_reauth_status(
+ &self,
+ daemon_id: Uuid,
+ request_id: Uuid,
+ status: String,
+ login_url: Option<String>,
+ error: Option<String>,
+ ) {
+ self.daemon_reauth_status.insert(
+ (daemon_id, request_id),
+ DaemonReauthStatus {
+ request_id,
+ status,
+ login_url,
+ error,
+ },
+ );
+ }
+
+ /// Get a daemon reauth status (for frontend polling).
+ pub fn get_daemon_reauth_status(
+ &self,
+ daemon_id: Uuid,
+ request_id: Uuid,
+ ) -> Option<DaemonReauthStatus> {
+ self.daemon_reauth_status
+ .get(&(daemon_id, request_id))
+ .map(|entry| entry.value().clone())
+ }
+
+ // =========================================================================
// Supervisor Notifications
// =========================================================================