From 7908cd066e6794c142176443bd78d116c59ba86c Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 5 Aug 2026 11:42:33 -0600 Subject: [PATCH] desktop: route authoritative deletion through PMA Preserve authority evidence through single-agent and persona-cascade deletion, enqueue a minimal next-generation aggregate tombstone for relay-authoritative agents, and retain legacy kind:5 deletion only for LegacyOnly records. Co-authored-by: Carl Signed-off-by: Wes --- desktop/src-tauri/src/commands/agents.rs | 57 +++++++++++++++++-- .../src-tauri/src/commands/personas/mod.rs | 14 ++++- desktop/src-tauri/src/managed_agents/mod.rs | 2 +- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 98882f85e6..a05b64ec56 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -74,12 +74,14 @@ pub(super) fn tombstone_managed_agent_pending( app: &AppHandle, state: &AppState, agent_pubkey: &str, + relay_authority: &crate::managed_agents::RelayAuthority, ) { use crate::managed_agents::{ agent_events::build_agent_delete, + migration::build_tombstone_event, retention::{ - delete_retained_event, open_retention_db, retain_event, tombstone_retention_d_tag, - RetainedEvent, + delete_retained_event, open_retention_db, retain_event, retain_managed_agent_aggregate, + tombstone_retention_d_tag, RetainedEvent, RetainedManagedAgentAggregate, }, }; use buzz_core_pkg::kind::KIND_MANAGED_AGENT; @@ -90,11 +92,51 @@ pub(super) fn tombstone_managed_agent_pending( let result = (|| -> Result<(), String> { let scope = crate::managed_agents::retention::active_retention_scope(app, state)?; let owner_pubkey = scope.owner_keys.public_key().to_hex(); + let mut conn = open_retention_db(&scope.db_path)?; + delete_retained_event(&conn, KIND_MANAGED_AGENT, &owner_pubkey, agent_pubkey)?; + + if let Some(evidence) = relay_authority.evidence() { + let timestamp = crate::util::now_iso(); + let event = build_tombstone_event( + &scope.owner_keys, + agent_pubkey, + evidence.generation, + &evidence.private_event_id, + ×tamp, + nostr::Timestamp::now().as_secs(), + ) + .map_err(|error| { + format!("failed to build managed-agent aggregate tombstone: {error:?}") + })?; + let generation = evidence + .generation + .checked_add(1) + .ok_or_else(|| "managed-agent aggregate generation overflow".to_string())?; + let request_json = serde_json::to_string(&serde_json::json!({ + "private_event": event, + "definition_event": null, + "instance_event": null, + "expected_definition_revision": null + })) + .map_err(|error| format!("failed to serialize managed-agent tombstone: {error}"))?; + return retain_managed_agent_aggregate( + &mut conn, + &RetainedManagedAgentAggregate { + owner_pubkey, + agent_pubkey: agent_pubkey.to_string(), + generation, + private_event_id: event.id.to_hex(), + state: "deleted".to_string(), + request_json, + pending_sync: true, + last_error: None, + }, + ); + } + let event = build_agent_delete(agent_pubkey, &owner_pubkey)? .sign_with_keys(&scope.owner_keys) .map_err(|e| format!("failed to sign managed-agent tombstone: {e}"))?; - let conn = open_retention_db(&scope.db_path)?; - delete_retained_event(&conn, KIND_MANAGED_AGENT, &owner_pubkey, agent_pubkey)?; retain_event( &conn, &RetainedEvent { @@ -1322,6 +1364,11 @@ pub async fn delete_managed_agent( } } + let relay_authority = records + .iter() + .find(|record| record.pubkey == pubkey) + .map(|record| record.relay_authority.clone()) + .ok_or_else(|| format!("agent {pubkey} not found"))?; if let Some(record) = records.iter_mut().find(|record| record.pubkey == pubkey) { stop_managed_agent_process(&app, record, &mut runtimes)?; } @@ -1338,7 +1385,7 @@ pub async fn delete_managed_agent( // guard above and a confirmed removal — never orphan a live remote // deployment's relay record. Inside the lock, before the block closes // (no .await here). Every agent published, so every delete tombstones. - tombstone_managed_agent_pending(&app, &state, &pubkey); + tombstone_managed_agent_pending(&app, &state, &pubkey, &relay_authority); // NIP-IA: archive the deleted agent's identity on the relay so it // stops appearing in member pickers and autocomplete. Same // best-effort, inside-the-lock contract as the tombstone above. diff --git a/desktop/src-tauri/src/commands/personas/mod.rs b/desktop/src-tauri/src/commands/personas/mod.rs index 0cd7ad0324..42607aa7ec 100644 --- a/desktop/src-tauri/src/commands/personas/mod.rs +++ b/desktop/src-tauri/src/commands/personas/mod.rs @@ -181,6 +181,11 @@ pub async fn delete_persona(id: String, app: AppHandle) -> Result<(), String> { remote_deployed.join(", ") )); } + let cascade_authorities: std::collections::HashMap<_, _> = agents + .iter() + .filter(|agent| cascade.contains(&agent.pubkey)) + .map(|agent| (agent.pubkey.clone(), agent.relay_authority.clone())) + .collect(); // ── Phase 2: Stop ─────────────────────────────────────────────── // @@ -236,7 +241,14 @@ pub async fn delete_persona(id: String, app: AppHandle) -> Result<(), String> { state.clear_agent_session_caches(pk); // Remove nsec from keyring after the record is gone. delete_agent_key(pk); - super::agents::tombstone_managed_agent_pending(&app, &state, pk); + super::agents::tombstone_managed_agent_pending( + &app, + &state, + pk, + cascade_authorities + .get(pk) + .ok_or_else(|| format!("agent {pk} authority missing"))?, + ); super::agents::archive_managed_agent_pending(&app, &state, pk); } tombstone_persona_pending(&app, &state, &d_tag); diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index e641368394..e89c48704a 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod agent_events; pub(crate) mod agent_snapshot; pub(crate) mod agent_snapshot_envelope; mod authority; -mod migration; +pub(crate) mod migration; pub(crate) mod team_snapshot; pub(crate) use agent_env::{ baked_build_env, build_buzz_agent_provider_defaults, discovery_env_with_baked_floor,