diff --git a/desktop/scripts/check-registered-agent-boundary.mjs b/desktop/scripts/check-registered-agent-boundary.mjs
new file mode 100755
index 00000000000..cdc57e3132d
--- /dev/null
+++ b/desktop/scripts/check-registered-agent-boundary.mjs
@@ -0,0 +1,114 @@
+#!/usr/bin/env node
+import { readFileSync, readdirSync, statSync } from "node:fs";
+import { join, relative } from "node:path";
+
+const root = process.cwd();
+const src = join(root, "src");
+// Registered references are a display/navigation-only data source. Keep the
+// complete consumer list explicit: a new integration file must be reviewed
+// and added here instead of evading the check because its filename does not
+// happen to contain "registeredAgent".
+const registeredAgentDataFiles = new Set([
+ "src/features/agents/hooks.ts",
+ "src/features/agents/hooksRegistered.test.mjs",
+ "src/features/agents/lib/registeredAgentCards.test.mjs",
+ "src/features/agents/lib/registeredAgentCards.ts",
+ "src/features/agents/lib/useAgentsDataRefresh.ts",
+ "src/features/agents/registeredAgentBoundary.test.mjs",
+ "src/features/agents/ui/AgentsView.tsx",
+ "src/features/agents/ui/RegisterExistingAgentDialog.tsx",
+ "src/features/agents/ui/RegisteredAgentIdentityCard.tsx",
+ "src/features/agents/ui/RemoveRegisteredAgentDialog.tsx",
+ "src/features/agents/ui/UnifiedAgentsSection.tsx",
+ "src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs",
+ "src/shared/api/registeredAgents.test.mjs",
+ "src/shared/api/tauriRegisteredAgents.ts",
+ "src/testing/e2eBridge.ts",
+]);
+const registeredAgentDisplayFiles = new Set([
+ "src/features/agents/lib/registeredAgentCards.ts",
+ "src/features/agents/ui/RegisterExistingAgentDialog.tsx",
+ "src/features/agents/ui/RegisteredAgentIdentityCard.tsx",
+ "src/features/agents/ui/RemoveRegisteredAgentDialog.tsx",
+ "src/shared/api/tauriRegisteredAgents.ts",
+]);
+const registeredAgentDataMarkers = [
+ "RegisteredAgentReference",
+ "listRegisteredAgentReferences",
+ "registerExistingAgentReference",
+ "registeredAgentsQueryKey",
+ "registeredReferences",
+ "unregisterExistingAgentReference",
+ "useRegisteredAgentsQuery",
+];
+const forbiddenTrustMarkers = [
+ "KnownAgentPubkeys",
+ "configNudgeAuthPubkey",
+ "mergeKnownAgentPubkeys",
+ "mentionableAgentPubkeys",
+ "useKnownAgentPubkeys",
+];
+const forbiddenInRegisteredAgentFiles = [
+ "createManagedAgent",
+ "startManagedAgent",
+ "stopManagedAgent",
+ "deleteManagedAgent",
+ "managedAgentRuntime",
+ "privateKeyNsec",
+ "private_key_nsec",
+ "envVars",
+ "agentCommand",
+ "agent_command",
+ "pid",
+];
+
+function walk(dir) {
+ return readdirSync(dir).flatMap((entry) => {
+ const path = join(dir, entry);
+ if (entry === "node_modules" || entry === "dist") return [];
+ if (statSync(path).isDirectory()) return walk(path);
+ return /\.(ts|tsx|mjs)$/.test(path) ? [path] : [];
+ });
+}
+
+const offenders = [];
+for (const path of walk(src)) {
+ const rel = relative(root, path);
+ const text = readFileSync(path, "utf8");
+ const dataHits = registeredAgentDataMarkers.filter((needle) =>
+ text.includes(needle),
+ );
+ if (dataHits.length > 0 && !registeredAgentDataFiles.has(rel)) {
+ offenders.push(
+ `${rel}: registered-reference data (${dataHits.join(", ")})`,
+ );
+ }
+ if (
+ dataHits.length > 0 &&
+ registeredAgentDataFiles.has(rel) &&
+ rel !== "src/features/agents/registeredAgentBoundary.test.mjs"
+ ) {
+ const trustHits = forbiddenTrustMarkers.filter((needle) =>
+ text.includes(needle),
+ );
+ if (trustHits.length > 0) {
+ offenders.push(
+ `${rel}: registered-reference trust leak (${trustHits.join(", ")})`,
+ );
+ }
+ }
+ if (!registeredAgentDisplayFiles.has(rel)) continue;
+ const forbiddenHits = forbiddenInRegisteredAgentFiles.filter((needle) =>
+ text.includes(needle),
+ );
+ if (forbiddenHits.length > 0) {
+ offenders.push(`${rel}: ${forbiddenHits.join(", ")}`);
+ }
+}
+
+if (offenders.length > 0) {
+ console.error("Registered agent boundary violations:");
+ for (const offender of offenders) console.error(`- ${offender}`);
+ process.exit(1);
+}
+console.log("Registered agent boundary OK");
diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs
index cb809b6c04a..15ad006e128 100644
--- a/desktop/src-tauri/src/commands/agent_models.rs
+++ b/desktop/src-tauri/src/commands/agent_models.rs
@@ -699,9 +699,16 @@ use databricks::{discover_databricks_models, DatabricksAuthIntent};
#[path = "agent_models_update.rs"]
mod update;
-pub use update::update_managed_agent;
pub(super) use update::{flush_managed_agent_policy, managed_agent_access_policy_changed};
+pub(super) async fn update_managed_agent_unchecked(
+ input: UpdateManagedAgentRequest,
+ app: AppHandle,
+ state: State<'_, AppState>,
+) -> Result {
+ update::update_managed_agent_impl(input, app, state).await
+}
+
// ── Model normalization ───────────────────────────────────────────────────────
/// Normalize raw `buzz-acp models --json` output into a typed DTO for the frontend.
diff --git a/desktop/src-tauri/src/commands/agent_models_update.rs b/desktop/src-tauri/src/commands/agent_models_update.rs
index bb045b81a24..b81def4a1e9 100644
--- a/desktop/src-tauri/src/commands/agent_models_update.rs
+++ b/desktop/src-tauri/src/commands/agent_models_update.rs
@@ -57,8 +57,7 @@ pub(crate) async fn flush_managed_agent_policy(
/// Most runtime config changes take effect on the next agent spawn. Access
/// policy changes stop active local pairs before saving and restart those exact
/// pairs after the relay policy is flushed.
-#[tauri::command]
-pub async fn update_managed_agent(
+pub(super) async fn update_managed_agent_impl(
input: UpdateManagedAgentRequest,
app: AppHandle,
state: State<'_, AppState>,
diff --git a/desktop/src-tauri/src/commands/agent_registered_targets.rs b/desktop/src-tauri/src/commands/agent_registered_targets.rs
new file mode 100644
index 00000000000..2fb99092d3e
--- /dev/null
+++ b/desktop/src-tauri/src/commands/agent_registered_targets.rs
@@ -0,0 +1,60 @@
+use tauri::{AppHandle, State};
+
+use crate::{
+ app_state::AppState,
+ managed_agents::{
+ reject_registered_reference_target, ManagedAgentSummary, UpdateManagedAgentRequest,
+ UpdateManagedAgentResponse,
+ },
+};
+
+/// Validate ownership before dispatching commands whose implementation lives in
+/// oversized legacy modules. Registered references and unknown pubkeys fail
+/// before any lifecycle, config, or delete side effect.
+#[tauri::command]
+pub async fn update_managed_agent(
+ input: UpdateManagedAgentRequest,
+ app: AppHandle,
+ state: State<'_, AppState>,
+) -> Result {
+ reject_registered_reference_target(&app, &input.pubkey)?;
+ super::agent_models::update_managed_agent_unchecked(input, app, state).await
+}
+
+#[tauri::command]
+pub async fn start_managed_agent(
+ pubkey: String,
+ expected_relay_url: Option,
+ expected_signer_pubkey: Option,
+ app: AppHandle,
+ state: State<'_, AppState>,
+) -> Result {
+ reject_registered_reference_target(&app, &pubkey)?;
+ super::agents::start_managed_agent_unchecked(
+ pubkey,
+ expected_relay_url,
+ expected_signer_pubkey,
+ app,
+ state,
+ )
+ .await
+}
+
+#[tauri::command]
+pub async fn stop_managed_agent(
+ pubkey: String,
+ app: AppHandle,
+) -> Result {
+ reject_registered_reference_target(&app, &pubkey)?;
+ super::agents::stop_managed_agent_unchecked(pubkey, app).await
+}
+
+#[tauri::command]
+pub async fn delete_managed_agent(
+ pubkey: String,
+ force_remote_delete: Option,
+ app: AppHandle,
+) -> Result<(), String> {
+ reject_registered_reference_target(&app, &pubkey)?;
+ super::agents::delete_managed_agent_unchecked(pubkey, force_remote_delete, app).await
+}
diff --git a/desktop/src-tauri/src/commands/agent_settings.rs b/desktop/src-tauri/src/commands/agent_settings.rs
index 6135c671606..6fee3e58132 100644
--- a/desktop/src-tauri/src/commands/agent_settings.rs
+++ b/desktop/src-tauri/src/commands/agent_settings.rs
@@ -23,6 +23,7 @@ pub async fn set_managed_agent_start_on_app_launch(
start_on_app_launch: bool,
app: AppHandle,
) -> Result {
+ crate::managed_agents::reject_registered_reference_target(&app, &pubkey)?;
tokio::task::spawn_blocking(move || {
let state = app.state::();
let _store_guard = state
@@ -67,6 +68,7 @@ pub async fn set_managed_agent_auto_restart(
auto_restart_on_config_change: bool,
app: AppHandle,
) -> Result {
+ crate::managed_agents::reject_registered_reference_target(&app, &pubkey)?;
tokio::task::spawn_blocking(move || {
let state = app.state::();
let _store_guard = state
diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs
index 33b6ae44620..599e7b0babe 100644
--- a/desktop/src-tauri/src/commands/agents.rs
+++ b/desktop/src-tauri/src/commands/agents.rs
@@ -857,8 +857,7 @@ pub async fn create_managed_agent(
}
/// Data needed for background profile reconciliation after agent start.
-#[tauri::command]
-pub async fn start_managed_agent(
+pub(super) async fn start_managed_agent_unchecked(
pubkey: String,
expected_relay_url: Option,
expected_signer_pubkey: Option,
@@ -1036,8 +1035,7 @@ pub async fn start_managed_agent(
result
}
-#[tauri::command]
-pub async fn stop_managed_agent(
+pub(super) async fn stop_managed_agent_unchecked(
pubkey: String,
app: AppHandle,
) -> Result {
@@ -1089,8 +1087,7 @@ pub async fn stop_managed_agent(
// Async so the blocking body (disk reads/writes, process termination, keyring
// delete, nest regeneration) runs off the main UI thread via spawn_blocking.
-#[tauri::command]
-pub async fn delete_managed_agent(
+pub(super) async fn delete_managed_agent_unchecked(
pubkey: String,
force_remote_delete: Option,
app: AppHandle,
diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs
index 761bee9cd32..70a9d9b9a70 100644
--- a/desktop/src-tauri/src/commands/mod.rs
+++ b/desktop/src-tauri/src/commands/mod.rs
@@ -8,6 +8,7 @@ mod agent_model_process;
mod agent_models;
mod agent_models_env;
mod agent_providers;
+mod agent_registered_targets;
mod agent_settings;
mod agent_update_rollback;
mod agents;
@@ -76,6 +77,7 @@ pub use agent_logs::*;
pub use agent_metric_archive::*;
pub use agent_models::*;
pub use agent_providers::*;
+pub use agent_registered_targets::*;
pub use agent_settings::*;
pub use agents::*;
pub use canvas::*;
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 31d4ff37133..f39df5a0e5c 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -71,9 +71,10 @@ use huddle::{
use initial_window::*;
use managed_agents::{
backfill_persona_snapshots, ensure_nest, list_managed_agent_runtimes,
- put_managed_agent_runtime_lifecycle, reconcile_managed_agent_runtimes,
+ list_registered_agent_references, put_managed_agent_runtime_lifecycle,
+ reconcile_managed_agent_runtimes, register_existing_agent_reference,
restart_managed_agent_runtime, start_managed_agent_runtime, stop_managed_agent_runtime,
- try_regenerate_nest,
+ try_regenerate_nest, unregister_existing_agent_reference,
};
#[cfg(not(feature = "mesh-llm"))]
use mesh_llm_stubs::*;
@@ -795,6 +796,9 @@ pub fn run() {
mesh_installed_models,
mesh_model_catalog,
update_managed_agent,
+ list_registered_agent_references,
+ register_existing_agent_reference,
+ unregister_existing_agent_reference,
discover_backend_providers,
probe_backend_provider,
list_personas,
diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs
index 272c03348b9..10fa500aa27 100644
--- a/desktop/src-tauri/src/managed_agents/mod.rs
+++ b/desktop/src-tauri/src/managed_agents/mod.rs
@@ -28,6 +28,7 @@ mod personas;
mod process_lifecycle;
pub(crate) mod readiness;
pub(crate) mod reconcile;
+mod registered_references;
mod relay_mesh;
mod repos;
mod restore;
@@ -76,6 +77,10 @@ pub(crate) use readiness::{
agent_readiness, resolve_effective_agent_env, resolve_effective_harness_descriptor,
AgentReadiness, Requirement,
};
+pub(crate) use registered_references::{
+ list_registered_agent_references, register_existing_agent_reference,
+ reject_registered_reference_target, unregister_existing_agent_reference,
+};
pub use relay_mesh::*;
pub use repos::{
effective_repos_dir, ensure_repos_symlink, resolve_repos_at_boot, validate_repos_dir,
diff --git a/desktop/src-tauri/src/managed_agents/registered_references.rs b/desktop/src-tauri/src/managed_agents/registered_references.rs
new file mode 100644
index 00000000000..5511cea9ef8
--- /dev/null
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -0,0 +1,719 @@
+use std::path::{Path, PathBuf};
+
+use serde::{Deserialize, Serialize};
+use tauri::{AppHandle, Emitter, Manager};
+
+use super::storage::{atomic_write_json_restricted, backup_invalid_store, managed_agents_base_dir};
+use crate::app_state::AppState;
+
+const STORE_FILENAME: &str = "registered-agent-references.json";
+const AGENTS_DATA_CHANGED_EVENT: &str = "agents-data-changed";
+const LABEL_LIMIT_BYTES: usize = 80;
+const ROLE_SUMMARY_LIMIT_BYTES: usize = 240;
+
+/// A keyless reference to an already-existing agent identity.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RegisteredAgentReference {
+ /// The referenced agent public key as normalized 64-character lowercase hex.
+ pub pubkey: String,
+ /// Optional user-facing label for the reference.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub label: Option,
+ /// Optional short description of the agent's role.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub role_summary: Option,
+ /// Creation timestamp in ISO-8601 UTC form.
+ pub created_at: String,
+ /// Last update timestamp in ISO-8601 UTC form.
+ pub updated_at: String,
+}
+
+/// Request payload for registering or updating an existing agent reference.
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct RegisterAgentReferenceRequest {
+ /// Agent public key, accepted with leading/trailing whitespace and mixed case.
+ pub pubkey: String,
+ /// Optional label; blank strings are stored as `None`.
+ #[serde(default)]
+ pub label: Option,
+ /// Optional role summary; blank strings are stored as `None`.
+ #[serde(default)]
+ pub role_summary: Option,
+}
+
+fn store_path(app: &AppHandle) -> Result {
+ Ok(managed_agents_base_dir(app)?.join(STORE_FILENAME))
+}
+
+fn normalize_pubkey(input: &str) -> Result {
+ let trimmed = input.trim();
+ if trimmed.len() != 64 || !trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
+ return Err("invalid public key".to_string());
+ }
+ Ok(trimmed.to_ascii_lowercase())
+}
+
+fn normalize_optional(
+ value: Option,
+ limit: usize,
+ field: &str,
+) -> Result
) : null}
+ {registeredReferencesError ? (
+
+ {registeredReferencesError.message}
+
+ ) : null}
);
}
diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs b/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs
index 690a921040e..0eff46fc604 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs
@@ -94,6 +94,11 @@ function baseProps(overrides = {}) {
onStartAgent: () => {},
onStartPersona: () => {},
personas: [],
+ registeredReferences: [],
+ registeredReferencesError: null,
+ isRegisteredReferencesLoading: false,
+ isRegisteredReferencePending: false,
+ onRemoveRegisteredReference: () => {},
personasError: null,
personaFeedbackErrorMessage: null,
personaFeedbackNoticeMessage: null,
diff --git a/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs b/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
new file mode 100644
index 00000000000..7032ed1b72a
--- /dev/null
+++ b/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
@@ -0,0 +1,48 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+const registerDialog = readFileSync(
+ new URL("./RegisterExistingAgentDialog.tsx", import.meta.url),
+ "utf8",
+);
+const registeredCard = readFileSync(
+ new URL("./RegisteredAgentIdentityCard.tsx", import.meta.url),
+ "utf8",
+);
+const agentsView = readFileSync(
+ new URL("./AgentsView.tsx", import.meta.url),
+ "utf8",
+);
+
+test("registration rejection stays handled and leaves the dialog open", () => {
+ assert.match(
+ registerDialog,
+ /try\s*\{[\s\S]*await onSubmit\([\s\S]*onOpenChange\(false\)[\s\S]*\}\s*catch\s*\{/,
+ );
+});
+
+test("clipboard rejection is handled inside the registered-reference card", () => {
+ assert.match(
+ registeredCard,
+ /try\s*\{[\s\S]*await navigator\.clipboard\?\.writeText\([\s\S]*\}\s*catch\s*\{/,
+ );
+});
+
+test("closing registration resets stale mutation errors", () => {
+ assert.match(
+ agentsView,
+ /onOpenChange=\{\(open\) => \{[\s\S]*setIsRegisterExistingOpen\(open\)[\s\S]*if \(!open\) registerReferenceMutation\.reset\(\)/,
+ );
+});
+
+test("unregister rejection is consumed instead of escaping the UI event", () => {
+ assert.match(
+ agentsView,
+ /unregisterReferenceMutation[\s\S]*\.mutateAsync\(reference\)[\s\S]*\.then\([\s\S]*\.catch\(/,
+ );
+ assert.match(
+ agentsView,
+ /\.catch\(\(error\) =>[\s\S]*agents\.setActionErrorMessage\(/,
+ );
+});
diff --git a/desktop/src/shared/api/registeredAgents.test.mjs b/desktop/src/shared/api/registeredAgents.test.mjs
new file mode 100644
index 00000000000..b68b7e5ed90
--- /dev/null
+++ b/desktop/src/shared/api/registeredAgents.test.mjs
@@ -0,0 +1,63 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ fromRawRegisteredAgentReference,
+ toRawRegisterExistingAgentInput,
+} from "./tauriRegisteredAgents.ts";
+
+const PUBKEY = `${"ABCDEF".repeat(10)}ABCD`;
+
+test("maps registered agent references from Tauri snake_case to TS camelCase without runtime fields", () => {
+ const mapped = fromRawRegisteredAgentReference({
+ pubkey: PUBKEY,
+ label: "Existing Goose",
+ role_summary: "reviewer",
+ created_at: "2026-08-18T12:00:00Z",
+ updated_at: "2026-08-18T12:30:00Z",
+ private_key_nsec: "redacted-test-value",
+ agent_command: "goose",
+ status: "running",
+ pid: 123,
+ });
+
+ assert.deepEqual(mapped, {
+ pubkey: PUBKEY.toLowerCase(),
+ label: "Existing Goose",
+ roleSummary: "reviewer",
+ createdAt: "2026-08-18T12:00:00Z",
+ updatedAt: "2026-08-18T12:30:00Z",
+ });
+ assert.equal("privateKeyNsec" in mapped, false);
+ assert.equal("agentCommand" in mapped, false);
+ assert.equal("status" in mapped, false);
+ assert.equal("pid" in mapped, false);
+});
+
+test("register input sends only pubkey, label, and role summary with omitted blanks as null", () => {
+ const raw = toRawRegisterExistingAgentInput({
+ pubkey: ` ${PUBKEY} `,
+ label: " Existing Goose ",
+ roleSummary: " ",
+ });
+
+ assert.deepEqual(raw, {
+ pubkey: PUBKEY.toLowerCase(),
+ label: "Existing Goose",
+ roleSummary: null,
+ });
+});
+
+test("malformed registered agent store entries fail visibly instead of being filtered", () => {
+ assert.throws(
+ () =>
+ fromRawRegisteredAgentReference({
+ pubkey: "not-a-key",
+ label: "Bad",
+ role_summary: null,
+ created_at: "2026-08-18T12:00:00Z",
+ updated_at: "2026-08-18T12:30:00Z",
+ }),
+ /invalid public key/i,
+ );
+});
diff --git a/desktop/src/shared/api/tauriRegisteredAgents.ts b/desktop/src/shared/api/tauriRegisteredAgents.ts
new file mode 100644
index 00000000000..499a02364ea
--- /dev/null
+++ b/desktop/src/shared/api/tauriRegisteredAgents.ts
@@ -0,0 +1,105 @@
+import { normalizePubkey } from "@/shared/lib/pubkey";
+import { invokeTauri } from "./tauri";
+
+export type RegisteredAgentReference = {
+ pubkey: string;
+ label: string | null;
+ roleSummary: string | null;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type RawRegisteredAgentReference = {
+ pubkey: string;
+ label?: string | null;
+ role_summary?: string | null;
+ created_at: string;
+ updated_at: string;
+};
+
+type RegisterExistingAgentInput = {
+ pubkey: string;
+ label?: string | null;
+ roleSummary?: string | null;
+};
+
+export type RawRegisterExistingAgentInput = {
+ pubkey: string;
+ label: string | null;
+ roleSummary: string | null;
+};
+
+const HEX_PUBKEY_RE = /^[0-9a-f]{64}$/;
+
+function requiredString(value: unknown, field: string): string {
+ if (typeof value !== "string") {
+ throw new Error(`Malformed registered agent ${field}.`);
+ }
+ return value;
+}
+
+function nullableString(value: unknown, field: string): string | null {
+ if (value === undefined || value === null) return null;
+ if (typeof value !== "string") {
+ throw new Error(`Malformed registered agent ${field}.`);
+ }
+ const trimmed = value.trim();
+ return trimmed.length > 0 ? trimmed : null;
+}
+
+function normalizeRegisteredPubkey(value: unknown): string {
+ const pubkey = normalizePubkey(requiredString(value, "pubkey"));
+ if (!HEX_PUBKEY_RE.test(pubkey)) {
+ throw new Error("invalid public key");
+ }
+ return pubkey;
+}
+
+export function fromRawRegisteredAgentReference(
+ raw: RawRegisteredAgentReference,
+): RegisteredAgentReference {
+ return {
+ pubkey: normalizeRegisteredPubkey(raw.pubkey),
+ label: nullableString(raw.label, "label"),
+ roleSummary: nullableString(raw.role_summary, "role_summary"),
+ createdAt: requiredString(raw.created_at, "created_at"),
+ updatedAt: requiredString(raw.updated_at, "updated_at"),
+ };
+}
+
+export function toRawRegisterExistingAgentInput(
+ input: RegisterExistingAgentInput,
+): RawRegisterExistingAgentInput {
+ return {
+ pubkey: normalizeRegisteredPubkey(input.pubkey),
+ label: nullableString(input.label, "label"),
+ roleSummary: nullableString(input.roleSummary, "roleSummary"),
+ };
+}
+
+export async function listRegisteredAgentReferences(): Promise<
+ RegisteredAgentReference[]
+> {
+ const raw = await invokeTauri(
+ "list_registered_agent_references",
+ );
+ return raw.map(fromRawRegisteredAgentReference);
+}
+
+export async function registerExistingAgentReference(
+ input: RegisterExistingAgentInput,
+): Promise {
+ const raw = await invokeTauri(
+ "register_existing_agent_reference",
+ { input: toRawRegisterExistingAgentInput(input) },
+ );
+ return fromRawRegisteredAgentReference(raw);
+}
+
+export async function unregisterExistingAgentReference(
+ pubkey: string,
+): Promise {
+ await invokeTauri("unregister_existing_agent_reference", {
+ pubkey: normalizeRegisteredPubkey(pubkey),
+ });
+}
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 53b17fc7ef0..632f3b6d192 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -111,6 +111,14 @@ type MockManagedAgentRuntimeSeed = {
lifecycle?: MockManagedAgentRuntimeRow["lifecycle"];
};
+type MockRegisteredAgentReference = {
+ pubkey: string;
+ label?: string | null;
+ role_summary?: string | null;
+ created_at: string;
+ updated_at: string;
+};
+
type MockRelayAgentSeed = {
pubkey: string;
ownerPubkey?: string | null;
@@ -278,6 +286,8 @@ type E2eConfig = {
mcp?: MockCommandAvailability;
};
managedAgents?: MockManagedAgentSeed[];
+ registeredAgents?: MockRegisteredAgentReference[];
+ registeredAgentsError?: string;
/** Result returned by the mocked `add_agent_to_huddle` command. */
addAgentToHuddleResult?: {
ephemeral_added: boolean;
@@ -3089,6 +3099,7 @@ let mockClosedChannelLiveSubscription = false;
const realSockets = new Map();
let mockManagedAgents: MockManagedAgent[] = [];
let mockManagedAgentRuntimes: MockManagedAgentRuntimeRow[] = [];
+let mockRegisteredAgents: MockRegisteredAgentReference[] = [];
// Mutable `save_subscriptions` table mirror — TEST-ONLY.
//
@@ -10396,6 +10407,7 @@ export function maybeInstallE2eTauriMocks() {
resetMockRelayMembers(config);
resetMockRelayAgents(config);
resetMockManagedAgents(config);
+ mockRegisteredAgents = structuredClone(config.mock?.registeredAgents ?? []);
resetMockPersonas(config);
resetMockTeams(config);
seedMockSearchProfiles(config);
@@ -12725,6 +12737,69 @@ export function maybeInstallE2eTauriMocks() {
}
case "list_managed_agents":
return handleListManagedAgents(activeConfig);
+ case "list_registered_agent_references":
+ if (activeConfig?.mock?.registeredAgentsError) {
+ throw new Error(activeConfig.mock.registeredAgentsError);
+ }
+ return structuredClone(mockRegisteredAgents).sort((left, right) =>
+ left.pubkey.localeCompare(right.pubkey),
+ );
+ case "register_existing_agent_reference": {
+ const input = (payload as { input?: Record } | null)
+ ?.input;
+ if (
+ !input ||
+ Object.keys(input).some(
+ (key) => !["pubkey", "label", "roleSummary"].includes(key),
+ )
+ ) {
+ throw new Error("invalid registered agent input");
+ }
+ const pubkey = String(input.pubkey ?? "")
+ .trim()
+ .toLowerCase();
+ if (!/^[0-9a-f]{64}$/.test(pubkey)) {
+ throw new Error("invalid public key");
+ }
+ if (
+ mockManagedAgents.some(
+ (agent) => agent.pubkey.toLowerCase() === pubkey,
+ )
+ ) {
+ throw new Error(`agent ${pubkey} is already a managed agent`);
+ }
+ const now = new Date().toISOString();
+ const existing = mockRegisteredAgents.find(
+ (reference) => reference.pubkey === pubkey,
+ );
+ const reference: MockRegisteredAgentReference = {
+ pubkey,
+ label:
+ typeof input.label === "string" && input.label.trim()
+ ? input.label.trim()
+ : null,
+ role_summary:
+ typeof input.roleSummary === "string" && input.roleSummary.trim()
+ ? input.roleSummary.trim()
+ : null,
+ created_at: existing?.created_at ?? now,
+ updated_at: now,
+ };
+ mockRegisteredAgents = [
+ ...mockRegisteredAgents.filter((item) => item.pubkey !== pubkey),
+ reference,
+ ];
+ return structuredClone(reference);
+ }
+ case "unregister_existing_agent_reference": {
+ const pubkey = String(
+ (payload as { pubkey?: unknown } | null)?.pubkey ?? "",
+ ).toLowerCase();
+ mockRegisteredAgents = mockRegisteredAgents.filter(
+ (item) => item.pubkey !== pubkey,
+ );
+ return null;
+ }
case "get_agent_memory":
return handleGetAgentMemory(
(payload as Parameters[0]) ?? {},
diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts
index b81c5889f3a..21c17dfa723 100644
--- a/desktop/tests/e2e/agents.spec.ts
+++ b/desktop/tests/e2e/agents.spec.ts
@@ -2708,3 +2708,218 @@ test("duplicate instances move from the agents gallery into the agent profile",
page.getByTestId(`user-profile-agent-delete-${additionalPubkey}`),
).toHaveCount(0);
});
+
+test("register existing agent stays keyless and has no lifecycle controls", async ({
+ page,
+}) => {
+ const pubkey = "a1".repeat(32);
+ await installMockBridge(page, { registeredAgents: [] });
+ await gotoApp(page);
+ await page.getByTestId("open-agents-view").click();
+
+ await page.getByRole("button", { name: "Register existing agent" }).click();
+ const dialog = page.getByRole("dialog", { name: "Register existing agent" });
+ await expect(dialog).toContainText(
+ "Registers an existing identity for this device. Buzz will not import its key or run it.",
+ );
+ await dialog.getByTestId("register-existing-agent-pubkey").fill(pubkey);
+ await dialog
+ .getByTestId("register-existing-agent-label")
+ .fill("Outside Goose");
+ await dialog
+ .getByTestId("register-existing-agent-role-summary")
+ .fill("Reviewer");
+ await dialog.getByRole("button", { name: "Register reference" }).click();
+
+ const card = page.getByTestId(`registered-agent-${pubkey}`);
+ await expect(card).toContainText("Outside Goose");
+ await expect(card).toContainText("Reviewer · Externally managed");
+ await expect(card).toContainText(pubkey.slice(-4));
+ await expect(page.getByTestId(`agent-runtime-start-${pubkey}`)).toHaveCount(
+ 0,
+ );
+ await expect(card.getByText(pubkey, { exact: true })).toHaveCount(0);
+
+ const commands = await page.evaluate(() =>
+ (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).map((call) => call.command),
+ );
+ expect(commands).toContain("register_existing_agent_reference");
+ expect(commands).not.toContain("create_managed_agent");
+ expect(commands).not.toContain("start_managed_agent");
+});
+
+test("registered reference opens exact profile and remains display-only", async ({
+ page,
+}) => {
+ const pubkey = "b2".repeat(32);
+ const timestamp = "2026-08-18T00:00:00Z";
+ await installMockBridge(page, {
+ registeredAgents: [
+ {
+ pubkey,
+ label: "External Finch",
+ role_summary: "Research",
+ created_at: timestamp,
+ updated_at: timestamp,
+ },
+ ],
+ });
+ await gotoApp(page);
+ await page.getByTestId("open-agents-view").click();
+
+ const card = page.getByTestId(`registered-agent-${pubkey}`);
+ const truncatedPubkey = `${pubkey.slice(0, 8)}…${pubkey.slice(-4)}`;
+ await expect(
+ card.getByRole("button", { name: new RegExp(truncatedPubkey) }),
+ ).toBeVisible();
+ for (const control of [
+ "Start",
+ "Restart",
+ "Deploy",
+ "Auto-start",
+ "Model",
+ "Runtime error",
+ "Reveal secret",
+ ]) {
+ await expect(
+ card.getByRole("button", { name: new RegExp(control, "i") }),
+ ).toHaveCount(0);
+ }
+
+ await card.click();
+ await expect(page.getByTestId("user-profile-panel")).toBeVisible();
+ const profileCalls = await page.evaluate(() =>
+ (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).filter(
+ (call) => call.command === "get_user_profile",
+ ),
+ );
+ expect(
+ profileCalls.some(
+ (call) =>
+ (call.payload as { pubkey?: unknown } | null)?.pubkey === pubkey,
+ ),
+ ).toBe(true);
+});
+
+test("registration rejects invalid and managed collisions without cards and normalizes blanks", async ({
+ page,
+}) => {
+ const managedPubkey = "c3".repeat(32);
+ const validPubkey = "d4".repeat(32);
+ await installMockBridge(page, {
+ managedAgents: [
+ {
+ pubkey: managedPubkey,
+ name: "Managed Collision",
+ },
+ ],
+ registeredAgents: [],
+ });
+ await gotoApp(page);
+ await page.getByTestId("open-agents-view").click();
+
+ for (const [input, error] of [
+ ["not-a-pubkey", "invalid public key"],
+ [managedPubkey, `agent ${managedPubkey} is already a managed agent`],
+ ] as const) {
+ await page.getByRole("button", { name: "Register existing agent" }).click();
+ const dialog = page.getByRole("dialog", {
+ name: "Register existing agent",
+ });
+ await dialog.getByTestId("register-existing-agent-pubkey").fill(input);
+ await dialog.getByRole("button", { name: "Register reference" }).click();
+ await expect(dialog).toContainText(error);
+ await expect(page.getByTestId(`registered-agent-${input}`)).toHaveCount(0);
+ await dialog.getByRole("button", { name: "Cancel" }).click();
+ }
+
+ await page.getByRole("button", { name: "Register existing agent" }).click();
+ const dialog = page.getByRole("dialog", { name: "Register existing agent" });
+ await dialog.getByTestId("register-existing-agent-pubkey").fill(validPubkey);
+ await dialog.getByTestId("register-existing-agent-label").fill(" ");
+ await dialog.getByTestId("register-existing-agent-role-summary").fill(" \t ");
+ await dialog.getByRole("button", { name: "Register reference" }).click();
+ await expect(
+ page.getByTestId(`registered-agent-${validPubkey}`),
+ ).toBeVisible();
+ const registerInput = await page.evaluate(() => {
+ const call = (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).findLast(
+ (entry) => entry.command === "register_existing_agent_reference",
+ );
+ return (call?.payload as { input?: unknown } | null)?.input;
+ });
+ expect(registerInput).toMatchObject({
+ pubkey: validPubkey,
+ label: null,
+ roleSummary: null,
+ });
+});
+
+test("same-name references stay distinct and removing one only unregisters that reference", async ({
+ page,
+}) => {
+ const first = "e5".repeat(32);
+ const second = "f6".repeat(32);
+ const timestamp = "2026-08-18T00:00:00Z";
+ await installMockBridge(page, {
+ registeredAgents: [first, second].map((pubkey) => ({
+ pubkey,
+ label: "Same Name",
+ role_summary: null,
+ created_at: timestamp,
+ updated_at: timestamp,
+ })),
+ });
+ await gotoApp(page);
+ await page.getByTestId("open-agents-view").click();
+
+ for (const pubkey of [first, second]) {
+ const card = page.getByTestId(`registered-agent-${pubkey}`);
+ await expect(card).toContainText(pubkey.slice(-4));
+ const truncatedPubkey = `${pubkey.slice(0, 8)}…${pubkey.slice(-4)}`;
+ await expect(
+ card.getByRole("button", { name: new RegExp(truncatedPubkey) }),
+ ).toBeVisible();
+ }
+
+ await page.getByTestId(`registered-agent-actions-${first}`).click();
+ await page.getByRole("menuitem", { name: "Remove reference" }).click();
+ const confirm = page.getByRole("alertdialog");
+ await expect(confirm).toContainText(
+ "removes only the local card and reference",
+ );
+ await confirm.getByRole("button", { name: "Remove reference" }).click();
+ await expect(page.getByTestId(`registered-agent-${first}`)).toHaveCount(0);
+ await expect(page.getByTestId(`registered-agent-${second}`)).toBeVisible();
+
+ const mutations = await page.evaluate(() =>
+ (window.__BUZZ_E2E_COMMAND_LOG__ ?? []).filter((call) =>
+ ["unregister_existing_agent_reference", "delete_managed_agent"].includes(
+ call.command,
+ ),
+ ),
+ );
+ expect(mutations).toEqual([
+ expect.objectContaining({
+ command: "unregister_existing_agent_reference",
+ payload: { pubkey: first },
+ }),
+ ]);
+});
+
+test("malformed registered-reference store renders error and zero reference cards", async ({
+ page,
+}) => {
+ await installMockBridge(page, {
+ registeredAgentsError: "failed to parse registered agent references",
+ });
+ await gotoApp(page);
+ await page.getByTestId("open-agents-view").click();
+
+ await expect(
+ page.getByText("failed to parse registered agent references"),
+ ).toBeVisible();
+ await expect(page.locator('[data-testid^="registered-agent-"]')).toHaveCount(
+ 0,
+ );
+});
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index ed94e6b1767..27ee8804609 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -220,6 +220,14 @@ type MockBridgeOptions = {
mcp?: MockCommandAvailability;
};
managedAgents?: MockManagedAgentSeed[];
+ registeredAgents?: Array<{
+ pubkey: string;
+ label?: string | null;
+ role_summary?: string | null;
+ created_at: string;
+ updated_at: string;
+ }>;
+ registeredAgentsError?: string;
/** Result returned by the mocked `add_agent_to_huddle` command. */
addAgentToHuddleResult?: {
ephemeral_added: boolean;