desktop: reconstruct relay-canonical agents

Subscribe to owner-private PMA heads, strictly decrypt and validate their bound projections, and reconstruct the portable managed-agent record on a fresh device without allowing unsolicited heads to overwrite LegacyOnly state. Carry the relay-mesh marker that the field classification already declares private-canonical.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-05 12:23:12 -06:00
committed by Carl
parent 1d702afc21
commit 94b0a6e83a
7 changed files with 155 additions and 5 deletions

View File

@@ -77,7 +77,9 @@ fn reconcile_inbound_persona_event_blocking(
save_managed_agents, save_teams,
team_events::team_content_from_event,
};
use buzz_core_pkg::kind::{KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA, KIND_TEAM};
use buzz_core_pkg::kind::{
KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA, KIND_PRIVATE_MANAGED_AGENT, KIND_TEAM,
};
use nostr::JsonUtil;
let state = app.state::<AppState>();
@@ -96,10 +98,17 @@ fn reconcile_inbound_persona_event_blocking(
return reconcile_inbound_tombstone(&event, &arrival_relay_url, &app, &state);
}
if !matches!(kind, KIND_PERSONA | KIND_TEAM | KIND_MANAGED_AGENT) {
if !matches!(
kind,
KIND_PERSONA | KIND_TEAM | KIND_MANAGED_AGENT | KIND_PRIVATE_MANAGED_AGENT
) {
return Ok(());
}
if kind == KIND_PRIVATE_MANAGED_AGENT {
return reconcile_inbound_private_managed_agent(&event, &arrival_relay_url, &app, &state);
}
// The d-tag identifies the record within its kind. Persona derives it from
// the parsed record (`persona_d_tag`); team/agent carry it as the event's
// d-tag directly. The persona is parsed once here and reused in the apply
@@ -182,6 +191,129 @@ fn reconcile_inbound_persona_event_blocking(
Ok(())
}
fn reconcile_inbound_private_managed_agent(
event: &nostr::Event,
arrival_relay_url: &str,
app: &AppHandle,
state: &AppState,
) -> Result<(), String> {
use buzz_core_pkg::private_managed_agent::{self as pma, State};
let owner_keys = state.signing_keys()?;
if event.pubkey != owner_keys.public_key() {
return Ok(());
}
if crate::managed_agents::retention::arrival_retention_scope(app, state, arrival_relay_url)?
.is_none()
{
return Ok(());
}
let (_, payload) = pma::validate_and_decrypt(event, &owner_keys)
.map_err(|error| format!("invalid private managed-agent aggregate: {error}"))?;
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let mut agents = crate::managed_agents::load_managed_agents(app)?;
let existing_index = agents
.iter()
.position(|record| record.pubkey == payload.agent_pubkey);
if let Some(index) = existing_index {
let authority = agents[index].relay_authority.evidence();
if authority.is_some_and(|evidence| {
payload.generation <= evidence.generation
|| payload.previous_event_id.as_deref() != Some(&evidence.private_event_id)
}) {
return Ok(());
}
// A local legacy record is still authoritative until its own migration
// persists verified evidence. Never replace it from an unsolicited head.
if authority.is_none() {
return Ok(());
}
}
match payload.state {
State::Deleted => {
if let Some(index) = existing_index {
agents.remove(index);
crate::managed_agents::delete_agent_key(&payload.agent_pubkey);
state.clear_agent_session_caches(&payload.agent_pubkey);
crate::managed_agents::save_managed_agents(app, &agents)?;
}
}
State::Active => {
let reconstructed = reconstruct_managed_agent_from_payload(&payload, event)?;
match existing_index {
Some(index) => agents[index] = reconstructed,
None => agents.push(reconstructed),
}
crate::managed_agents::save_managed_agents(app, &agents)?;
}
}
try_regenerate_nest(app);
let _ = app.emit("agents-data-changed", ());
Ok(())
}
fn reconstruct_managed_agent_from_payload(
payload: &buzz_core_pkg::private_managed_agent::Payload,
private_event: &nostr::Event,
) -> Result<ManagedAgentRecord, String> {
use crate::managed_agents::{
agent_events::managed_agent_content_from_event, persona_events::persona_from_event,
RelayAuthority, RelayAuthorityEvidence, RelayMeshConfig, VersionedBackend,
};
let active = payload
.active
.as_ref()
.ok_or_else(|| "active private aggregate is missing active payload".to_string())?;
let definition_event = &active.definition.recovery.signed_event;
let instance_event = &active.instance_projection.recovery.signed_event;
let definition = persona_from_event(definition_event)?;
let instance = managed_agent_content_from_event(instance_event)?;
let backend: VersionedBackend = serde_json::from_value(active.config.backend.clone())
.map_err(|error| format!("invalid private backend envelope: {error}"))?;
let relay_mesh = active
.config
.relay_mesh
.clone()
.map(serde_json::from_value::<RelayMeshConfig>)
.transpose()
.map_err(|error| format!("invalid private relay mesh config: {error}"))?;
let mut record = definition.into_agent_record();
record.pubkey = payload.agent_pubkey.clone();
record.name = instance.name;
record.persona_id = instance.persona_id;
record.private_key_nsec = active.identity.private_key_nsec.clone();
record.auth_tag = active.identity.auth_tag.clone();
record.relay_url = active.config.relay_url.clone();
record.agent_command_override = active.config.agent_command_override.clone();
record.agent_args = active.config.agent_args.clone();
record.idle_timeout_seconds = active.config.idle_timeout_seconds;
record.max_turn_duration_seconds = active.config.max_turn_duration_seconds;
record.env_vars = active.config.env_vars.clone().into_iter().collect();
record.backend = backend.backend;
record.backend_agent_id = active.config.backend_agent_id.clone();
record.team_id = active.config.team_id.clone();
record.persona_name_in_team = active.config.persona_name_in_team.clone();
record.relay_mesh = relay_mesh;
record.parallelism = instance.parallelism;
record.respond_to = instance.respond_to;
record.respond_to_allowlist = instance.respond_to_allowlist;
record.updated_at = payload.updated_at.clone();
record.relay_authority = RelayAuthority::relay_authoritative(RelayAuthorityEvidence {
generation: payload.generation,
private_event_id: private_event.id.to_hex(),
});
Ok(record)
}
/// Parse an inbound wire event and enforce the signature gate. Everything
/// downstream trusts `event.pubkey` (ownership routing, tombstone scoping,
/// behavioral-quad application), so a forged pubkey must die here — the

View File

@@ -294,7 +294,12 @@ where
backend_agent_id: record.backend_agent_id.clone(),
team_id: record.team_id.clone(),
persona_name_in_team: record.persona_name_in_team.clone(),
relay_mesh: None,
relay_mesh: record
.relay_mesh
.as_ref()
.map(serde_json::to_value)
.transpose()
.map_err(|e| MigrationError::Codec(format!("relay mesh envelope: {e}")))?,
};
let payload = Payload {

View File

@@ -94,7 +94,9 @@ fn fully_valid_record() -> ManagedAgentRecord {
definition_respond_to: None,
definition_respond_to_allowlist: vec![],
definition_parallelism: None,
relay_mesh: None,
relay_mesh: Some(crate::managed_agents::RelayMeshConfig {
model_ref: "mesh/model".to_string(),
}),
relay_authority: super::super::authority::RelayAuthority::legacy(),
}
}
@@ -187,6 +189,11 @@ fn happy_round_trip_yields_promotion_evidence() {
assert_eq!(evidence.generation, 1);
assert_eq!(evidence.previous_event_id, None);
assert_eq!(evidence.definition_revision, 7);
assert_eq!(
candidate.payload.active.as_ref().unwrap().config.relay_mesh,
Some(json!({ "model_ref": "mesh/model" })),
"relay mesh is private-canonical and must be carried",
);
}
#[test]

View File

@@ -56,7 +56,7 @@ pub use backend::*;
// solely by `authority`'s own tests via `super::*`, so they need no re-export
// yet; the not-yet-built migration path will add one when it actually consumes
// them. No blanket `#[allow(dead_code)]` — unused API stays unexported.
pub(crate) use authority::RelayAuthority;
pub(crate) use authority::{RelayAuthority, RelayAuthorityEvidence, VersionedBackend};
pub use discovery::*;
pub use env_vars::*;
#[cfg(windows)]

View File

@@ -6,6 +6,7 @@ import {
KIND_DELETION,
KIND_MANAGED_AGENT,
KIND_PERSONA,
KIND_PRIVATE_MANAGED_AGENT,
KIND_TEAM,
} from "@/shared/constants/kinds";
import { startPersonaSync } from "./usePersonaSync.ts";
@@ -14,6 +15,7 @@ const EXPECTED_KINDS = [
KIND_PERSONA,
KIND_TEAM,
KIND_MANAGED_AGENT,
KIND_PRIVATE_MANAGED_AGENT,
KIND_DELETION,
];

View File

@@ -7,6 +7,7 @@ import {
KIND_DELETION,
KIND_MANAGED_AGENT,
KIND_PERSONA,
KIND_PRIVATE_MANAGED_AGENT,
KIND_TEAM,
} from "@/shared/constants/kinds";
@@ -17,6 +18,7 @@ const PERSONA_SYNC_KINDS = [
KIND_PERSONA,
KIND_TEAM,
KIND_MANAGED_AGENT,
KIND_PRIVATE_MANAGED_AGENT,
KIND_DELETION,
];

View File

@@ -52,6 +52,8 @@ export const KIND_CHANNEL_SORT = 30078;
export const KIND_PERSONA = 30175;
export const KIND_TEAM = 30176;
export const KIND_MANAGED_AGENT = 30177;
/** Owner-encrypted relay-canonical managed-agent aggregate (NIP-PMA). */
export const KIND_PRIVATE_MANAGED_AGENT = 30179;
export const KIND_USER_STATUS = 30315;
export const KIND_AGENT_OBSERVER_FRAME = 24200;
export const KIND_AGENT_TURN_METRIC = 44200;