diff --git a/desktop/src-tauri/src/commands/agent_config_tests.rs b/desktop/src-tauri/src/commands/agent_config_tests.rs index 9c9aa58c1fd..5928a6415af 100644 --- a/desktop/src-tauri/src/commands/agent_config_tests.rs +++ b/desktop/src-tauri/src/commands/agent_config_tests.rs @@ -85,6 +85,7 @@ fn agent_record() -> ManagedAgentRecord { model: None, env_vars: Default::default(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: BackendKind::Local, diff --git a/desktop/src-tauri/src/commands/agent_settings.rs b/desktop/src-tauri/src/commands/agent_settings.rs index 6135c671606..7b418fcdcf4 100644 --- a/desktop/src-tauri/src/commands/agent_settings.rs +++ b/desktop/src-tauri/src/commands/agent_settings.rs @@ -61,6 +61,50 @@ pub async fn set_managed_agent_start_on_app_launch( .map_err(|e| format!("spawn_blocking failed: {e}"))? } +#[tauri::command] +pub async fn set_managed_agent_disable_local_spawn( + pubkey: String, + disable_local_spawn: bool, + app: AppHandle, +) -> Result { + tokio::task::spawn_blocking(move || { + let state = app.state::(); + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|error| error.to_string())?; + let mut records = load_managed_agents(&app)?; + let mut runtimes = state + .managed_agent_processes + .lock() + .map_err(|error| error.to_string())?; + + let (sync_changed, exited_pubkeys) = + sync_managed_agent_processes(&mut records, &mut runtimes, ¤t_instance_id(&app)); + if sync_changed { + save_managed_agents(&app, &records)?; + } + for pubkey in &exited_pubkeys { + state.clear_agent_session_caches(pubkey); + } + + { + let record = find_managed_agent_mut(&mut records, &pubkey)?; + record.disable_local_spawn = disable_local_spawn; + record.updated_at = now_iso(); + } + + save_managed_agents(&app, &records)?; + let record = records + .iter() + .find(|record| record.pubkey == pubkey) + .ok_or_else(|| format!("agent {pubkey} not found"))?; + super::agents::summarize_from_disk(&app, record, &runtimes) + }) + .await + .map_err(|e| format!("spawn_blocking failed: {e}"))? +} + #[tauri::command] pub async fn set_managed_agent_auto_restart( pubkey: String, diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 33b6ae44620..2d95c4a9d83 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -685,6 +685,7 @@ pub async fn create_managed_agent( } else { input.start_on_app_launch }, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: input.backend.clone(), diff --git a/desktop/src-tauri/src/commands/agents_tests.rs b/desktop/src-tauri/src/commands/agents_tests.rs index 1c222ae23a4..dddcda663ee 100644 --- a/desktop/src-tauri/src/commands/agents_tests.rs +++ b/desktop/src-tauri/src/commands/agents_tests.rs @@ -31,6 +31,7 @@ fn bare_agent_record( persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, runtime_pid: None, backend: BackendKind::Local, backend_agent_id: None, diff --git a/desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs b/desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs index a4bbdeb677c..bab9bf68100 100644 --- a/desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs +++ b/desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs @@ -39,6 +39,7 @@ fn make_agent( persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, runtime_pid, backend: BackendKind::Local, backend_agent_id: None, diff --git a/desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs b/desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs index fbfede35886..5bb997976d1 100644 --- a/desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs +++ b/desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs @@ -181,6 +181,7 @@ fn local_agent() -> ManagedAgentRecord { persona_source_version: Some("local-hash".to_string()), env_vars: BTreeMap::from([("API_KEY".to_string(), "localsecret".to_string())]), start_on_app_launch: true, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: Some(1234), backend: crate::managed_agents::BackendKind::Provider { diff --git a/desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs b/desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs index 341426fe940..ce2899f2053 100644 --- a/desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs +++ b/desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs @@ -35,6 +35,7 @@ fn make_definition(slug: &str) -> ManagedAgentRecord { persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: false, runtime_pid: None, backend: BackendKind::Local, diff --git a/desktop/src-tauri/src/commands/personas/snapshot/import.rs b/desktop/src-tauri/src/commands/personas/snapshot/import.rs index 75a1edea65e..d96db8923da 100644 --- a/desktop/src-tauri/src/commands/personas/snapshot/import.rs +++ b/desktop/src-tauri/src/commands/personas/snapshot/import.rs @@ -622,6 +622,7 @@ pub async fn confirm_agent_snapshot_import( persona_source_version: None, env_vars: std::collections::BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: crate::managed_agents::BackendKind::Local, diff --git a/desktop/src-tauri/src/commands/personas/snapshot/tests.rs b/desktop/src-tauri/src/commands/personas/snapshot/tests.rs index fedb0e60585..983b55bc2c8 100644 --- a/desktop/src-tauri/src/commands/personas/snapshot/tests.rs +++ b/desktop/src-tauri/src/commands/personas/snapshot/tests.rs @@ -44,6 +44,7 @@ fn make_definition(slug: &str) -> ManagedAgentRecord { persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: false, runtime_pid: None, backend: BackendKind::Local, diff --git a/desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs b/desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs index 556127373bf..7ffcf3cac7c 100644 --- a/desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs +++ b/desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs @@ -27,6 +27,7 @@ fn agent(persona_id: &str, name: &str, display_name: Option<&str>) -> ManagedAge persona_source_version: None, env_vars: std::collections::BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: Default::default(), diff --git a/desktop/src-tauri/src/commands/team_snapshot.rs b/desktop/src-tauri/src/commands/team_snapshot.rs index e4c08a14be0..a0e039002dd 100644 --- a/desktop/src-tauri/src/commands/team_snapshot.rs +++ b/desktop/src-tauri/src/commands/team_snapshot.rs @@ -575,6 +575,7 @@ pub async fn confirm_team_snapshot_import( persona_source_version: None, env_vars: std::collections::BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: crate::managed_agents::BackendKind::Local, diff --git a/desktop/src-tauri/src/commands/team_snapshot/tests.rs b/desktop/src-tauri/src/commands/team_snapshot/tests.rs index bec7f43bf8a..0896a3df866 100644 --- a/desktop/src-tauri/src/commands/team_snapshot/tests.rs +++ b/desktop/src-tauri/src/commands/team_snapshot/tests.rs @@ -202,6 +202,7 @@ fn team_export_with_instance_and_memory_level_uses_supplied_entries() { persona_source_version: None, env_vars: Default::default(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: crate::managed_agents::BackendKind::Local, diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c120ac12679..c44f841bec9 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -701,6 +701,7 @@ pub fn run() { stop_managed_agent, set_agent_managed_profiles, set_managed_agent_start_on_app_launch, + set_managed_agent_disable_local_spawn, set_managed_agent_auto_restart, delete_managed_agent, get_managed_agent_log, diff --git a/desktop/src-tauri/src/managed_agents/agent_events.rs b/desktop/src-tauri/src/managed_agents/agent_events.rs index f0a4fabfed8..ec87027445e 100644 --- a/desktop/src-tauri/src/managed_agents/agent_events.rs +++ b/desktop/src-tauri/src/managed_agents/agent_events.rs @@ -186,6 +186,7 @@ mod tests { persona_source_version: Some("abc123".to_string()), env_vars: BTreeMap::from([("OPENAI_API_KEY".to_string(), "sk-secret".to_string())]), start_on_app_launch: true, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: Some(4242), backend: super::super::BackendKind::Provider { diff --git a/desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs b/desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs index de2f71577a6..04d89b12f7f 100644 --- a/desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs +++ b/desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs @@ -385,6 +385,7 @@ mod tests { model: None, env_vars: std::collections::BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: crate::managed_agents::types::BackendKind::Local, diff --git a/desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs b/desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs index 9f234749bc9..008c2992564 100644 --- a/desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs +++ b/desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs @@ -39,6 +39,7 @@ fn minimal_record() -> ManagedAgentRecord { m }, start_on_app_launch: true, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: Some(12345), // MUST NOT appear backend: BackendKind::Provider { diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs index 36b6022b53b..7b9605153d5 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs @@ -84,6 +84,7 @@ fn test_record() -> ManagedAgentRecord { model: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: crate::managed_agents::types::BackendKind::Local, diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 2d1db692932..812a818715e 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -232,6 +232,7 @@ fn record_with( provider: None, persona_source_version: None, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: Default::default(), diff --git a/desktop/src-tauri/src/managed_agents/effective_config/tests.rs b/desktop/src-tauri/src/managed_agents/effective_config/tests.rs index 5b048b815cb..a35b6991e8a 100644 --- a/desktop/src-tauri/src/managed_agents/effective_config/tests.rs +++ b/desktop/src-tauri/src/managed_agents/effective_config/tests.rs @@ -61,6 +61,7 @@ fn record( persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, runtime_pid: None, backend: BackendKind::Local, backend_agent_id: None, diff --git a/desktop/src-tauri/src/managed_agents/global_config/tests.rs b/desktop/src-tauri/src/managed_agents/global_config/tests.rs index 65cde47f26b..2896c8dc1d5 100644 --- a/desktop/src-tauri/src/managed_agents/global_config/tests.rs +++ b/desktop/src-tauri/src/managed_agents/global_config/tests.rs @@ -321,6 +321,7 @@ fn bare_record() -> ManagedAgentRecord { persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, runtime_pid: None, backend: BackendKind::Local, backend_agent_id: None, diff --git a/desktop/src-tauri/src/managed_agents/nest/render_tests.rs b/desktop/src-tauri/src/managed_agents/nest/render_tests.rs index ed4ee2c1f9b..b0014d10212 100644 --- a/desktop/src-tauri/src/managed_agents/nest/render_tests.rs +++ b/desktop/src-tauri/src/managed_agents/nest/render_tests.rs @@ -57,6 +57,7 @@ fn make_agent(name: &str, persona_id: Option<&str>) -> ManagedAgentRecord { provider: None, persona_source_version: None, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: BackendKind::default(), diff --git a/desktop/src-tauri/src/managed_agents/parallelism.rs b/desktop/src-tauri/src/managed_agents/parallelism.rs index 734772d73d9..0be897b32be 100644 --- a/desktop/src-tauri/src/managed_agents/parallelism.rs +++ b/desktop/src-tauri/src/managed_agents/parallelism.rs @@ -85,6 +85,7 @@ mod tests { provider: None, persona_source_version: None, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: Default::default(), diff --git a/desktop/src-tauri/src/managed_agents/persona_events/tests.rs b/desktop/src-tauri/src/managed_agents/persona_events/tests.rs index af8cfe66182..5d37be4479e 100644 --- a/desktop/src-tauri/src/managed_agents/persona_events/tests.rs +++ b/desktop/src-tauri/src/managed_agents/persona_events/tests.rs @@ -27,6 +27,7 @@ pub(super) fn sample_record() -> ManagedAgentRecord { persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: BackendKind::Local, diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index f7f5d5c5d0e..bb7550ad134 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -1498,6 +1498,7 @@ mod tests { persona_source_version: None, env_vars, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: Default::default(), diff --git a/desktop/src-tauri/src/managed_agents/restore.rs b/desktop/src-tauri/src/managed_agents/restore.rs index a225f492d33..8fc277dee5e 100644 --- a/desktop/src-tauri/src/managed_agents/restore.rs +++ b/desktop/src-tauri/src/managed_agents/restore.rs @@ -167,7 +167,11 @@ pub async fn restore_managed_agents_on_launch( let candidates: Vec = records .iter() - .filter(|record| record.start_on_app_launch && record.backend == BackendKind::Local) + .filter(|record| { + record.start_on_app_launch + && record.backend == BackendKind::Local + && !record.disable_local_spawn + }) .map(|record| record.pubkey.clone()) .collect(); diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 0ce5ca7b219..ee4e152eafe 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -336,6 +336,7 @@ pub fn build_managed_agent_summary( last_error: record.last_error.clone(), last_error_code: record.last_error_code, start_on_app_launch: record.start_on_app_launch, + disable_local_spawn: record.disable_local_spawn, auto_restart_on_config_change: record.auto_restart_on_config_change, log_path, respond_to: record.respond_to, diff --git a/desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs b/desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs index 9076766b2e6..6155700c0bf 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs @@ -58,6 +58,7 @@ pub(super) fn fixture( persona_source_version: None, env_vars: std::collections::BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: Default::default(), diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands.rs b/desktop/src-tauri/src/managed_agents/runtime_commands.rs index c0e55184b19..7d761fff501 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_commands.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_commands.rs @@ -260,6 +260,9 @@ fn start_pair( if record.backend != BackendKind::Local { return Err("managed runtime pairs require a local agent".into()); } + if record.disable_local_spawn { + return Err("local spawn disabled — runtime is externally managed".into()); + } if expected_updated_at.is_some_and(|expected| record.updated_at != expected) { return Err("managed agent changed while runtime reconciliation was in flight".into()); } @@ -468,7 +471,11 @@ pub async fn reconcile_managed_agent_runtimes( for community in communities { for record in records .iter() - .filter(|record| record.start_on_app_launch && record.backend == BackendKind::Local) + .filter(|record| { + record.start_on_app_launch + && record.backend == BackendKind::Local + && !record.disable_local_spawn + }) // The legacy per-record relay pin is deliberately ignored here — see // `effective_agent_relay_url`. Every local auto-start agent fans out // to every configured community. diff --git a/desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs b/desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs index b007e0b2ffa..985f215ed06 100644 --- a/desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs +++ b/desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs @@ -64,6 +64,7 @@ fn record() -> ManagedAgentRecord { persona_source_version: None, env_vars: BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: Default::default(), diff --git a/desktop/src-tauri/src/managed_agents/team_snapshot.rs b/desktop/src-tauri/src/managed_agents/team_snapshot.rs index 2b6918b16e4..44db379526b 100644 --- a/desktop/src-tauri/src/managed_agents/team_snapshot.rs +++ b/desktop/src-tauri/src/managed_agents/team_snapshot.rs @@ -279,6 +279,7 @@ mod tests { m }, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: BackendKind::Local, diff --git a/desktop/src-tauri/src/managed_agents/teams_tests.rs b/desktop/src-tauri/src/managed_agents/teams_tests.rs index ff7900d3923..8eab8c40386 100644 --- a/desktop/src-tauri/src/managed_agents/teams_tests.rs +++ b/desktop/src-tauri/src/managed_agents/teams_tests.rs @@ -186,6 +186,7 @@ fn managed_agent(name: &str) -> ManagedAgentRecord { persona_source_version: None, env_vars: std::collections::BTreeMap::new(), start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: false, runtime_pid: None, backend: crate::managed_agents::BackendKind::Local, diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 9049482de3a..72f2f748715 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -121,6 +121,7 @@ impl AgentDefinition { persona_source_version: None, env_vars: self.env_vars, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: true, runtime_pid: None, backend: BackendKind::default(), @@ -309,6 +310,13 @@ pub struct ManagedAgentRecord { pub env_vars: BTreeMap, #[serde(default = "default_start_on_app_launch")] pub start_on_app_launch: bool, + /// When `true`, Desktop never spawns a local process for this agent — neither + /// at boot-time reconciliation nor on reactive triggers (channel open, @mention). + /// The persona and key remain in the store so the agent can be re-activated at + /// any time by flipping this back to `false`. Intended for agents whose canonical + /// runtime is an external host (e.g. a VPS systemd unit). + #[serde(default)] + pub disable_local_spawn: bool, /// Auto-restart this agent when its effective spawn config drifts from /// the running process (Chunk F). Default ON; the policy loop in the /// frontend only fires when the agent is idle, connected, and local. @@ -550,6 +558,7 @@ pub struct ManagedAgentSummary { pub last_error: Option, pub last_error_code: Option, pub start_on_app_launch: bool, + pub disable_local_spawn: bool, pub auto_restart_on_config_change: bool, pub log_path: String, pub respond_to: RespondTo, diff --git a/desktop/src-tauri/src/managed_agents/types/tests.rs b/desktop/src-tauri/src/managed_agents/types/tests.rs index 0ae584e4acd..1742d76d78d 100644 --- a/desktop/src-tauri/src/managed_agents/types/tests.rs +++ b/desktop/src-tauri/src/managed_agents/types/tests.rs @@ -755,6 +755,7 @@ fn summary_fixture( last_error: None, last_error_code: None, start_on_app_launch: false, + disable_local_spawn: false, auto_restart_on_config_change: false, log_path: String::new(), respond_to: RespondTo::OwnerOnly, diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index 5d0be06109e..32316bfe57e 100644 --- a/desktop/src/features/agents/hooks.ts +++ b/desktop/src/features/agents/hooks.ts @@ -45,6 +45,7 @@ import type { HarnessDefinitionInput } from "@/shared/api/tauri"; import { discoverAcpRuntimes } from "@/shared/api/tauriAcpDiscovery"; import { setManagedAgentAutoRestart, + setManagedAgentDisableLocalSpawn, setManagedAgentStartOnAppLaunch, startManagedAgent, stopManagedAgent, @@ -648,6 +649,24 @@ export function useSetManagedAgentStartOnAppLaunchMutation() { }); } +export function useSetManagedAgentDisableLocalSpawnMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + pubkey, + disableLocalSpawn, + }: { + pubkey: string; + disableLocalSpawn: boolean; + }) => setManagedAgentDisableLocalSpawn(pubkey, disableLocalSpawn), + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey }); + await queryClient.invalidateQueries({ queryKey: relayAgentsQueryKey }); + }, + }); +} + export function useDeleteManagedAgentMutation() { const queryClient = useQueryClient(); diff --git a/desktop/src/features/agents/ui/ManagedAgentRow.tsx b/desktop/src/features/agents/ui/ManagedAgentRow.tsx index 3d205ba3cce..5a430b45014 100644 --- a/desktop/src/features/agents/ui/ManagedAgentRow.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentRow.tsx @@ -261,7 +261,11 @@ function AgentSummary({ {agent.backend.type === "local" ? ( - {agent.startOnAppLaunch ? "Auto-start" : "Manual start"} + {agent.disableLocalSpawn + ? "Remote runtime" + : agent.startOnAppLaunch + ? "Auto-start" + : "Manual start"} ) : ( Remote deployment diff --git a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx index 5919a16e29b..048d6b97ae7 100644 --- a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx +++ b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx @@ -5,6 +5,7 @@ import { CopyPlus, Download, Power, + Server, Settings, } from "lucide-react"; @@ -29,6 +30,7 @@ export function UserProfileAgentSettingsMenu({ onDuplicatePersona, onExportPersona, onToggleAutoStart, + onToggleDisableLocalSpawn, personaActionKey, }: { archiveActions?: IdentityArchiveActions; @@ -38,6 +40,7 @@ export function UserProfileAgentSettingsMenu({ onDuplicatePersona?: () => void; onExportPersona?: () => void; onToggleAutoStart?: () => void; + onToggleDisableLocalSpawn?: () => void; personaActionKey?: string; }) { const [archiveConfirmOpen, setArchiveConfirmOpen] = React.useState(false); @@ -47,13 +50,21 @@ export function UserProfileAgentSettingsMenu({ managedAgent !== undefined && managedAgent.backend.type === "local" && onToggleAutoStart !== undefined; + const canToggleDisableLocalSpawn = + managedAgent !== undefined && + managedAgent.backend.type === "local" && + onToggleDisableLocalSpawn !== undefined; const autoStartSwitchId = `user-profile-agent-auto-start-${actionKey}`; + const disableLocalSpawnSwitchId = `user-profile-agent-disable-local-spawn-${actionKey}`; const hasPrimaryActions = Boolean(onDuplicatePersona || onExportPersona); const hasArchiveAction = archiveActions?.canArchive === true && archiveActions.isArchived !== undefined; const hasActions = - canToggleAutoStart || hasPrimaryActions || hasArchiveAction; + canToggleAutoStart || + canToggleDisableLocalSpawn || + hasPrimaryActions || + hasArchiveAction; if (!hasActions) { return null; @@ -105,6 +116,30 @@ export function UserProfileAgentSettingsMenu({ /> ) : null} + {canToggleDisableLocalSpawn ? ( + { + event.preventDefault(); + onToggleDisableLocalSpawn(); + }} + > + + + Remote runtime only + + event.stopPropagation()} + /> + + ) : null} {onDuplicatePersona ? ( void; onExportPersona: () => void; onToggleAutoStart: () => void; + onToggleDisableLocalSpawn?: () => void; personaActionKey?: string; viewerIsOwner: boolean; }) { @@ -212,6 +249,7 @@ export function UserProfileAgentSettingsMenuSlot({ {...sharedProps} managedAgent={managedAgent} onToggleAutoStart={onToggleAutoStart} + onToggleDisableLocalSpawn={onToggleDisableLocalSpawn} /> ); } diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index f02b5aee097..5634ca74246 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -15,6 +15,7 @@ import { useRelayAgentsQuery, useManagedAgentsQuery, usePersonasQuery, + useSetManagedAgentDisableLocalSpawnMutation, useSetManagedAgentStartOnAppLaunchMutation, useSetPersonaActiveMutation, useStartManagedAgentMutation, @@ -244,6 +245,7 @@ export function UserProfilePanel({ const stopAgentMutation = useStopManagedAgentMutation(); const deleteAgentMutation = useDeleteManagedAgentMutation(); const startOnLaunchMutation = useSetManagedAgentStartOnAppLaunchMutation(); + const disableLocalSpawnMutation = useSetManagedAgentDisableLocalSpawnMutation(); const createPersonaMutation = useCreatePersonaMutation(); const updatePersonaMutation = useUpdatePersonaMutation(); const deletePersonaMutation = useDeletePersonaMutation(); @@ -502,6 +504,28 @@ export function UserProfilePanel({ } }, [managedAgent, startOnLaunchMutation.mutateAsync]); + const handleToggleDisableLocalSpawn = React.useCallback(async () => { + if (managedAgent?.backend.type !== "local") return; + + try { + const updated = await disableLocalSpawnMutation.mutateAsync({ + pubkey: managedAgent.pubkey, + disableLocalSpawn: !managedAgent.disableLocalSpawn, + }); + toast.success( + updated.disableLocalSpawn + ? `${updated.name} will not spawn locally — remote runtime owns it.` + : `${updated.name} will spawn locally on Desktop again.`, + ); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : "Failed to update spawn preference.", + ); + } + }, [managedAgent, disableLocalSpawnMutation.mutateAsync]); + const handleDeleteAgent = React.useCallback(async () => { if (!managedAgent) return; @@ -729,6 +753,7 @@ export function UserProfilePanel({ onDuplicatePersona={handleDuplicatePersona} onExportPersona={handleExportPersona} onToggleAutoStart={handleToggleAgentAutoStart} + onToggleDisableLocalSpawn={handleToggleDisableLocalSpawn} personaActionKey={resolvedPersona?.id} viewerIsOwner={viewerIsOwner} /> diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts index b4f0df6fc09..dd13ebca955 100644 --- a/desktop/src/shared/api/tauri.ts +++ b/desktop/src/shared/api/tauri.ts @@ -148,6 +148,7 @@ export type RawManagedAgent = { last_error_code: number | null; log_path: string; start_on_app_launch: boolean; + disable_local_spawn?: boolean; auto_restart_on_config_change?: boolean; backend: ManagedAgentBackend; backend_agent_id: string | null; @@ -668,6 +669,7 @@ export function fromRawManagedAgent(agent: RawManagedAgent): ManagedAgent { lastErrorCode: agent.last_error_code ?? null, logPath: agent.log_path, startOnAppLaunch: agent.start_on_app_launch, + disableLocalSpawn: agent.disable_local_spawn ?? false, autoRestartOnConfigChange: agent.auto_restart_on_config_change ?? true, backend: agent.backend, backendAgentId: agent.backend_agent_id, diff --git a/desktop/src/shared/api/tauriManagedAgents.ts b/desktop/src/shared/api/tauriManagedAgents.ts index 9f77566da99..d62f872a902 100644 --- a/desktop/src/shared/api/tauriManagedAgents.ts +++ b/desktop/src/shared/api/tauriManagedAgents.ts @@ -49,6 +49,20 @@ export async function setManagedAgentStartOnAppLaunch( return fromRawManagedAgent(response); } +export async function setManagedAgentDisableLocalSpawn( + pubkey: string, + disableLocalSpawn: boolean, +): Promise { + const response = await invokeTauri( + "set_managed_agent_disable_local_spawn", + { + pubkey, + disableLocalSpawn, + }, + ); + return fromRawManagedAgent(response); +} + export async function setManagedAgentAutoRestart( pubkey: string, autoRestartOnConfigChange: boolean, diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index d1e624ad530..58614150d99 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -361,6 +361,8 @@ export type ManagedAgent = { lastErrorCode: number | null; logPath: string; startOnAppLaunch: boolean; + /** When true, Desktop never spawns a local process for this agent — neither at boot nor on reactive triggers. */ + disableLocalSpawn: boolean; autoRestartOnConfigChange: boolean; backend: ManagedAgentBackend; backendAgentId: string | null;