Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions crates/buzz-cli/src/agent_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ pub struct UpdateAgentDraft {
pub respond_to: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdoptAgentDraft {
pub channel_id: String,
pub agent_pubkey: String,
pub display_name: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ManagementRequest<T> {
Expand Down Expand Up @@ -185,6 +193,25 @@ pub fn build_update(
build(keys, owner, channel_id, "update", request)
}

pub fn build_adopt(
keys: &Keys,
owner: &PublicKey,
draft: AdoptAgentDraft,
) -> Result<BuiltDraftRequest, CliError> {
let channel_id = required(draft.channel_id, "channel", 128)?;
uuid::Uuid::parse_str(&channel_id)
.map_err(|_| CliError::Usage(format!("invalid channel UUID: {channel_id}")))?;
let agent_pubkey = PublicKey::parse(draft.agent_pubkey.trim())
.map_err(|_| CliError::Usage("agent pubkey must be valid hex or npub".into()))?
.to_hex();
let request = AdoptAgentDraft {
channel_id: channel_id.clone(),
agent_pubkey,
display_name: required(draft.display_name, "display name", MAX_NAME_CHARS)?,
};
build(keys, owner, channel_id, "adopt", request)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -274,4 +301,47 @@ mod tests {
.unwrap_err();
assert!(error.to_string().contains("invalid channel UUID"));
}

#[test]
fn adopt_is_owner_encrypted_and_contains_only_public_registration_fields() {
let agent = Keys::generate();
let owner = Keys::generate();
let external = Keys::generate();
let built = build_adopt(
&agent,
&owner.public_key(),
AdoptAgentDraft {
channel_id: CHANNEL.into(),
agent_pubkey: external.public_key().to_hex(),
display_name: "Remote helper".into(),
},
)
.unwrap();

let payload: serde_json::Value = decrypt_observer_payload(&owner, &built.event).unwrap();
assert_eq!(payload["payload"]["action"], "adopt");
assert_eq!(
payload["payload"]["request"],
serde_json::json!({
"channelId": CHANNEL,
"agentPubkey": external.public_key().to_hex(),
"displayName": "Remote helper"
})
);
}

#[test]
fn adopt_rejects_an_invalid_agent_pubkey() {
let error = build_adopt(
&Keys::generate(),
&Keys::generate().public_key(),
AdoptAgentDraft {
channel_id: CHANNEL.into(),
agent_pubkey: "not-a-pubkey".into(),
display_name: "Remote helper".into(),
},
)
.unwrap_err();
assert!(error.to_string().contains("agent pubkey"));
}
}
38 changes: 36 additions & 2 deletions crates/buzz-cli/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use buzz_sdk::builders::{build_archive_identity_request, build_unarchive_identit
use nostr::PublicKey;
use serde_json::json;

use crate::agent_management::{build_create, build_update, CreateAgentDraft, UpdateAgentDraft};
use crate::agent_management::{
build_adopt, build_create, build_update, AdoptAgentDraft, CreateAgentDraft, UpdateAgentDraft,
};
use crate::client::BuzzClient;
use crate::error::CliError;
use crate::validate::{read_or_stdin, validate_hex64};
Expand Down Expand Up @@ -85,6 +87,38 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli
Ok(())
}

AgentsCmd::DraftAdopt {
channel,
agent_pubkey,
display_name,
} => {
let owner = require_owner(client)?;
let built = build_adopt(
client.keys(),
&owner,
AdoptAgentDraft {
channel_id: channel,
agent_pubkey,
display_name,
},
)?;
let response = client.publish_ephemeral_event(built.event).await?;
let mut output: serde_json::Value = serde_json::from_str(&response)
.map_err(|e| CliError::Other(format!("invalid relay response: {e}")))?;
if let Some(obj) = output.as_object_mut() {
obj.insert("request_id".into(), built.request_id.into());
obj.insert("action".into(), built.action.into());
obj.insert("saved".into(), false.into());
obj.insert(
"message".into(),
"Registration draft sent to Buzz Desktop for owner review. The existing agent is unchanged until the owner saves it."
.into(),
);
}
println!("{output}");
Ok(())
}

AgentsCmd::Archive {
target_pubkey,
reason,
Expand Down Expand Up @@ -168,7 +202,7 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli
}

/// Require `BUZZ_AUTH_TAG` and parse the owner pubkey from it. Used only by
/// the `draft-create` and `draft-update` paths.
/// the owner-reviewed agent draft paths.
fn require_owner(client: &BuzzClient) -> Result<PublicKey, CliError> {
let hex = client
.auth_tag_owner_hex()
Expand Down
15 changes: 14 additions & 1 deletion crates/buzz-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,18 @@ pub enum AgentsCmd {
#[arg(long, value_enum)]
respond_to: Option<RespondToArg>,
},
/// Open an owner-reviewed form to register an existing remote agent identity
DraftAdopt {
/// Current channel UUID; used only to authorize the review request
#[arg(long)]
channel: String,
/// Existing agent public key (hex or npub); no private key is imported
#[arg(long)]
agent_pubkey: String,
/// Proposed directory display name
#[arg(long)]
display_name: String,
},
/// Submit a NIP-IA archive request for an identity (kind 9035)
#[command(
after_help = "Auth flow: when target != signer, the CLI fetches the target's kind:0 and \
Expand Down Expand Up @@ -2262,6 +2274,7 @@ mod tests {
vec![
"archive",
"archived",
"draft-adopt",
"draft-create",
"draft-update",
"unarchive"
Expand Down Expand Up @@ -2402,7 +2415,7 @@ mod tests {
#[test]
fn subcommand_counts_are_stable() {
let expected: Vec<(&str, usize)> = vec![
("agents", 5),
("agents", 6),
("canvas", 2),
("channels", 16),
("dms", 4),
Expand Down
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default defineConfig({
"**/agent-readiness-screenshots.spec.ts",
"**/agent-error-state-screenshots.spec.ts",
"**/edit-agent.spec.ts",
"**/register-existing-agent.spec.ts",
"**/doctor-cta-screenshots.spec.ts",
"**/pubkey-display-screenshots.spec.ts",
"**/file-attachment.spec.ts",
Expand Down
6 changes: 3 additions & 3 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,12 +1022,12 @@ pub async fn discover_managed_agent_prereqs(
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
}

mod relay_directory;
#[cfg(test)]
use relay_directory::advance_relay_cursor;
pub use relay_directory::{list_relay_agents, revalidate_relay_agents};

pub use relay_directory::{
list_relay_agents, register_existing_relay_agent, revalidate_relay_agents,
};
#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading