feat(desktop): allow agents to manage relay profiles (#2009)

Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Tyler
2026-07-16 20:47:32 -04:00
committed by GitHub
parent 1d8d006aaf
commit 7b3513a34a
11 changed files with 71 additions and 3 deletions

View File

@@ -25,6 +25,10 @@ pub struct AppState {
/// restore. `apply_workspace` consumes it after installing the workspace
/// relay and identity, so agents never start against the fallback relay.
pub managed_agent_restore_pending: AtomicBool,
/// Whether desktop may repair managed-agent kind:0 profiles from its local
/// records. Disabled by the agent-managed profiles experiment so an agent's
/// own profile updates are not overwritten on start or restore.
pub managed_agent_profile_reconcile_enabled: AtomicBool,
/// Shared shutdown signal checked by launch-time agent restoration.
pub shutdown_started: AtomicBool,
/// Serializes the restore spawn/register transition with shutdown cleanup,
@@ -161,6 +165,7 @@ pub fn build_app_state() -> AppState {
.unwrap_or_else(|_| reqwest::Client::new()),
relay_url_override: Mutex::new(None),
managed_agent_restore_pending: AtomicBool::new(false),
managed_agent_profile_reconcile_enabled: AtomicBool::new(true),
shutdown_started: AtomicBool::new(false),
managed_agent_restore_transition: Mutex::new(()),
identity_mutation: Mutex::new(()),

View File

@@ -1,4 +1,5 @@
use tauri::{AppHandle, Manager};
use std::sync::atomic::Ordering;
use tauri::{AppHandle, Manager, State};
use crate::{
app_state::AppState,
@@ -10,6 +11,13 @@ use crate::{
util::now_iso,
};
#[tauri::command]
pub fn set_agent_managed_profiles(enabled: bool, state: State<'_, AppState>) {
state
.managed_agent_profile_reconcile_enabled
.store(!enabled, Ordering::Release);
}
#[tauri::command]
pub async fn set_managed_agent_start_on_app_launch(
pubkey: String,

View File

@@ -991,7 +991,11 @@ pub async fn start_managed_agent(
// profile is published on the relay. This self-heals cases where the initial
// profile sync at creation time failed silently. For legacy records (pre-PR-921)
// with no persisted avatar, this also backfills the avatar from the relay.
if result.is_ok() {
if result.is_ok()
&& state
.managed_agent_profile_reconcile_enabled
.load(std::sync::atomic::Ordering::Acquire)
{
let reconcile_pubkey = pubkey.clone();
let reconcile_app = app.clone();
tauri::async_runtime::spawn(async move {

View File

@@ -81,6 +81,13 @@ pub(crate) async fn reconcile_agent_profile(
&relay_ws_url_with_override(state),
);
if !state
.managed_agent_profile_reconcile_enabled
.load(std::sync::atomic::Ordering::Acquire)
{
return Ok(());
}
// Query the relay for the agent's existing kind:0 profile.
let existing = query_agent_profile(state, &relay_url, agent_pubkey).await?;
@@ -136,6 +143,13 @@ pub(crate) async fn reconcile_agent_profile(
let agent_keys = Keys::parse(&data.private_key_nsec)
.map_err(|e| format!("failed to parse agent keys: {e}"))?;
if !state
.managed_agent_profile_reconcile_enabled
.load(std::sync::atomic::Ordering::Acquire)
{
return Ok(());
}
sync_managed_agent_profile(
state,
&relay_url,

View File

@@ -103,6 +103,7 @@ pub async fn apply_workspace(
relay_url: String,
nsec: Option<String>,
repos_dir: Option<String>,
agent_managed_profiles: Option<bool>,
app: AppHandle,
) -> Result<(), String> {
let restore_app = app.clone();
@@ -148,6 +149,13 @@ pub async fn apply_workspace(
*keys_guard = keys;
}
// Keep the backend-side reconcile guard aligned with the frontend
// experiment before launch-time restore can spawn any agents. Missing
// means the stable behavior: desktop remains authoritative.
state
.managed_agent_profile_reconcile_enabled
.store(!agent_managed_profiles.unwrap_or(false), Ordering::Release);
// ── Filesystem side-effect (non-fatal) ────────────────────────────────
// Persist the *effective* repos_dir (None when the candidate failed
// validation) for the backend to read at boot, then re-point REPOS to

View File

@@ -847,6 +847,7 @@ pub fn run() {
create_managed_agent,
start_managed_agent,
stop_managed_agent,
set_agent_managed_profiles,
set_managed_agent_start_on_app_launch,
set_managed_agent_auto_restart,
delete_managed_agent,

View File

@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
import { relayClient } from "@/shared/api/relayClient";
import { applyCommunity, getDefaultRelayUrl } from "@/shared/api/tauri";
import { getIdentity } from "@/shared/api/tauriIdentity";
import { getOverrides } from "@/shared/features";
import { resetMediaCaches } from "@/shared/lib/mediaUrl";
import { clearSearchHitEventCache } from "@/app/navigation/searchHitEventCache";
import { initDraftStore } from "@/features/messages/lib/useDrafts";
@@ -157,6 +158,7 @@ export function useCommunityInit(
undefined,
activeCommunity.token,
activeCommunity.reposDir,
getOverrides().agentManagedProfiles === true,
);
} catch (error) {
// A bad `repos_dir` no longer reaches here — `apply_workspace` treats

View File

@@ -1,3 +1,4 @@
import { setAgentManagedProfiles } from "@/shared/api/tauri";
import { desktopFeatures, useFeatureToggle } from "@/shared/features";
import type { FeatureDefinition } from "@/shared/features";
import { Switch } from "@/shared/ui/switch";
@@ -19,7 +20,17 @@ function FeatureRow({ feature }: { feature: FeatureDefinition }) {
aria-labelledby={`${switchId}-label`}
checked={enabled}
data-testid={switchId}
onCheckedChange={toggle}
onCheckedChange={(value) => {
toggle(value);
if (feature.id === "agentManagedProfiles") {
void setAgentManagedProfiles(value).catch((error) => {
console.error(
"Failed to apply agent-managed profiles setting:",
error,
);
});
}
}}
/>
</div>
);

View File

@@ -1061,12 +1061,14 @@ export async function applyCommunity(
nsec?: string,
token?: string,
reposDir?: string,
agentManagedProfiles?: boolean,
): Promise<void> {
await invokeTauri("apply_workspace", {
relayUrl,
nsec: nsec ?? null,
token: token ?? null,
reposDir: reposDir ?? null,
agentManagedProfiles: agentManagedProfiles ?? false,
});
}
@@ -1079,6 +1081,9 @@ export async function validateReposDir(dir: string): Promise<void> {
export const setPreventSleepActive = (active: boolean) =>
invokeTauri("set_prevent_sleep_active", { active });
export const setAgentManagedProfiles = (enabled: boolean) =>
invokeTauri("set_agent_managed_profiles", { enabled });
/** Returns true on macOS, Windows, and Linux AppImage installs.
* Returns false on Linux non-AppImage packages (e.g. .deb) where
* Tauri's updater cannot swap the binary. */

View File

@@ -9267,6 +9267,8 @@ export function maybeInstallE2eTauriMocks() {
return handleStopManagedAgent(
payload as Parameters<typeof handleStopManagedAgent>[0],
);
case "set_agent_managed_profiles":
return undefined;
case "set_managed_agent_auto_restart":
return handleSetManagedAgentAutoRestart(
payload as Parameters<typeof handleSetManagedAgentAutoRestart>[0],

View File

@@ -32,6 +32,14 @@
"platforms": [
"desktop"
]
},
{
"id": "agentManagedProfiles",
"name": "Agent-managed profiles",
"description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy",
"platforms": [
"desktop"
]
}
]
}