Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ pub(crate) use login_shell::{
is_login_shell_path_uninit, is_safe_nvm_tag, login_shell_candidates, parse_semver_tag,
};
pub(crate) use presets::{
canonical_harness_command, command_for_runtime_id, preset_harness_definitions,
preset_harness_ids,
canonical_harness_command, command_for_runtime_id, preset_args_for_command,
preset_harness_definitions, preset_harness_ids,
};
use presets::{preset_catalog_entry, PRESET_HARNESSES};
pub(crate) use runtime_metadata::KnownAcpRuntime;
Expand Down
21 changes: 20 additions & 1 deletion desktop/src-tauri/src/managed_agents/discovery/presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::managed_agents::{
AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, HarnessSource,
};

use super::normalize_agent_args;
use super::{normalize_agent_args, normalize_command_identity};

/// Static data for a well-known tier-2 ACP harness.
pub(super) struct PresetHarness {
Expand Down Expand Up @@ -206,6 +206,25 @@ pub(crate) fn preset_harness_ids() -> &'static [&'static str] {
.as_slice()
}

/// Launch args for the [`PRESET_HARNESSES`] entry whose command matches the
/// given command, when an agent has a command but no resolvable runtime preset
/// (e.g. an explicit `agent_command_override` like `omp` with no `runtime` id).
///
/// Single source of truth: per-runtime launch args live only in
/// `PRESET_HARNESSES`, mirroring [`preset_harness_ids`], so every preset — omp,
/// grok, opencode, kimi, cursor, openclaw, … — is covered at the source instead
/// of being hand-mirrored. Without this, an agent pinned to e.g. `omp` with no
/// runtime id launches the bare command, which under headless Buzz (no
/// controlling TTY) drops into the interactive TUI instead of `omp acp` and the
/// ACP `initialize` handshake times out.
pub(crate) fn preset_args_for_command(command: &str) -> Option<Vec<String>> {
let identity = normalize_command_identity(command);
PRESET_HARNESSES
.iter()
.find(|preset| normalize_command_identity(preset.command) == identity)
.map(|preset| preset.args.iter().map(|arg| arg.to_string()).collect())
}

/// Return the primary command for a preset harness by id, or `None` if the id
/// is not a known preset.
///
Expand Down
76 changes: 76 additions & 0 deletions desktop/src-tauri/src/managed_agents/launch_log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Managed-agent runtime-log markers: generic run-lifecycle lines and the
//! resolved-launch marker recorded immediately before spawn.
//!
//! Split out of `storage.rs` to keep that module under the desktop file-size
//! ratchet. The resolved-launch marker deliberately serializes only the agent
//! command and the argument count — never argument values — mirroring the
//! `spawn_snapshot::diff` `MaskedBare` policy for `args`.

use std::io::Write;
use std::path::Path;

use super::storage::open_log_file;
use crate::managed_agents::resolve_command;

pub(crate) fn append_log_marker(path: &Path, message: &str) -> Result<(), String> {
let mut file = open_log_file(path)?;
writeln!(file, "{message}").map_err(|error| format!("failed to write log marker: {error}"))
}

/// Resolve `command` to a full path (DMG launches have a minimal PATH), append
/// the resolved-launch marker, and return the resolved command for the spawn
/// env (`BUZZ_ACP_AGENT_COMMAND`). The marker records the agent command and
/// the argument count, and nothing else: `agent_args` is user-controlled and
/// may legally carry credentials (`--token=...`), and the runtime log is
/// retrievable end-to-end via `get_managed_agent_log`, so no argument value
/// may ever be serialized — mirroring the `spawn_snapshot::diff` policy,
/// which masks `args` as `MaskedBare` for exactly this reason. When
/// resolution fails, both the marker and the return value carry the
/// unresolved command verbatim.
pub(crate) fn append_resolved_launch_marker(
path: &Path,
command: &str,
args: &[String],
) -> Result<String, String> {
let resolved = resolve_command(command)
.map(|p| p.display().to_string())
.unwrap_or_else(|| command.to_string());
let marker = format!(
"resolved launch: agent_command={resolved:?} args_count={}",
args.len()
);
append_log_marker(path, &marker)?;
Ok(resolved)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn append_resolved_launch_marker_never_writes_argument_values() {
// `agent_args` is user-controlled and may legally carry credentials
// (`--token=...`), and the runtime log is retrievable end-to-end via
// `get_managed_agent_log`, so the marker may carry only the command and
// the argument count. The command is a nonexistent absolute path so
// `acp` can only reach the log through an argument value.
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("agent.log");

let resolved = append_resolved_launch_marker(
&path,
"/nonexistent/zuzu-agent-cli",
&["acp".to_string(), "--token=supersecret-value".to_string()],
)
.expect("append marker");

// Unresolvable commands fall through verbatim for the spawn env.
assert_eq!(resolved, "/nonexistent/zuzu-agent-cli");
let logged = std::fs::read_to_string(&path).expect("read log");
assert!(logged.contains("args_count=2"));
assert!(logged.contains("/nonexistent/zuzu-agent-cli"));
assert!(!logged.contains("supersecret-value"));
assert!(!logged.contains("--token"));
assert!(!logged.contains("acp"));
}
}
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/managed_agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) mod effective_config;
mod env_vars;
pub(crate) mod git_bash;
pub(crate) mod global_config;
mod launch_log;
mod managed_node_paths;
mod nest;
pub(crate) mod parallelism;
Expand Down Expand Up @@ -67,6 +68,7 @@ pub(crate) use global_config::{
load_global_agent_config, resolve_effective_model_provider, save_global_agent_config,
validate_global_config, GlobalAgentConfig,
};
pub(crate) use launch_log::*;
pub(crate) use managed_node_paths::*;
pub use nest::*;
pub use parallelism::{acp_agents_value, effective_parallelism, harness_max_parallelism};
Expand Down
104 changes: 16 additions & 88 deletions desktop/src-tauri/src/managed_agents/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ pub(crate) fn resolve_effective_harness_descriptor(
normalize_agent_args(&effective_command, record_args)
} else if let Some(ref def) = harness_def {
normalize_agent_args(&effective_command, def.args.clone())
} else if let Some(preset_args) =
crate::managed_agents::discovery::preset_args_for_command(&effective_command)
{
// No runtime id resolved a harness preset, but the command itself
// is a known ACP runtime (e.g. an explicit override like `omp`).
// Use that preset's launch args so we start its ACP/stdio mode
// rather than the bare command, which would launch the interactive
// TUI and time out at ACP `initialize` under headless Buzz.
normalize_agent_args(&effective_command, preset_args)
} else {
normalize_agent_args(&effective_command, record_args)
}
Expand Down Expand Up @@ -1458,96 +1467,9 @@ mod tests {
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json["surface"], "cli_login");
assert!(json["probe_args"].is_array());
assert!(json["setup_copy"].as_str().unwrap().contains("codex login"));
}

// ── resolve_effective_agent_env ─────────────────────────────────────────

#[test]
fn resolve_effective_agent_env_user_env_wins_over_structured_fields() {
// User env_vars must win over baked defaults; in OSS builds baked map is empty,
// so this validates the user-env layer is present in the output.
let mut env_vars = BTreeMap::new();
env_vars.insert("BUZZ_AGENT_PROVIDER".to_string(), "anthropic".to_string());
env_vars.insert(
"BUZZ_AGENT_MODEL".to_string(),
"claude-opus-4-5".to_string(),
);

// Minimal record: only the fields resolve_effective_agent_env reads.
let record = crate::managed_agents::types::ManagedAgentRecord {
pubkey: "test-pubkey".to_string(),
name: "test-agent".to_string(),
persona_id: None,
private_key_nsec: String::new(),
auth_tag: None,
relay_url: String::new(),
avatar_url: None,
acp_command: "buzz-acp".to_string(),
agent_command: "buzz-agent".to_string(),
agent_command_override: None,
agent_args: vec![],
mcp_command: String::new(),
turn_timeout_seconds: 320,
idle_timeout_seconds: None,
max_turn_duration_seconds: None,
parallelism: 1,
system_prompt: None,
model: None,
provider: None,
persona_source_version: None,
env_vars,
start_on_app_launch: false,
auto_restart_on_config_change: true,
runtime_pid: None,
backend: Default::default(),
backend_agent_id: None,
provider_policy_pending: false,
provider_binary_path: None,
team_id: None,
persona_team_dir: None,
persona_name_in_team: None,
created_at: String::new(),
updated_at: String::new(),
last_started_at: None,
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: Default::default(),
respond_to_allowlist: vec![],
display_name: None,
slug: None,
runtime: None,
name_pool: Vec::new(),
is_builtin: false,
is_active: true,
shared: false,
source_team: None,
source_team_persona_slug: None,
catalog_source: None,
team_catalog_source: None,
definition_respond_to: None,
definition_respond_to_allowlist: Vec::new(),
definition_parallelism: None,
relay_mesh: None,
effort_level: None,
};

let runtime = known_acp_runtime_exact("buzz-agent");
let effective = resolve_effective_agent_env(&record, &[], runtime, &Default::default());

// User env_vars must be present in the output (last-write-wins).
assert_eq!(
effective.env.get("BUZZ_AGENT_PROVIDER").map(String::as_str),
Some("anthropic")
);
assert_eq!(
effective.env.get("BUZZ_AGENT_MODEL").map(String::as_str),
Some("claude-opus-4-5")
);
}

// ── provider-specific model fallback tests ────────────────────────────
#[test]
fn buzz_agent_databricks_v2_with_databricks_model_but_no_buzz_agent_model_is_ready() {
// The baked buzz-releases env sets DATABRICKS_MODEL but not BUZZ_AGENT_MODEL.
Expand Down Expand Up @@ -1738,3 +1660,9 @@ mod tests {
#[cfg(test)]
#[path = "readiness_goose_file_config_tests.rs"]
mod goose_file_config_tests;

// Effective-launch resolution tests live in a sibling file so this module
// stays under the desktop file-size ratchet.
#[cfg(test)]
#[path = "readiness_effective_launch_tests.rs"]
mod effective_launch_tests;
Loading