Skip to content
Closed
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
7 changes: 7 additions & 0 deletions desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@ fn main() {
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_RELAY_RECONNECT_CMD");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_OBSERVER_ARCHIVE_DEFAULT");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AGENT_METRIC_ARCHIVE_DEFAULT");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_INTERNAL");
println!("cargo:rerun-if-env-changed=BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY");
println!("cargo:rustc-check-cfg=cfg(buzz_updater_enabled)");

// Explicit distribution identity. Internal packaging sets this presence-only
// marker; OSS/custom builds remain public regardless of baked defaults.
if std::env::var("BUZZ_BUILD_INTERNAL").is_ok() {
println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_INTERNAL=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
33 changes: 11 additions & 22 deletions desktop/src-tauri/src/commands/agent_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,12 @@ async fn discover_databricks_models(
}))
}

/// Return whether this build enforces owner-only managed-agent access.
#[tauri::command]
pub fn agent_access_owner_only() -> bool {
crate::managed_agents::internal_build()
}

/// Apply an `UpdateManagedAgentRequest`'s model/provider/system_prompt patch
/// to `record`, enforcing the linked-instance write guard: a definition-linked
/// record's model/provider/prompt are definition-authoritative (see
Expand Down Expand Up @@ -906,28 +912,11 @@ pub async fn update_managed_agent(
record.relay_mesh = Some(crate::managed_agents::RelayMeshConfig { model_ref });
}

// Inbound author gate: merge patch onto current values, then validate
// the merged state. This lets a single update switch to Allowlist AND
// supply pubkeys atomically.
let prospective_mode = input.respond_to.unwrap_or(record.respond_to);
let prospective_allowlist = match input.respond_to_allowlist.as_ref() {
Some(list) => crate::managed_agents::validate_respond_to_allowlist(list)?,
None => record.respond_to_allowlist.clone(),
};
if prospective_mode == crate::managed_agents::RespondTo::Allowlist
&& prospective_allowlist.is_empty()
{
return Err(
"respond-to mode 'allowlist' requires at least one pubkey in the allowlist"
.to_string(),
);
}
record.respond_to = prospective_mode;
// Preserve the persisted allowlist across mode toggles — only replace
// when the caller explicitly supplied a new list.
if input.respond_to_allowlist.is_some() {
record.respond_to_allowlist = prospective_allowlist;
}
crate::managed_agents::apply_update_access(
record,
input.respond_to,
input.respond_to_allowlist.as_deref(),
)?;

record.updated_at = now_iso();

Expand Down
23 changes: 6 additions & 17 deletions desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,22 +584,11 @@ pub async fn create_managed_agent(
}
crate::managed_agents::validate_user_env_keys(&input.env_vars)?;

// Validate & normalize the respond-to allowlist BEFORE any side effects.
// The harness has its own validator (buzz-acp/src/config.rs) but we want
// to catch malformed input at the boundary so the agent never tries to
// start with a list that will crash it on launch. The mode/allowlist
// pairing (and the definition-default fallback) is resolved later at the
// mint site via `resolve_mint_behavioral_defaults`, where the linked
// definition is in hand.
let respond_to_allowlist =
crate::managed_agents::validate_respond_to_allowlist(&input.respond_to_allowlist)?;
if input.respond_to == Some(crate::managed_agents::RespondTo::Allowlist)
&& respond_to_allowlist.is_empty()
{
return Err(
"respond-to mode 'allowlist' requires at least one pubkey in the allowlist".to_string(),
);
}
let (requested_respond_to, respond_to_allowlist) =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the round-1 subtlety is now doubly protected: resolve_create_access returns an explicit Some(OwnerOnly) for internal local creates (overriding any definition default in resolve_mint_behavioral_defaults), AND definitions themselves are normalized to None in internal builds — so the mint can no longer inherit a widened definition default through either route.

crate::managed_agents::resolve_create_access(
input.respond_to,
&input.respond_to_allowlist,
)?;

// Snapshot the workspace owner pubkey for the legacy-record auth_tag
// fallback. Computed outside the records lock to keep lock ordering simple.
Expand Down Expand Up @@ -823,7 +812,7 @@ pub async fn create_managed_agent(
// point for definition behavioral strings — fails loudly on a bad
// mode/range instead of minting an agent the author didn't describe.
let minted = crate::managed_agents::resolve_mint_behavioral_defaults(
input.respond_to,
requested_respond_to,
respond_to_allowlist.clone(),
input.parallelism,
linked_persona.as_ref(),
Expand Down
8 changes: 6 additions & 2 deletions desktop/src-tauri/src/commands/agents_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub(super) fn build_deploy_payload(
effective_provider,
effective_prompt,
merged_env,
crate::managed_agents::internal_build(),
))
}

Expand All @@ -112,7 +113,10 @@ pub(super) fn deploy_payload_json(
effective_provider: Option<String>,
effective_prompt: Option<String>,
merged_env: std::collections::BTreeMap<String, String>,
internal: bool,
) -> serde_json::Value {
let (respond_to, respond_to_allowlist) =
crate::managed_agents::projected_access_with_policy(record, internal);
serde_json::json!({
"name": &record.name,
"relay_url": relay_url,
Expand All @@ -127,8 +131,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,
})
}
46 changes: 38 additions & 8 deletions desktop/src-tauri/src/commands/agents_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,18 @@ fn legacy_avatar_empty_when_nothing_resolves() {

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

fn deploy_payload_for_policy(record: &ManagedAgentRecord, internal: 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(),
internal,
)
}

/// Regression (PR #1667 review, Thufir): the provider deploy payload must
/// carry every behavioral field the local spawn path applies — a field
/// missing here silently strips it from provider-backed agents.
Expand Down Expand Up @@ -429,14 +441,7 @@ fn deploy_payload_carries_the_full_behavioral_quad() {
))
.expect("sample record");

let payload = deploy_payload_json(
&record,
"wss://relay.example".to_string(),
Some("gpt-x".to_string()),
Some("openai".to_string()),
None,
std::collections::BTreeMap::new(),
);
let payload = deploy_payload_for_policy(&record, false);

assert_eq!(payload["parallelism"], 4);
assert_eq!(payload["respond_to"], "allowlist");
Expand All @@ -445,3 +450,28 @@ fn deploy_payload_carries_the_full_behavioral_quad() {
assert_eq!(payload["provider"], "openai");
assert_eq!(payload["relay_url"], "wss://relay.example");
}

#[test]
fn internal_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",
"internal deploy payload widened stale access"
);
assert_eq!(
payload["respond_to_allowlist"],
serde_json::json!([]),
"internal deploy payload retained a stale allowlist"
);
}
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/personas/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub async fn create_persona(
updated_at: now,
};
apply_persona_behavior(&mut persona, input.behavior)?;
crate::managed_agents::normalize_definition_access(&mut persona);
personas.push(persona.clone());
save_personas(&app, &personas)?;
retain_persona_pending(&app, &state, &persona);
Expand Down
21 changes: 20 additions & 1 deletion desktop/src-tauri/src/commands/personas/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,14 @@ fn event_d_tag(event: &nostr::Event) -> Result<String, String> {
/// `persona_from_event` sets `id = d_tag`, an in-app persona reuses its d-tag as
/// the id and a re-received event stays idempotent (no duplicate row).
fn apply_inbound_persona(personas: &mut Vec<AgentDefinition>, inbound: AgentDefinition) {
apply_inbound_persona_with_policy(personas, inbound, crate::managed_agents::internal_build());
}

fn apply_inbound_persona_with_policy(
personas: &mut Vec<AgentDefinition>,
inbound: AgentDefinition,
internal: bool,
) {
let d_tag = persona_d_tag(&inbound);
match personas
.iter_mut()
Expand All @@ -357,9 +365,19 @@ fn apply_inbound_persona(personas: &mut Vec<AgentDefinition>, inbound: AgentDefi
local.respond_to_allowlist = inbound.respond_to_allowlist;
local.parallelism = inbound.parallelism;
local.shared = inbound.shared;
crate::managed_agents::access_policy::normalize_definition_access_with_policy(
local, internal,
);
local.updated_at = inbound.updated_at;
}
None => personas.push(inbound),
None => {
let mut inbound = inbound;
crate::managed_agents::access_policy::normalize_definition_access_with_policy(
&mut inbound,
internal,
);
personas.push(inbound);
}
}
}

Expand Down Expand Up @@ -401,6 +419,7 @@ fn apply_inbound_managed_agent(
local.parallelism = inbound.parallelism;
local.respond_to = inbound.respond_to;
local.respond_to_allowlist = inbound.respond_to_allowlist;
crate::managed_agents::normalize_managed_agent_access(local);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use super::*;
use std::collections::BTreeMap;

const UUID: &str = "11111111-2222-3333-4444-555555555555";
const UUID: &str = "11111111-2222-3333-4444-555555555555"; // sadscan:disable sq.pii.cc.visa

/// A local in-app persona: `source_team_persona_slug` is None, so its d-tag
/// IS its UUID id. Carries env_vars + source_team that must survive a patch.
Expand Down Expand Up @@ -151,6 +151,27 @@ fn no_local_match_inserts_inbound_reusing_d_tag_as_id() {
assert_eq!(personas.len(), 2, "re-receive of inserted record no-ops");
}

#[test]
fn internal_policy_clamps_inbound_persona_insert() {
let d_tag = "99999999-8888-7777-6666-555555555555";
let mut inbound = inbound_for(d_tag, "New");
inbound.respond_to = Some("anyone".to_string());
inbound.respond_to_allowlist = vec!["a".repeat(64)];
let mut personas = Vec::new();

apply_inbound_persona_with_policy(&mut personas, inbound, true);

let inserted = personas.first().expect("inbound persona inserted");
assert_eq!(
inserted.respond_to, None,
"internal inbound persona insert retained stale access"
);
assert!(
inserted.respond_to_allowlist.is_empty(),
"internal inbound persona insert retained a stale allowlist"
);
}

// ── Managed-agent (30177) inbound ────────────────────────────────────────

const AGENT_PUBKEY: &str = "agentpubkeyhex0000000000000000000000000000000000000000000000000000";
Expand Down
6 changes: 4 additions & 2 deletions desktop/src-tauri/src/commands/personas/snapshot/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ pub async fn confirm_agent_snapshot_import(
let persona_id = uuid::Uuid::new_v4().to_string();

// Build persona from snapshot definition.
let persona = AgentDefinition {
let mut persona = AgentDefinition {
id: persona_id.clone(),
display_name: display_name.clone(),
avatar_url: effective_avatar.clone(),
Expand All @@ -470,6 +470,7 @@ pub async fn confirm_agent_snapshot_import(
created_at: now.clone(),
updated_at: now.clone(),
};
crate::managed_agents::normalize_definition_access(&mut persona);

personas.push(persona.clone());
save_personas(&app, &personas)?;
Expand All @@ -479,7 +480,7 @@ pub async fn confirm_agent_snapshot_import(

// Build the managed agent record — no machine-local commands, no
// secrets, no lineage from the snapshot.
let record = ManagedAgentRecord {
let mut record = ManagedAgentRecord {
pubkey: pubkey.clone(),
name: display_name.clone(),
display_name: None,
Expand Down Expand Up @@ -540,6 +541,7 @@ pub async fn confirm_agent_snapshot_import(
runtime: snapshot.definition.runtime.clone(),
name_pool: snapshot.definition.name_pool.clone(),
};
crate::managed_agents::normalize_managed_agent_access(&mut record);

records.push(record.clone());
save_managed_agents(&app, &records)?;
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/personas/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub(super) async fn update_persona_with<R: Send + 'static>(
persona.env_vars = env_vars;
}
apply_persona_behavior(persona, input.behavior)?;
crate::managed_agents::normalize_definition_access(persona);
persona.updated_at = now_iso();

let result = persona.clone();
Expand Down
9 changes: 6 additions & 3 deletions desktop/src-tauri/src/commands/team_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn definition_from_snapshot(
let respond_to = (behavior.respond_to != crate::managed_agents::RespondTo::default())
.then(|| behavior.respond_to.as_str().to_string());

Ok(AgentDefinition {
let mut definition = AgentDefinition {
id: Uuid::new_v4().to_string(),
display_name: member.profile.display_name.trim().to_string(),
avatar_url: effective_avatar(member),
Expand All @@ -139,7 +139,9 @@ fn definition_from_snapshot(
parallelism: behavior.parallelism,
created_at: now.to_string(),
updated_at: now.to_string(),
})
};
crate::managed_agents::normalize_definition_access(&mut definition);
Ok(definition)
}

pub(crate) fn build_import_definitions(
Expand Down Expand Up @@ -549,7 +551,7 @@ pub async fn confirm_team_snapshot_import(
};

// Build the ManagedAgentRecord for this member.
let record = ManagedAgentRecord {
let mut record = ManagedAgentRecord {
pubkey: pubkey.clone(),
name: display_name.clone(),
display_name: None,
Expand Down Expand Up @@ -612,6 +614,7 @@ pub async fn confirm_team_snapshot_import(
runtime: member.definition.runtime.clone(),
name_pool: member.definition.name_pool.clone(),
};
crate::managed_agents::normalize_managed_agent_access(&mut record);

minted.push(MintedMember {
definition,
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 @@ -817,6 +817,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