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 <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-08-05 11:42:33 -06:00
committed by Carl
parent aa7efe0e32
commit 7908cd066e
3 changed files with 66 additions and 7 deletions

View File

@@ -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,
&timestamp,
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.

View File

@@ -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);

View File

@@ -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,