mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
fix(desktop): re-enable mcp_command reconciliation and harden spawn site (#909)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -499,6 +499,7 @@ pub fn run() {
|
||||
// restore_managed_agents_on_launch (which reads managed-agents.json).
|
||||
migration::sync_shared_agent_data(&app_handle);
|
||||
migration::reconcile_persona_pack_paths(&app_handle);
|
||||
migration::reconcile_provider_mcp_commands(&app_handle);
|
||||
migration::migrate_persona_provider_to_runtime(&app_handle);
|
||||
|
||||
// Resolve persisted identity key (env var → file → generate+save).
|
||||
|
||||
@@ -797,14 +797,20 @@ pub fn spawn_agent_child(
|
||||
let agent_args = normalize_agent_args(&record.agent_command, record.agent_args.clone());
|
||||
let resolved_acp_command = resolve_command(&record.acp_command)
|
||||
.ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
|
||||
let resolved_mcp_command: Option<std::path::PathBuf> =
|
||||
if record.mcp_command.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(resolve_command(&record.mcp_command).ok_or_else(|| {
|
||||
missing_command_message(&record.mcp_command, "MCP server command")
|
||||
})?)
|
||||
};
|
||||
let resolved_mcp_command: Option<std::path::PathBuf> = if record.mcp_command.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match resolve_command(&record.mcp_command) {
|
||||
Some(path) => Some(path),
|
||||
None => {
|
||||
eprintln!(
|
||||
"sprout-desktop: mcp_command {:?} not found, skipping",
|
||||
record.mcp_command
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
// Resolve agent command to a full path (DMG launches have minimal PATH).
|
||||
let resolved_agent_command = resolve_command(&record.agent_command)
|
||||
.map(|p| p.display().to_string())
|
||||
|
||||
@@ -318,6 +318,65 @@ pub fn reconcile_persona_pack_paths(app: &tauri::AppHandle) {
|
||||
reconcile_pack_paths_in_file(&path, &canonical_dir);
|
||||
}
|
||||
|
||||
fn reconcile_mcp_commands_in_file(path: &Path) {
|
||||
patch_json_records(path, |obj| {
|
||||
let agent_command = match obj.get("agent_command").and_then(|v| v.as_str()) {
|
||||
Some(cmd) => cmd.to_string(),
|
||||
None => return false,
|
||||
};
|
||||
let Some(runtime) = crate::managed_agents::known_acp_runtime(&agent_command) else {
|
||||
return false;
|
||||
};
|
||||
let expected = runtime.mcp_command.unwrap_or("");
|
||||
let current = obj
|
||||
.get("mcp_command")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if current == expected {
|
||||
return false;
|
||||
}
|
||||
// Only fix values that are clearly stale (empty or a removed binary).
|
||||
// Leave user-customized values untouched.
|
||||
if !current.is_empty() && current != "sprout-mcp-server" {
|
||||
return false;
|
||||
}
|
||||
eprintln!(
|
||||
"sprout-desktop: runtime-reconcile: {:?} ({:?}): mcp_command {:?} → {:?}",
|
||||
obj.get("name").and_then(|v| v.as_str()).unwrap_or("?"),
|
||||
agent_command,
|
||||
current,
|
||||
expected,
|
||||
);
|
||||
obj.insert(
|
||||
"mcp_command".to_string(),
|
||||
serde_json::Value::String(expected.to_string()),
|
||||
);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
/// Reconcile `mcp_command` values in managed-agents.json against the
|
||||
/// discovery table. Known runtimes get their canonical mcp_command;
|
||||
/// unknown/custom agents are left untouched. Covers both the current
|
||||
/// app data dir and the canonical dev data dir (for worktree instances).
|
||||
pub fn reconcile_provider_mcp_commands(app: &tauri::AppHandle) {
|
||||
let Ok(current_dir) = app.path().app_data_dir() else {
|
||||
return;
|
||||
};
|
||||
let mut dirs = vec![current_dir.clone()];
|
||||
if let Some(canonical) = canonical_dev_data_dir(¤t_dir) {
|
||||
if canonical.exists() && canonical != current_dir {
|
||||
dirs.push(canonical);
|
||||
}
|
||||
}
|
||||
for dir in dirs {
|
||||
let path = dir.join("agents/managed-agents.json");
|
||||
if path.exists() {
|
||||
reconcile_mcp_commands_in_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_provider_to_runtime_in_personas(path: &Path) {
|
||||
patch_json_records(path, |obj| {
|
||||
if obj.contains_key("runtime") {
|
||||
@@ -899,4 +958,118 @@ mod tests {
|
||||
// provider key should still be there since the closure returns false when runtime exists
|
||||
assert_eq!(records[0]["provider"], "old-value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_clears_stale_sprout_mcp_server() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_agents_json(
|
||||
dir.path(),
|
||||
&serde_json::json!([{
|
||||
"name": "Solo",
|
||||
"agent_command": "goose",
|
||||
"mcp_command": "sprout-mcp-server"
|
||||
}]),
|
||||
);
|
||||
reconcile_mcp_commands_in_file(&dir.path().join("agents/managed-agents.json"));
|
||||
let records = read_agents_json(dir.path());
|
||||
assert_eq!(records[0]["mcp_command"], "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_sets_canonical_for_sprout_agent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_agents_json(
|
||||
dir.path(),
|
||||
&serde_json::json!([{
|
||||
"name": "Stilgar",
|
||||
"agent_command": "sprout-agent",
|
||||
"mcp_command": "sprout-mcp-server"
|
||||
}]),
|
||||
);
|
||||
reconcile_mcp_commands_in_file(&dir.path().join("agents/managed-agents.json"));
|
||||
let records = read_agents_json(dir.path());
|
||||
assert_eq!(records[0]["mcp_command"], "sprout-dev-mcp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_leaves_custom_value_untouched() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let json = serde_json::json!([{
|
||||
"name": "Solo",
|
||||
"agent_command": "goose",
|
||||
"mcp_command": "my-custom-mcp"
|
||||
}]);
|
||||
write_agents_json(dir.path(), &json);
|
||||
let path = dir.path().join("agents/managed-agents.json");
|
||||
let before = std::fs::read_to_string(&path).unwrap();
|
||||
reconcile_mcp_commands_in_file(&path);
|
||||
assert_eq!(before, std::fs::read_to_string(&path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_leaves_unknown_runtime_untouched() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let json = serde_json::json!([{
|
||||
"name": "Custom",
|
||||
"agent_command": "my-custom-agent",
|
||||
"mcp_command": "sprout-mcp-server"
|
||||
}]);
|
||||
write_agents_json(dir.path(), &json);
|
||||
let path = dir.path().join("agents/managed-agents.json");
|
||||
let before = std::fs::read_to_string(&path).unwrap();
|
||||
reconcile_mcp_commands_in_file(&path);
|
||||
assert_eq!(before, std::fs::read_to_string(&path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_is_idempotent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_agents_json(
|
||||
dir.path(),
|
||||
&serde_json::json!([{
|
||||
"name": "Solo",
|
||||
"agent_command": "goose",
|
||||
"mcp_command": "sprout-mcp-server"
|
||||
}]),
|
||||
);
|
||||
let path = dir.path().join("agents/managed-agents.json");
|
||||
reconcile_mcp_commands_in_file(&path);
|
||||
let after_first = std::fs::read_to_string(&path).unwrap();
|
||||
reconcile_mcp_commands_in_file(&path);
|
||||
assert_eq!(after_first, std::fs::read_to_string(&path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_handles_mixed_agents() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_agents_json(
|
||||
dir.path(),
|
||||
&serde_json::json!([
|
||||
{"name": "Stale Goose", "agent_command": "goose", "mcp_command": "sprout-mcp-server"},
|
||||
{"name": "Clean Goose", "agent_command": "goose", "mcp_command": ""},
|
||||
{"name": "Custom Agent", "agent_command": "goose", "mcp_command": "my-custom-mcp"},
|
||||
{"name": "Stale Sprout", "agent_command": "sprout-agent", "mcp_command": "sprout-mcp-server"}
|
||||
]),
|
||||
);
|
||||
reconcile_mcp_commands_in_file(&dir.path().join("agents/managed-agents.json"));
|
||||
let records = read_agents_json(dir.path());
|
||||
assert_eq!(records[0]["mcp_command"], "");
|
||||
assert_eq!(records[1]["mcp_command"], "");
|
||||
assert_eq!(records[2]["mcp_command"], "my-custom-mcp");
|
||||
assert_eq!(records[3]["mcp_command"], "sprout-dev-mcp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mcp_commands_skips_record_without_agent_command() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let json = serde_json::json!([{
|
||||
"name": "No Command",
|
||||
"mcp_command": "sprout-mcp-server"
|
||||
}]);
|
||||
write_agents_json(dir.path(), &json);
|
||||
let path = dir.path().join("agents/managed-agents.json");
|
||||
let before = std::fs::read_to_string(&path).unwrap();
|
||||
reconcile_mcp_commands_in_file(&path);
|
||||
assert_eq!(before, std::fs::read_to_string(&path).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user