mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
## Summary - discover shared managed agents from authenticated relay directory records instead of treating channel membership as sufficient proof - publish and refresh access-policy changes immediately so running clients converge across machines without a restart or five-minute poll - route profile edits through the exact managed instance and stop/restart runtimes around access changes so unrelated edits cannot silently widen access - keep mention send-time revalidation and Block owner-only build enforcement fail closed - explain invalid custom provider/model configuration instead of leaving Save silently disabled ### Related issue Fixes #3204 ### Known residuals - a brand-new remote agent's first policy record can wait for the bounded directory poll when no authenticated directory coordinate exists yet; send-time mention revalidation remains fail closed - a failed remote-provider policy redeploy is recorded but cannot undeploy the older provider instance until the provider protocol gains the destructor tracked by #5570 ### Testing - full Desktop unit suite: 4,961 tests passed - focused profile editor Playwright workflow passed, including Customize access edits and prompt-only edits after tightening an instance - Desktop TypeScript, Biome formatting, file-size ratchet, Tauri checks, and pre-push suites passed - independently reviewed for authenticated directory trust, live subscription teardown, runtime revocation ordering, fail-open edit paths, and per-agent provider deployment serialization --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz> Co-authored-by: diegorumo <diegorumo@gmail.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
479 lines
19 KiB
Rust
479 lines
19 KiB
Rust
//! Provider deploy payload construction, split from `agents.rs` (file-size
|
|
//! guard). The launch block is derived from the same effective descriptor and
|
|
//! policy helpers as local spawn so remote execution does not reimplement them.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use tauri::AppHandle;
|
|
|
|
#[cfg(test)]
|
|
use crate::managed_agents::AgentDefinition;
|
|
use crate::{
|
|
app_state::AppState,
|
|
managed_agents::{load_personas, ManagedAgentRecord},
|
|
relay::relay_ws_url_with_override,
|
|
};
|
|
|
|
/// Effective projection fields for the deploy payload — all derived from the
|
|
/// resolved descriptor and effective config so that the serialised payload and
|
|
/// the `launch` block are always internally consistent.
|
|
pub(super) struct DeployProjections {
|
|
pub effective_model: Option<String>,
|
|
pub effective_provider: Option<String>,
|
|
pub effective_prompt: Option<String>,
|
|
/// Effective parallelism derived from the same resolved `descriptor.command`
|
|
/// as `launch.policy_env["BUZZ_ACP_AGENTS"]`.
|
|
pub effective_parallelism: u32,
|
|
/// Access fields projected from the same build policy that gates local starts.
|
|
pub owner_only_access: bool,
|
|
}
|
|
|
|
/// Resolve the deploy-specific structured model/provider for a managed agent.
|
|
#[cfg(test)]
|
|
pub(crate) fn resolve_deploy_model_provider(
|
|
record: &ManagedAgentRecord,
|
|
personas: &[AgentDefinition],
|
|
global: &crate::managed_agents::GlobalAgentConfig,
|
|
) -> (Option<String>, Option<String>) {
|
|
crate::managed_agents::effective_config::resolve_effective_model_provider_pair(
|
|
record, personas, global,
|
|
)
|
|
.unwrap_or((None, None))
|
|
}
|
|
|
|
/// Serialize the portable launch contract shared with provider-backed agents.
|
|
///
|
|
/// `descriptor.env` is the authoritative six-layer environment. Policy values
|
|
/// are deliberately separate because providers apply them below that layered
|
|
/// environment, preserving the local spawn's power-user override semantics.
|
|
pub(super) fn build_launch_block(
|
|
record: &ManagedAgentRecord,
|
|
descriptor: &crate::managed_agents::readiness::EffectiveHarnessDescriptor,
|
|
teams: &[crate::managed_agents::TeamRecord],
|
|
effective_prompt: Option<&str>,
|
|
effective_model: Option<&str>,
|
|
owner_pubkey: &str,
|
|
) -> serde_json::Value {
|
|
use crate::managed_agents::{
|
|
known_acp_runtime, resolve_session_title, DISPLAY_NAME_ENV_VAR, SESSION_TITLE_ENV_VAR,
|
|
};
|
|
|
|
let runtime = known_acp_runtime(&descriptor.command);
|
|
let mut policy_env = BTreeMap::new();
|
|
|
|
if let Some(runtime) = runtime {
|
|
policy_env.extend(
|
|
runtime
|
|
.default_env
|
|
.iter()
|
|
.map(|(key, value)| ((*key).to_string(), (*value).to_string())),
|
|
);
|
|
if runtime.mcp_hooks {
|
|
policy_env.insert("MCP_HOOK_SERVERS".into(), "*".into());
|
|
}
|
|
}
|
|
policy_env.insert("BUZZ_ACP_RELAY_OBSERVER".into(), "true".into());
|
|
policy_env.insert("BUZZ_ACP_LAZY_POOL".into(), "true".into());
|
|
policy_env.insert(
|
|
"BUZZ_ACP_AGENTS".into(),
|
|
crate::managed_agents::acp_agents_value(&descriptor.command, record.parallelism),
|
|
);
|
|
|
|
if let Some(value) = effective_prompt {
|
|
policy_env.insert("BUZZ_ACP_SYSTEM_PROMPT".into(), value.to_string());
|
|
}
|
|
if let Some(value) = effective_model {
|
|
policy_env.insert("BUZZ_ACP_MODEL".into(), value.to_string());
|
|
}
|
|
if let Some(value) = record.idle_timeout_seconds {
|
|
policy_env.insert("BUZZ_ACP_IDLE_TIMEOUT".into(), value.to_string());
|
|
}
|
|
if let Some(value) = record.max_turn_duration_seconds {
|
|
policy_env.insert("BUZZ_ACP_MAX_TURN_DURATION".into(), value.to_string());
|
|
}
|
|
if let Some(value) = resolve_session_title(record.display_name.as_deref(), &record.name) {
|
|
policy_env.insert(SESSION_TITLE_ENV_VAR.into(), value.clone());
|
|
policy_env.insert(DISPLAY_NAME_ENV_VAR.into(), value);
|
|
}
|
|
if let Some(value) =
|
|
crate::managed_agents::spawn_snapshot::effective_team_instructions(record, teams)
|
|
{
|
|
policy_env.insert("BUZZ_ACP_TEAM_INSTRUCTIONS".into(), value);
|
|
}
|
|
|
|
serde_json::json!({
|
|
"command": descriptor.command,
|
|
"args": descriptor.args,
|
|
"env": descriptor.env,
|
|
"policy_env": policy_env,
|
|
"owner_pubkey": owner_pubkey,
|
|
})
|
|
}
|
|
|
|
pub(super) fn ensure_remote_provider_supported(provider: Option<&str>) -> Result<(), String> {
|
|
if provider.map(str::trim) == Some(crate::managed_agents::RELAY_MESH_PROVIDER_ID) {
|
|
return Err(
|
|
"shared-compute agents cannot be deployed remotely because the mesh endpoint is local to the desktop"
|
|
.to_string(),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Build the standard agent JSON payload for provider deploy calls.
|
|
pub(crate) fn build_deploy_payload(
|
|
app: &AppHandle,
|
|
state: &AppState,
|
|
record: &ManagedAgentRecord,
|
|
) -> Result<serde_json::Value, String> {
|
|
if let Some(err) = crate::managed_agents::spawn_key_refusal(record) {
|
|
return Err(err);
|
|
}
|
|
|
|
let global = crate::managed_agents::load_global_agent_config(app).unwrap_or_default();
|
|
let personas = load_personas(app).unwrap_or_default();
|
|
let teams = crate::managed_agents::load_teams(app).unwrap_or_default();
|
|
let persona_env =
|
|
crate::managed_agents::live_persona_env(&personas, record.persona_id.as_deref());
|
|
let global_persona_env = crate::managed_agents::merged_user_env(&global.env_vars, &persona_env);
|
|
let merged_user_env =
|
|
crate::managed_agents::merged_user_env(&global_persona_env, &record.env_vars);
|
|
let effective = crate::managed_agents::effective_config::resolve_effective_config(
|
|
record, &personas, &global,
|
|
)
|
|
.require_resolved()?;
|
|
|
|
ensure_remote_provider_supported(effective.provider.value.as_deref())?;
|
|
|
|
let descriptor =
|
|
crate::managed_agents::resolve_effective_harness_descriptor(record, &personas, &global)
|
|
.map_err(|error| crate::managed_agents::user_facing_harness_error(&error))?;
|
|
let owner_pubkey = super::workspace_owner_hex(state)?;
|
|
let launch = build_launch_block(
|
|
record,
|
|
&descriptor,
|
|
&teams,
|
|
effective.system_prompt.value.as_deref(),
|
|
effective.model.value.as_deref(),
|
|
&owner_pubkey,
|
|
);
|
|
|
|
let effective_parallelism =
|
|
crate::managed_agents::effective_parallelism(&descriptor.command, record.parallelism);
|
|
|
|
Ok(deploy_payload_json(
|
|
record,
|
|
crate::relay::effective_agent_relay_url(
|
|
&record.relay_url,
|
|
&relay_ws_url_with_override(state),
|
|
),
|
|
DeployProjections {
|
|
effective_model: effective.model.value,
|
|
effective_provider: effective.provider.value,
|
|
effective_prompt: effective.system_prompt.value,
|
|
effective_parallelism,
|
|
owner_only_access: crate::managed_agents::owner_only_access_build(),
|
|
},
|
|
merged_user_env,
|
|
launch,
|
|
))
|
|
}
|
|
|
|
/// Pure serialization half of [`build_deploy_payload`]. Legacy top-level fields
|
|
/// remain for display/bookkeeping; providers execute the resolved `launch` block.
|
|
/// `projections.effective_parallelism` is pre-computed from the same resolved
|
|
/// descriptor as `launch.policy_env["BUZZ_ACP_AGENTS"]`. Access is projected from
|
|
/// the same compiled policy that gates local starts.
|
|
pub(super) fn deploy_payload_json(
|
|
record: &ManagedAgentRecord,
|
|
relay_url: String,
|
|
projections: DeployProjections,
|
|
merged_env: BTreeMap<String, String>,
|
|
launch: serde_json::Value,
|
|
) -> serde_json::Value {
|
|
let (respond_to, respond_to_allowlist) =
|
|
crate::managed_agents::projected_access_with_policy(record, projections.owner_only_access);
|
|
serde_json::json!({
|
|
"name": &record.name,
|
|
"relay_url": relay_url,
|
|
"private_key_nsec": &record.private_key_nsec,
|
|
"auth_tag": &record.auth_tag,
|
|
"agent_command": &record.agent_command,
|
|
"agent_args": &record.agent_args,
|
|
"system_prompt": projections.effective_prompt,
|
|
"model": projections.effective_model,
|
|
"provider": projections.effective_provider,
|
|
"turn_timeout_seconds": record.turn_timeout_seconds,
|
|
"idle_timeout_seconds": record.idle_timeout_seconds,
|
|
"max_turn_duration_seconds": record.max_turn_duration_seconds,
|
|
// Legacy top-level field: projected from the same resolved descriptor as
|
|
// launch.policy_env["BUZZ_ACP_AGENTS"] — the two are always consistent.
|
|
"parallelism": projections.effective_parallelism,
|
|
"respond_to": respond_to,
|
|
"respond_to_allowlist": respond_to_allowlist,
|
|
"env_vars": merged_env,
|
|
"launch": launch,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::managed_agents::{readiness::EffectiveHarnessDescriptor, RespondTo, TeamRecord};
|
|
|
|
fn record() -> ManagedAgentRecord {
|
|
serde_json::from_value(serde_json::json!({
|
|
"pubkey": "abcd1234",
|
|
"name": "agent-handle",
|
|
"display_name": "Agent\u{0000} Name",
|
|
"private_key_nsec": "nsec1fake",
|
|
"relay_url": "wss://relay.example",
|
|
"acp_command": "buzz-acp",
|
|
"agent_command": "goose",
|
|
"agent_args": [],
|
|
"mcp_command": "",
|
|
"turn_timeout_seconds": 320,
|
|
"idle_timeout_seconds": 17,
|
|
"max_turn_duration_seconds": 23,
|
|
"parallelism": 4,
|
|
"respond_to": RespondTo::OwnerOnly,
|
|
"respond_to_allowlist": [],
|
|
"team_id": "team-1",
|
|
"created_at": "2026-01-01T00:00:00Z",
|
|
"updated_at": "2026-01-01T00:00:00Z"
|
|
}))
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn launch_block_preserves_descriptor_and_spawn_policy() {
|
|
let record = record();
|
|
let descriptor = EffectiveHarnessDescriptor {
|
|
command: "goose".into(),
|
|
args: vec!["acp".into()],
|
|
env: BTreeMap::from([
|
|
("GOOSE_MODE".into(), "custom".into()),
|
|
("SECRET_FROM_PERSONA".into(), "secret".into()),
|
|
]),
|
|
};
|
|
let teams: Vec<TeamRecord> = serde_json::from_value(serde_json::json!([{
|
|
"id": "team-1", "name": "Team", "instructions": "Coordinate", "persona_ids": [], "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z"
|
|
}])).unwrap();
|
|
|
|
let launch = build_launch_block(
|
|
&record,
|
|
&descriptor,
|
|
&teams,
|
|
Some("prompt"),
|
|
Some("model"),
|
|
"owner-hex",
|
|
);
|
|
|
|
assert_eq!(launch["command"], "goose");
|
|
assert_eq!(launch["args"], serde_json::json!(["acp"]));
|
|
assert_eq!(launch["env"]["GOOSE_MODE"], "custom");
|
|
// policy_env is applied first, so this default remains separate from
|
|
// the descriptor value that wins in launch.env.
|
|
assert_eq!(launch["policy_env"]["GOOSE_MODE"], "auto");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_LAZY_POOL"], "true");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_RELAY_OBSERVER"], "true");
|
|
assert_eq!(
|
|
launch["policy_env"]["BUZZ_ACP_TEAM_INSTRUCTIONS"],
|
|
"Coordinate"
|
|
);
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_SESSION_TITLE"], "Agent Name");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_DISPLAY_NAME"], "Agent Name");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_SYSTEM_PROMPT"], "prompt");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_MODEL"], "model");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_IDLE_TIMEOUT"], "17");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_MAX_TURN_DURATION"], "23");
|
|
assert_eq!(launch["policy_env"]["BUZZ_ACP_AGENTS"], "4");
|
|
assert_eq!(launch["owner_pubkey"], "owner-hex");
|
|
}
|
|
|
|
/// OpenClaw descriptor: `launch.policy_env["BUZZ_ACP_AGENTS"]` must be "5"
|
|
/// even when the record's requested parallelism is 10. This is the direct
|
|
/// `launch.policy_env` seam test — the executable contract for remote providers.
|
|
#[test]
|
|
fn launch_block_openclaw_over_cap_policy_env_is_capped() {
|
|
let mut record = record();
|
|
record.agent_command = "openclaw".into();
|
|
record.parallelism = 10; // above the OpenClaw spawn-time cap
|
|
let descriptor = EffectiveHarnessDescriptor {
|
|
command: "openclaw".into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
};
|
|
|
|
let launch = build_launch_block(&record, &descriptor, &[], None, None, "owner-hex");
|
|
|
|
assert_eq!(
|
|
launch["policy_env"]["BUZZ_ACP_AGENTS"],
|
|
crate::managed_agents::parallelism::OPENCLAW_MAX_PARALLELISM.to_string(),
|
|
"launch.policy_env[BUZZ_ACP_AGENTS] must be capped at {} for OpenClaw, not 10",
|
|
crate::managed_agents::parallelism::OPENCLAW_MAX_PARALLELISM
|
|
);
|
|
}
|
|
|
|
/// Uncapped harness (goose): `launch.policy_env["BUZZ_ACP_AGENTS"]` passes
|
|
/// the requested value through unchanged.
|
|
#[test]
|
|
fn launch_block_goose_policy_env_is_not_capped() {
|
|
let mut record = record();
|
|
record.parallelism = 8;
|
|
let descriptor = EffectiveHarnessDescriptor {
|
|
command: "goose".into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
};
|
|
|
|
let launch = build_launch_block(&record, &descriptor, &[], None, None, "owner-hex");
|
|
|
|
assert_eq!(
|
|
launch["policy_env"]["BUZZ_ACP_AGENTS"], "8",
|
|
"goose: policy_env[BUZZ_ACP_AGENTS] must pass through requested value 8"
|
|
);
|
|
}
|
|
|
|
/// deploy_payload_json: legacy top-level `parallelism` is the effective value
|
|
/// derived from the descriptor, not `record.agent_command`.
|
|
///
|
|
/// Stale-persona scenario: `record.agent_command` is "goose" (created before
|
|
/// the user switched the persona to OpenClaw), but the live descriptor resolves
|
|
/// OpenClaw. Both `launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy
|
|
/// top-level `parallelism` must be the effective OpenClaw value (5), not the
|
|
/// record's stale Goose identity (requested 10).
|
|
#[test]
|
|
fn deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped() {
|
|
let mut record = record();
|
|
// Stale agent_command from record creation — persona has since switched to OpenClaw.
|
|
record.agent_command = "goose".into();
|
|
record.parallelism = 10;
|
|
// Resolved descriptor reflects the live persona (OpenClaw).
|
|
let descriptor = EffectiveHarnessDescriptor {
|
|
command: "openclaw".into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
};
|
|
let cap = crate::managed_agents::parallelism::OPENCLAW_MAX_PARALLELISM;
|
|
|
|
let launch = build_launch_block(&record, &descriptor, &[], None, None, "owner-hex");
|
|
let effective_parallelism =
|
|
crate::managed_agents::effective_parallelism(&descriptor.command, record.parallelism);
|
|
let payload = deploy_payload_json(
|
|
&record,
|
|
"wss://relay.example".to_string(),
|
|
DeployProjections {
|
|
effective_model: None,
|
|
effective_provider: None,
|
|
effective_prompt: None,
|
|
effective_parallelism,
|
|
owner_only_access: false,
|
|
},
|
|
BTreeMap::new(),
|
|
launch.clone(),
|
|
);
|
|
|
|
assert_eq!(
|
|
launch["policy_env"]["BUZZ_ACP_AGENTS"],
|
|
cap.to_string(),
|
|
"launch.policy_env[BUZZ_ACP_AGENTS] must be capped at {cap} for live OpenClaw descriptor"
|
|
);
|
|
assert_eq!(
|
|
payload["parallelism"], cap,
|
|
"legacy top-level parallelism must match launch.policy_env — both must be {cap}"
|
|
);
|
|
}
|
|
|
|
/// Inverse stale-persona scenario: `record.agent_command` is "openclaw"
|
|
/// (created before the user switched the persona to Goose), but the live
|
|
/// descriptor resolves Goose. Both projections must be the uncapped requested
|
|
/// value (4), not the old OpenClaw cap.
|
|
#[test]
|
|
fn deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped() {
|
|
let mut record = record();
|
|
// Stale agent_command from record creation — persona has since switched to Goose.
|
|
record.agent_command = "openclaw".into();
|
|
record.parallelism = 4;
|
|
// Resolved descriptor reflects the live persona (Goose).
|
|
let descriptor = EffectiveHarnessDescriptor {
|
|
command: "goose".into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
};
|
|
|
|
let launch = build_launch_block(&record, &descriptor, &[], None, None, "owner-hex");
|
|
let effective_parallelism =
|
|
crate::managed_agents::effective_parallelism(&descriptor.command, record.parallelism);
|
|
let payload = deploy_payload_json(
|
|
&record,
|
|
"wss://relay.example".to_string(),
|
|
DeployProjections {
|
|
effective_model: None,
|
|
effective_provider: None,
|
|
effective_prompt: None,
|
|
effective_parallelism,
|
|
owner_only_access: false,
|
|
},
|
|
BTreeMap::new(),
|
|
launch.clone(),
|
|
);
|
|
|
|
assert_eq!(
|
|
launch["policy_env"]["BUZZ_ACP_AGENTS"],
|
|
"4",
|
|
"launch.policy_env[BUZZ_ACP_AGENTS] must pass through requested 4 for live Goose descriptor"
|
|
);
|
|
assert_eq!(
|
|
payload["parallelism"], 4,
|
|
"legacy top-level parallelism must match launch.policy_env — both must be 4 (uncapped)"
|
|
);
|
|
}
|
|
|
|
/// Explicit agent_command_override direction: record has an explicit override
|
|
/// pinning OpenClaw while the persona default is Goose. The override wins
|
|
/// via the descriptor — both projections must be capped at the OpenClaw limit.
|
|
#[test]
|
|
fn deploy_payload_json_explicit_openclaw_override_both_capped() {
|
|
let mut record = record();
|
|
// Explicit override: user pinned OpenClaw on this agent.
|
|
record.agent_command_override = Some("openclaw".into());
|
|
record.agent_command = "goose".into(); // persona default, overridden
|
|
record.parallelism = 10;
|
|
// Descriptor reflects the resolved override (OpenClaw wins).
|
|
let descriptor = EffectiveHarnessDescriptor {
|
|
command: "openclaw".into(),
|
|
args: vec![],
|
|
env: BTreeMap::new(),
|
|
};
|
|
let cap = crate::managed_agents::parallelism::OPENCLAW_MAX_PARALLELISM;
|
|
|
|
let launch = build_launch_block(&record, &descriptor, &[], None, None, "owner-hex");
|
|
let effective_parallelism =
|
|
crate::managed_agents::effective_parallelism(&descriptor.command, record.parallelism);
|
|
let payload = deploy_payload_json(
|
|
&record,
|
|
"wss://relay.example".to_string(),
|
|
DeployProjections {
|
|
effective_model: None,
|
|
effective_provider: None,
|
|
effective_prompt: None,
|
|
effective_parallelism,
|
|
owner_only_access: false,
|
|
},
|
|
BTreeMap::new(),
|
|
launch.clone(),
|
|
);
|
|
|
|
assert_eq!(
|
|
launch["policy_env"]["BUZZ_ACP_AGENTS"],
|
|
cap.to_string(),
|
|
"launch.policy_env[BUZZ_ACP_AGENTS] must be {cap} for explicit OpenClaw override"
|
|
);
|
|
assert_eq!(
|
|
payload["parallelism"], cap,
|
|
"legacy top-level parallelism must match launch.policy_env — both must be {cap}"
|
|
);
|
|
}
|
|
}
|