Skip to content
Open
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
82 changes: 81 additions & 1 deletion crates/buzz-cli/src/commands/channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,19 @@ pub async fn cmd_remove_channel_member(
Ok(())
}

/// Merge `channel_add_policy` into an existing kind:10100 content object,
/// preserving every other field (#3663). Unparseable or absent head content
/// merges into an empty object β€” matching `agents_from_events`, which treats
/// non-object content as empty rather than failing.
fn merge_add_policy(head_content: Option<&str>, policy: &str) -> String {
let mut obj = head_content
.and_then(|c| serde_json::from_str::<serde_json::Value>(c).ok())
.and_then(|v| v.as_object().cloned())
.unwrap_or_default();
obj.insert("channel_add_policy".to_string(), serde_json::json!(policy));
serde_json::Value::Object(obj).to_string()
}

/// Set the channel addition policy β€” sign and submit a kind:10100 (agent profile) event.
pub async fn cmd_set_add_policy(client: &BuzzClient, policy: &str) -> Result<(), CliError> {
match policy {
Expand Down Expand Up @@ -1045,7 +1058,33 @@ pub async fn cmd_set_add_policy(client: &BuzzClient, policy: &str) -> Result<(),
}
}

let content = serde_json::json!({ "channel_add_policy": policy }).to_string();
// Merge into the current head record instead of replacing it (#3663).
// kind:10100 is replaceable: a bare `{"channel_add_policy"}` write wipes
// the directory fields (name, agent_type, channels, channel_ids,
// respond_to, status) that make this agent @-mentionable from other
// installs (`agentAutocompleteEligibility` in the desktop, #4489). Same
// carry-forward stance as the harness publisher: never overwrite a record
// we could not read β€” abort instead of destroying state.
let own_pubkey = client.keys().public_key().to_hex();
let head = client
.query_paginated(
serde_json::json!({
"kinds": [buzz_sdk::kind::KIND_AGENT_PROFILE],
"authors": [own_pubkey],
}),
1,
)
.await
.map_err(|e| {
CliError::Other(format!(
"could not read current kind:10100 record (refusing to overwrite it blind): {e}"
))
})?;
let head_content = head
.first()
.and_then(|ev| ev.get("content"))
.and_then(|c| c.as_str());
let content = merge_add_policy(head_content, policy);
use nostr::{EventBuilder, Kind};
let builder = EventBuilder::new(
Kind::Custom(buzz_sdk::kind::KIND_AGENT_PROFILE as u16),
Expand Down Expand Up @@ -1741,4 +1780,45 @@ mod tests {
"no warning key expected: {report}"
);
}

// ── merge_add_policy (#3663) ─────────────────────────────────────────

#[test]
fn merge_add_policy_preserves_directory_fields() {
let head = r#"{"name":"Fizz","agent_type":"claude-agent-acp","channels":["general"],"channel_ids":["8d888ccd-c50a-5f3a-b927-aa05c8e7e8b2"],"respond_to":"anyone","status":"online"}"#;
let merged: serde_json::Value =
serde_json::from_str(&super::merge_add_policy(Some(head), "owner_only")).unwrap();
assert_eq!(merged["channel_add_policy"], "owner_only");
assert_eq!(merged["name"], "Fizz");
assert_eq!(merged["agent_type"], "claude-agent-acp");
assert_eq!(merged["respond_to"], "anyone");
assert_eq!(merged["status"], "online");
assert_eq!(
merged["channel_ids"][0],
"8d888ccd-c50a-5f3a-b927-aa05c8e7e8b2"
);
}

#[test]
fn merge_add_policy_overwrites_existing_policy() {
let head = r#"{"channel_add_policy":"anyone","name":"Fizz"}"#;
let merged: serde_json::Value =
serde_json::from_str(&super::merge_add_policy(Some(head), "nobody")).unwrap();
assert_eq!(merged["channel_add_policy"], "nobody");
assert_eq!(merged["name"], "Fizz");
}

#[test]
fn merge_add_policy_absent_head_yields_minimal_record() {
let merged: serde_json::Value =
serde_json::from_str(&super::merge_add_policy(None, "anyone")).unwrap();
assert_eq!(merged, serde_json::json!({"channel_add_policy": "anyone"}));
}

#[test]
fn merge_add_policy_non_object_head_treated_as_empty() {
let merged: serde_json::Value =
serde_json::from_str(&super::merge_add_policy(Some("[1,2]"), "anyone")).unwrap();
assert_eq!(merged, serde_json::json!({"channel_add_policy": "anyone"}));
}
}
Loading