Skip to content
Merged
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
21 changes: 17 additions & 4 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,10 @@ desktop-tauri-test: _ensure-sidecar-stubs
desktop-terminal-performance-test:
cargo test --manifest-path desktop/src-tauri/crates/buzz-terminal/Cargo.toml --release --test latency g3_renderer_acquire_stays_within_frame_budget -- --ignored --exact --nocapture

# Verify compiled-flag behavior under both compile states (clean + internal).
# Runs the auto-connect compiled-flag test twice with independently supplied
# expected values; build.rs rerun-if-env-changed triggers recompilation.
# Verify compiled-flag behavior under both compile states (clean + capability set).
# Runs the auto-connect and owner-only access focused tests twice with
# independently supplied expected values; build.rs rerun-if-env-changed
# triggers recompilation.
desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
Expand All @@ -223,10 +224,22 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
env -u BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY \
BUZZ_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY=false \
cargo test compiled_flag_matches_expected -- --ignored --nocapture
echo "=== Internal build (flag set) → expect true ==="
env -u BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=false \
cargo test --lib
env -u BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=false \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
echo "=== Internal build (flags set) → expect true ==="
BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY=1 \
BUZZ_TEST_EXPECTED_AUTO_CONNECT_DEFAULT_RELAY=true \
cargo test compiled_flag_matches_expected -- --ignored --nocapture
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test --lib
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
echo "Both compiled states verified."

# Build the full desktop Tauri app locally (unsigned, for testing)
Expand Down
24 changes: 24 additions & 0 deletions desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Shared schema, included from the same source the runtime command parses with,
// so the build-time validation below and the runtime parse cannot drift.
include!("src/commands/reconnect_hook_config.rs");
// Same source of truth the runtime filters with, so a baked build env cannot
// carry a reserved key the runtime believes it already rejected.
include!("src/managed_agents/reserved_env_keys.rs");

use base64::Engine as _;

Expand All @@ -13,9 +16,16 @@ fn main() {
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_BUZZ_AGENT_MODEL");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ENV");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY");
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");

// Explicit owner-only agent-access capability. Release packaging sets this
// presence-only marker; OSS/custom builds leave agent access configurable.
if std::env::var("BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY").is_ok() {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AGENT_ACCESS_OWNER_ONLY=1");
}

if let Ok(relay_url) = std::env::var("BUZZ_RELAY_URL") {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_RELAY_URL={relay_url}");
}
Expand Down Expand Up @@ -59,6 +69,20 @@ fn main() {
line
);
}
// The baked env is written into every spawned agent's environment
// LAST (see `managed_agents/runtime.rs`), after Buzz sets the
// access gates and identity vars. A baked reserved key would
// therefore silently override the gate the UI promises, so reject
// it at build time instead of shipping a binary that bypasses its
// own enforcement.
if is_reserved_env_key(key) {
panic!(
"BUZZ_BUILD_AGENT_ENV line {}: `{}` is reserved by Buzz and cannot be baked \
into a build (it would override Buzz's own identity/access env)",
line_no + 1,
key
);
}
}
let encoded = base64::engine::general_purpose::STANDARD.encode(raw.as_bytes());
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AGENT_ENV={encoded}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,10 @@ fn child_shell_is_the_resolved_shell_not_the_inherited_one() {
/// grows a new secret, this points at the file to update.
#[test]
fn reserved_keys_are_covered() {
let source = include_str!("../../../src/managed_agents/env_vars.rs");
// The list lives in its own file because `build.rs` `include!`s the same
// source (see `managed_agents/reserved_env_keys.rs`); read it there rather
// than through the module that includes it.
let source = include_str!("../../../src/managed_agents/reserved_env_keys.rs");
let declared: Vec<&str> = source
.lines()
.skip_while(|line| !line.contains("RESERVED_ENV_KEYS"))
Expand Down
18 changes: 18 additions & 0 deletions desktop/src-tauri/src/commands/agent_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// Return whether this build enforces owner-only managed-agent access.
#[tauri::command]
pub fn agent_access_owner_only() -> bool {
crate::managed_agents::owner_only_access_build()
}

#[cfg(test)]
mod tests {
#[test]
#[ignore = "requires BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY"]
fn compiled_policy_matches_expected() {
let expected = std::env::var("BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY")
.expect("BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY must be set")
.parse::<bool>()
.expect("BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY must be true or false");
assert_eq!(super::agent_access_owner_only(), expected);
}
}
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,9 +1355,9 @@ pub async fn delete_managed_agent(
// 2. Harness sees it, exits gracefully, sets presence to "offline"
// 3. Desktop's existing presence polling sees "offline" — UI updates automatically
// No backend Tauri command needed. Presence IS the status.

#[path = "agents_deploy.rs"]
mod deploy;
pub(super) mod provider_access;
use deploy::build_deploy_payload;
#[cfg(test)]
use deploy::{deploy_payload_json, DeployProjections};
Expand Down
196 changes: 196 additions & 0 deletions desktop/src-tauri/src/commands/agents/provider_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//! Upgrade reconciliation for provider-backed managed-agent access.

use tauri::AppHandle;

use crate::{
app_state::AppState,
managed_agents::{
find_managed_agent_mut, load_managed_agents, save_managed_agents, BackendKind,
ManagedAgentRecord,
},
util::now_iso,
};

pub(super) fn needs_reconciliation_with_policy(
record: &ManagedAgentRecord,
owner_only_access: bool,
) -> bool {
owner_only_access && record.backend != BackendKind::Local && record.backend_agent_id.is_some()
}

#[derive(Debug)]
struct ProviderAccessTarget {
pubkey: String,
provider_id: String,
config: serde_json::Value,
cached_binary_path: Option<String>,
agent_json: Result<serde_json::Value, String>,
}

fn collect_targets_with(
records: Vec<ManagedAgentRecord>,
owner_only_access: bool,
mut build_payload: impl FnMut(&ManagedAgentRecord) -> Result<serde_json::Value, String>,
) -> Vec<ProviderAccessTarget> {
records
.into_iter()
.filter(|record| needs_reconciliation_with_policy(record, owner_only_access))
.map(|record| match record.backend.clone() {
BackendKind::Provider { id, config } => ProviderAccessTarget {
agent_json: build_payload(&record),
pubkey: record.pubkey,
provider_id: id,
config,
cached_binary_path: record.provider_binary_path,
},
BackendKind::Local => {
unreachable!("provider access reconciliation selected a local agent")
}
})
.collect()
}

/// Redeploy every existing provider agent in an owner-only access build.
///
/// The saved `backend_agent_id` only proves that some provider deployment
/// exists. A marked build sends the current owner-only payload before each
/// community UI load. Workspace apply fails closed if any provider rejects it.
pub(crate) async fn reconcile_on_workspace_apply(
app: &AppHandle,
state: &AppState,
) -> Result<(), String> {
if !crate::managed_agents::owner_only_access_build() {
return Ok(());
}

let targets = {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
collect_targets_with(load_managed_agents(app)?, true, |record| {
super::build_deploy_payload(app, state, record)
})
};

for target in targets {
let ProviderAccessTarget {
pubkey,
provider_id,
config,
cached_binary_path,
agent_json,
} = target;
let agent_json = match agent_json {
Ok(agent_json) => agent_json,
Err(error) => {
persist_failure(app, state, &pubkey, &error)?;
return Err(format!(
"provider access reconciliation failed for agent {pubkey}: {error}"
));
}
};
if let Err(error) = super::deploy_to_provider(
app,
state,
&pubkey,
&provider_id,
&config,
agent_json,
cached_binary_path.as_deref(),
)
.await
{
return Err(format!(
"provider access reconciliation failed for agent {pubkey}: {error}"
));
}
}

Ok(())
}

fn persist_failure(
app: &AppHandle,
state: &AppState,
pubkey: &str,
error: &str,
) -> Result<(), String> {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|lock_error| lock_error.to_string())?;
let mut records = load_managed_agents(app)?;
let record = find_managed_agent_mut(&mut records, pubkey)?;
record.last_error = Some(error.to_string());
record.updated_at = now_iso();
save_managed_agents(app, &records)
}

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

fn record(backend: BackendKind, backend_agent_id: Option<&str>) -> ManagedAgentRecord {
let mut record: ManagedAgentRecord = serde_json::from_value(serde_json::json!({
"pubkey": "agent", "name": "Agent", "relay_url": "", "acp_command": "",
"agent_command": "", "agent_args": [], "mcp_command": "",
"turn_timeout_seconds": 0, "system_prompt": null, "created_at": "",
"updated_at": "", "last_started_at": null, "last_stopped_at": null,
"last_exit_code": null, "last_error": null
}))
.unwrap();
record.backend = backend;
record.backend_agent_id = backend_agent_id.map(str::to_string);
record
}

#[test]
fn upgrade_collects_existing_provider_and_builds_projected_payload() {
let records = vec![
record(
BackendKind::Provider {
id: "provider".into(),
config: serde_json::json!({"region": "test"}),
},
Some("existing"),
),
record(
BackendKind::Provider {
id: "not-deployed".into(),
config: serde_json::json!({}),
},
None,
),
record(BackendKind::Local, Some("stale")),
];

let targets = collect_targets_with(records, true, |_| {
Ok(serde_json::json!({"respond_to": "owner-only"}))
});

assert_eq!(targets.len(), 1);
assert_eq!(targets[0].pubkey, "agent");
assert_eq!(targets[0].provider_id, "provider");
assert_eq!(targets[0].config["region"], "test");
assert_eq!(
targets[0].agent_json.as_ref().unwrap()["respond_to"],
"owner-only"
);
}

#[test]
fn unmarked_build_collects_no_upgrade_targets() {
let records = vec![record(
BackendKind::Provider {
id: "provider".into(),
config: serde_json::json!({}),
},
Some("existing"),
)];

assert!(
collect_targets_with(records, false, |_| { Ok(serde_json::Value::Null) }).is_empty()
);
}
}
16 changes: 12 additions & 4 deletions desktop/src-tauri/src/commands/agents_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub(super) struct DeployProjections {
/// 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.
Expand Down Expand Up @@ -170,6 +172,7 @@ pub(super) fn build_deploy_payload(
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,
Expand All @@ -179,15 +182,17 @@ pub(super) fn build_deploy_payload(
/// 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"]` — the two fields are
/// always consistent regardless of stale `record.agent_command` pins.
/// 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,
Expand All @@ -204,8 +209,8 @@ pub(super) fn deploy_payload_json(
// 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": record.respond_to,
"respond_to_allowlist": &record.respond_to_allowlist,
"respond_to": respond_to,
"respond_to_allowlist": respond_to_allowlist,
"env_vars": merged_env,
"launch": launch,
})
Expand Down Expand Up @@ -363,6 +368,7 @@ mod tests {
effective_provider: None,
effective_prompt: None,
effective_parallelism,
owner_only_access: false,
},
BTreeMap::new(),
launch.clone(),
Expand Down Expand Up @@ -407,6 +413,7 @@ mod tests {
effective_provider: None,
effective_prompt: None,
effective_parallelism,
owner_only_access: false,
},
BTreeMap::new(),
launch.clone(),
Expand Down Expand Up @@ -452,6 +459,7 @@ mod tests {
effective_provider: None,
effective_prompt: None,
effective_parallelism,
owner_only_access: false,
},
BTreeMap::new(),
launch.clone(),
Expand Down
Loading