diff --git a/desktop/src-tauri/src/commands/personas/update.rs b/desktop/src-tauri/src/commands/personas/update.rs index b3830e62b5..0e064ddbb1 100644 --- a/desktop/src-tauri/src/commands/personas/update.rs +++ b/desktop/src-tauri/src/commands/personas/update.rs @@ -140,6 +140,10 @@ pub(super) async fn update_persona_with( // If the avatar or display_name changed, propagate to linked agent // records and collect relay profile sync params for the async phase. + // The respond_to propagation is handled by the frontend's + // personaManagedAgentUpdate, which builds a patch for the single + // linked agent whose profile the user edited — not a global + // backend overwrite of every linked instance (#6026). let sync_params: ProfileSyncParams = if avatar_changed || name_changed { let mut records = load_managed_agents(&app)?; let mut params: ProfileSyncParams = Vec::new(); diff --git a/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs b/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs index 0c983fa3e8..007e7ff765 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs +++ b/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs @@ -61,6 +61,8 @@ function persona(overrides = {}) { respondTo: "owner-only", respondToAllowlist: [], envVars: { NEW_KEY: "2" }, + respondTo: null, + respondToAllowlist: [], createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", ...overrides, @@ -191,6 +193,82 @@ test("personaManagedAgentUpdate leaves runtime fields alone when runtime is unch ); }); +test("personaManagedAgentUpdate syncs respond_to mode from persona to linked agent", () => { + // Persona changes from owner-only (null defaults to owner-only) to anyone; + // the agent's instance must be updated. + assert.deepEqual( + personaManagedAgentUpdate( + agent({ respondTo: "owner-only" }), + persona({ respondTo: "anyone" }), + ), + { + pubkey: "deadbeef".repeat(8), + name: "Fizz Prime", + systemPrompt: "New prompt", + model: "new-model", + envVars: { NEW_KEY: "2" }, + respondTo: "anyone", + }, + ); +}); + +test("personaManagedAgentUpdate syncs respond_to allowlist when mode is allowlist", () => { + const allowlist = ["a".repeat(64), "b".repeat(64)]; + assert.deepEqual( + personaManagedAgentUpdate( + agent({ respondTo: "owner-only", respondToAllowlist: [] }), + persona({ respondTo: "allowlist", respondToAllowlist: allowlist }), + ), + { + pubkey: "deadbeef".repeat(8), + name: "Fizz Prime", + systemPrompt: "New prompt", + model: "new-model", + envVars: { NEW_KEY: "2" }, + respondTo: "allowlist", + respondToAllowlist: allowlist, + }, + ); +}); + +test("personaManagedAgentUpdate clears allowlist when mode switches away from allowlist", () => { + // Agent was allowlist with entries; persona switches to anyone — the + // instance's stale allowlist must be cleared. + assert.deepEqual( + personaManagedAgentUpdate( + agent({ respondTo: "allowlist", respondToAllowlist: ["a".repeat(64)] }), + persona({ respondTo: "anyone" }), + ), + { + pubkey: "deadbeef".repeat(8), + name: "Fizz Prime", + systemPrompt: "New prompt", + model: "new-model", + envVars: { NEW_KEY: "2" }, + respondTo: "anyone", + respondToAllowlist: [], + }, + ); +}); + +test("personaManagedAgentUpdate treats null persona respondTo as owner-only", () => { + // Persona has null (unset) = owner-only; agent is currently anyone. + assert.deepEqual( + personaManagedAgentUpdate( + agent({ respondTo: "anyone" }), + persona({ respondTo: null }), + ), + { + pubkey: "deadbeef".repeat(8), + name: "Fizz Prime", + systemPrompt: "New prompt", + model: "new-model", + envVars: { NEW_KEY: "2" }, + respondTo: "owner-only", + }, + ); +}); + test("parseProfilePanelView accepts all profile panel subviews", () => { for (const view of [ "summary", diff --git a/desktop/src/features/profile/ui/UserProfilePanelUtils.ts b/desktop/src/features/profile/ui/UserProfilePanelUtils.ts index be1a57c112..01c7c9da4e 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelUtils.ts +++ b/desktop/src/features/profile/ui/UserProfilePanelUtils.ts @@ -298,19 +298,28 @@ export function personaManagedAgentUpdate( hasChanges = true; } - // Definition edits expose the access policy in the same dialog as identity - // and runtime settings. Keep the exact linked instance in sync when the - // definition carries an explicit policy; otherwise the dialog reopens with - // the new value while the running agent and sidebar retain the old one. - if (persona.respondTo != null && persona.respondTo !== agent.respondTo) { - input.respondTo = persona.respondTo; + // Sync the inbound author gate (respond_to) from the persona to the linked + // instance. The persona stores the wire-shape mode + allowlist; the instance + // stores the typed enum + allowlist that build_respond_to_env reads at spawn. + // Without this, a respond_to-only edit saves to the definition but the + // instance keeps booting with the stale gate (#6026). + // + // This intentionally overwrites the instance's respond_to with the persona's + // value — the persona edit is the owner's explicit intent for this linked + // agent. Instance-level overrides are preserved by apply_persona_snapshot + // during re-snapshot at start/restore, not during persona edit. + const personaMode = persona.respondTo ?? "owner-only"; + if (personaMode !== agent.respondTo) { + input.respondTo = personaMode; hasChanges = true; } - if ( - persona.respondTo === "allowlist" && - !stringArrayEqual(persona.respondToAllowlist, agent.respondToAllowlist) - ) { - input.respondToAllowlist = [...persona.respondToAllowlist]; + + // Sync the allowlist only when the mode is "allowlist". For other modes, + // clear the instance's allowlist so stale entries do not survive a mode + // switch back to "allowlist" later. + const personaAllowlist = personaMode === "allowlist" ? persona.respondToAllowlist : []; + if (!stringArrayEqual(personaAllowlist, agent.respondToAllowlist)) { + input.respondToAllowlist = personaAllowlist; hasChanges = true; }