Skip to main content

agentmux_srv/backend/storage/agents/
purge.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Removing an agent's dependent rows when the agent itself is deleted.
5
6use super::*;
7
8#[cfg(test)]
9pub(crate) fn purge_agent_dependents_for_tests(
10    conn: &rusqlite::Connection,
11    id: &str,
12    tokens: Option<&super::super::agent_tokens::TokenIndex>,
13) -> Result<usize, StoreError> {
14    purge_agent_dependents(conn, id, tokens)
15}
16
17pub(super) fn purge_agent_dependents(
18    conn: &rusqlite::Connection,
19    id: &str,
20    tokens: Option<&super::super::agent_tokens::TokenIndex>,
21) -> Result<usize, StoreError> {
22    const BY_AGENT_ID: &[&str] = &[
23        "db_agent_content",
24        "db_agent_skills",
25        "db_agent_history",
26        "db_agent_identity_links",
27        "db_agent_skills_ref",
28        "db_agent_mcp_ref",
29        "db_agent_credentials",
30        "db_agent_native_memory",
31        "db_agent_native_memory_versions",
32        "db_agent_jekt_keys",
33        "db_agent_lan_keys",
34        // v37, identity M1: the agent's local identity token dies with it.
35        "db_agent_tokens",
36        // The agent's Bundles list.
37        "db_agent_bundles",
38    ];
39    let present: std::collections::HashSet<String> = {
40        let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table'")?;
41        let rows = stmt.query_map([], |r| r.get::<_, String>(0))?;
42        rows.collect::<Result<_, _>>()?
43    };
44    let mut removed = 0usize;
45    for table in BY_AGENT_ID {
46        if !present.contains(*table) {
47            continue;
48        }
49        // Table names are compile-time constants from the list above, never
50        // caller input — the id itself is still bound as a parameter.
51        removed += conn.execute(&format!("DELETE FROM {table} WHERE agent_id=?1"), params![id])?;
52        // Identity M4a: the index follows the token row's own delete, not
53        // the purge's overall result (spec §6.5.3).
54        if *table == "db_agent_tokens" {
55            if let Some(tokens) = tokens {
56                tokens.forget_uid(id);
57            }
58        }
59    }
60    // A channel's cached cloud credentials sit behind its `<channel>/` prefix
61    // when channels share the store (`muxbus::agent_credential_prefix_for`), so
62    // the plain delete above misses them: a deleted agent's client secret must
63    // not stay on disk in any channel's rows.
64    if present.contains("db_agent_credentials") {
65        removed += conn.execute(
66            "DELETE FROM db_agent_credentials WHERE instr(agent_id, '/') > 0 AND substr(agent_id, instr(agent_id, '/') + 1) = ?1",
67            params![id],
68        )?;
69    }
70    // Identity M4d-2: the UID-keyed signing keys, keyed by `uid`.
71    for table in ["db_agent_lan_keys_by_uid", "db_agent_wan_keys_by_uid"] {
72        if present.contains(table) {
73            removed += conn.execute(&format!("DELETE FROM {table} WHERE uid=?1"), params![id])?;
74        }
75    }
76    // The two that key on the agent differently.
77    if present.contains("db_conversation_trust_grants") {
78        removed += conn.execute(
79            "DELETE FROM db_conversation_trust_grants WHERE agent_id=?1 OR granted_peer_agent_id=?1",
80            params![id],
81        )?;
82    }
83    if present.contains("db_agent_activity_summaries") {
84        removed += conn.execute(
85            "DELETE FROM db_agent_activity_summaries WHERE definition_id=?1",
86            params![id],
87        )?;
88    }
89    // v40, durable jekt: messages held for the deleted agent die with it —
90    // their bodies must not linger, nor be delivered if the UID is ever
91    // recreated (Codex P1 on #3632).
92    if present.contains("db_jekt_held") {
93        removed += conn.execute("DELETE FROM db_jekt_held WHERE target_uid=?1", params![id])?;
94    }
95    Ok(removed)
96}