am
2026-08-19 15:40:56 -07:00
parent c450a9e993
commit 8721ed1480
10 changed files with 318 additions and 109 deletions

View File

@@ -101,10 +101,13 @@ pub async fn get_agent_models(
// so a build-provided provider still gets live discovery.
let mut effective_provider =
effective_discovery_provider(saved_provider.as_deref(), provider_env_var, &merged_env);
effective_provider.value = crate::managed_agents::readiness::canonicalize_openai_provider_env(
&mut merged_env,
effective_provider.as_deref(),
);
if known_acp_runtime(&discovery.command).is_some_and(|runtime| runtime.id == "buzz-agent") {
effective_provider.value =
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut merged_env,
effective_provider.as_deref(),
);
}
if let Some(models) = discover_openrouter_models(
&state.http_client,
&effective_provider,
@@ -243,10 +246,13 @@ pub async fn discover_agent_models(
runtime_meta.and_then(|meta| meta.provider_env_var),
&merged_env,
);
effective_provider.value = crate::managed_agents::readiness::canonicalize_openai_provider_env(
&mut merged_env,
effective_provider.as_deref(),
);
if runtime_meta.is_some_and(|runtime| runtime.id == "buzz-agent") {
effective_provider.value =
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut merged_env,
effective_provider.as_deref(),
);
}
// Buzz shared compute discovery must not depend on the local OpenAI ingress: that
// client endpoint is started only after a live target is selected.

View File

@@ -20,6 +20,7 @@ pub(crate) mod git_bash;
pub(crate) mod global_config;
mod managed_node_paths;
mod nest;
pub(crate) mod openai_env;
pub(crate) mod parallelism;
mod persona_avatars;
pub(crate) mod persona_events;

View File

@@ -0,0 +1,64 @@
use std::collections::BTreeMap;
pub(crate) fn effective_runtime_provider(
command: &str,
configured_provider: Option<String>,
env: &BTreeMap<String, String>,
) -> Option<String> {
if crate::managed_agents::known_acp_runtime(command)
.is_some_and(|runtime| runtime.id == "buzz-agent")
{
env.get("BUZZ_AGENT_PROVIDER")
.cloned()
.or(configured_provider)
} else {
configured_provider
}
}
pub(crate) fn canonicalize_openai_provider_env(
env: &mut BTreeMap<String, String>,
provider: Option<&str>,
) -> Option<String> {
let mut provider = provider.map(str::trim).map(str::to_ascii_lowercase);
if provider.as_deref() == Some("openai") {
let official_key = env
.get("OPENAI_API_KEY")
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let legacy_key = env
.get("OPENAI_COMPAT_API_KEY")
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty());
let has_custom_origin = env
.get("OPENAI_COMPAT_BASE_URL")
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.is_some_and(|value| value.trim_end_matches('/') != "https://api.openai.com/v1");
if legacy_key.is_some()
&& has_custom_origin
&& (official_key.is_none() || official_key == legacy_key)
{
provider = Some("openai-compat".to_string());
}
}
match provider.as_deref() {
Some("openai") => {
env.remove("OPENAI_COMPAT_BASE_URL");
env.remove("OPENAI_COMPAT_API_KEY");
}
Some("openai-compat") => {
env.remove("OPENAI_API_KEY");
env.entry("OPENAI_COMPAT_API_KEY".to_string()).or_default();
}
_ => {}
}
if let (Some(value), Some(slot)) = (provider.as_ref(), env.get_mut("BUZZ_AGENT_PROVIDER")) {
*slot = value.clone();
}
provider
}

View File

@@ -269,7 +269,14 @@ fn resolve_effective_agent_env_with_def(
);
env.extend(user_env);
let _ = canonicalize_openai_provider_env(&mut env, effective_provider.as_deref());
if crate::managed_agents::known_acp_runtime(&effective_command)
.is_some_and(|runtime| runtime.id == "buzz-agent")
{
let _ = crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut env,
effective_provider.as_deref(),
);
}
// Buzz shared compute is a native Buzz provider. Translate it to buzz-agent's
// OpenAI-compatible transport only in the effective runtime environment.
@@ -287,46 +294,6 @@ fn resolve_effective_agent_env_with_def(
}
}
pub(crate) fn canonicalize_openai_provider_env(
env: &mut BTreeMap<String, String>,
provider: Option<&str>,
) -> Option<String> {
let mut provider = provider.map(str::trim).map(str::to_ascii_lowercase);
if provider.as_deref() == Some("openai") {
let has_official_key = env
.get("OPENAI_API_KEY")
.is_some_and(|value| !value.trim().is_empty());
let has_legacy_key = env
.get("OPENAI_COMPAT_API_KEY")
.is_some_and(|value| !value.trim().is_empty());
let has_custom_origin = env
.get("OPENAI_COMPAT_BASE_URL")
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.is_some_and(|value| value.trim_end_matches('/') != "https://api.openai.com/v1");
if !has_official_key && has_legacy_key && has_custom_origin {
provider = Some("openai-compat".to_string());
}
}
match provider.as_deref() {
Some("openai") => {
env.remove("OPENAI_COMPAT_BASE_URL");
env.remove("OPENAI_COMPAT_API_KEY");
}
Some("openai-compat") => {
env.remove("OPENAI_API_KEY");
env.entry("OPENAI_COMPAT_API_KEY".to_string()).or_default();
}
_ => {}
}
if let (Some(value), Some(slot)) = (provider.as_ref(), env.get_mut("BUZZ_AGENT_PROVIDER")) {
*slot = value.clone();
}
provider
}
// ── Requirement types ─────────────────────────────────────────────────────────
/// A single missing piece of configuration, tagged with the UI surface that
/// owns it so the UI can route each gap to the right affordance.

View File

@@ -22,7 +22,10 @@ fn provider_env_canonicalization_isolates_consumers() {
),
]);
let mut official = shared.clone();
canonicalize_openai_provider_env(&mut official, Some("openai"));
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut official,
Some("openai"),
);
assert_eq!(
official.get("OPENAI_API_KEY").map(String::as_str),
Some("official")
@@ -31,7 +34,10 @@ fn provider_env_canonicalization_isolates_consumers() {
assert!(!official.contains_key("OPENAI_COMPAT_BASE_URL"));
let mut compatible = shared;
canonicalize_openai_provider_env(&mut compatible, Some("openai-compat"));
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut compatible,
Some("openai-compat"),
);
assert!(!compatible.contains_key("OPENAI_API_KEY"));
assert_eq!(
compatible.get("OPENAI_COMPAT_API_KEY").map(String::as_str),
@@ -50,7 +56,11 @@ fn inherited_legacy_custom_configuration_normalizes_per_consumer() {
),
]);
assert_eq!(
canonicalize_openai_provider_env(&mut env, Some("openai")).as_deref(),
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut env,
Some("openai")
)
.as_deref(),
Some("openai-compat")
);
assert_eq!(
@@ -60,13 +70,52 @@ fn inherited_legacy_custom_configuration_normalizes_per_consumer() {
assert!(!env.contains_key("OPENAI_API_KEY"));
}
#[test]
fn copied_official_key_does_not_mask_legacy_custom_identity() {
let mut env = BTreeMap::from([
("BUZZ_AGENT_PROVIDER".into(), "openai".into()),
("OPENAI_API_KEY".into(), "copied-legacy".into()),
("OPENAI_COMPAT_API_KEY".into(), "copied-legacy".into()),
(
"OPENAI_COMPAT_BASE_URL".into(),
"https://gateway.example/v1".into(),
),
]);
assert_eq!(
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut env,
Some("openai")
)
.as_deref(),
Some("openai-compat")
);
assert!(!env.contains_key("OPENAI_API_KEY"));
}
#[test]
fn non_buzz_harness_keeps_its_configured_provider() {
let env = BTreeMap::from([("BUZZ_AGENT_PROVIDER".into(), "openai-compat".into())]);
assert_eq!(
crate::managed_agents::openai_env::effective_runtime_provider(
"goose",
Some("openai".into()),
&env,
)
.as_deref(),
Some("openai")
);
}
#[test]
fn keyless_compatible_provider_gets_explicit_blank_credential() {
let mut env = BTreeMap::from([(
"OPENAI_COMPAT_BASE_URL".into(),
"http://localhost:11434/v1".into(),
)]);
canonicalize_openai_provider_env(&mut env, Some("openai-compat"));
crate::managed_agents::openai_env::canonicalize_openai_provider_env(
&mut env,
Some("openai-compat"),
);
assert_eq!(
env.get("OPENAI_COMPAT_API_KEY").map(String::as_str),
Some("")

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use tauri::AppHandle;
use super::agent_env::{build_buzz_agent_provider_defaults, idle_pool_sleep_env};
use super::openai_env::effective_runtime_provider;
use crate::{
managed_agents::{
append_log_marker, known_acp_runtime, login_shell_path, managed_agent_log_path,
@@ -204,7 +204,7 @@ pub fn build_managed_agent_summary(
personas,
global_config,
);
let (effective_model, effective_provider, effective_prompt, model_source) = match effective_cfg
let (effective_model, configured_provider, effective_prompt, model_source) = match effective_cfg
{
crate::managed_agents::effective_config::EffectiveConfigResult::Resolved(cfg) => {
let source = cfg.model.source.clone();
@@ -293,6 +293,8 @@ pub fn build_managed_agent_summary(
env: Default::default(),
}
});
let effective_provider =
effective_runtime_provider(&descriptor.command, configured_provider, &descriptor.env);
let effective_mcp_command = known_acp_runtime(&descriptor.command)
.and_then(|r| r.mcp_command)
.unwrap_or("")
@@ -711,11 +713,9 @@ pub fn spawn_agent_child(
let mesh_model_id = effective_cfg.relay_mesh_model_id();
let effective_prompt = effective_cfg.system_prompt.value;
let effective_model = effective_cfg.model.value;
let effective_provider = descriptor
.env
.get("BUZZ_AGENT_PROVIDER")
.cloned()
.or(effective_cfg.provider.value);
let configured_provider = effective_cfg.provider.value;
let effective_provider =
effective_runtime_provider(&descriptor.command, configured_provider, &descriptor.env);
if let Some(prompt) = &effective_prompt {
command.env("BUZZ_ACP_SYSTEM_PROMPT", prompt);

View File

@@ -291,12 +291,18 @@ pub(crate) fn prospective_spawn_config_snapshot(
// definition) resolves as if all three were absent: `spawn_agent_child`
// refuses to spawn an orphan regardless, and `eligible_restart_diff`
// suppresses the badge for one.
let (prompt, model, provider) = match resolve_effective_config(record, personas, global) {
EffectiveConfigResult::Resolved(cfg) => {
(cfg.system_prompt.value, cfg.model.value, cfg.provider.value)
}
EffectiveConfigResult::OrphanedInstance { .. } => (None, None, None),
};
let (prompt, model, configured_provider) =
match resolve_effective_config(record, personas, global) {
EffectiveConfigResult::Resolved(cfg) => {
(cfg.system_prompt.value, cfg.model.value, cfg.provider.value)
}
EffectiveConfigResult::OrphanedInstance { .. } => (None, None, None),
};
let provider = crate::managed_agents::openai_env::effective_runtime_provider(
&descriptor.command,
configured_provider,
&descriptor.env,
);
SpawnConfigSnapshot::from_inputs(SpawnConfigInputs {
record,

View File

@@ -34,6 +34,29 @@ fn snapshot(
snapshot_with_policy(record, personas, teams, workspace_relay, global, false)
}
#[test]
fn legacy_custom_buzz_agent_snapshot_uses_runtime_provider_identity() {
let mut rec = record();
rec.runtime = Some("buzz-agent".into());
rec.provider = Some("openai".into());
rec.env_vars
.insert("OPENAI_COMPAT_API_KEY".into(), "shared-key".into());
rec.env_vars.insert(
"OPENAI_COMPAT_BASE_URL".into(),
"https://gateway.example/v1".into(),
);
let global = GlobalAgentConfig {
env_vars: BTreeMap::from([("OPENAI_API_KEY".into(), "shared-key".into())]),
..Default::default()
};
let value = snapshot(&rec, &[], &[], "wss://ws.example", &global);
assert_eq!(
value.get("provider").and_then(serde_json::Value::as_str),
Some("openai-compat")
);
}
/// `snapshot` with the fixed no-persona/no-team/default-global shape the effort
/// tests share, so their call sites read as `snap(&record)` instead of wrapping.
fn snap(record: &ManagedAgentRecord) -> serde_json::Value {

View File

@@ -235,56 +235,79 @@ fn classify_endpoint(endpoint: Option<&str>) -> Result<Identity, String> {
})
}
fn is_buzz_agent_consumer(record: &ManagedAgentRecord, defs: &[AgentDefinition]) -> bool {
crate::managed_agents::known_acp_runtime(&crate::managed_agents::record_agent_command(
record, defs,
))
.is_some_and(|runtime| runtime.id == "buzz-agent")
}
fn global_defaults_target_buzz_agent(store: &Store) -> bool {
match store.global.preferred_runtime.as_deref().map(str::trim) {
Some("") | None => true,
Some(runtime_id) => crate::managed_agents::known_acp_runtime_exact(runtime_id)
.is_some_and(|runtime| runtime.id == "buzz-agent"),
}
}
fn collect_consumers(store: &Store) -> Result<Vec<Consumer>, String> {
let defs = definitions(store);
let has_managed_buzz_agent = store
.records
.iter()
.filter(|record| !record.pubkey.is_empty())
.any(|record| is_buzz_agent_consumer(record, &defs));
let mut consumers = Vec::new();
if let Some(provider) = store
.global
.provider
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.filter(|value| {
matches!(
value.to_ascii_lowercase().as_str(),
"openai" | "openai-compat"
)
})
{
let endpoint = store
if !has_managed_buzz_agent && global_defaults_target_buzz_agent(store) {
if let Some(provider) = store
.global
.env_vars
.get(OPENAI_COMPAT_BASE_URL)
.map(|value| ResolvedValue {
value: value.clone(),
owner: Owner::Global,
.provider
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.filter(|value| {
matches!(
value.to_ascii_lowercase().as_str(),
"openai" | "openai-compat"
)
})
{
let endpoint = store
.global
.env_vars
.get(OPENAI_COMPAT_BASE_URL)
.map(|value| ResolvedValue {
value: value.clone(),
owner: Owner::Global,
});
consumers.push(Consumer {
label: "global defaults".into(),
record_index: None,
provider: provider.to_ascii_lowercase(),
provider_owner: Owner::Global,
identity: classify_endpoint(endpoint.as_ref().map(|value| value.value.as_str()))?,
endpoint,
legacy_key: store
.global
.env_vars
.get(OPENAI_COMPAT_API_KEY)
.map(|value| ResolvedValue {
value: value.clone(),
owner: Owner::Global,
}),
official_key: store.global.env_vars.get(OPENAI_API_KEY).map(|value| {
ResolvedValue {
value: value.clone(),
owner: Owner::Global,
}
}),
});
consumers.push(Consumer {
label: "global defaults".into(),
record_index: None,
provider: provider.to_ascii_lowercase(),
provider_owner: Owner::Global,
identity: classify_endpoint(endpoint.as_ref().map(|value| value.value.as_str()))?,
endpoint,
legacy_key: store
.global
.env_vars
.get(OPENAI_COMPAT_API_KEY)
.map(|value| ResolvedValue {
value: value.clone(),
owner: Owner::Global,
}),
official_key: store
.global
.env_vars
.get(OPENAI_API_KEY)
.map(|value| ResolvedValue {
value: value.clone(),
owner: Owner::Global,
}),
});
}
}
for (index, record) in store.records.iter().enumerate() {
if record.pubkey.is_empty() || !is_buzz_agent_consumer(record, &defs) {
continue;
}
let def = selected_definition(record, &defs);
if record.persona_id.is_some() && def.is_none() {
return Err(format!(
@@ -354,6 +377,7 @@ fn collect_consumers(store: &Store) -> Result<Vec<Consumer>, String> {
});
}
if consumers.is_empty()
&& global_defaults_target_buzz_agent(store)
&& [OPENAI_COMPAT_API_KEY, OPENAI_COMPAT_BASE_URL]
.iter()
.any(|key| store.global.env_vars.contains_key(*key))

View File

@@ -239,6 +239,75 @@ fn unrelated_consumer_does_not_claim_global_openai_credential() {
);
}
#[test]
fn shared_global_legacy_key_supports_official_and_custom_buzz_agents() {
let global = GlobalAgentConfig {
provider: Some("openai".into()),
env_vars: BTreeMap::from([(OPENAI_COMPAT_API_KEY.into(), "shared-key".into())]),
..Default::default()
};
let official = record("official", "official");
let mut custom = record("custom", "custom");
custom.env_vars.insert(
OPENAI_COMPAT_BASE_URL.into(),
"https://gateway.example/v1".into(),
);
let (store, before) = plan_store(Store {
global,
records: vec![official, custom],
})
.unwrap();
assert_eq!(
store
.global
.env_vars
.get(OPENAI_API_KEY)
.map(String::as_str),
Some("shared-key")
);
verify_post_split(&before, &store).unwrap();
let defs = definitions(&store);
let custom_descriptor = crate::managed_agents::resolve_effective_harness_descriptor(
&store.records[1],
&defs,
&store.global,
)
.unwrap();
assert_eq!(
custom_descriptor
.env
.get("BUZZ_AGENT_PROVIDER")
.map(String::as_str),
Some("openai-compat")
);
assert!(!custom_descriptor.env.contains_key(OPENAI_API_KEY));
}
#[test]
fn goose_records_are_not_openai_migration_consumers() {
let global = GlobalAgentConfig {
provider: Some("openai".into()),
preferred_runtime: Some("goose".into()),
env_vars: BTreeMap::from([
(OPENAI_COMPAT_API_KEY.into(), "goose-owned".into()),
(
OPENAI_COMPAT_BASE_URL.into(),
"https://gateway.example/v1".into(),
),
]),
..Default::default()
};
let mut goose = record("goose", "goose");
goose.runtime = Some("goose".into());
goose.provider = Some("openai".into());
let store = planned(global.clone(), vec![goose.clone()]).unwrap();
assert_eq!(store.global, global);
assert_eq!(store.records[0].provider, goose.provider);
}
#[test]
fn linked_provider_snapshot_is_ignored_and_definition_is_mutated() {
let mut def = definition("shared");