From 350db73ee71926ebfa02319690b8fda4709c7a48 Mon Sep 17 00:00:00 2001 From: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Date: Tue, 18 Aug 2026 16:07:46 -0700 Subject: [PATCH] fix: migrate overridden dangling runtimes Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> --- .../src/migration/openai_credentials.rs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/desktop/src-tauri/src/migration/openai_credentials.rs b/desktop/src-tauri/src/migration/openai_credentials.rs index c0b331b105..993472769e 100644 --- a/desktop/src-tauri/src/migration/openai_credentials.rs +++ b/desktop/src-tauri/src/migration/openai_credentials.rs @@ -97,6 +97,7 @@ struct DefinitionContext { fn harness_env_for_runtime( runtime: Option<&str>, + command_override_present: bool, harnesses: &std::collections::HashMap>, ) -> Result, String> { let Some(runtime) = runtime.map(str::trim).filter(|value| !value.is_empty()) else { @@ -105,6 +106,12 @@ fn harness_env_for_runtime( if let Some(env) = harnesses.get(runtime) { return Ok(env.clone()); } + if command_override_present { + // Spawn accepts an explicit command before resolving the runtime. A + // deleted runtime then contributes no definition env rather than + // making the otherwise runnable record fail. + return Ok(Map::new()); + } if crate::managed_agents::custom_harnesses::check_id_collision(runtime).is_ok() { return Err(format!("unresolved custom harness {runtime:?}")); } @@ -214,8 +221,17 @@ fn migrate_openai_credentials_in_file( .or_else(|| definition.and_then(|definition| definition.runtime.as_deref())) .map(str::trim) .filter(|runtime| !runtime.is_empty()); + let command_override_present = record + .get("agent_command_override") + .and_then(Value::as_str) + .map(str::trim) + .is_some_and(|command| !command.is_empty()); let merged_inherited_env = { - let mut merged = harness_env_for_runtime(selected_runtime, harnesses)?; + let mut merged = harness_env_for_runtime( + selected_runtime, + command_override_present, + harnesses, + )?; if let Some(inherited) = inherited_env_vars { merged.extend(inherited.clone()); } @@ -610,6 +626,40 @@ mod tests { assert!(sibling_path(&path, MIGRATION_SUFFIX).exists()); } + #[test] + fn raw_command_override_allows_migration_past_dangling_runtime() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("managed-agents.json"); + std::fs::write( + &path, + serde_json::to_vec(&serde_json::json!([ + { + "pubkey": "overridden-agent", + "runtime": "missing-custom-harness", + "agent_command_override": "/opt/custom/my-agent", + "provider": "openai", + "env_vars": { "OPENAI_COMPAT_API_KEY": "overridden-secret" } + }, + { + "pubkey": "unrelated-agent", + "provider": "openai", + "env_vars": { "OPENAI_COMPAT_API_KEY": "unrelated-secret" } + } + ])) + .unwrap(), + ) + .unwrap(); + + migrate_openai_credentials_in_file(&path, None, None, &std::collections::HashMap::new()) + .unwrap(); + let records: Value = + serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); + + assert_eq!(records[0]["env_vars"][OPENAI_API_KEY], "overridden-secret"); + assert_eq!(records[1]["env_vars"][OPENAI_API_KEY], "unrelated-secret"); + assert!(sibling_path(&path, MIGRATION_SUFFIX).exists()); + } + #[test] fn global_credentials_ignore_preferred_runtime_harness_env() { let dir = tempfile::tempdir().unwrap();