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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default defineConfig({
"**/cloud-provenance.spec.ts",
"**/mention-recipients.spec.ts",
"**/remote-owned-mentions.spec.ts",
"**/client-only-agents.spec.ts",
"**/forum-agent-invitation.spec.ts",
"**/team-mentions.spec.ts",
"**/persistent-agent-audience.spec.ts",
Expand Down
8 changes: 8 additions & 0 deletions desktop/src-tauri/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ pub struct AppState {
/// PID set: spawn/register, adoption, stop, shutdown, and sweep snapshots.
/// Never perform network I/O while holding this lock.
pub managed_agent_runtime_transition: Mutex<()>,
/// Device execution policy is fixed until restart; never synchronized.
pub(crate) agent_device_policy: std::sync::OnceLock<
Result<crate::managed_agents::device_policy::model::DeviceAgentPolicy, String>,
>,
/// Serializes local agent name checks through durable creation or rename.
pub(crate) agent_name_transition: Arc<tokio::sync::Mutex<()>>,
pub managed_agents_store_lock: Mutex<()>,
pub channel_templates_store_lock: Mutex<()>,
pub managed_agent_processes: Mutex<HashMap<ManagedAgentRuntimeKey, ManagedAgentPairRuntime>>,
Expand Down Expand Up @@ -220,6 +226,8 @@ pub fn build_app_state() -> AppState {
shutdown_started: AtomicBool::new(false),
managed_agent_runtime_transition: Mutex::new(()),
identity_mutation: Mutex::new(()),
agent_device_policy: std::sync::OnceLock::new(),
agent_name_transition: Arc::new(tokio::sync::Mutex::new(())),
managed_agents_store_lock: Mutex::new(()),
channel_templates_store_lock: Mutex::new(()),
managed_agent_processes: Mutex::new(HashMap::new()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,22 @@ async fn list_relay_agents_for_selection(
}

#[tauri::command]
pub async fn list_relay_agents(state: State<'_, AppState>) -> Result<Vec<RelayAgentInfo>, String> {
list_relay_agents_for_state(&state).await
pub async fn list_relay_agents(
state: State<'_, AppState>,
app: tauri::AppHandle,
) -> Result<Vec<RelayAgentInfo>, String> {
let policy = crate::managed_agents::device_policy::active(&app)?;
let relay_url = crate::relay::relay_api_base_url_with_override(&state);
let mut agents = list_relay_agents_for_state(&state).await?;
agents.retain(|agent| {
policy.allows_identity(
&relay_url,
agent.owner_pubkey.as_deref(),
&agent.name,
&agent.pubkey,
)
});
Ok(agents)
}

/// Revalidate only the selected relay agents in the target channel.
Expand Down
52 changes: 47 additions & 5 deletions desktop/src-tauri/src/commands/agent_models_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,44 @@ pub async fn update_managed_agent(
app: AppHandle,
state: State<'_, AppState>,
) -> Result<UpdateManagedAgentResponse, String> {
crate::managed_agents::device_policy::require_hosting(&app)?;
let _name_guard = state.agent_name_transition.clone().lock_owned().await;
{
let records = load_managed_agents(&app)?;
let record = records
.iter()
.find(|record| record.pubkey == input.pubkey)
.ok_or("Agent not found")?;
crate::managed_agents::device_policy::require_record(&app, record)?;
if let Some(name) = input.name.as_deref() {
crate::managed_agents::device_policy::active(&app)?
.check_name_update(
&record.name,
name,
Some(&record.pubkey),
record.persona_id.as_deref(),
|| {
crate::managed_agents::device_policy::unique_names::preflight(
&app,
&state,
name,
record.persona_id.as_deref(),
Some(&record.pubkey),
)
},
)
.await?;
}
}
// Phase 1: local save (synchronous, under lock)
let (mut summary, sync_params, rollback, access_policy_changed, access_restart_relays) = {
let (
mut summary,
sync_params,
rollback,
access_policy_changed,
access_restart_relays,
retention_error,
) = {
let _store_guard = state
.managed_agents_store_lock
.lock()
Expand All @@ -163,6 +199,7 @@ pub async fn update_managed_agent(
}

let record = find_managed_agent_mut(&mut records, &input.pubkey)?;
crate::managed_agents::device_policy::require_record(&app, record)?;
let previous_record = record.clone();

let mut name_changed = false;
Expand Down Expand Up @@ -330,7 +367,8 @@ pub async fn update_managed_agent(
// Publish the edit to the relay. After-save, inside the lock, before
// any .await. The retention upsert hashes the opt-IN projection, so an
// update that touched only runtime/local fields is a no-op publish.
super::super::agents::retain_managed_agent_pending(&app, &state, record);
let retention_error =
super::super::agents::retain_managed_agent_pending(&app, &state, record).err();

let sync_params = if name_changed {
let agent_keys = Keys::parse(&record.private_key_nsec)
Expand Down Expand Up @@ -374,6 +412,7 @@ pub async fn update_managed_agent(
rollback,
access_policy_changed,
access_restart_relays,
retention_error,
)
}; // lock dropped here

Expand Down Expand Up @@ -421,7 +460,10 @@ pub async fn update_managed_agent(
let rollback = rollback.ok_or_else(|| {
"missing local rollback state after relay profile sync failure".to_string()
})?;
rollback_failed_agent_update(&app, &state, &summary.pubkey, rollback)?;
let rollback_sync_error =
rollback_failed_agent_update(&app, &state, &summary.pubkey, rollback)?
.map(|error| format!(" Rollback synchronization also failed: {error}"))
.unwrap_or_default();
let restart_suffix = if access_restart_relays.is_empty() {
String::new()
} else {
Expand All @@ -445,7 +487,7 @@ pub async fn update_managed_agent(
"No changes were saved"
};
return Err(format!(
"Agent rename failed because its relay profile could not be updated. {rollback_message}: {sync_error}.{restart_suffix}"
"Agent rename failed because its relay profile could not be updated. {rollback_message}: {sync_error}.{restart_suffix}{rollback_sync_error}"
));
}
}
Expand All @@ -467,7 +509,7 @@ pub async fn update_managed_agent(

Ok(UpdateManagedAgentResponse {
agent: summary,
profile_sync_error: profile_sync_error.take(),
profile_sync_error: retention_error.or(profile_sync_error.take()),
})
}

Expand Down
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/commands/agent_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub async fn set_managed_agent_start_on_app_launch(

{
let record = find_managed_agent_mut(&mut records, &pubkey)?;
crate::managed_agents::device_policy::require_record(&app, record)?;
record.start_on_app_launch = start_on_app_launch;
record.updated_at = now_iso();
}
Expand Down Expand Up @@ -97,6 +98,7 @@ pub async fn set_managed_agent_auto_restart(

{
let record = find_managed_agent_mut(&mut records, &pubkey)?;
crate::managed_agents::device_policy::require_record(&app, record)?;
record.auto_restart_on_config_change = auto_restart_on_config_change;
record.updated_at = now_iso();
}
Expand Down
10 changes: 5 additions & 5 deletions desktop/src-tauri/src/commands/agent_update_rollback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ pub(super) fn rollback_failed_agent_update(
state: &AppState,
pubkey: &str,
rollback: AgentUpdateRollback,
) -> Result<(), String> {
{
) -> Result<Option<String>, String> {
let retention_error = {
let _store_guard = state
.managed_agents_store_lock
.lock()
Expand All @@ -103,10 +103,10 @@ pub(super) fn rollback_failed_agent_update(
.iter()
.find(|record| record.pubkey == pubkey)
.ok_or_else(|| format!("agent {pubkey} not found after failed rename rollback"))?;
super::agents::retain_managed_agent_pending(app, state, restored);
}
super::agents::retain_managed_agent_pending(app, state, restored).err()
};
try_regenerate_nest(app);
Ok(())
Ok(retention_error)
}

#[cfg(test)]
Expand Down
Loading