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:
Eva
2026-08-07 12:10:56 -04:00
parent d945edf71a
commit 692fdafa61
5 changed files with 207 additions and 2 deletions

View File

@@ -493,7 +493,14 @@ pub fn card_mint_key_status(
.lock()
.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 = state
.private_managed_agent_overlay
.lock()
.map_err(|e| e.to_string())?
.resolved_records(&instances);
let definitions = load_agent_definitions(&app)?;
let (record, _) = resolve_from_lists(&id, &instances, &definitions)?;
@@ -549,7 +556,15 @@ pub async fn mint_agent_card(
.lock()
.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 = state
.private_managed_agent_overlay
.lock()
.map_err(|e| e.to_string())?
.resolved_records(&instances);
let definitions = load_agent_definitions(&app)?;
let (record, is_definition) =
resolve_from_lists(&id, &instances, &definitions).map(|(r, d)| (r.clone(), d))?;

View File

@@ -245,7 +245,17 @@ pub(crate) async fn materialize_snapshot_bytes(
.lock()
.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 = state
.private_managed_agent_overlay
.lock()
.map_err(|e| e.to_string())?
.resolved_records(&instances);
let definitions = load_agent_definitions(&app)?;
let (def_record, is_definition) = resolve_from_lists(&id, &instances, &definitions)
.map(|(r, is_def)| (r.clone(), is_def))?;

View File

@@ -161,6 +161,120 @@ fn definition_slug_resolves_to_definition_and_linked_instance_is_valid_memory_so
// ── 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]
fn resolve_by_pubkey_finds_keyed_instance() {
let inst = make_instance("pubkey-xyz", "my-agent");

View File

@@ -194,6 +194,23 @@ pub fn build_snapshot(
// Use definition-level fields (respond_to, allowlist, parallelism) for
// portability — instance-level equivalents are spawn-time snapshots and
// 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 {
name: record
.display_name
@@ -205,8 +222,8 @@ pub fn build_snapshot(
model: record.model.clone(),
provider: record.provider.clone(),
parallelism: record.definition_parallelism.or(Some(record.parallelism)),
respond_to: record.definition_respond_to.clone(),
respond_to_allowlist: record.definition_respond_to_allowlist.clone(),
respond_to,
respond_to_allowlist,
name_pool: record.name_pool.clone(),
idle_timeout_seconds: record.idle_timeout_seconds,
max_turn_duration_seconds: record.max_turn_duration_seconds,

View File

@@ -540,6 +540,55 @@ fn definition_fields_present_in_snapshot() {
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]
fn profile_fields_present_in_snapshot() {
let record = minimal_record();