Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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 @@ -1360,7 +1360,7 @@ pub async fn delete_managed_agent(
mod deploy;
use deploy::build_deploy_payload;
#[cfg(test)]
use deploy::deploy_payload_json;
use deploy::{deploy_payload_json, deploy_payload_json_for_current_build};
#[cfg(test)]
use deploy::{ensure_remote_provider_supported, resolve_deploy_model_provider};

Expand Down
32 changes: 29 additions & 3 deletions desktop/src-tauri/src/commands/agents_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub(super) fn build_deploy_payload(
&owner_pubkey,
);

Ok(deploy_payload_json(
Ok(deploy_payload_json_for_current_build(
record,
crate::relay::effective_agent_relay_url(
&record.relay_url,
Expand All @@ -155,8 +155,31 @@ pub(super) fn build_deploy_payload(
))
}

/// Serialize a deploy payload using this build's managed-agent access policy.
pub(super) fn deploy_payload_json_for_current_build(
record: &ManagedAgentRecord,
relay_url: String,
effective_model: Option<String>,
effective_provider: Option<String>,
effective_prompt: Option<String>,
merged_env: BTreeMap<String, String>,
launch: serde_json::Value,
) -> serde_json::Value {
deploy_payload_json(
record,
relay_url,
effective_model,
effective_provider,
effective_prompt,
merged_env,
launch,
crate::managed_agents::owner_only_access_build(),
)
}

/// Pure serialization half of [`build_deploy_payload`]. Legacy top-level fields
/// remain for display/bookkeeping; providers execute the resolved `launch` block.
#[allow(clippy::too_many_arguments)]
pub(super) fn deploy_payload_json(
record: &ManagedAgentRecord,
relay_url: String,
Expand All @@ -165,7 +188,10 @@ pub(super) fn deploy_payload_json(
effective_prompt: Option<String>,
merged_env: BTreeMap<String, String>,
launch: serde_json::Value,
owner_only_access: bool,
) -> serde_json::Value {
let (respond_to, respond_to_allowlist) =
crate::managed_agents::projected_access_with_policy(record, owner_only_access);
serde_json::json!({
"name": &record.name,
"relay_url": relay_url,
Expand All @@ -180,8 +206,8 @@ pub(super) fn deploy_payload_json(
"idle_timeout_seconds": record.idle_timeout_seconds,
"max_turn_duration_seconds": record.max_turn_duration_seconds,
"parallelism": record.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
108 changes: 108 additions & 0 deletions desktop/src-tauri/src/commands/agents_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,24 @@ fn legacy_avatar_empty_when_nothing_resolves() {

// ── Provider deploy payload completeness ─────────────────────────────────────

fn deploy_payload_for_policy(
record: &ManagedAgentRecord,
owner_only_access: bool,
) -> serde_json::Value {
deploy_payload_json(
record,
"wss://relay.example".to_string(),
Some("gpt-x".to_string()),
Some("openai".to_string()),
None,
std::collections::BTreeMap::new(),
// Access projection is the subject here; the launch block is exercised
// by the shared provider fixture test below.
serde_json::Value::Null,
owner_only_access,
)
}

/// The shared provider fixture is the contract arbiter: it must be the exact
/// richest deploy request produced by the real desktop serializers.
#[test]
Expand Down Expand Up @@ -470,6 +488,9 @@ fn deploy_payload_matches_the_shared_full_launch_fixture() {
None,
std::collections::BTreeMap::from([("USER_KEY".into(), "user-value".into())]),
launch,
// Fixture asserts the record's own access fields survive, so the
// owner-only projection must be off for this comparison.
false,
);

assert_eq!(
Expand Down Expand Up @@ -501,3 +522,90 @@ fn tauri_platform_configs_bundle_kubernetes_only_on_supported_hosts() {
);
}
}

#[test]
fn current_build_deploy_payload_forwards_compiled_policy() {
use crate::managed_agents::{BackendKind, RespondTo};

let expected_owner_only = match std::env::var("BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY") {
Ok(value) => value
.parse::<bool>()
.expect("BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY must be true or false"),
Err(std::env::VarError::NotPresent)
if !crate::managed_agents::owner_only_access_build() =>
{
false
}
Err(std::env::VarError::NotPresent) => {
panic!(
"BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY must be set for owner-only-access-build tests"
)
}
Err(std::env::VarError::NotUnicode(_)) => {
panic!("BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY must be valid UTF-8")
}
};
let mut record = bare_agent_record(None, None, None);
record.backend = BackendKind::Provider {
id: "provider".to_string(),
config: serde_json::json!({}),
};
record.respond_to = RespondTo::Anyone;
record.respond_to_allowlist = vec!["a".repeat(64)];

let payload = deploy_payload_json_for_current_build(
&record,
"wss://relay.example".to_string(),
None,
None,
None,
std::collections::BTreeMap::new(),
// The compiled access policy is the subject here; the launch block is
// exercised by the shared provider fixture test above.
serde_json::Value::Null,
);
let expected_mode = if expected_owner_only {
"owner-only"
} else {
"anyone"
};

assert_eq!(
payload["respond_to"], expected_mode,
"current-build deploy payload did not forward the compiled policy",
);
let expected_allowlist = if expected_owner_only {
serde_json::json!([])
} else {
serde_json::json!(["a".repeat(64)])
};
assert_eq!(
payload["respond_to_allowlist"], expected_allowlist,
"current-build deploy payload did not apply the compiled policy to the stale allowlist",
);
}

#[test]
fn owner_only_access_deploy_payload_clamps_stale_access() {
use crate::managed_agents::{BackendKind, RespondTo};

let mut record = bare_agent_record(None, None, None);
record.backend = BackendKind::Provider {
id: "provider".to_string(),
config: serde_json::json!({}),
};
record.respond_to = RespondTo::Anyone;
record.respond_to_allowlist = vec!["a".repeat(64)];

let payload = deploy_payload_for_policy(&record, true);

assert_eq!(
payload["respond_to"], "owner-only",
"owner-only-access deploy payload widened stale access"
);
assert_eq!(
payload["respond_to_allowlist"],
serde_json::json!([]),
"owner-only-access deploy payload retained a stale allowlist"
);
}
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod agent_access;
mod agent_auth;
mod agent_config;
mod agent_discovery;
Expand Down Expand Up @@ -63,6 +64,7 @@ mod window_vibrancy;
mod workflows;
mod workspace;

pub use agent_access::*;
pub use agent_auth::*;
pub use agent_config::*;
pub use agent_discovery::*;
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ pub fn run() {
get_managed_agent_log,
get_agent_models,
discover_agent_models,
agent_access_owner_only,
get_agent_config_surface,
get_runtime_file_config,
get_baked_build_env_keys,
Expand Down
Loading
Loading