mirror of
https://github.com/block/buzz.git
synced 2026-11-11 20:01:09 +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).
|
// restore_managed_agents_on_launch (which reads managed-agents.json).
|
||||||
migration::sync_shared_agent_data(&app_handle);
|
migration::sync_shared_agent_data(&app_handle);
|
||||||
migration::reconcile_persona_pack_paths(&app_handle);
|
migration::reconcile_persona_pack_paths(&app_handle);
|
||||||
|
migration::reconcile_provider_mcp_commands(&app_handle);
|
||||||
migration::migrate_persona_provider_to_runtime(&app_handle);
|
migration::migrate_persona_provider_to_runtime(&app_handle);
|
||||||
|
|
||||||
// Resolve persisted identity key (env var → file → generate+save).
|
// 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 agent_args = normalize_agent_args(&record.agent_command, record.agent_args.clone());
|
||||||
let resolved_acp_command = resolve_command(&record.acp_command)
|
let resolved_acp_command = resolve_command(&record.acp_command)
|
||||||
.ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
|
.ok_or_else(|| missing_command_message(&record.acp_command, "ACP harness command"))?;
|
||||||
let resolved_mcp_command: Option<std::path::PathBuf> =
|
let resolved_mcp_command: Option<std::path::PathBuf> = if record.mcp_command.is_empty() {
|
||||||
if record.mcp_command.is_empty() {
|
None
|
||||||
None
|
} else {
|
||||||
} else {
|
match resolve_command(&record.mcp_command) {
|
||||||
Some(resolve_command(&record.mcp_command).ok_or_else(|| {
|
Some(path) => Some(path),
|
||||||
missing_command_message(&record.mcp_command, "MCP server command")
|
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).
|
// Resolve agent command to a full path (DMG launches have minimal PATH).
|
||||||
let resolved_agent_command = resolve_command(&record.agent_command)
|
let resolved_agent_command = resolve_command(&record.agent_command)
|
||||||
.map(|p| p.display().to_string())
|
.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);
|
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) {
|
fn rename_provider_to_runtime_in_personas(path: &Path) {
|
||||||
patch_json_records(path, |obj| {
|
patch_json_records(path, |obj| {
|
||||||
if obj.contains_key("runtime") {
|
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
|
// provider key should still be there since the closure returns false when runtime exists
|
||||||
assert_eq!(records[0]["provider"], "old-value");
|
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