diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands.rs b/desktop/src-tauri/src/managed_agents/runtime_commands.rs index 3349eb011..32831a6d7 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_commands.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_commands.rs @@ -79,80 +79,17 @@ fn emit_status(app: &tauri::AppHandle, status: &ManagedAge let _ = app.emit(STATUS_EVENT, status); } -fn observer_lifecycle_key( - outer_pubkey: &str, - payload: &super::ManagedAgentRuntimeLifecycleObserverPayload, -) -> Result { - if !outer_pubkey.eq_ignore_ascii_case(&payload.pubkey) { - return Err("observer signer does not match lifecycle payload pubkey".into()); - } - if matches!( - payload.lifecycle, - ManagedAgentRuntimeLifecycle::Starting | ManagedAgentRuntimeLifecycle::Stopped - ) { - return Err("observer cannot author starting or stopped lifecycle".into()); - } - if payload.lifecycle == ManagedAgentRuntimeLifecycle::Failed && payload.error.is_none() { - return Err("failed lifecycle requires an error".into()); - } - if payload.lifecycle != ManagedAgentRuntimeLifecycle::Failed && payload.error.is_some() { - return Err("lifecycle error is only valid for failed".into()); - } - ManagedAgentRuntimeKey::new(payload.pubkey.clone(), &payload.relay_url) -} - -#[tauri::command] -pub fn put_managed_agent_runtime_lifecycle( - outer_pubkey: String, - payload: super::ManagedAgentRuntimeLifecycleObserverPayload, - app: AppHandle, -) -> Result { - put_managed_agent_runtime_lifecycle_for(outer_pubkey, payload, &app) -} - -/// Runtime-generic core so the sibling capability check is testable under `MockRuntime` (§3.3a / §7). -pub(crate) fn put_managed_agent_runtime_lifecycle_for( - outer_pubkey: String, - payload: super::ManagedAgentRuntimeLifecycleObserverPayload, - app: &tauri::AppHandle, -) -> Result { - let key = observer_lifecycle_key(&outer_pubkey, &payload)?; - let state = app.state::(); - let records = load_managed_agents(app)?; - let record = records - .iter() - .find(|record| record.pubkey.eq_ignore_ascii_case(&key.pubkey)) - .ok_or_else(|| format!("agent {} not found", key.pubkey))?; - // Capture the active scope BEFORE taking the runtime lock so a concurrent - // workspace switch cannot slip a stale-scope frame past the check. - let current_scope_id = state.capture_active_scope().map(|scope| scope.scope_id); - let mut runtimes = state - .managed_agent_processes - .lock() - .map_err(|e| e.to_string())?; - let runtime = runtimes - .get_mut(&key) - .ok_or_else(|| "lifecycle frame does not match a tracked runtime pair".to_string())?; - if runtime.start_nonce != payload.start_nonce { - return Err("lifecycle frame does not match the current harness generation".into()); - } - if runtime.scope_id != current_scope_id { - return Err("lifecycle frame does not match the current workspace scope".into()); - } - let exited = runtime - .child - .try_wait() - .map_err(|e| e.to_string())? - .is_some(); - if exited { - return Err("lifecycle frame arrived after process exit".into()); - } - runtime.lifecycle = payload.lifecycle; - runtime.error = payload.error; - let status = status_for(app, record, &key, Some(runtime), None); - emit_status(app, &status); - Ok(status) -} +// Observer-authored runtime-lifecycle capability command (§3.3a): extracted to +// keep this file under the 1000-line size ratchet. +#[path = "runtime_commands_observer.rs"] +mod observer; +pub use observer::put_managed_agent_runtime_lifecycle; +// Re-exported for the capability and unit test suites only; production code +// reaches the core through the `#[tauri::command]` wrapper inside `observer`. +#[cfg(test)] +use observer::observer_lifecycle_key; +#[cfg(test)] +pub(crate) use observer::put_managed_agent_runtime_lifecycle_for; #[tauri::command] pub fn list_managed_agent_runtimes( diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands_observer.rs b/desktop/src-tauri/src/managed_agents/runtime_commands_observer.rs new file mode 100644 index 000000000..3a33b679c --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/runtime_commands_observer.rs @@ -0,0 +1,82 @@ +//! Observer-authored runtime-lifecycle capability command (§3.3a). +//! +//! Extracted here to keep `runtime_commands.rs` under the 1000-line size +//! ratchet; `#[path]`-included from there. Behavior is identical to the inline +//! definition — this is a mechanical move. + +use super::*; + +pub(super) fn observer_lifecycle_key( + outer_pubkey: &str, + payload: &crate::managed_agents::ManagedAgentRuntimeLifecycleObserverPayload, +) -> Result { + if !outer_pubkey.eq_ignore_ascii_case(&payload.pubkey) { + return Err("observer signer does not match lifecycle payload pubkey".into()); + } + if matches!( + payload.lifecycle, + ManagedAgentRuntimeLifecycle::Starting | ManagedAgentRuntimeLifecycle::Stopped + ) { + return Err("observer cannot author starting or stopped lifecycle".into()); + } + if payload.lifecycle == ManagedAgentRuntimeLifecycle::Failed && payload.error.is_none() { + return Err("failed lifecycle requires an error".into()); + } + if payload.lifecycle != ManagedAgentRuntimeLifecycle::Failed && payload.error.is_some() { + return Err("lifecycle error is only valid for failed".into()); + } + ManagedAgentRuntimeKey::new(payload.pubkey.clone(), &payload.relay_url) +} + +#[tauri::command] +pub fn put_managed_agent_runtime_lifecycle( + outer_pubkey: String, + payload: crate::managed_agents::ManagedAgentRuntimeLifecycleObserverPayload, + app: AppHandle, +) -> Result { + put_managed_agent_runtime_lifecycle_for(outer_pubkey, payload, &app) +} + +/// Runtime-generic core so the sibling capability check is testable under `MockRuntime` (§3.3a / §7). +pub(crate) fn put_managed_agent_runtime_lifecycle_for( + outer_pubkey: String, + payload: crate::managed_agents::ManagedAgentRuntimeLifecycleObserverPayload, + app: &tauri::AppHandle, +) -> Result { + let key = observer_lifecycle_key(&outer_pubkey, &payload)?; + let state = app.state::(); + let records = load_managed_agents(app)?; + let record = records + .iter() + .find(|record| record.pubkey.eq_ignore_ascii_case(&key.pubkey)) + .ok_or_else(|| format!("agent {} not found", key.pubkey))?; + // Capture the active scope BEFORE taking the runtime lock so a concurrent + // workspace switch cannot slip a stale-scope frame past the check. + let current_scope_id = state.capture_active_scope().map(|scope| scope.scope_id); + let mut runtimes = state + .managed_agent_processes + .lock() + .map_err(|e| e.to_string())?; + let runtime = runtimes + .get_mut(&key) + .ok_or_else(|| "lifecycle frame does not match a tracked runtime pair".to_string())?; + if runtime.start_nonce != payload.start_nonce { + return Err("lifecycle frame does not match the current harness generation".into()); + } + if runtime.scope_id != current_scope_id { + return Err("lifecycle frame does not match the current workspace scope".into()); + } + let exited = runtime + .child + .try_wait() + .map_err(|e| e.to_string())? + .is_some(); + if exited { + return Err("lifecycle frame arrived after process exit".into()); + } + runtime.lifecycle = payload.lifecycle; + runtime.error = payload.error; + let status = status_for(app, record, &key, Some(runtime), None); + emit_status(app, &status); + Ok(status) +}