fix(desktop): enforce the native Bestie capability boundary

On-behalf-of: mahanti <arjun.mahanti@gmail.com>
Signed-off-by: Fizz <fizz@buzz.local>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Fizz
2026-08-28 10:44:00 -04:00
parent 38115b7aec
commit b04317f46a
6 changed files with 188 additions and 9 deletions

View File

@@ -245,6 +245,9 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
env -u BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=false \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
env -u VITE_BUZZ_BESTIE \
BUZZ_TEST_EXPECTED_BESTIE=false \
cargo test compiled_bestie_flag_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 \
@@ -255,6 +258,9 @@ desktop-tauri-test-compiled-flags: _ensure-sidecar-stubs
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY=1 \
BUZZ_TEST_EXPECTED_AGENT_ACCESS_OWNER_ONLY=true \
cargo test compiled_policy_matches_expected -- --ignored --nocapture
VITE_BUZZ_BESTIE=1 \
BUZZ_TEST_EXPECTED_BESTIE=true \
cargo test compiled_bestie_flag_matches_expected -- --ignored --nocapture
echo "Both compiled states verified."
# Build the full desktop Tauri app locally (unsigned, for testing)

View File

@@ -9,6 +9,7 @@ use crate::managed_agents::{
AgentSnapshot, AgentSnapshotDefinition, AgentSnapshotMemory, AgentSnapshotMemoryEntry,
AgentSnapshotProfile, FORMAT_DISCRIMINATOR, FORMAT_VERSION,
},
storage::{filter_agent_definitions_for_build, filter_managed_agents_for_build},
BackendKind, ManagedAgentRecord, RespondTo,
};
use std::collections::BTreeMap;
@@ -180,6 +181,25 @@ fn resolve_unknown_id_returns_error() {
assert!(result.unwrap_err().contains("ghost"));
}
#[test]
fn capability_off_shared_snapshot_and_card_lookup_rejects_known_bestie_ids() {
let mut instances = vec![
make_instance("bestie-pubkey", "builtin:bestie"),
make_instance("fizz-pubkey", "builtin:fizz"),
];
let mut definitions = vec![
make_definition("builtin:bestie"),
make_definition("builtin:fizz"),
];
filter_managed_agents_for_build(&mut instances, false);
filter_agent_definitions_for_build(&mut definitions, false);
assert!(resolve_from_lists("bestie-pubkey", &instances, &definitions).is_err());
assert!(resolve_from_lists("builtin:bestie", &instances, &definitions).is_err());
assert!(resolve_from_lists("fizz-pubkey", &instances, &definitions).is_ok());
assert!(resolve_from_lists("builtin:fizz", &instances, &definitions).is_ok());
}
// ── Validator fail-closed cases ───────────────────────────────────────────
#[test]

View File

@@ -94,7 +94,7 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[
},
];
fn bestie_build_enabled() -> bool {
pub(crate) fn bestie_build_enabled() -> bool {
option_env!("VITE_BUZZ_BESTIE") == Some("1")
}
@@ -195,7 +195,7 @@ fn built_in_order(id: &str) -> Option<usize> {
.position(|persona| persona.id == id)
}
fn persona_available_in_build(id: &str, include_bestie: bool) -> bool {
pub(crate) fn persona_available_in_build(id: &str, include_bestie: bool) -> bool {
include_bestie || id != BESTIE_PERSONA_ID
}

View File

@@ -1,6 +1,6 @@
use super::{
built_in_persona_records, built_in_persona_records_for_build, definitions_for_save,
ensure_persona_ids_are_active, ensure_persona_is_active, merge_personas,
bestie_build_enabled, built_in_persona_records, built_in_persona_records_for_build,
definitions_for_save, ensure_persona_ids_are_active, ensure_persona_is_active, merge_personas,
merge_personas_for_build, migrate_retired_personas, validate_persona_activation_change,
validate_persona_deletion, visible_personas_for_build, BUILT_IN_PERSONAS, RETIRED_PERSONAS,
};
@@ -35,7 +35,7 @@ fn custom_persona(id: &str, display_name: &str) -> AgentDefinition {
#[test]
fn merge_personas_adds_missing_built_ins() {
let (records, changed) = merge_personas(Vec::new(), "2026-03-19T00:00:00Z");
let (records, changed) = merge_personas_for_build(Vec::new(), "2026-03-19T00:00:00Z", false);
assert!(changed);
assert_eq!(records.len(), BUILT_IN_PERSONAS.len() - 1);
@@ -76,6 +76,16 @@ fn bestie_persona_requires_the_internal_build_capability() {
assert!(bestie.is_active);
}
#[test]
#[ignore = "run explicitly in both compiled feature states"]
fn compiled_bestie_flag_matches_expected() {
let expected = std::env::var("BUZZ_TEST_EXPECTED_BESTIE")
.expect("BUZZ_TEST_EXPECTED_BESTIE must be set")
.parse::<bool>()
.expect("BUZZ_TEST_EXPECTED_BESTIE must be true or false");
assert_eq!(bestie_build_enabled(), expected);
}
#[test]
fn ineligible_build_hides_bestie_without_demoting_it() {
let now = "2026-03-19T00:00:00Z";

View File

@@ -271,16 +271,57 @@ pub fn load_managed_agents<R: tauri::Runtime>(
app: &AppHandle<R>,
) -> Result<Vec<ManagedAgentRecord>, String> {
let mut records = load_agent_store(app)?;
records.retain(|record| !record.pubkey.is_empty());
filter_managed_agents_for_build(
&mut records,
crate::managed_agents::personas::bestie_build_enabled(),
);
hydrate_keys(&mut records);
Ok(records)
}
pub(crate) fn filter_managed_agents_for_build(
records: &mut Vec<ManagedAgentRecord>,
include_bestie: bool,
) {
records.retain(|record| {
!record.pubkey.is_empty()
&& record.persona_id.as_deref().is_none_or(|persona_id| {
crate::managed_agents::personas::persona_available_in_build(
persona_id,
include_bestie,
)
})
});
}
/// Load the key-less agent *definitions* (former personas) from the unified
/// store. The persona compatibility shim (`load_personas`) presents these in
/// the legacy shape via `to_definition_view`.
pub(crate) fn load_agent_definitions<R: tauri::Runtime>(
app: &AppHandle<R>,
) -> Result<Vec<ManagedAgentRecord>, String> {
let mut records = load_agent_definitions_unfiltered(app)?;
filter_agent_definitions_for_build(
&mut records,
crate::managed_agents::personas::bestie_build_enabled(),
);
Ok(records)
}
pub(crate) fn filter_agent_definitions_for_build(
records: &mut Vec<ManagedAgentRecord>,
include_bestie: bool,
) {
records.retain(|record| {
record.pubkey.is_empty()
&& record.slug.as_deref().is_none_or(|slug| {
crate::managed_agents::personas::persona_available_in_build(slug, include_bestie)
})
});
}
pub(crate) fn load_agent_definitions_unfiltered<R: tauri::Runtime>(
app: &AppHandle<R>,
) -> Result<Vec<ManagedAgentRecord>, String> {
let mut records = load_agent_store(app)?;
records.retain(|record| record.pubkey.is_empty());
@@ -376,8 +417,14 @@ pub fn save_managed_agents<R: tauri::Runtime>(
app: &AppHandle<R>,
records: &[ManagedAgentRecord],
) -> Result<(), String> {
let definitions = load_agent_definitions(app).unwrap_or_default();
let mut sorted = records.to_vec();
let existing = load_agent_store(app).unwrap_or_default();
let definitions = existing
.iter()
.filter(|record| record.pubkey.is_empty())
.cloned()
.collect();
let include_bestie = crate::managed_agents::personas::bestie_build_enabled();
let mut sorted = instances_for_save(records, &existing, include_bestie);
// A caller-supplied key-less record would collide with the definition
// half re-read below; instances always carry a pubkey.
sorted.retain(|record| !record.pubkey.is_empty());
@@ -396,6 +443,48 @@ pub fn save_managed_agents<R: tauri::Runtime>(
write_agent_store(app, definitions, sorted)
}
fn instances_for_save(
records: &[ManagedAgentRecord],
existing: &[ManagedAgentRecord],
include_bestie: bool,
) -> Vec<ManagedAgentRecord> {
let mut complete: Vec<_> = records
.iter()
.filter(|record| {
!record.pubkey.is_empty()
&& record.persona_id.as_deref().is_none_or(|persona_id| {
crate::managed_agents::personas::persona_available_in_build(
persona_id,
include_bestie,
)
})
})
.cloned()
.collect();
if !include_bestie {
let hidden: Vec<_> = existing
.iter()
.filter(|record| {
!record.pubkey.is_empty()
&& record.persona_id.as_deref().is_some_and(|persona_id| {
!crate::managed_agents::personas::persona_available_in_build(
persona_id,
include_bestie,
)
})
&& !complete
.iter()
.any(|candidate| candidate.pubkey == record.pubkey)
})
.cloned()
.collect();
complete.extend(hidden);
}
complete
}
/// Save the key-less agent *definitions*, preserving the keyed instances —
/// the definition-side mirror of [`save_managed_agents`].
pub(crate) fn save_agent_definitions<R: tauri::Runtime>(

View File

@@ -12,7 +12,8 @@ use std::path::Path;
use tempfile::NamedTempFile;
use super::{
agent_keyring_name, hydrate_keys_with, migrate_inline_key, persist_agent_keys_with,
agent_keyring_name, filter_agent_definitions_for_build, filter_managed_agents_for_build,
hydrate_keys_with, instances_for_save, migrate_inline_key, persist_agent_keys_with,
KeyMigration, KeyStore, KeyringProbe, ManagedAgentRecord,
};
@@ -143,6 +144,59 @@ fn record_with_pubkey_and_key(pubkey: &str, nsec: &str) -> ManagedAgentRecord {
.expect("sample record")
}
fn linked_record(pubkey: &str, persona_id: &str) -> ManagedAgentRecord {
let mut record = record_with_pubkey_and_key(pubkey, "");
record.persona_id = Some(persona_id.to_string());
record
}
fn definition_record(slug: &str) -> ManagedAgentRecord {
let mut record = record_with_pubkey_and_key("", "");
record.slug = Some(slug.to_string());
record
}
#[test]
fn capability_off_filters_bestie_instances_and_definitions() {
let bestie = linked_record("bestie-pubkey", "builtin:bestie");
let fizz = linked_record("fizz-pubkey", "builtin:fizz");
let custom = linked_record("custom-pubkey", "custom:helper");
let mut instances = vec![bestie.clone(), fizz.clone(), custom.clone()];
filter_managed_agents_for_build(&mut instances, false);
assert_eq!(instances, vec![fizz, custom]);
let bestie_definition = definition_record("builtin:bestie");
let fizz_definition = definition_record("builtin:fizz");
let mut definitions = vec![bestie_definition, fizz_definition.clone()];
filter_agent_definitions_for_build(&mut definitions, false);
assert_eq!(definitions, vec![fizz_definition]);
let mut eligible_instances = vec![bestie];
filter_managed_agents_for_build(&mut eligible_instances, true);
assert_eq!(eligible_instances.len(), 1);
}
#[test]
fn capability_off_save_preserves_hidden_bestie_instance_unchanged() {
let original_bestie = linked_record("bestie-pubkey", "builtin:bestie");
let fizz = linked_record("fizz-pubkey", "builtin:fizz");
let mut forged_bestie = original_bestie.clone();
forged_bestie.name = "forged edit".to_string();
let saved = instances_for_save(
&[fizz.clone(), forged_bestie],
&[fizz.clone(), original_bestie.clone()],
false,
);
assert_eq!(saved.len(), 2);
assert!(saved.contains(&fizz));
assert!(saved.contains(&original_bestie));
assert!(!saved.iter().any(|record| record.name == "forged edit"));
}
#[test]
fn migrate_persists_and_signals_stripping_when_keyring_reachable() {
// Item 2: an inline key (residue from a prior keyring-unreachable save)