From 1d325fb52063c581f5386222ca8c3347878b5114 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 04:25:59 +0000
Subject: [PATCH 01/12] feat(agents): register existing agent references
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../check-registered-agent-boundary.mjs | 43 +++
.../src-tauri/src/commands/agent_models.rs | 9 +-
.../src/commands/agent_registered_targets.rs | 51 +++
.../src-tauri/src/commands/agent_settings.rs | 2 +
desktop/src-tauri/src/commands/agents.rs | 9 +-
desktop/src-tauri/src/commands/mod.rs | 2 +
desktop/src-tauri/src/lib.rs | 8 +-
desktop/src-tauri/src/managed_agents/mod.rs | 5 +
.../managed_agents/registered_references.rs | 356 ++++++++++++++++++
.../src/managed_agents/runtime_commands.rs | 3 +
.../src-tauri/src/managed_agents/storage.rs | 8 +
desktop/src/features/agents/hooks.ts | 48 +++
.../features/agents/hooksRegistered.test.mjs | 37 ++
.../agents/lib/registeredAgentCards.test.mjs | 61 +++
.../agents/lib/registeredAgentCards.ts | 46 +++
.../agents/lib/useAgentsDataRefresh.ts | 8 +-
.../agents/registeredAgentBoundary.test.mjs | 41 ++
desktop/src/features/agents/ui/AgentsView.tsx | 74 +++-
.../agents/ui/RegisterExistingAgentDialog.tsx | 128 +++++++
.../agents/ui/RegisteredAgentIdentityCard.tsx | 90 +++++
.../agents/ui/RemoveRegisteredAgentDialog.tsx | 51 +++
.../agents/ui/UnifiedAgentsSection.tsx | 36 +-
.../features/agents/useKnownAgentPubkeys.tsx | 12 +-
.../src/shared/api/registeredAgents.test.mjs | 63 ++++
.../src/shared/api/tauriRegisteredAgents.ts | 105 ++++++
desktop/src/testing/e2eBridge.ts | 73 ++++
desktop/tests/e2e/agents.spec.ts | 39 ++
desktop/tests/helpers/bridge.ts | 8 +
28 files changed, 1399 insertions(+), 17 deletions(-)
create mode 100755 desktop/scripts/check-registered-agent-boundary.mjs
create mode 100644 desktop/src-tauri/src/commands/agent_registered_targets.rs
create mode 100644 desktop/src-tauri/src/managed_agents/registered_references.rs
create mode 100644 desktop/src/features/agents/hooksRegistered.test.mjs
create mode 100644 desktop/src/features/agents/lib/registeredAgentCards.test.mjs
create mode 100644 desktop/src/features/agents/lib/registeredAgentCards.ts
create mode 100644 desktop/src/features/agents/registeredAgentBoundary.test.mjs
create mode 100644 desktop/src/features/agents/ui/RegisterExistingAgentDialog.tsx
create mode 100644 desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
create mode 100644 desktop/src/features/agents/ui/RemoveRegisteredAgentDialog.tsx
create mode 100644 desktop/src/shared/api/registeredAgents.test.mjs
create mode 100644 desktop/src/shared/api/tauriRegisteredAgents.ts
diff --git a/desktop/scripts/check-registered-agent-boundary.mjs b/desktop/scripts/check-registered-agent-boundary.mjs
new file mode 100755
index 00000000000..a4e70695c53
--- /dev/null
+++ b/desktop/scripts/check-registered-agent-boundary.mjs
@@ -0,0 +1,43 @@
+#!/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");
+const forbidden = [
+ "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 (statSync(path).isDirectory()) return walk(path);
+ return /\.(ts|tsx)$/.test(path) ? [path] : [];
+ });
+}
+
+const offenders = [];
+for (const path of walk(src)) {
+ const rel = relative(root, path);
+ if (!/registeredAgent|RegisteredAgent/.test(rel)) continue;
+ const text = readFileSync(path, "utf8");
+ const hits = forbidden.filter((needle) => text.includes(needle));
+ if (hits.length > 0) offenders.push(`${rel}: ${hits.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..770aa064fba 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(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_registered_targets.rs b/desktop/src-tauri/src/commands/agent_registered_targets.rs
new file mode 100644
index 00000000000..899043114d6
--- /dev/null
+++ b/desktop/src-tauri/src/commands/agent_registered_targets.rs
@@ -0,0 +1,51 @@
+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,
+ app: AppHandle,
+ state: State<'_, AppState>,
+) -> Result {
+ reject_registered_reference_target(&app, &pubkey)?;
+ super::agents::start_managed_agent_unchecked(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..9f92b2ccff0
--- /dev/null
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -0,0 +1,356 @@
+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-byte 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("agent pubkey must be exactly 64 ASCII hex characters".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/useKnownAgentPubkeys.tsx b/desktop/src/features/agents/useKnownAgentPubkeys.tsx
index e9fe7b9a9b0..5d2f281ded4 100644
--- a/desktop/src/features/agents/useKnownAgentPubkeys.tsx
+++ b/desktop/src/features/agents/useKnownAgentPubkeys.tsx
@@ -2,6 +2,7 @@ import * as React from "react";
import {
useManagedAgentsQuery,
+ useRegisteredAgentsQuery,
useRelayAgentsQuery,
} from "@/features/agents/hooks";
import { mergeKnownAgentPubkeys } from "@/features/agents/knownAgentPubkeys";
@@ -41,10 +42,17 @@ export function KnownAgentPubkeysProvider({
}) {
const managedAgents = useManagedAgentsQuery().data;
const relayAgents = useRelayAgentsQuery().data;
+ const registeredAgents = useRegisteredAgentsQuery().data;
const merged = React.useMemo(
- () => mergeKnownAgentPubkeys(managedAgents, relayAgents),
- [managedAgents, relayAgents],
+ () =>
+ mergeKnownAgentPubkeys(managedAgents, [
+ ...(relayAgents ?? []),
+ ...(registeredAgents ?? []).map((reference) => ({
+ pubkey: reference.pubkey,
+ })),
+ ]),
+ [managedAgents, relayAgents, registeredAgents],
);
const stable = useStableSet(merged);
diff --git a/desktop/src/shared/api/registeredAgents.test.mjs b/desktop/src/shared/api/registeredAgents.test.mjs
new file mode 100644
index 00000000000..e8e7666d830
--- /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",
+ }),
+ /registered agent pubkey/i,
+ );
+});
diff --git a/desktop/src/shared/api/tauriRegisteredAgents.ts b/desktop/src/shared/api/tauriRegisteredAgents.ts
new file mode 100644
index 00000000000..00d7a42f7a4
--- /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("Malformed registered agent pubkey.");
+ }
+ 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..0ea53cc2e59 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,67 @@ 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);
+ 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..d6b4bc04e39 100644
--- a/desktop/tests/e2e/agents.spec.ts
+++ b/desktop/tests/e2e/agents.spec.ts
@@ -2708,3 +2708,42 @@ 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");
+});
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;
From ab5774687b5ab850ca371b3bbb2fda03e7cbb282 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 14:24:49 +0000
Subject: [PATCH 02/12] test(agents): harden registered reference boundaries
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../check-registered-agent-boundary.mjs | 74 +++++-
.../src-tauri/src/commands/agent_models.rs | 2 +-
.../src/commands/agent_models_update.rs | 3 +-
.../src/commands/agent_registered_targets.rs | 11 +-
.../managed_agents/registered_references.rs | 251 +++++++++++++++++-
.../src-tauri/src/managed_agents/storage.rs | 21 +-
.../features/agents/hooksRegistered.test.mjs | 22 +-
.../agents/registeredAgentBoundary.test.mjs | 70 ++++-
.../UnifiedAgentsSectionCardTarget.test.mjs | 5 +
.../features/agents/useKnownAgentPubkeys.tsx | 12 +-
desktop/tests/e2e/agents.spec.ts | 174 ++++++++++++
11 files changed, 611 insertions(+), 34 deletions(-)
diff --git a/desktop/scripts/check-registered-agent-boundary.mjs b/desktop/scripts/check-registered-agent-boundary.mjs
index a4e70695c53..db331b667ee 100755
--- a/desktop/scripts/check-registered-agent-boundary.mjs
+++ b/desktop/scripts/check-registered-agent-boundary.mjs
@@ -4,7 +4,51 @@ import { join, relative } from "node:path";
const root = process.cwd();
const src = join(root, "src");
-const forbidden = [
+// 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",
@@ -29,10 +73,32 @@ function walk(dir) {
const offenders = [];
for (const path of walk(src)) {
const rel = relative(root, path);
- if (!/registeredAgent|RegisteredAgent/.test(rel)) continue;
const text = readFileSync(path, "utf8");
- const hits = forbidden.filter((needle) => text.includes(needle));
- if (hits.length > 0) offenders.push(`${rel}: ${hits.join(", ")}`);
+ 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)) {
+ 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) {
diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs
index 770aa064fba..15ad006e128 100644
--- a/desktop/src-tauri/src/commands/agent_models.rs
+++ b/desktop/src-tauri/src/commands/agent_models.rs
@@ -706,7 +706,7 @@ pub(super) async fn update_managed_agent_unchecked(
app: AppHandle,
state: State<'_, AppState>,
) -> Result {
- update::update_managed_agent(input, app, state).await
+ update::update_managed_agent_impl(input, app, state).await
}
// ── Model normalization ───────────────────────────────────────────────────────
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
index 899043114d6..2fb99092d3e 100644
--- a/desktop/src-tauri/src/commands/agent_registered_targets.rs
+++ b/desktop/src-tauri/src/commands/agent_registered_targets.rs
@@ -24,11 +24,20 @@ pub async fn update_managed_agent(
#[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, app, state).await
+ super::agents::start_managed_agent_unchecked(
+ pubkey,
+ expected_relay_url,
+ expected_signer_pubkey,
+ app,
+ state,
+ )
+ .await
}
#[tauri::command]
diff --git a/desktop/src-tauri/src/managed_agents/registered_references.rs b/desktop/src-tauri/src/managed_agents/registered_references.rs
index 9f92b2ccff0..c8e94df30ce 100644
--- a/desktop/src-tauri/src/managed_agents/registered_references.rs
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -176,10 +176,18 @@ pub(crate) fn reject_registered_reference_target(
pubkey: &str,
) -> Result<(), String> {
let normalized = normalize_pubkey(pubkey).unwrap_or_else(|_| pubkey.to_string());
- if !super::storage::managed_agent_record_exists(app, &normalized)? {
- return Err(format!("agent {normalized} not found"));
+ reject_registered_reference_target_at_path(
+ &super::storage::managed_agents_store_path(app)?,
+ &normalized,
+ )
+}
+
+fn reject_registered_reference_target_at_path(path: &Path, pubkey: &str) -> Result<(), String> {
+ if super::storage::managed_agent_record_exists_at_path(path, pubkey)? {
+ Ok(())
+ } else {
+ Err(format!("agent {pubkey} not found"))
}
- Ok(())
}
#[cfg(test)]
@@ -192,6 +200,9 @@ mod tests {
const PUBKEY_A_UPPER: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
const PUBKEY_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const PUBKEY_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
+ const COMMAND_TARGETS_SOURCE: &str = include_str!("../commands/agent_registered_targets.rs");
+ const SETTINGS_SOURCE: &str = include_str!("../commands/agent_settings.rs");
+ const RUNTIME_SOURCE: &str = include_str!("runtime_commands.rs");
fn request(
pubkey: &str,
@@ -353,4 +364,238 @@ mod tests {
save_to_path(path, refs)?;
Ok(reference)
}
+
+ /// Extract one small command body so the regression tests verify the real
+ /// production routing as well as the path-based ownership decision. These
+ /// commands deliberately contain no nested brace-bearing literals before
+ /// their ownership check.
+ fn command_body<'a>(source: &'a str, name: &str) -> &'a str {
+ let signature = format!("fn {name}(");
+ let start = source
+ .find(&signature)
+ .unwrap_or_else(|| panic!("missing command {name}"));
+ let body_start = source[start..]
+ .find('{')
+ .map(|offset| start + offset)
+ .unwrap_or_else(|| panic!("missing body for command {name}"));
+ let mut depth = 0usize;
+ for (offset, byte) in source[body_start..].bytes().enumerate() {
+ match byte {
+ b'{' => depth += 1,
+ b'}' => {
+ depth -= 1;
+ if depth == 0 {
+ return &source[body_start..=body_start + offset];
+ }
+ }
+ _ => {}
+ }
+ }
+ panic!("unterminated body for command {name}")
+ }
+
+ fn assert_before(body: &str, first: &str, second: &str) {
+ let first_index = body
+ .find(first)
+ .unwrap_or_else(|| panic!("missing preflight `{first}` in {body}"));
+ let second_index = body
+ .find(second)
+ .unwrap_or_else(|| panic!("missing mutation/delegation `{second}` in {body}"));
+ assert!(
+ first_index < second_index,
+ "`{first}` must precede `{second}`"
+ );
+ }
+
+ fn assert_production_command_is_guarded(target: &str) {
+ match target {
+ "start_managed_agent"
+ | "stop_managed_agent"
+ | "update_managed_agent"
+ | "delete_managed_agent" => assert_before(
+ command_body(COMMAND_TARGETS_SOURCE, target),
+ "reject_registered_reference_target",
+ &format!("{target}_unchecked"),
+ ),
+ "set_managed_agent_start_on_app_launch" | "set_managed_agent_auto_restart" => {
+ assert_before(
+ command_body(SETTINGS_SOURCE, target),
+ "reject_registered_reference_target",
+ "spawn_blocking",
+ );
+ }
+ "put_managed_agent_runtime_lifecycle" => assert_before(
+ command_body(RUNTIME_SOURCE, target),
+ "reject_registered_reference_target",
+ "app.state::()",
+ ),
+ "start_managed_agent_runtime" => {
+ assert!(command_body(RUNTIME_SOURCE, target)
+ .contains("start_managed_agent_runtime_pair_lazy"));
+ assert_before(
+ command_body(RUNTIME_SOURCE, "start_pair"),
+ "reject_registered_reference_target",
+ "app.state::()",
+ );
+ }
+ "stop_managed_agent_runtime" => assert_before(
+ command_body(RUNTIME_SOURCE, target),
+ "reject_registered_reference_target",
+ "app.state::()",
+ ),
+ "restart_managed_agent_runtime" => assert_before(
+ command_body(RUNTIME_SOURCE, target),
+ "stop_managed_agent_runtime",
+ "start_pair",
+ ),
+ _ => panic!("unmapped registered-reference command target {target}"),
+ }
+ }
+
+ fn snapshot_tree(root: &Path) -> Vec<(PathBuf, Vec)> {
+ fn visit(root: &Path, dir: &Path, out: &mut Vec<(PathBuf, Vec)>) {
+ let mut entries = fs::read_dir(dir)
+ .unwrap()
+ .map(|entry| entry.unwrap())
+ .collect::>();
+ entries.sort_by_key(|entry| entry.file_name());
+ for entry in entries {
+ let path = entry.path();
+ if path.is_dir() {
+ visit(root, &path, out);
+ } else {
+ out.push((
+ path.strip_prefix(root).unwrap().to_path_buf(),
+ fs::read(path).unwrap(),
+ ));
+ }
+ }
+ }
+ let mut snapshot = Vec::new();
+ visit(root, root, &mut snapshot);
+ snapshot
+ }
+
+ fn assert_registered_reference_command_fails_closed(target: &str) {
+ let temp = tempfile::tempdir().unwrap();
+ let agents_dir = temp.path().join("agents");
+ fs::create_dir_all(agents_dir.join("agent-pids")).unwrap();
+ let managed_store = agents_dir.join("managed-agents.json");
+ let registered_store = agents_dir.join(STORE_FILENAME);
+
+ // The target exists only in the keyless reference store. The managed
+ // store contains an unrelated record with deliberately stale runtime
+ // metadata, matching the state that used to be synchronized before a
+ // missing-target error was returned.
+ let unrelated = serde_json::json!({
+ "pubkey": PUBKEY_B,
+ "name": "unrelated-exited-runtime",
+ "relay_url": "wss://relay.example",
+ "acp_command": "buzz-acp",
+ "agent_command": "goose",
+ "agent_args": [],
+ "mcp_command": "",
+ "turn_timeout_seconds": 320,
+ "system_prompt": "",
+ "runtime_pid": 424242,
+ "last_error": "stale exited child",
+ "created_at": "2026-01-01T00:00:00Z",
+ "updated_at": "2026-01-01T00:00:00Z"
+ });
+ fs::write(
+ &managed_store,
+ serde_json::to_vec_pretty(&vec![unrelated]).unwrap(),
+ )
+ .unwrap();
+ fs::write(
+ ®istered_store,
+ serde_json::to_vec_pretty(&vec![RegisteredAgentReference {
+ pubkey: PUBKEY_A.to_string(),
+ label: Some("external".to_string()),
+ role_summary: None,
+ created_at: "2026-01-01T00:00:00Z".to_string(),
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
+ }])
+ .unwrap(),
+ )
+ .unwrap();
+
+ // Sentinels cover every side-effect class at issue: event emission,
+ // keyring writes/deletes, in-memory lifecycle state, runtime receipts,
+ // and process identity. A command reaching its mutation body would
+ // necessarily alter at least one of these or the managed store.
+ fs::write(agents_dir.join("event-sentinel"), b"0 events").unwrap();
+ fs::write(
+ agents_dir.join("keyring-sentinel"),
+ b"agent:b = nsec-sentinel",
+ )
+ .unwrap();
+ fs::write(
+ agents_dir.join("runtime-state-sentinel"),
+ br#"{"pubkey":"bbbb","lifecycle":"exited","pid":424242}"#,
+ )
+ .unwrap();
+ fs::write(
+ agents_dir.join("agent-pids").join("stale.json"),
+ br#"{"pid":424242,"status":"exited"}"#,
+ )
+ .unwrap();
+
+ let before = snapshot_tree(temp.path());
+ let error = reject_registered_reference_target_at_path(&managed_store, PUBKEY_A)
+ .expect_err("registered reference must not authorize a managed command");
+ assert_eq!(error, format!("agent {PUBKEY_A} not found"));
+ assert_eq!(snapshot_tree(temp.path()), before, "{target} mutated state");
+ assert_production_command_is_guarded(target);
+ }
+
+ macro_rules! fail_closed_command_test {
+ ($name:ident, $target:literal) => {
+ #[test]
+ fn $name() {
+ assert_registered_reference_command_fails_closed($target);
+ }
+ };
+ }
+
+ fail_closed_command_test!(
+ registered_agent_references_start_managed_agent_fails_closed,
+ "start_managed_agent"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_stop_managed_agent_fails_closed,
+ "stop_managed_agent"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_start_runtime_fails_closed,
+ "start_managed_agent_runtime"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_stop_runtime_fails_closed,
+ "stop_managed_agent_runtime"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_restart_runtime_fails_closed,
+ "restart_managed_agent_runtime"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_lifecycle_observer_write_fails_closed,
+ "put_managed_agent_runtime_lifecycle"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_start_on_launch_fails_closed,
+ "set_managed_agent_start_on_app_launch"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_auto_restart_fails_closed,
+ "set_managed_agent_auto_restart"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_update_fails_closed,
+ "update_managed_agent"
+ );
+ fail_closed_command_test!(
+ registered_agent_references_delete_fails_closed,
+ "delete_managed_agent"
+ );
}
diff --git a/desktop/src-tauri/src/managed_agents/storage.rs b/desktop/src-tauri/src/managed_agents/storage.rs
index 4665c8fad42..daa8252c6ff 100644
--- a/desktop/src-tauri/src/managed_agents/storage.rs
+++ b/desktop/src-tauri/src/managed_agents/storage.rs
@@ -259,7 +259,26 @@ fn load_agent_store(app: &AppHandle) -> Result, String>
/// Check managed-agent ownership without hydrating private keys or touching
/// runtime state. Command boundaries use this before any lifecycle side effect.
pub(crate) fn managed_agent_record_exists(app: &AppHandle, pubkey: &str) -> Result {
- Ok(load_agent_store(app)?
+ managed_agent_record_exists_at_path(&managed_agents_store_path(app)?, pubkey)
+}
+
+/// Path-based ownership lookup used by the command-boundary regression tests.
+/// It intentionally shares the raw, non-hydrating read used in production so
+/// proving a rejected target cannot touch the keyring does not itself touch it.
+pub(crate) fn managed_agent_record_exists_at_path(
+ path: &Path,
+ pubkey: &str,
+) -> Result {
+ if !path.exists() {
+ return Ok(false);
+ }
+ let content =
+ fs::read_to_string(path).map_err(|error| format!("failed to read agent store: {error}"))?;
+ let records: Vec = serde_json::from_str(&content).map_err(|error| {
+ backup_invalid_store(path);
+ format!("failed to parse agent store (preserved as .invalid): {error}")
+ })?;
+ Ok(records
.iter()
.any(|record| !record.pubkey.is_empty() && record.pubkey == pubkey))
}
diff --git a/desktop/src/features/agents/hooksRegistered.test.mjs b/desktop/src/features/agents/hooksRegistered.test.mjs
index ece48838fff..9d3f6afe830 100644
--- a/desktop/src/features/agents/hooksRegistered.test.mjs
+++ b/desktop/src/features/agents/hooksRegistered.test.mjs
@@ -23,15 +23,21 @@ test("registered mutations invalidate only the registered-agent query key", () =
);
});
-test("agents-data-changed invalidates registered key alongside existing library keys", () => {
- const block = refresh.slice(
+test("agents-data-changed invalidates registered key alongside existing local library keys", () => {
+ const localKeysBlock = refresh.slice(
+ refresh.indexOf("LOCAL_AGENT_DATA_QUERY_KEYS"),
+ refresh.indexOf("] as const"),
+ );
+ assert.match(localKeysBlock, /registeredAgentsQueryKey/);
+ assert.match(localKeysBlock, /personasQueryKey/);
+ assert.match(localKeysBlock, /teamsQueryKey/);
+ assert.match(localKeysBlock, /managedAgentsQueryKey/);
+ assert.doesNotMatch(localKeysBlock, /relayAgentsQueryKey/);
+
+ const listenerBlock = refresh.slice(
refresh.indexOf('listen("agents-data-changed"'),
refresh.indexOf("return () =>"),
);
- assert.match(block, /registeredAgentsQueryKey/);
- assert.match(block, /personasQueryKey/);
- assert.match(block, /teamsQueryKey/);
- assert.match(block, /managedAgentsQueryKey/);
- assert.match(block, /relayAgentsQueryKey/);
- assert.doesNotMatch(block, /managedAgentRuntimesQueryKey/);
+ assert.match(listenerBlock, /LOCAL_AGENT_DATA_QUERY_KEYS/);
+ assert.doesNotMatch(listenerBlock, /managedAgentRuntimesQueryKey/);
});
diff --git a/desktop/src/features/agents/registeredAgentBoundary.test.mjs b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
index 0b10073297f..ca52d89ad22 100644
--- a/desktop/src/features/agents/registeredAgentBoundary.test.mjs
+++ b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
@@ -5,6 +5,47 @@ import test from "node:test";
const ROOT = new URL("../../..", import.meta.url).pathname;
const SRC = join(ROOT, "src");
+const REGISTERED_AGENT_DATA_FILES = 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 REGISTERED_AGENT_DISPLAY_FILES = 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 REGISTERED_AGENT_DATA_MARKERS = [
+ "RegisteredAgentReference",
+ "listRegisteredAgentReferences",
+ "registerExistingAgentReference",
+ "registeredAgentsQueryKey",
+ "registeredReferences",
+ "unregisterExistingAgentReference",
+ "useRegisteredAgentsQuery",
+];
+const FORBIDDEN_TRUST_MARKERS = [
+ "KnownAgentPubkeys",
+ "configNudgeAuthPubkey",
+ "mergeKnownAgentPubkeys",
+ "mentionableAgentPubkeys",
+ "useKnownAgentPubkeys",
+];
function walk(dir) {
return readdirSync(dir).flatMap((entry) => {
@@ -15,14 +56,34 @@ function walk(dir) {
});
}
-test("registered-agent frontend does not import or call managed lifecycle/create APIs", () => {
+test("registered-agent data stays inside the reviewed display/navigation integration files", () => {
const offenders = [];
for (const path of walk(SRC)) {
const rel = relative(ROOT, path);
- if (!/registeredAgent|RegisteredAgent/.test(rel)) continue;
- if (rel.endsWith("registeredAgentBoundary.test.mjs")) continue;
- if (rel.endsWith("registeredAgents.test.mjs")) continue;
const text = readFileSync(path, "utf8");
+ const dataHits = REGISTERED_AGENT_DATA_MARKERS.filter((needle) =>
+ text.includes(needle),
+ );
+ if (dataHits.length > 0 && !REGISTERED_AGENT_DATA_FILES.has(rel)) {
+ offenders.push(
+ `${rel}: registered-reference data (${dataHits.join(", ")})`,
+ );
+ continue;
+ }
+ if (
+ dataHits.length > 0 &&
+ rel !== "src/features/agents/registeredAgentBoundary.test.mjs"
+ ) {
+ const trustHits = FORBIDDEN_TRUST_MARKERS.filter((needle) =>
+ text.includes(needle),
+ );
+ if (trustHits.length > 0) {
+ offenders.push(
+ `${rel}: registered-reference trust leak (${trustHits.join(", ")})`,
+ );
+ }
+ }
+ if (!REGISTERED_AGENT_DISPLAY_FILES.has(rel)) continue;
const forbidden = [
"createManagedAgent",
"startManagedAgent",
@@ -34,6 +95,7 @@ test("registered-agent frontend does not import or call managed lifecycle/create
"envVars",
"agentCommand",
"agent_command",
+ "pid",
].filter((needle) => text.includes(needle));
if (forbidden.length > 0) offenders.push(`${rel}: ${forbidden.join(", ")}`);
}
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/useKnownAgentPubkeys.tsx b/desktop/src/features/agents/useKnownAgentPubkeys.tsx
index 5d2f281ded4..e9fe7b9a9b0 100644
--- a/desktop/src/features/agents/useKnownAgentPubkeys.tsx
+++ b/desktop/src/features/agents/useKnownAgentPubkeys.tsx
@@ -2,7 +2,6 @@ import * as React from "react";
import {
useManagedAgentsQuery,
- useRegisteredAgentsQuery,
useRelayAgentsQuery,
} from "@/features/agents/hooks";
import { mergeKnownAgentPubkeys } from "@/features/agents/knownAgentPubkeys";
@@ -42,17 +41,10 @@ export function KnownAgentPubkeysProvider({
}) {
const managedAgents = useManagedAgentsQuery().data;
const relayAgents = useRelayAgentsQuery().data;
- const registeredAgents = useRegisteredAgentsQuery().data;
const merged = React.useMemo(
- () =>
- mergeKnownAgentPubkeys(managedAgents, [
- ...(relayAgents ?? []),
- ...(registeredAgents ?? []).map((reference) => ({
- pubkey: reference.pubkey,
- })),
- ]),
- [managedAgents, relayAgents, registeredAgents],
+ () => mergeKnownAgentPubkeys(managedAgents, relayAgents),
+ [managedAgents, relayAgents],
);
const stable = useStableSet(merged);
diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts
index d6b4bc04e39..fab74bd089e 100644
--- a/desktop/tests/e2e/agents.spec.ts
+++ b/desktop/tests/e2e/agents.spec.ts
@@ -2747,3 +2747,177 @@ test("register existing agent stays keyless and has no lifecycle controls", asyn
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}`);
+ await expect(
+ card.getByRole("button", { name: new RegExp(pubkey) }),
+ ).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));
+ await expect(
+ card.getByRole("button", { name: new RegExp(pubkey) }),
+ ).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,
+ );
+});
From c76f5768053f5592676935780b510bb25dda4a7d Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 16:46:50 +0000
Subject: [PATCH 03/12] fix(agents): handle reference action failures
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
desktop/src/features/agents/ui/AgentsView.tsx | 8 +++-
.../agents/ui/RegisterExistingAgentDialog.tsx | 9 +++-
.../agents/ui/RegisteredAgentIdentityCard.tsx | 7 ++-
.../ui/registeredAgentAsyncErrors.test.mjs | 44 +++++++++++++++++++
4 files changed, 63 insertions(+), 5 deletions(-)
create mode 100644 desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index a1b7c2d8876..f02740646dd 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -354,7 +354,10 @@ export function AgentsView() {
: null
}
isPending={registerReferenceMutation.isPending}
- onOpenChange={setIsRegisterExistingOpen}
+ onOpenChange={(open) => {
+ setIsRegisterExistingOpen(open);
+ if (!open) registerReferenceMutation.reset();
+ }}
onSubmit={(input) => registerReferenceMutation.mutateAsync(input)}
open={isRegisterExistingOpen}
/>
@@ -363,7 +366,8 @@ export function AgentsView() {
onConfirm={(reference) => {
void unregisterReferenceMutation
.mutateAsync(reference)
- .then(() => setReferenceToRemove(null));
+ .then(() => setReferenceToRemove(null))
+ .catch(() => undefined);
}}
onOpenChange={(open) => {
if (!open) setReferenceToRemove(null);
diff --git a/desktop/src/features/agents/ui/RegisterExistingAgentDialog.tsx b/desktop/src/features/agents/ui/RegisterExistingAgentDialog.tsx
index bc040fddf32..589bcd78664 100644
--- a/desktop/src/features/agents/ui/RegisterExistingAgentDialog.tsx
+++ b/desktop/src/features/agents/ui/RegisterExistingAgentDialog.tsx
@@ -46,8 +46,13 @@ export function RegisterExistingAgentDialog({
async function submit(event: React.FormEvent) {
event.preventDefault();
- await onSubmit({ pubkey, label, roleSummary });
- onOpenChange(false);
+ try {
+ await onSubmit({ pubkey, label, roleSummary });
+ onOpenChange(false);
+ } catch {
+ // The mutation owns the error state rendered above. Keep the dialog open
+ // and consume the rejection at this UI event boundary.
+ }
}
return (
diff --git a/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx b/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
index d806c58c6bd..3d55251c2ee 100644
--- a/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
+++ b/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
@@ -35,7 +35,12 @@ export function RegisteredAgentIdentityCard({
const summary = registeredAgentRoleSummary(reference.roleSummary);
async function copyPubkey() {
- await navigator.clipboard?.writeText(reference.pubkey);
+ try {
+ await navigator.clipboard?.writeText(reference.pubkey);
+ } catch {
+ // Clipboard availability is best-effort; never leak a rejected browser
+ // permission promise from a menu event.
+ }
}
return (
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..7bf72eb2a73
--- /dev/null
+++ b/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
@@ -0,0 +1,44 @@
+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\(/,
+ );
+});
From 36b2acc8d069a57f27112229064230353f34da8c Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 16:51:58 +0000
Subject: [PATCH 04/12] test(agents): scan registered reference mjs consumers
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
desktop/scripts/check-registered-agent-boundary.mjs | 9 +++++++--
.../features/agents/registeredAgentBoundary.test.mjs | 12 ++++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/desktop/scripts/check-registered-agent-boundary.mjs b/desktop/scripts/check-registered-agent-boundary.mjs
index db331b667ee..cdc57e3132d 100755
--- a/desktop/scripts/check-registered-agent-boundary.mjs
+++ b/desktop/scripts/check-registered-agent-boundary.mjs
@@ -65,8 +65,9 @@ const forbiddenInRegisteredAgentFiles = [
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)$/.test(path) ? [path] : [];
+ return /\.(ts|tsx|mjs)$/.test(path) ? [path] : [];
});
}
@@ -82,7 +83,11 @@ for (const path of walk(src)) {
`${rel}: registered-reference data (${dataHits.join(", ")})`,
);
}
- if (dataHits.length > 0 && registeredAgentDataFiles.has(rel)) {
+ if (
+ dataHits.length > 0 &&
+ registeredAgentDataFiles.has(rel) &&
+ rel !== "src/features/agents/registeredAgentBoundary.test.mjs"
+ ) {
const trustHits = forbiddenTrustMarkers.filter((needle) =>
text.includes(needle),
);
diff --git a/desktop/src/features/agents/registeredAgentBoundary.test.mjs b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
index ca52d89ad22..c9cdfdfb98d 100644
--- a/desktop/src/features/agents/registeredAgentBoundary.test.mjs
+++ b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
@@ -5,6 +5,10 @@ import test from "node:test";
const ROOT = new URL("../../..", import.meta.url).pathname;
const SRC = join(ROOT, "src");
+const boundaryScript = readFileSync(
+ join(ROOT, "scripts/check-registered-agent-boundary.mjs"),
+ "utf8",
+);
const REGISTERED_AGENT_DATA_FILES = new Set([
"src/features/agents/hooks.ts",
"src/features/agents/hooksRegistered.test.mjs",
@@ -101,3 +105,11 @@ test("registered-agent data stays inside the reviewed display/navigation integra
}
assert.deepEqual(offenders, []);
});
+
+test("standalone boundary scan includes mjs consumers and skips generated trees", () => {
+ assert.match(
+ boundaryScript,
+ /entry === "node_modules" \|\| entry === "dist"/,
+ );
+ assert.match(boundaryScript, /\\\.\(ts\|tsx\|mjs\)\$/);
+});
From 696827708fe5e362ff62626a182ec649bca5a9f3 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 16:59:51 +0000
Subject: [PATCH 05/12] fix(agents): reject duplicate registered references
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../managed_agents/registered_references.rs | 43 +++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/desktop/src-tauri/src/managed_agents/registered_references.rs b/desktop/src-tauri/src/managed_agents/registered_references.rs
index c8e94df30ce..e08a9a9bd82 100644
--- a/desktop/src-tauri/src/managed_agents/registered_references.rs
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -84,6 +84,16 @@ fn load_from_path(path: &Path) -> Result, String>
format!("failed to parse registered agent references (preserved as .invalid): {error}")
})?;
refs.sort_by(|left, right| left.pubkey.cmp(&right.pubkey));
+ if let Some(duplicate) = refs
+ .windows(2)
+ .find(|pair| pair[0].pubkey == pair[1].pubkey)
+ {
+ backup_invalid_store(path);
+ return Err(format!(
+ "duplicate registered agent pubkey {} (preserved as .invalid)",
+ duplicate[0].pubkey
+ ));
+ }
Ok(refs)
}
@@ -298,6 +308,39 @@ mod tests {
);
}
+ #[test]
+ fn duplicate_pubkeys_fail_closed_and_preserve_invalid_bytes() {
+ let temp = tempfile::tempdir().unwrap();
+ let path = temp.path().join(STORE_FILENAME);
+ let refs = vec![
+ RegisteredAgentReference {
+ pubkey: PUBKEY_A.to_string(),
+ label: Some("first".to_string()),
+ role_summary: None,
+ created_at: "2026-01-01T00:00:00Z".to_string(),
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
+ },
+ RegisteredAgentReference {
+ pubkey: PUBKEY_A.to_string(),
+ label: Some("duplicate".to_string()),
+ role_summary: None,
+ created_at: "2026-01-02T00:00:00Z".to_string(),
+ updated_at: "2026-01-02T00:00:00Z".to_string(),
+ },
+ ];
+ let bytes = serde_json::to_vec_pretty(&refs).unwrap();
+ fs::write(&path, &bytes).unwrap();
+
+ let error = load_from_path(&path).unwrap_err();
+
+ assert!(error.contains("duplicate registered agent pubkey"));
+ assert_eq!(fs::read(&path).unwrap(), bytes);
+ assert_eq!(
+ fs::read(path.with_extension("json.invalid")).unwrap(),
+ bytes
+ );
+ }
+
#[test]
fn update_preserves_created_at_and_replaces_optionals() {
let temp = tempfile::tempdir().unwrap();
From 351ae004639188476a88dc454500ea1271059b2c Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:10:42 +0000
Subject: [PATCH 06/12] fix(agents): surface registered reference errors
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../src/managed_agents/registered_references.rs | 4 ++--
desktop/src/features/agents/ui/AgentsView.tsx | 9 ++++++++-
.../agents/ui/registeredAgentAsyncErrors.test.mjs | 4 ++++
desktop/src/shared/api/registeredAgents.test.mjs | 2 +-
desktop/src/shared/api/tauriRegisteredAgents.ts | 2 +-
5 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/desktop/src-tauri/src/managed_agents/registered_references.rs b/desktop/src-tauri/src/managed_agents/registered_references.rs
index e08a9a9bd82..3ea930d9ccc 100644
--- a/desktop/src-tauri/src/managed_agents/registered_references.rs
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -49,7 +49,7 @@ fn store_path(app: &AppHandle) -> Result {
fn normalize_pubkey(input: &str) -> Result {
let trimmed = input.trim();
if trimmed.len() != 64 || !trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
- return Err("agent pubkey must be exactly 64 ASCII hex characters".to_string());
+ return Err("invalid public key".to_string());
}
Ok(trimmed.to_ascii_lowercase())
}
@@ -242,7 +242,7 @@ mod tests {
#[test]
fn rejects_non_exact_ascii_hex_pubkeys_and_overlong_fields() {
- assert!(normalize_pubkey("abc").is_err());
+ assert_eq!(normalize_pubkey("abc").unwrap_err(), "invalid public key");
assert!(normalize_pubkey(
"g000000000000000000000000000000000000000000000000000000000000000"
)
diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx
index f02740646dd..9a0e0eb3100 100644
--- a/desktop/src/features/agents/ui/AgentsView.tsx
+++ b/desktop/src/features/agents/ui/AgentsView.tsx
@@ -364,10 +364,17 @@ export function AgentsView() {
{
+ agents.setActionErrorMessage(null);
void unregisterReferenceMutation
.mutateAsync(reference)
.then(() => setReferenceToRemove(null))
- .catch(() => undefined);
+ .catch((error) => {
+ agents.setActionErrorMessage(
+ error instanceof Error
+ ? error.message
+ : "Couldn't remove registered agent reference.",
+ );
+ });
}}
onOpenChange={(open) => {
if (!open) setReferenceToRemove(null);
diff --git a/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs b/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
index 7bf72eb2a73..7032ed1b72a 100644
--- a/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
+++ b/desktop/src/features/agents/ui/registeredAgentAsyncErrors.test.mjs
@@ -41,4 +41,8 @@ test("unregister rejection is consumed instead of escaping the UI event", () =>
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
index e8e7666d830..b68b7e5ed90 100644
--- a/desktop/src/shared/api/registeredAgents.test.mjs
+++ b/desktop/src/shared/api/registeredAgents.test.mjs
@@ -58,6 +58,6 @@ test("malformed registered agent store entries fail visibly instead of being fil
created_at: "2026-08-18T12:00:00Z",
updated_at: "2026-08-18T12:30:00Z",
}),
- /registered agent pubkey/i,
+ /invalid public key/i,
);
});
diff --git a/desktop/src/shared/api/tauriRegisteredAgents.ts b/desktop/src/shared/api/tauriRegisteredAgents.ts
index 00d7a42f7a4..499a02364ea 100644
--- a/desktop/src/shared/api/tauriRegisteredAgents.ts
+++ b/desktop/src/shared/api/tauriRegisteredAgents.ts
@@ -50,7 +50,7 @@ function nullableString(value: unknown, field: string): string | null {
function normalizeRegisteredPubkey(value: unknown): string {
const pubkey = normalizePubkey(requiredString(value, "pubkey"));
if (!HEX_PUBKEY_RE.test(pubkey)) {
- throw new Error("Malformed registered agent pubkey.");
+ throw new Error("invalid public key");
}
return pubkey;
}
From 3e1aea0337c6c9d5832fca112c3c241f7cf15620 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 22:47:14 +0000
Subject: [PATCH 07/12] fix(agents): hide stale registered references on error
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../agents/lib/registeredAgentCards.test.mjs | 17 +++++++++++++++++
.../features/agents/lib/registeredAgentCards.ts | 9 +++++++++
.../features/agents/ui/UnifiedAgentsSection.tsx | 11 ++++++++---
3 files changed, 34 insertions(+), 3 deletions(-)
diff --git a/desktop/src/features/agents/lib/registeredAgentCards.test.mjs b/desktop/src/features/agents/lib/registeredAgentCards.test.mjs
index 5ca078c0b94..a91c7c29870 100644
--- a/desktop/src/features/agents/lib/registeredAgentCards.test.mjs
+++ b/desktop/src/features/agents/lib/registeredAgentCards.test.mjs
@@ -5,6 +5,7 @@ import {
dedupeRegisteredAgentsAgainstManaged,
resolveRegisteredAgentDisplay,
registeredAgentRoleSummary,
+ visibleRegisteredAgentReferences,
} from "./registeredAgentCards.ts";
const PUBKEY = "a1".repeat(32);
@@ -59,3 +60,19 @@ test("registered references are deduped against managed pubkeys", () => {
[reference(OTHER, "visible")],
);
});
+
+test("registered-reference errors fail closed over stale cached data", () => {
+ const staleReferences = [reference(OTHER, "stale")];
+ assert.deepEqual(
+ visibleRegisteredAgentReferences(
+ staleReferences,
+ [],
+ new Error("reference store invalid"),
+ ),
+ [],
+ );
+ assert.deepEqual(
+ visibleRegisteredAgentReferences(staleReferences, [], null),
+ staleReferences,
+ );
+});
diff --git a/desktop/src/features/agents/lib/registeredAgentCards.ts b/desktop/src/features/agents/lib/registeredAgentCards.ts
index d91592262ba..44459a2bfd9 100644
--- a/desktop/src/features/agents/lib/registeredAgentCards.ts
+++ b/desktop/src/features/agents/lib/registeredAgentCards.ts
@@ -44,3 +44,12 @@ export function dedupeRegisteredAgentsAgainstManaged(
(reference) => !managedPubkeys.has(normalizePubkey(reference.pubkey)),
);
}
+
+export function visibleRegisteredAgentReferences(
+ references: readonly RegisteredAgentReference[],
+ managed: readonly T[],
+ error: Error | null,
+): RegisteredAgentReference[] {
+ if (error) return [];
+ return dedupeRegisteredAgentsAgainstManaged(references, managed);
+}
diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
index c9b3efa5a23..2ddab30f3ea 100644
--- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
+++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
@@ -22,7 +22,7 @@ import { AgentRuntimeAvatarControl } from "./AgentRuntimeAvatarControl";
import { CreateIdentityCard } from "./CreateIdentityCard";
import { PersonaActionsMenu } from "./PersonaActionsMenu";
import { RegisteredAgentIdentityCard } from "./RegisteredAgentIdentityCard";
-import { dedupeRegisteredAgentsAgainstManaged } from "../lib/registeredAgentCards";
+import { visibleRegisteredAgentReferences } from "../lib/registeredAgentCards";
import { buildUnifiedGroups } from "./unifiedAgentGroups";
type UnifiedAgentsSectionProps = {
@@ -128,8 +128,13 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
const isLoading =
isAgentsLoading || isPersonasLoading || isRegisteredReferencesLoading;
const visibleRegisteredReferences = React.useMemo(
- () => dedupeRegisteredAgentsAgainstManaged(registeredReferences, agents),
- [registeredReferences, agents],
+ () =>
+ visibleRegisteredAgentReferences(
+ registeredReferences,
+ agents,
+ registeredReferencesError,
+ ),
+ [registeredReferences, agents, registeredReferencesError],
);
return (
From c2ff116da357a5481a0f7f0b088acc5c34e2c664 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 22:58:53 +0000
Subject: [PATCH 08/12] fix(agents): minimize ownership preflight data
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../src-tauri/src/managed_agents/storage.rs | 22 +++++++++++-----
.../src/managed_agents/storage_tests.rs | 26 +++++++++++++++++--
.../agents/lib/registeredAgentCards.test.mjs | 7 +++++
.../agents/lib/registeredAgentCards.ts | 9 +++++++
.../agents/ui/RegisteredAgentIdentityCard.tsx | 3 ++-
5 files changed, 58 insertions(+), 9 deletions(-)
diff --git a/desktop/src-tauri/src/managed_agents/storage.rs b/desktop/src-tauri/src/managed_agents/storage.rs
index daa8252c6ff..f03084d3013 100644
--- a/desktop/src-tauri/src/managed_agents/storage.rs
+++ b/desktop/src-tauri/src/managed_agents/storage.rs
@@ -262,9 +262,18 @@ pub(crate) fn managed_agent_record_exists(app: &AppHandle, pubkey: &str) -> Resu
managed_agent_record_exists_at_path(&managed_agents_store_path(app)?, pubkey)
}
+/// Minimal projection for ownership preflight. Unknown fields—including any
+/// inline private key fallback—are skipped by Serde instead of materialized in
+/// a `ManagedAgentRecord`.
+#[derive(serde::Deserialize)]
+struct ManagedAgentOwnershipRecord {
+ #[serde(default)]
+ pubkey: String,
+}
+
/// Path-based ownership lookup used by the command-boundary regression tests.
-/// It intentionally shares the raw, non-hydrating read used in production so
-/// proving a rejected target cannot touch the keyring does not itself touch it.
+/// It intentionally reads only the public-key projection used in production so
+/// proving a rejected target cannot touch the keyring or materialize secrets.
pub(crate) fn managed_agent_record_exists_at_path(
path: &Path,
pubkey: &str,
@@ -274,10 +283,11 @@ pub(crate) fn managed_agent_record_exists_at_path(
}
let content =
fs::read_to_string(path).map_err(|error| format!("failed to read agent store: {error}"))?;
- let records: Vec = serde_json::from_str(&content).map_err(|error| {
- backup_invalid_store(path);
- format!("failed to parse agent store (preserved as .invalid): {error}")
- })?;
+ let records: Vec =
+ serde_json::from_str(&content).map_err(|error| {
+ backup_invalid_store(path);
+ format!("failed to parse agent store (preserved as .invalid): {error}")
+ })?;
Ok(records
.iter()
.any(|record| !record.pubkey.is_empty() && record.pubkey == pubkey))
diff --git a/desktop/src-tauri/src/managed_agents/storage_tests.rs b/desktop/src-tauri/src/managed_agents/storage_tests.rs
index 9943c6b3ac3..bcce2eb2236 100644
--- a/desktop/src-tauri/src/managed_agents/storage_tests.rs
+++ b/desktop/src-tauri/src/managed_agents/storage_tests.rs
@@ -12,8 +12,8 @@ use std::path::Path;
use tempfile::NamedTempFile;
use super::{
- agent_keyring_name, hydrate_keys_with, migrate_inline_key, persist_agent_keys_with,
- KeyMigration, KeyStore, KeyringProbe, ManagedAgentRecord,
+ agent_keyring_name, hydrate_keys_with, managed_agent_record_exists_at_path, migrate_inline_key,
+ persist_agent_keys_with, KeyMigration, KeyStore, KeyringProbe, ManagedAgentRecord,
};
/// In-memory [`KeyStore`] for testing the migrate decision without the OS
@@ -143,6 +143,28 @@ fn record_with_pubkey_and_key(pubkey: &str, nsec: &str) -> ManagedAgentRecord {
.expect("sample record")
}
+#[test]
+fn ownership_lookup_deserializes_only_pubkey() {
+ let mut file = NamedTempFile::new().expect("temp agent store");
+ file.write_all(
+ br#"[{"pubkey":"target-pubkey","private_key_nsec":{"must":"stay ignored"},"future_schema":{"nested":[1,2,3]}}]"#,
+ )
+ .expect("write agent store");
+
+ assert!(
+ serde_json::from_str::>(
+ &std::fs::read_to_string(file.path()).expect("read fixture")
+ )
+ .is_err(),
+ "fixture must fail full-record deserialization"
+ );
+ assert_eq!(
+ managed_agent_record_exists_at_path(file.path(), "target-pubkey"),
+ Ok(true),
+ "ownership preflight must ignore secret and unrelated record fields"
+ );
+}
+
#[test]
fn migrate_persists_and_signals_stripping_when_keyring_reachable() {
// Item 2: an inline key (residue from a prior keyring-unreachable save)
diff --git a/desktop/src/features/agents/lib/registeredAgentCards.test.mjs b/desktop/src/features/agents/lib/registeredAgentCards.test.mjs
index a91c7c29870..3144d7878a4 100644
--- a/desktop/src/features/agents/lib/registeredAgentCards.test.mjs
+++ b/desktop/src/features/agents/lib/registeredAgentCards.test.mjs
@@ -3,6 +3,7 @@ import test from "node:test";
import {
dedupeRegisteredAgentsAgainstManaged,
+ registeredAgentAriaLabel,
resolveRegisteredAgentDisplay,
registeredAgentRoleSummary,
visibleRegisteredAgentReferences,
@@ -50,6 +51,12 @@ test("role summary is always an externally managed suffix", () => {
assert.equal(registeredAgentRoleSummary(null), "Externally managed");
});
+test("accessible card label distinguishes the key without reading all 64 characters", () => {
+ const label = registeredAgentAriaLabel("External", PUBKEY);
+ assert.match(label, new RegExp(`${PUBKEY.slice(0, 8)}.*${PUBKEY.slice(-4)}`));
+ assert.equal(label.includes(PUBKEY), false);
+});
+
test("registered references are deduped against managed pubkeys", () => {
const references = [
reference(PUBKEY.toUpperCase(), "external"),
diff --git a/desktop/src/features/agents/lib/registeredAgentCards.ts b/desktop/src/features/agents/lib/registeredAgentCards.ts
index 44459a2bfd9..1c7b049c132 100644
--- a/desktop/src/features/agents/lib/registeredAgentCards.ts
+++ b/desktop/src/features/agents/lib/registeredAgentCards.ts
@@ -15,6 +15,15 @@ export function registeredAgentRoleSummary(roleSummary: string | null): string {
: "Externally managed";
}
+export function registeredAgentAriaLabel(
+ label: string,
+ pubkey: string,
+): string {
+ return `${label} externally managed agent profile, public key ${truncatePubkey(
+ normalizePubkey(pubkey),
+ )}`;
+}
+
export function resolveRegisteredAgentDisplay({
reference,
profile,
diff --git a/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx b/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
index 3d55251c2ee..6c7ee943c6e 100644
--- a/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
+++ b/desktop/src/features/agents/ui/RegisteredAgentIdentityCard.tsx
@@ -1,6 +1,7 @@
import { Copy, EllipsisVertical, Trash2 } from "lucide-react";
import {
+ registeredAgentAriaLabel,
registeredAgentRoleSummary,
resolveRegisteredAgentDisplay,
} from "@/features/agents/lib/registeredAgentCards";
@@ -79,7 +80,7 @@ export function RegisteredAgentIdentityCard({
}
- ariaLabel={`${display.label} externally managed agent profile, public key ${reference.pubkey}`}
+ ariaLabel={registeredAgentAriaLabel(display.label, reference.pubkey)}
avatarUrl={display.avatarUrl}
dataTestId={`registered-agent-${reference.pubkey}`}
label={display.label}
From 7c791f73e8c1a12e97775fa117ae677fc2c22975 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:12:30 +0000
Subject: [PATCH 09/12] test(agents): match registered reference ordering
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../src/features/agents/registeredAgentBoundary.test.mjs | 8 ++++++++
desktop/src/testing/e2eBridge.ts | 4 +++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/desktop/src/features/agents/registeredAgentBoundary.test.mjs b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
index c9cdfdfb98d..9d66337e2ca 100644
--- a/desktop/src/features/agents/registeredAgentBoundary.test.mjs
+++ b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
@@ -9,6 +9,7 @@ const boundaryScript = readFileSync(
join(ROOT, "scripts/check-registered-agent-boundary.mjs"),
"utf8",
);
+const e2eBridgeSource = readFileSync(join(SRC, "testing/e2eBridge.ts"), "utf8");
const REGISTERED_AGENT_DATA_FILES = new Set([
"src/features/agents/hooks.ts",
"src/features/agents/hooksRegistered.test.mjs",
@@ -113,3 +114,10 @@ test("standalone boundary scan includes mjs consumers and skips generated trees"
);
assert.match(boundaryScript, /\\\.\(ts\|tsx\|mjs\)\$/);
});
+
+test("E2E registered-reference listing preserves production pubkey ordering", () => {
+ assert.match(
+ e2eBridgeSource,
+ /case "list_registered_agent_references":[\s\S]*structuredClone\(mockRegisteredAgents\)\.sort\(\(left, right\) =>[\s\S]*left\.pubkey\.localeCompare\(right\.pubkey\)/,
+ );
+});
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 0ea53cc2e59..632f3b6d192 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -12741,7 +12741,9 @@ export function maybeInstallE2eTauriMocks() {
if (activeConfig?.mock?.registeredAgentsError) {
throw new Error(activeConfig.mock.registeredAgentsError);
}
- return structuredClone(mockRegisteredAgents);
+ return structuredClone(mockRegisteredAgents).sort((left, right) =>
+ left.pubkey.localeCompare(right.pubkey),
+ );
case "register_existing_agent_reference": {
const input = (payload as { input?: Record } | null)
?.input;
From 93f0c6029beac3b1601b16065001e7ee2bca5d81 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:22:35 +0000
Subject: [PATCH 10/12] fix(agents): validate registered reference store
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
.../managed_agents/registered_references.rs | 75 +++++++++++++++++++
desktop/tests/e2e/agents.spec.ts | 6 +-
2 files changed, 79 insertions(+), 2 deletions(-)
diff --git a/desktop/src-tauri/src/managed_agents/registered_references.rs b/desktop/src-tauri/src/managed_agents/registered_references.rs
index 3ea930d9ccc..4a99a2eaa66 100644
--- a/desktop/src-tauri/src/managed_agents/registered_references.rs
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -72,6 +72,26 @@ fn normalize_optional(
Ok(Some(trimmed.to_string()))
}
+fn validate_loaded_reference(reference: &RegisteredAgentReference) -> Result<(), String> {
+ let pubkey = normalize_pubkey(&reference.pubkey)?;
+ if pubkey != reference.pubkey {
+ return Err("stored pubkey is not canonical lowercase hex".to_string());
+ }
+ let label = normalize_optional(reference.label.clone(), LABEL_LIMIT_BYTES, "label")?;
+ if label != reference.label {
+ return Err("stored label is not canonical".to_string());
+ }
+ let role_summary = normalize_optional(
+ reference.role_summary.clone(),
+ ROLE_SUMMARY_LIMIT_BYTES,
+ "roleSummary",
+ )?;
+ if role_summary != reference.role_summary {
+ return Err("stored role summary is not canonical".to_string());
+ }
+ Ok(())
+}
+
fn load_from_path(path: &Path) -> Result, String> {
if !path.exists() {
return Ok(Vec::new());
@@ -83,6 +103,14 @@ fn load_from_path(path: &Path) -> Result, String>
backup_invalid_store(path);
format!("failed to parse registered agent references (preserved as .invalid): {error}")
})?;
+ for reference in &refs {
+ if let Err(error) = validate_loaded_reference(reference) {
+ backup_invalid_store(path);
+ return Err(format!(
+ "invalid registered agent reference (preserved as .invalid): {error}"
+ ));
+ }
+ }
refs.sort_by(|left, right| left.pubkey.cmp(&right.pubkey));
if let Some(duplicate) = refs
.windows(2)
@@ -341,6 +369,53 @@ mod tests {
);
}
+ #[test]
+ fn noncanonical_store_records_fail_closed_and_preserve_invalid_bytes() {
+ let cases = [
+ ("invalid pubkey", "not-a-key".to_string(), None, None),
+ ("mixed-case pubkey", PUBKEY_A_UPPER.to_string(), None, None),
+ (
+ "whitespace label",
+ PUBKEY_A.to_string(),
+ Some(" padded ".to_string()),
+ None,
+ ),
+ (
+ "overlong role summary",
+ PUBKEY_A.to_string(),
+ None,
+ Some("x".repeat(ROLE_SUMMARY_LIMIT_BYTES + 1)),
+ ),
+ ];
+
+ for (name, pubkey, label, role_summary) in cases {
+ let temp = tempfile::tempdir().unwrap();
+ let path = temp.path().join(STORE_FILENAME);
+ let refs = [RegisteredAgentReference {
+ pubkey,
+ label,
+ role_summary,
+ created_at: "2026-01-01T00:00:00Z".to_string(),
+ updated_at: "2026-01-01T00:00:00Z".to_string(),
+ }];
+ let bytes = serde_json::to_vec_pretty(&refs).unwrap();
+ fs::write(&path, &bytes).unwrap();
+
+ let error = load_from_path(&path).unwrap_err();
+
+ assert!(
+ error.contains("invalid registered agent reference"),
+ "{name}"
+ );
+ assert_eq!(fs::read(&path).unwrap(), bytes, "{name}");
+ assert_eq!(
+ fs::read(path.with_extension("json.invalid")).unwrap(),
+ bytes,
+ "{name}"
+ );
+ }
+ }
+
#[test]
fn update_preserves_created_at_and_replaces_optionals() {
let temp = tempfile::tempdir().unwrap();
diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts
index fab74bd089e..21c17dfa723 100644
--- a/desktop/tests/e2e/agents.spec.ts
+++ b/desktop/tests/e2e/agents.spec.ts
@@ -2768,8 +2768,9 @@ test("registered reference opens exact profile and remains display-only", async
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(pubkey) }),
+ card.getByRole("button", { name: new RegExp(truncatedPubkey) }),
).toBeVisible();
for (const control of [
"Start",
@@ -2875,8 +2876,9 @@ test("same-name references stay distinct and removing one only unregisters that
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(pubkey) }),
+ card.getByRole("button", { name: new RegExp(truncatedPubkey) }),
).toBeVisible();
}
From 6b723b3cade5ec485ad1fb589c72591755a1f0e2 Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:26:41 +0000
Subject: [PATCH 11/12] test(agents): resolve boundary paths portably
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
desktop/src/features/agents/registeredAgentBoundary.test.mjs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/desktop/src/features/agents/registeredAgentBoundary.test.mjs b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
index 9d66337e2ca..8113fc9afee 100644
--- a/desktop/src/features/agents/registeredAgentBoundary.test.mjs
+++ b/desktop/src/features/agents/registeredAgentBoundary.test.mjs
@@ -1,9 +1,10 @@
import assert from "node:assert/strict";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
+import { fileURLToPath } from "node:url";
import test from "node:test";
-const ROOT = new URL("../../..", import.meta.url).pathname;
+const ROOT = fileURLToPath(new URL("../../..", import.meta.url));
const SRC = join(ROOT, "src");
const boundaryScript = readFileSync(
join(ROOT, "scripts/check-registered-agent-boundary.mjs"),
From c19facaafbaa0b3d307c3152641e6b1068ba75cc Mon Sep 17 00:00:00 2001
From: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:30:41 +0000
Subject: [PATCH 12/12] docs(agents): correct registered key length
Signed-off-by: Cameron Aragon <69489633+camaragon@users.noreply.github.com>
---
desktop/src-tauri/src/managed_agents/registered_references.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/desktop/src-tauri/src/managed_agents/registered_references.rs b/desktop/src-tauri/src/managed_agents/registered_references.rs
index 4a99a2eaa66..5511cea9ef8 100644
--- a/desktop/src-tauri/src/managed_agents/registered_references.rs
+++ b/desktop/src-tauri/src/managed_agents/registered_references.rs
@@ -14,7 +14,7 @@ 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-byte lowercase hex.
+ /// 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")]