Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use nostr::{Keys, ToBech32};
use tauri::{AppHandle, State};
use tauri::{AppHandle, Manager as _, State};

use super::managed_agent_definition::validate_create_definition;

Expand All @@ -8,13 +8,14 @@ use crate::{
managed_agents::{
bestie_assignment::{recover_pending_assignment_cleanup, with_agent_assignments_cleared},
build_managed_agent_summary, current_instance_id, ensure_persona_is_active,
find_managed_agent_mut, load_managed_agents, load_personas, load_teams,
managed_agents_base_dir, normalize_agent_args, resolve_provider_binary,
save_managed_agents, start_managed_agent_process, stop_managed_agent_process,
stop_managed_agent_workspace_pair, sync_managed_agent_processes, try_regenerate_nest,
validate_provider_config, BackendKind, CreateManagedAgentRequest,
CreateManagedAgentResponse, ManagedAgentRecord, ManagedAgentSummary, RelayMeshConfig,
DEFAULT_ACP_COMMAND, DEFAULT_AGENT_PARALLELISM, DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
find_managed_agent_mut, load_managed_agents, load_managed_agents_for_active_community,
load_personas, load_teams, managed_agents_base_dir, normalize_agent_args,
resolve_provider_binary, save_managed_agents, start_managed_agent_process,
stop_managed_agent_process, stop_managed_agent_workspace_pair,
sync_managed_agent_processes, try_regenerate_nest, validate_provider_config, BackendKind,
CreateManagedAgentRequest, CreateManagedAgentResponse, ManagedAgentRecord,
ManagedAgentSummary, RelayMeshConfig, DEFAULT_ACP_COMMAND, DEFAULT_AGENT_PARALLELISM,
DEFAULT_AGENT_TURN_TIMEOUT_SECONDS,
},
relay::relay_ws_url_with_override,
util::now_iso,
Expand Down Expand Up @@ -298,7 +299,7 @@ pub async fn list_managed_agents(app: AppHandle) -> Result<Vec<ManagedAgentSumma
.managed_agents_store_lock
.lock()
.map_err(|error| error.to_string())?;
let mut records = load_managed_agents(&app)?;
let mut records = load_managed_agents_for_active_community(&app)?;
let mut runtimes = state
.managed_agent_processes
.lock()
Expand Down Expand Up @@ -413,12 +414,29 @@ pub async fn create_managed_agent(
// Store the relay override exactly as supplied (trimmed). An explicit
// value pins the agent; empty stays empty and resolves to the active
// workspace relay at read-time. Uniform for Local and Provider.
let resolved_relay_url = input
.relay_url
.as_deref()
.map(str::trim)
.unwrap_or("")
.to_string();
//
// #7184 tenant isolation: an unpinned record is visible in EVERY
// community (fail-open visibility rule in storage). To scope new
// agents to the community they were created in, an empty request
// relay is stamped with the ACTIVE workspace relay at mint time. The
// spawn path still ignores the pin (#2122 agents-everywhere), so this
// only scopes roster visibility/storage — never where an agent may
// run.
let active_workspace_relay =
relay_ws_url_with_override(&app.state::<AppState>());
let resolved_relay_url = {
let supplied = input
.relay_url
.as_deref()
.map(str::trim)
.unwrap_or("")
.to_string();
if supplied.is_empty() && !active_workspace_relay.is_empty() {
active_workspace_relay
} else {
supplied
}
};

(keys, private_key_nsec, pubkey, resolved_relay_url, input)
};
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub(crate) use session_policy::{
AcpSessionPolicy, ManagedAgentExperimentState, ACP_SESSION_POLICY_ENV_VAR,
};
pub use storage::*;
pub(crate) use storage::load_managed_agents_for_active_community;
pub use teams::*;
pub use types::*;

Expand Down
36 changes: 25 additions & 11 deletions desktop/src-tauri/src/managed_agents/reconcile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,32 @@ fn reconcile_agents_in_dir_at(
keys: &nostr::Keys,
db_path: &Path,
) -> Result<u32, String> {
let store_path = base_dir.join("managed-agents.json");
if !store_path.exists() {
return Ok(0);
}

let content = std::fs::read_to_string(&store_path)
.map_err(|e| format!("failed to read managed-agents.json: {e}"))?;
// Enumerate every store file: the legacy global store plus each community
// shard (#7184). Every record's kind:30177 head must stay reconciled
// regardless of which community it is scoped to — the retention store is
// relay-scoped, so a head published by a save in community A must not be
// orphaned just because A's shard was written while B is active.
let mut store_paths = vec![base_dir.join("managed-agents.json")];
store_paths.extend(super::storage::community_shard_paths(base_dir));

let mut records: Vec<ManagedAgentRecord> = Vec::new();
for store_path in &store_paths {
if !store_path.exists() {
continue;
}

let records: Vec<ManagedAgentRecord> = serde_json::from_str(&content).map_err(|e| {
super::storage::backup_invalid_store(&store_path);
format!("failed to parse managed-agents.json (preserved as .invalid): {e}")
})?;
let content = std::fs::read_to_string(store_path)
.map_err(|e| format!("failed to read {}: {e}", store_path.display()))?;

let shard_records: Vec<ManagedAgentRecord> = serde_json::from_str(&content).map_err(|e| {
super::storage::backup_invalid_store(store_path);
format!(
"failed to parse {} (preserved as .invalid): {e}",
store_path.display()
)
})?;
records.extend(shard_records);
}

if records.is_empty() {
return Ok(0);
Expand Down
Loading