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
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/commands/personas/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ pub(super) async fn update_persona_with<R: Send + 'static>(

// 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();
Expand Down
78 changes: 78 additions & 0 deletions desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
31 changes: 20 additions & 11 deletions desktop/src/features/profile/ui/UserProfilePanelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down