From 3e749fdb8a8e4ec60f9246091017f8850a427423 Mon Sep 17 00:00:00 2001
From: Larry
<627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Date: Thu, 24 Sep 2026 18:44:24 -0400
Subject: [PATCH] Finish incomplete local setup through a signed Use here
action without starting
Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
---
crates/agent-controller/src/community.rs | 85 +++++++
crates/agent-controller/src/config.rs | 5 +
crates/agent-controller/src/import.rs | 11 +-
crates/agent-controller/src/import/tests.rs | 2 +
crates/agent-controller/src/lib.rs | 2 +
crates/agent-controller/src/runtime.rs | 38 ++-
crates/agent-controller/src/runtime/tests.rs | 223 ++++++++++++++++++
crates/agent-controller/src/store.rs | 50 ++++
dev/relay-broker-api.test.mjs | 44 ++++
dev/relay-broker.mjs | 27 +++
src-tauri/build.rs | 1 +
src-tauri/capabilities/default.json | 1 +
src-tauri/src/agents.rs | 14 ++
src-tauri/src/browser_permissions_tests.rs | 1 +
src-tauri/src/lib.rs | 3 +-
src/bundled/agents/AgentEditor.tsx | 22 +-
src/bundled/agents/AgentsPage.test.tsx | 54 ++++-
src/bundled/agents/AgentsPage.tsx | 7 +
.../agents/LocalInventoryAction.test.tsx | 59 +++++
src/bundled/agents/LocalInventoryAction.tsx | 89 +++++++
src/bundled/agents/ManagedAgentActions.tsx | 41 +++-
.../agents/choices-enrollment.test.ts | 17 ++
src/features/agents/choices.ts | 5 +-
src/features/agents/control-native.test.ts | 16 ++
src/features/agents/control-native.ts | 2 +
src/features/agents/control-testing.ts | 9 +
src/features/agents/control.ts | 33 +++
27 files changed, 827 insertions(+), 34 deletions(-)
create mode 100644 crates/agent-controller/src/community.rs
create mode 100644 src/bundled/agents/LocalInventoryAction.test.tsx
create mode 100644 src/bundled/agents/LocalInventoryAction.tsx
create mode 100644 src/features/agents/choices-enrollment.test.ts
diff --git a/crates/agent-controller/src/community.rs b/crates/agent-controller/src/community.rs
new file mode 100644
index 000000000..bd5803eec
--- /dev/null
+++ b/crates/agent-controller/src/community.rs
@@ -0,0 +1,85 @@
+//! Owner-signed intent to configure one identity/community pair.
+//! Local import and existing setups do not depend on this confirmation.
+use crate::config::{canonical_key, canonical_relay};
+use crate::Result;
+use secp256k1::{schnorr::Signature, Secp256k1, XOnlyPublicKey};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct CommunityResolution {
+ pub pubkey: String,
+ pub relay_url: String,
+ pub owner: String,
+ pub signature: String,
+}
+impl CommunityResolution {
+ pub fn verify(&self, auth: &str) -> Result<()> {
+ crate::secret::validate_attestation(auth, &self.pubkey)?;
+ let tag: Vec = serde_json::from_str(auth).map_err(|_| "Invalid source owner")?;
+ if !canonical_key(&self.pubkey)
+ || tag[1] != self.owner
+ || canonical_relay(&self.relay_url)? != self.relay_url
+ {
+ return Err(
+ "Community resolution does not match the source owner or destination".into(),
+ );
+ }
+ let owner: XOnlyPublicKey = self.owner.parse().map_err(|_| "Invalid resolution owner")?;
+ let signature: Signature = self
+ .signature
+ .parse()
+ .map_err(|_| "Invalid resolution signature")?;
+ let digest = Sha256::digest(format!(
+ "nostr:agent-community:{}:{}",
+ self.pubkey, self.relay_url
+ ));
+ Secp256k1::verification_only()
+ .verify_schnorr(&signature, &digest, &owner)
+ .map_err(|_| "Community resolution was not signed by the source owner".into())
+ }
+}
+#[cfg(test)]
+pub(crate) mod tests {
+ use super::*;
+ use secp256k1::{Keypair, SecretKey};
+ const PUB: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
+ pub(crate) fn resolution(relay: &str) -> CommunityResolution {
+ let secp = Secp256k1::new();
+ let mut bytes = [0; 32];
+ bytes[31] = 2;
+ let pair = Keypair::from_secret_key(&secp, &SecretKey::from_byte_array(bytes).unwrap());
+ CommunityResolution {
+ pubkey: PUB.into(),
+ relay_url: relay.into(),
+ owner: pair.x_only_public_key().0.to_string(),
+ signature: secp
+ .sign_schnorr_no_aux_rand(
+ &Sha256::digest(format!("nostr:agent-community:{PUB}:{relay}")),
+ &pair,
+ )
+ .to_string(),
+ }
+ }
+ #[test]
+ fn owner_can_confirm_multiple_pairs_without_import_or_storage() {
+ let auth = crate::secret::test_attestation(PUB);
+ resolution("wss://one.example").verify(&auth).unwrap();
+ resolution("wss://two.example").verify(&auth).unwrap();
+ }
+ #[test]
+ fn unsigned_or_retargeted_resolution_is_rejected() {
+ let auth = crate::secret::test_attestation(PUB);
+ for change in ["destination", "identity", "owner", "signature"] {
+ let mut value = resolution("wss://one.example");
+ match change {
+ "destination" => value.relay_url = "wss://two.example".into(),
+ "identity" => value.pubkey = "ab".repeat(32),
+ "owner" => value.owner = "cd".repeat(32),
+ _ => value.signature = "00".repeat(64),
+ }
+ assert!(value.verify(&auth).is_err());
+ }
+ }
+}
diff --git a/crates/agent-controller/src/config.rs b/crates/agent-controller/src/config.rs
index 252ec5778..4ee89966f 100644
--- a/crates/agent-controller/src/config.rs
+++ b/crates/agent-controller/src/config.rs
@@ -50,6 +50,7 @@ pub struct AgentView {
pub launch_provider_env: Option<&'static str>,
/// Redacted saved-versus-running differences while the process is alive.
pub restart_diff: Vec,
+ pub configured: bool,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -115,6 +116,9 @@ pub(crate) struct Agent {
pub extra: BTreeMap,
}
impl Agent {
+ pub(crate) fn configured(&self) -> bool {
+ self.extra.get("configured") != Some(&Value::Bool(false))
+ }
pub fn view(&self) -> AgentView {
let defaults = crate::build_defaults();
let launch = defaults.launch_view(&self.harness, &self.environment);
@@ -139,6 +143,7 @@ impl Agent {
status: ProcessStatus::Stopped,
error: None,
diagnostics: Vec::new(),
+ configured: self.configured(),
profile_pending: self.extra.get("profilePending") == Some(&Value::Bool(true)),
start_on_app_launch: self.starts_on_launch(),
respond_to: self.respond_to(defaults.owner_only).ok().map(str::to_owned),
diff --git a/crates/agent-controller/src/import.rs b/crates/agent-controller/src/import.rs
index fd2da2976..ed3283aca 100644
--- a/crates/agent-controller/src/import.rs
+++ b/crates/agent-controller/src/import.rs
@@ -257,7 +257,16 @@ impl CredentialedImport {
{
return Err("Selected identity is already imported".into());
}
- store.insert(self.agents)
+ let agents = self
+ .agents
+ .into_iter()
+ .map(|mut agent| {
+ agent.extra.insert("configured".into(), Value::Bool(true));
+ agent.enabled = false;
+ agent
+ })
+ .collect();
+ store.insert(agents)
}
}
fn string<'a>(value: &'a Value, key: &str) -> &'a str {
diff --git a/crates/agent-controller/src/import/tests.rs b/crates/agent-controller/src/import/tests.rs
index 128daefad..b0247e990 100644
--- a/crates/agent-controller/src/import/tests.rs
+++ b/crates/agent-controller/src/import/tests.rs
@@ -104,6 +104,8 @@ fn preview_is_keyless_commit_resolves_preserves_and_never_enables_or_mutates_sou
assert!(!saved.enabled);
// Source `start_on_app_launch: true` does not auto-start an imported record.
assert_eq!(saved.start_on_app_launch, Some(false));
+ assert!(saved.configured());
+ assert_eq!(saved.relay_url, "wss://relay.example");
assert_eq!(saved.system_prompt, "definition-prompt");
assert_eq!(saved.harness.model, "definition-model");
assert_eq!(saved.harness.provider, "global-provider");
diff --git a/crates/agent-controller/src/lib.rs b/crates/agent-controller/src/lib.rs
index a0007369d..bb3b75742 100644
--- a/crates/agent-controller/src/lib.rs
+++ b/crates/agent-controller/src/lib.rs
@@ -1,7 +1,9 @@
//! Local configuration and process ownership; never tied to a page or relay session.
//! No legacy desktop dependency, implicit identity creation, or credential projection.
mod bundle;
+mod community;
mod config;
+pub use community::CommunityResolution;
pub mod connection;
mod create;
mod credentials;
diff --git a/crates/agent-controller/src/runtime.rs b/crates/agent-controller/src/runtime.rs
index 805f2b091..9770d2b9c 100644
--- a/crates/agent-controller/src/runtime.rs
+++ b/crates/agent-controller/src/runtime.rs
@@ -443,6 +443,14 @@ impl Controller {
|| !agent.imported.is_null(),
)
}
+ pub fn use_here(
+ &mut self,
+ id: &str,
+ resolution: crate::CommunityResolution,
+ ) -> Result {
+ self.store.use_here(id, resolution)?;
+ self.snapshot()
+ }
pub fn prepare_import(
&self,
imports: &mut crate::Imports,
@@ -459,12 +467,18 @@ impl Controller {
self.snapshot()
}
pub fn delete(&mut self, id: &str, revision: u64) -> Result {
- let agent = self
- .store
- .agents()?
- .into_iter()
+ let agents = self.store.agents()?;
+ let agent = agents
+ .iter()
.find(|agent| agent.id == id)
+ .cloned()
.ok_or("Agent no longer exists")?;
+ // Use here setups of one identity share its key; keep it for the others.
+ let shared = agents.iter().any(|other| {
+ other.id != agent.id
+ && other.credential_id == agent.credential_id
+ && other.pubkey == agent.pubkey
+ });
if agent.revision != revision {
return Err("Agent settings changed. Reload before deleting".into());
}
@@ -473,8 +487,10 @@ impl Controller {
self.store.enabled(id, false)?;
// A failed settings write leaves the card available for an explicit retry.
// Credential deletion is idempotent, so that retry can finish cleanup.
- self.credentials
- .delete(&agent.credential_id, &agent.pubkey)?;
+ if !shared {
+ self.credentials
+ .delete(&agent.credential_id, &agent.pubkey)?;
+ }
self.store.remove(id, revision)?;
self.errors.remove(id);
self.snapshot()
@@ -524,7 +540,7 @@ impl Controller {
.store
.agents()?
.into_iter()
- .filter(Agent::starts_on_launch)
+ .filter(|a| a.starts_on_launch() && a.configured())
{
// Like the host restore, a launch preference is a Start: it enables.
if let Err(error) = self
@@ -544,6 +560,9 @@ impl Controller {
.into_iter()
.find(|a| a.id == id)
.ok_or("Agent no longer exists")?;
+ if !agent.configured() {
+ return Err("Choose Use here before opening this identity’s credentials".into());
+ }
let workspace = effective_databricks(&agent)?.map(|s| s.host);
self.bundle.as_ref().map_err(Clone::clone)?;
Ok((agent.credential_id, agent.pubkey, agent.revision, workspace))
@@ -584,7 +603,7 @@ impl Controller {
.store
.agents()?
.into_iter()
- .filter(Agent::starts_on_launch)
+ .filter(|a| a.starts_on_launch() && a.configured())
.map(|a| a.id)
.collect())
}
@@ -609,6 +628,9 @@ impl Controller {
.into_iter()
.find(|a| a.id == id)
.ok_or("Agent no longer exists")?;
+ if !agent.configured() {
+ return Err("Choose Use here before starting this imported identity".into());
+ }
if !agent.enabled {
return Err("Agent is disabled".into());
}
diff --git a/crates/agent-controller/src/runtime/tests.rs b/crates/agent-controller/src/runtime/tests.rs
index 513d6f08d..2890f9b2d 100644
--- a/crates/agent-controller/src/runtime/tests.rs
+++ b/crates/agent-controller/src/runtime/tests.rs
@@ -72,6 +72,49 @@ fn delete_refuses_stale_revision_and_removes_stopped_agent() {
assert!(Store::open(root).unwrap().agents().unwrap().is_empty());
}
#[test]
+fn delete_keeps_a_key_shared_by_another_setup_of_the_same_identity() {
+ use std::sync::Mutex;
+ #[derive(Default)]
+ struct Tracked(Mutex>);
+ impl Credentials for Tracked {
+ fn read_legacy(&self, _: crate::LegacySource, _: &str) -> Result {
+ unreachable!()
+ }
+ fn read(&self, _: &str, _: &str) -> Result
- {agent.enabled
- ? !state.data?.runtimeAvailable
- ? "Enabled intent saved · execution unavailable"
+ {agent.configured === false
+ ? "Imported · close the editor and choose Use here before starting"
+ : agent.enabled
+ ? !state.data?.runtimeAvailable
+ ? "Enabled intent saved · execution unavailable"
+ : agent.startOnAppLaunch
+ ? "Enabled · starts with buzz-app"
+ : "Enabled · manual-start only"
: agent.startOnAppLaunch
- ? "Enabled · starts with buzz-app"
- : "Enabled · manual-start only"
- : agent.startOnAppLaunch
- ? "Stopped · starts with buzz-app"
- : "Stopped · a later sent mention can start this agent"}
+ ? "Stopped · starts with buzz-app"
+ : "Stopped · a later sent mention can start this agent"}
@@ -187,8 +189,8 @@ export function AgentEditor({
Saved revision {agent.revision} · Running revision{" "}
{agent.runningRevision ?? "none"}.
{unapplied && " Saved changes are not running yet."}{" "}
- Stop ends current work; a later sent mention can
- start it again.
+ Stop ends current work. After setup, a later sent
+ mention can start it again.
{agent.relayUrl}
@@ -43,21 +49,38 @@ export function ManagedAgentActions({
{imported && !agent.enabled && (
-
- Imported, not started. Mention this agent in a channel to start it.
+
+ Imported, not started.{" "}
+ {agent.configured === false
+ ? "Choose Use here to set up this identity in a community."
+ : "Start it when you are ready."}