mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
fix(desktop): export/card resolves through the 30179 overlay; snapshot respond_to falls back to instance fields
Relay-hosted agents made two gaps in the export/card resolvers user-visible: - Gap B: a relay-only agent (30179 head, never started on this device) has no disk record, so materialize_snapshot_bytes / card_mint_key_status / mint_agent_card failed with "agent not found" until first START. - Gap A: a follower device exported the stale disk snapshot instead of the effective 30179 config it actually runs. Fold the private-config overlay onto the disk list at all three resolver sites — the identical resolution the agent list already uses (resolved_records: applies patches to disk records AND synthesizes relay-only entries). Card mint then also inherits the correct live kind:0 avatar for free, since its existing profile fetch runs after resolution. Also give build_snapshot's respond_to/allowlist the same instance fallback parallelism already has: an overlay-materialized record carries its enforced respond_to only on the instance fields, so without the fallback a relay-hosted agent exported respond_to=None while actually enforcing an allowlist — a shared agent that behaves differently than advertised. Mode and list travel together (definition mode -> definition list, instance fallback -> instance list, never mixed). Deferred as fidelity-not-function (tracked for the relay-hosted cutover): kind:0 avatar hydration for ordinary JSON/PNG manifests, name_pool. Verified: cargo test in desktop/src-tauri — 2283 passed, 0 failed; clippy --tests clean; fmt clean. Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -493,7 +493,14 @@ pub fn card_mint_key_status(
|
|||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Overlay-fold (kind:30179) so the status reflects the agent's effective
|
||||||
|
// config — including relay-only agents with no local record yet.
|
||||||
let instances = load_managed_agents(&app)?;
|
let instances = load_managed_agents(&app)?;
|
||||||
|
let instances = state
|
||||||
|
.private_managed_agent_overlay
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.resolved_records(&instances);
|
||||||
let definitions = load_agent_definitions(&app)?;
|
let definitions = load_agent_definitions(&app)?;
|
||||||
let (record, _) = resolve_from_lists(&id, &instances, &definitions)?;
|
let (record, _) = resolve_from_lists(&id, &instances, &definitions)?;
|
||||||
|
|
||||||
@@ -549,7 +556,15 @@ pub async fn mint_agent_card(
|
|||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Overlay-fold (kind:30179) — same resolution as snapshot export and
|
||||||
|
// the agent list: mint sees effective config, and a relay-only agent
|
||||||
|
// resolves before its first START on this device.
|
||||||
let instances = load_managed_agents(&app)?;
|
let instances = load_managed_agents(&app)?;
|
||||||
|
let instances = state
|
||||||
|
.private_managed_agent_overlay
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.resolved_records(&instances);
|
||||||
let definitions = load_agent_definitions(&app)?;
|
let definitions = load_agent_definitions(&app)?;
|
||||||
let (record, is_definition) =
|
let (record, is_definition) =
|
||||||
resolve_from_lists(&id, &instances, &definitions).map(|(r, d)| (r.clone(), d))?;
|
resolve_from_lists(&id, &instances, &definitions).map(|(r, d)| (r.clone(), d))?;
|
||||||
|
|||||||
@@ -245,7 +245,17 @@ pub(crate) async fn materialize_snapshot_bytes(
|
|||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Fold the kind:30179 overlay onto the disk records (and synthesize
|
||||||
|
// relay-only entries) so export sees the EFFECTIVE config — the same
|
||||||
|
// resolution the agent list uses. Raw disk here would export stale
|
||||||
|
// values on a follower device and fail with "agent not found" for a
|
||||||
|
// relay-only agent that has never been started on this device.
|
||||||
let instances = load_managed_agents(&app)?;
|
let instances = load_managed_agents(&app)?;
|
||||||
|
let instances = state
|
||||||
|
.private_managed_agent_overlay
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.resolved_records(&instances);
|
||||||
let definitions = load_agent_definitions(&app)?;
|
let definitions = load_agent_definitions(&app)?;
|
||||||
let (def_record, is_definition) = resolve_from_lists(&id, &instances, &definitions)
|
let (def_record, is_definition) = resolve_from_lists(&id, &instances, &definitions)
|
||||||
.map(|(r, is_def)| (r.clone(), is_def))?;
|
.map(|(r, is_def)| (r.clone(), is_def))?;
|
||||||
|
|||||||
@@ -161,6 +161,120 @@ fn definition_slug_resolves_to_definition_and_linked_instance_is_valid_memory_so
|
|||||||
|
|
||||||
// ── Resolver edge cases ───────────────────────────────────────────────────
|
// ── Resolver edge cases ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── Overlay-fold resolution (kind:30179) ──────────────────────────────────
|
||||||
|
//
|
||||||
|
// Export resolves against `resolved_records(load_managed_agents(..))`, not
|
||||||
|
// raw disk. These tests pin the two behaviors that fold buys export:
|
||||||
|
// a relay-only agent resolves before its first START on this device, and a
|
||||||
|
// follower exports the effective relay config instead of stale disk values.
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
mod overlay_fold {
|
||||||
|
use super::*;
|
||||||
|
use crate::managed_agents::private_config_overlay::PrivateConfigOverlay;
|
||||||
|
use buzz_core_pkg::private_managed_agent::{
|
||||||
|
Payload, PrivateConfig, PrivateIdentity, FORMAT, VERSION,
|
||||||
|
};
|
||||||
|
use serde_json::{json, Map};
|
||||||
|
|
||||||
|
fn relay_payload(pubkey: &str, name: &str, prompt: &str) -> Payload {
|
||||||
|
Payload {
|
||||||
|
format: FORMAT.into(),
|
||||||
|
version: VERSION,
|
||||||
|
agent_pubkey: pubkey.into(),
|
||||||
|
owner_pubkey: "11".repeat(32),
|
||||||
|
generation: 1,
|
||||||
|
previous_event_id: None,
|
||||||
|
updated_at: "2026-08-07T00:00:00Z".into(),
|
||||||
|
identity: PrivateIdentity {
|
||||||
|
private_key_nsec: "nsec-test".into(),
|
||||||
|
auth_tag: None,
|
||||||
|
},
|
||||||
|
config: PrivateConfig {
|
||||||
|
relay_url: "wss://relay.example".into(),
|
||||||
|
name: name.into(),
|
||||||
|
persona_id: None,
|
||||||
|
runtime: Some("goose".into()),
|
||||||
|
model: Some("relay-model".into()),
|
||||||
|
provider: None,
|
||||||
|
system_prompt: Some(prompt.into()),
|
||||||
|
parallelism: None,
|
||||||
|
respond_to: Some("allowlist".into()),
|
||||||
|
respond_to_allowlist: vec!["ab".repeat(32)],
|
||||||
|
agent_command_override: None,
|
||||||
|
agent_args: vec![],
|
||||||
|
idle_timeout_seconds: None,
|
||||||
|
max_turn_duration_seconds: None,
|
||||||
|
env_vars: BTreeMap::new(),
|
||||||
|
backend: json!({"type":"local"}),
|
||||||
|
backend_agent_id: None,
|
||||||
|
team_id: None,
|
||||||
|
persona_name_in_team: None,
|
||||||
|
relay_mesh: None,
|
||||||
|
extra: Map::new(),
|
||||||
|
},
|
||||||
|
extensions: BTreeMap::new(),
|
||||||
|
extra: Map::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gap B: a relay-only agent (30179 head, no disk record — never started
|
||||||
|
/// on this device) must resolve for export once the disk list is folded
|
||||||
|
/// through the overlay. Raw disk alone returns "agent not found".
|
||||||
|
#[test]
|
||||||
|
fn relay_only_agent_resolves_for_export_after_overlay_fold() {
|
||||||
|
let mut overlay = PrivateConfigOverlay::default();
|
||||||
|
overlay
|
||||||
|
.insert(relay_payload("relay-only-pk", "Relay Only", "relay prompt"))
|
||||||
|
.unwrap();
|
||||||
|
let disk: Vec<ManagedAgentRecord> = vec![];
|
||||||
|
|
||||||
|
// Raw disk: not found (the pre-fix behavior).
|
||||||
|
assert!(resolve_from_lists("relay-only-pk", &disk, &[]).is_err());
|
||||||
|
|
||||||
|
// Folded: resolves, with the relay config as the effective record.
|
||||||
|
let folded = overlay.resolved_records(&disk);
|
||||||
|
let (record, is_def) = resolve_from_lists("relay-only-pk", &folded, &[]).unwrap();
|
||||||
|
assert!(!is_def);
|
||||||
|
assert_eq!(record.name, "Relay Only");
|
||||||
|
assert_eq!(record.system_prompt.as_deref(), Some("relay prompt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gap A: a follower device with a stale disk record must export the
|
||||||
|
/// effective 30179 values, not the disk snapshot — and the exported
|
||||||
|
/// manifest must advertise the enforced respond_to via the instance
|
||||||
|
/// fallback (the overlay patch only populates instance fields).
|
||||||
|
#[test]
|
||||||
|
fn follower_exports_effective_relay_values_not_stale_disk() {
|
||||||
|
use crate::managed_agents::agent_snapshot::build_snapshot;
|
||||||
|
|
||||||
|
let mut overlay = PrivateConfigOverlay::default();
|
||||||
|
overlay
|
||||||
|
.insert(relay_payload("follower-pk", "Fresh Name", "fresh prompt"))
|
||||||
|
.unwrap();
|
||||||
|
let mut stale = make_instance("follower-pk", "some-persona");
|
||||||
|
stale.name = "Stale Name".into();
|
||||||
|
stale.system_prompt = Some("stale prompt".into());
|
||||||
|
|
||||||
|
let folded = overlay.resolved_records(std::slice::from_ref(&stale));
|
||||||
|
let (record, _) = resolve_from_lists("follower-pk", &folded, &[]).unwrap();
|
||||||
|
assert_eq!(record.name, "Fresh Name");
|
||||||
|
assert_eq!(record.system_prompt.as_deref(), Some("fresh prompt"));
|
||||||
|
assert_eq!(record.model.as_deref(), Some("relay-model"));
|
||||||
|
|
||||||
|
let snapshot = build_snapshot(record, MemoryLevel::None, vec![], None);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.definition.system_prompt.as_deref(),
|
||||||
|
Some("fresh prompt")
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.definition.respond_to.as_deref(), Some("allowlist"));
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.definition.respond_to_allowlist,
|
||||||
|
vec!["ab".repeat(32)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_by_pubkey_finds_keyed_instance() {
|
fn resolve_by_pubkey_finds_keyed_instance() {
|
||||||
let inst = make_instance("pubkey-xyz", "my-agent");
|
let inst = make_instance("pubkey-xyz", "my-agent");
|
||||||
|
|||||||
@@ -194,6 +194,23 @@ pub fn build_snapshot(
|
|||||||
// Use definition-level fields (respond_to, allowlist, parallelism) for
|
// Use definition-level fields (respond_to, allowlist, parallelism) for
|
||||||
// portability — instance-level equivalents are spawn-time snapshots and
|
// portability — instance-level equivalents are spawn-time snapshots and
|
||||||
// would be stale.
|
// would be stale.
|
||||||
|
//
|
||||||
|
// respond_to falls back to the instance fields when the definition-level
|
||||||
|
// mode is unset — same rationale as the parallelism fallback below. A
|
||||||
|
// record materialized from a kind:30179 relay head carries its effective
|
||||||
|
// respond_to/allowlist ONLY on the instance fields (the overlay patch
|
||||||
|
// never populates the definition ones), so without this fallback a
|
||||||
|
// relay-hosted agent would export `respond_to: None` while actually
|
||||||
|
// enforcing an allowlist. Mode and list travel together: a definition
|
||||||
|
// mode uses the definition list, the instance fallback uses the instance
|
||||||
|
// list — never a mix.
|
||||||
|
let (respond_to, respond_to_allowlist) = match record.definition_respond_to.clone() {
|
||||||
|
Some(mode) => (Some(mode), record.definition_respond_to_allowlist.clone()),
|
||||||
|
None => (
|
||||||
|
Some(record.respond_to.as_str().to_string()),
|
||||||
|
record.respond_to_allowlist.clone(),
|
||||||
|
),
|
||||||
|
};
|
||||||
let definition = AgentSnapshotDefinition {
|
let definition = AgentSnapshotDefinition {
|
||||||
name: record
|
name: record
|
||||||
.display_name
|
.display_name
|
||||||
@@ -205,8 +222,8 @@ pub fn build_snapshot(
|
|||||||
model: record.model.clone(),
|
model: record.model.clone(),
|
||||||
provider: record.provider.clone(),
|
provider: record.provider.clone(),
|
||||||
parallelism: record.definition_parallelism.or(Some(record.parallelism)),
|
parallelism: record.definition_parallelism.or(Some(record.parallelism)),
|
||||||
respond_to: record.definition_respond_to.clone(),
|
respond_to,
|
||||||
respond_to_allowlist: record.definition_respond_to_allowlist.clone(),
|
respond_to_allowlist,
|
||||||
name_pool: record.name_pool.clone(),
|
name_pool: record.name_pool.clone(),
|
||||||
idle_timeout_seconds: record.idle_timeout_seconds,
|
idle_timeout_seconds: record.idle_timeout_seconds,
|
||||||
max_turn_duration_seconds: record.max_turn_duration_seconds,
|
max_turn_duration_seconds: record.max_turn_duration_seconds,
|
||||||
|
|||||||
@@ -540,6 +540,55 @@ fn definition_fields_present_in_snapshot() {
|
|||||||
assert!(!snapshot.definition.respond_to_allowlist.is_empty());
|
assert!(!snapshot.definition.respond_to_allowlist.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A definition-level mode must carry the DEFINITION list, never the
|
||||||
|
/// instance list — mode and list travel together (`minimal_record` has
|
||||||
|
/// distinct values on each level, so a mix-up is detectable).
|
||||||
|
#[test]
|
||||||
|
fn definition_respond_to_keeps_definition_list_not_instance_list() {
|
||||||
|
let record = minimal_record();
|
||||||
|
let snapshot = build_snapshot(&record, MemoryLevel::None, vec![], None);
|
||||||
|
assert_eq!(snapshot.definition.respond_to.as_deref(), Some("allowlist"));
|
||||||
|
assert_eq!(snapshot.definition.respond_to_allowlist, vec!["abc123def"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A record materialized from a kind:30179 relay head carries its effective
|
||||||
|
/// respond_to/allowlist only on the INSTANCE fields. Export must fall back
|
||||||
|
/// to them (like parallelism already does) instead of advertising
|
||||||
|
/// `respond_to: None` while the agent enforces an allowlist.
|
||||||
|
#[test]
|
||||||
|
fn respond_to_falls_back_to_instance_fields_when_definition_unset() {
|
||||||
|
let mut record = minimal_record();
|
||||||
|
record.definition_respond_to = None;
|
||||||
|
record.definition_respond_to_allowlist = vec![];
|
||||||
|
record.respond_to = RespondTo::Allowlist;
|
||||||
|
record.respond_to_allowlist = vec!["ef".repeat(32)];
|
||||||
|
|
||||||
|
let snapshot = build_snapshot(&record, MemoryLevel::None, vec![], None);
|
||||||
|
assert_eq!(snapshot.definition.respond_to.as_deref(), Some("allowlist"));
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.definition.respond_to_allowlist,
|
||||||
|
vec!["ef".repeat(32)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The instance fallback exports the enforced default explicitly rather
|
||||||
|
/// than `None` — an owner-only agent advertises owner-only.
|
||||||
|
#[test]
|
||||||
|
fn respond_to_fallback_exports_explicit_owner_only_default() {
|
||||||
|
let mut record = minimal_record();
|
||||||
|
record.definition_respond_to = None;
|
||||||
|
record.definition_respond_to_allowlist = vec![];
|
||||||
|
record.respond_to = RespondTo::default();
|
||||||
|
record.respond_to_allowlist = vec![];
|
||||||
|
|
||||||
|
let snapshot = build_snapshot(&record, MemoryLevel::None, vec![], None);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.definition.respond_to.as_deref(),
|
||||||
|
Some("owner-only")
|
||||||
|
);
|
||||||
|
assert!(snapshot.definition.respond_to_allowlist.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_fields_present_in_snapshot() {
|
fn profile_fields_present_in_snapshot() {
|
||||||
let record = minimal_record();
|
let record = minimal_record();
|
||||||
|
|||||||
Reference in New Issue
Block a user