From 384d1525a4a5f9f7fdbcb321f23a80b51123db12 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 15:44:59 -0700 Subject: [PATCH 01/20] docs(nips): add NIP-AD durable agent drafts spec Defines durable p-gated kinds 44300 (agent draft request) and 44301 (owner draft resolution), replacing the ephemeral agent_management_request telemetry payload on NIP-AO kind 24200. NIP-AO now points at NIP-AD. Closes BrianInAz/buzz#18 (part 1/12) Signed-off-by: Brian Charbonneau --- docs/nips/NIP-AD.md | 339 ++++++++++++++++++++++++++++++++++++++++++++ docs/nips/NIP-AO.md | 4 + 2 files changed, 343 insertions(+) create mode 100644 docs/nips/NIP-AD.md diff --git a/docs/nips/NIP-AD.md b/docs/nips/NIP-AD.md new file mode 100644 index 0000000000..1bb381ad29 --- /dev/null +++ b/docs/nips/NIP-AD.md @@ -0,0 +1,339 @@ +NIP-AD +====== + +Durable Agent Drafts +-------------------- + +`draft` `optional` `relay` + +This NIP defines two durable, encrypted event kinds for requesting and +resolving changes to a managed agent: `kind:44300` (agent → owner draft +request) and `kind:44301` (owner → agent resolution). An agent publishes a +`kind:44300` event, NIP-44 encrypted to its owner, to propose creating or +updating itself as a managed agent; the owner reviews it in their desktop +client and publishes a `kind:44301` resolution. Because both kinds are +durable and p-gated, a draft published while the owner's client is offline is +replayed on the next launch — the property the ephemeral telemetry path could +never provide. + +## Motivation + +`buzz agents draft-create` lets an agent propose itself for management by an +owner. The original implementation piggybacked this on the NIP-AO +`agent_management_request` telemetry payload carried on kind 24200. That kind +is deliberately ephemeral: relays MUST NOT persist it, so a draft published +while the owner's desktop is closed is lost forever, and the desktop's +subscription only opens once an agent already exists — a brand-new identity +can never be seen at all. + +NIP-AD replaces that path with two regular, durable, p-gated kinds. A draft +is a first-class stored event: it replays on the owner's next launch, it is +readable by both the owner and the requesting agent, and it is closed out to +everyone else at every read path. This makes "register me" a durable, +reviewable, owner-attested operation rather than a best-effort live frame. + +## Definitions + +- **Agent**: an AI process with its own Nostr keypair, executing sessions on + behalf of an owner. +- **Owner**: the human (or system) whose pubkey the agent was provisioned + under. +- **Draft request**: a single kind 44300 event proposing to create or update + a managed agent. +- **Draft resolution**: a single kind 44301 event accepting, declining, or + superseding a draft request. +- **Pending draft**: a draft request for which no resolution carrying the + same `requestId` exists. + +## Event Kinds + +| Kind | Name | Direction | +|-------|-------------------------|-------------------| +| 44300 | Agent Draft Request | agent → owner | +| 44301 | Agent Draft Resolution | owner → agent | + +Both kinds are regular, durable events by Buzz convention (alongside +44100/44101/44200): stored, append-only, never replaced. Neither carries an +`h` tag — channel identity lives inside the encrypted payload, exactly as +NIP-AM does it, so the event is community-global (owner-scoped) rather than +channel-scoped. + +## Event Structure + +### Kind 44300 — Agent Draft Request (agent → owner) + +```json +{ + "kind": 44300, + "pubkey": "", + "created_at": , + "content": "", + "tags": [ + ["p", ""], + ["p", ""], + ["agent", ""] + ], + "sig": "..." +} +``` + +### Kind 44301 — Agent Draft Resolution (owner → agent) + +```json +{ + "kind": 44301, + "pubkey": "", + "created_at": , + "content": "", + "tags": [ + ["p", ""], + ["p", ""], + ["agent", ""], + ["e", ""] + ], + "sig": "..." +} +``` + +### Envelope rules (relay-enforced at ingest) + +Both kinds MUST have: + +- exactly **two** `p` tags, both 64 lowercase hex, forming the set + `{owner, agent}`, with `owner != agent`; +- exactly **one** `agent` tag, 64 lowercase hex, equal to `event.pubkey`; +- **no** `h` tag; +- `content` that passes the NIP-44 v2 shape check; +- `is_agent_owner(agent, owner)` true in the requesting community. + +Kind 44301 additionally MUST have `event.pubkey == owner` (the owner authors +it, so `agent != event.pubkey`) and exactly **one** `e` tag of 64 lowercase +hex. + +> **Why two `p` tags.** `p_gated_filters_authorized` requires every `#p` +> value in the *filter* to equal the authenticated reader, and +> `reader_authorized_for_event` requires the *event* to carry a `#p` matching +> the reader. Two `p` tags let the owner read with `{"#p":[owner]}` and the +> agent read back its own drafts with `{"#p":[agent]}`, while any third party +> is still closed out at both layers. This is a deliberate divergence from +> NIP-AM's single `p` tag. + +## Encryption + +`content` MUST be encrypted with NIP-44 v2 (XChaCha20-Poly1305 over a +secp256k1 ECDH shared secret). + +- **44300**: encrypted with `(agent_privkey, owner_pubkey)`. +- **44301**: encrypted with `(owner_privkey, agent_pubkey)`. + +Plaintext SHOULD be zeroized from memory immediately after encrypt/decrypt. +Decrypted payload MUST NOT exceed 65,535 bytes. + +## Decrypted Payload + +### Kind 44300 — `AgentDraftRequestPayload` + +The `content` field decrypts to a UTF-8 JSON object (camelCase on the wire): + +```jsonc +{ + "version": 1, + "requestId": "", + "action": "create" | "update", + "timestamp": "", + "channelId": "", + "request": { + // action == "create" + "displayName": "<= 120 chars", + "systemPrompt": "<= 20000 chars" + // action == "update" — agentName required, >= 1 of the rest + // "agentName", "displayName", "systemPrompt", "runtime", "provider", "model", + // "respondTo": "owner-only" | "anyone" + } +} +``` + +`version` is REQUIRED and MUST be `1`. Consumers MUST ignore unknown fields. +Consumers MUST reject a payload whose `version` they do not understand (fail +closed — do **not** best-effort a future version). + +### Kind 44301 — `AgentDraftResolutionPayload` + +```jsonc +{ + "version": 1, + "requestId": "", + "status": "accepted" | "declined" | "superseded", + "timestamp": "", + "agentPubkey": "", + "reason": "" +} +``` + +`version` is REQUIRED and MUST be `1`. `agentPubkey` is the agent the owner +actually saved and is present when `status == "accepted"`. `reason` is +optional, at most 500 characters, and operator-visible. + +## Lifecycle + +A draft is **pending** iff a 44300 exists for `(owner, agent, requestId)` +with no 44301 carrying the same `requestId`. Both clients derive pending-ness +by querying both kinds — there is no server-side state. Retention follows the +relay's normal event TTL; no new retention policy. + +`superseded` exists so a second draft with the same `channelId` + `agentName` +can retire an older pending one without the owner having to act on both. + +## Authorization + +Both directions require relay confirmation of the agent-owner relationship via +authenticated ownership lookup (`is_agent_owner`). `#p` tag matching alone is +insufficient. + +- **44300** (agent → owner): `event.pubkey` MUST equal the `agent` tag, and + `is_agent_owner(agent, owner)` MUST hold. +- **44301** (owner → agent): `event.pubkey` MUST equal the owner, and + `is_agent_owner(agent, owner)` MUST hold. + +Reads MUST be gated: only an authenticated ([NIP-42](42.md)) reader whose +pubkey equals one of the `#p` tag values may receive the event. This gate +applies to **every** read path, including explicit `ids` filters — knowing an +event id MUST NOT grant access. Unauthenticated publish or subscribe attempts +MUST be rejected with `AUTH required`; authenticated attempts from a pubkey +that is not one of the event's `p` tags MUST be rejected with `restricted:`. + +## Relay Behavior + +On receiving a kind 44300 or 44301 event, a relay MUST: + +1. Validate the event signature per NIP-01. +2. Verify the envelope rules above, including `is_agent_owner(agent, owner)` + via authenticated ownership lookup. +3. Store the event durably, scoped to the owner (community-global; no channel + scope). +4. NOT index the event in any full-text search (the ciphertext is not + searchable and must not enter search indexes). + +## Client Behavior + +Owners recover pending drafts with: + +```json +{"kinds": [44300], "#p": [""], "limit": 100} +``` + +and resolve them by publishing a 44301. Agents read back their own drafts and +resolutions with: + +```json +{"kinds": [44300, 44301], "#p": [""]} +``` + +On receiving an event, a client MUST verify the signature, decrypt with its +own secret key and `event.pubkey`, and ignore events that fail to decrypt or +parse. Clients MUST reject a payload whose `version` they do not understand. +Clients SHOULD deduplicate by event id and derive pending-ness by joining +44300 against 44301 on `requestId`. + +## Relationship to Other NIPs + +- [NIP-AO](NIP-AO.md): same agent↔owner encryption and tag scoping, but + ephemeral and transcript-grade. **NIP-AD supersedes the + `agent_management_request` telemetry payload previously carried on NIP-AO + kind 24200**; that payload kind is no longer defined. NIP-AD events are + durable and MUST NOT be carried on kind 24200. +- [NIP-AM](NIP-AM.md): the durable, p-gated, FTS-excluded template this NIP + follows; NIP-AD diverges only in using two `p` tags (see above). +- [NIP-09](09.md): the authoring agent (or its owner via relay policy) may + request deletion; relays apply standard deletion semantics. +- [NIP-40](40.md): publishers MAY set `expiration` to bound retention. + +## Security Considerations + +**Metadata leakage.** `p`, `agent`, `e`, and `created_at` are cleartext: a +relay operator learns that agent X proposed a change to owner Y. The draft +content, channel, and resolution reason remain encrypted. + +**No forward secrecy.** NIP-44 does not provide forward secrecy; compromise +of the agent's or owner's private key allows decryption of captured +ciphertexts. + +**Draft content is sensitive.** A draft may contain a system prompt or +configuration the agent does not want public. It is encrypted to the owner +and p-gated at every read path; clients MUST NOT log decrypted payloads. + +**Resolution integrity.** Resolutions are self-authored by the owner. A +compromised owner key can forge acceptances; the agent SHOULD verify the +resolution's `requestId` matches a draft it actually sent. + +## Examples + +### 1. Draft request — create + +**Wire event (encrypted):** + +```json +{ + "id": "a1b2c3d4...", + "kind": 44300, + "pubkey": "agent_pubkey_hex", + "created_at": 1777464041, + "content": "", + "tags": [ + ["p", "owner_pubkey_hex"], + ["p", "agent_pubkey_hex"], + ["agent", "agent_pubkey_hex"] + ], + "sig": "..." +} +``` + +**Decrypted payload:** + +```json +{ + "version": 1, + "requestId": "9f1c2b3a-4d5e-4f6a-8b7c-1d2e3f4a5b6c", + "action": "create", + "timestamp": "2026-08-05T12:00:00.000Z", + "channelId": "f0347328-e105-4e62-9af8-807d20e484dd", + "request": { + "displayName": "dev-coder", + "systemPrompt": "You are a coding specialist..." + } +} +``` + +### 2. Draft resolution — accepted + +**Wire event (encrypted):** + +```json +{ + "id": "e5f6a7b8...", + "kind": 44301, + "pubkey": "owner_pubkey_hex", + "created_at": 1777464042, + "content": "", + "tags": [ + ["p", "owner_pubkey_hex"], + ["p", "agent_pubkey_hex"], + ["agent", "agent_pubkey_hex"], + ["e", "a1b2c3d4..."] + ], + "sig": "..." +} +``` + +**Decrypted payload:** + +```json +{ + "version": 1, + "requestId": "9f1c2b3a-4d5e-4f6a-8b7c-1d2e3f4a5b6c", + "status": "accepted", + "timestamp": "2026-08-05T12:05:00.000Z", + "agentPubkey": "agent_pubkey_hex", + "reason": "Approved" +} +``` diff --git a/docs/nips/NIP-AO.md b/docs/nips/NIP-AO.md index 36adea0487..e27183cbba 100644 --- a/docs/nips/NIP-AO.md +++ b/docs/nips/NIP-AO.md @@ -222,6 +222,10 @@ of decrypted payloads and MUST NOT log it at INFO level or above. - **NIP-XX (PR #2226)**: NIP-XX defines the agent *output* plane; this NIP defines the *observability* plane (internal agent activity). They are complementary and non-overlapping. +- **[NIP-AD](NIP-AD.md)**: the `agent_management_request` telemetry payload + previously carried on kind 24200 moved to NIP-AD and is no longer a defined + telemetry `kind` value. Agent draft requests/resolutions are durable kinds + 44300/44301, not NIP-AO frames. ## Examples From 2400d8473d26bc8501df581622113999df4b9ea0 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 15:50:20 -0700 Subject: [PATCH 02/20] feat(core): NIP-AD kinds 44300/44301, payload module, read gate Adds KIND_AGENT_DRAFT_REQUEST (44300) and KIND_AGENT_DRAFT_RESOLUTION (44301) to P_GATED_KINDS and RESULT_GATED_KINDS, a new agent_draft payload module (create/update request + resolution, NIP-44 encrypt/decrypt, fail-closed version validation), and widens reader_authorized_for_event to derive the gate from RESULT_GATED_KINDS so future kinds inherit it. Note: nostr's EventBuilder discards self-p-tags by default; the two-p-tag NIP-AD envelope requires allow_self_tagging() at build time. Closes BrianInAz/buzz#18 (part 2/12) Signed-off-by: Brian Charbonneau --- crates/buzz-core/src/agent_draft.rs | 603 ++++++++++++++++++++++++++++ crates/buzz-core/src/filter.rs | 79 +++- crates/buzz-core/src/kind.rs | 46 ++- crates/buzz-core/src/lib.rs | 2 + 4 files changed, 725 insertions(+), 5 deletions(-) create mode 100644 crates/buzz-core/src/agent_draft.rs diff --git a/crates/buzz-core/src/agent_draft.rs b/crates/buzz-core/src/agent_draft.rs new file mode 100644 index 0000000000..e3c255c209 --- /dev/null +++ b/crates/buzz-core/src/agent_draft.rs @@ -0,0 +1,603 @@ +//! NIP-AD: Agent Draft — payload types and encrypt/decrypt helpers. +//! +//! Two durable, p-gated kinds carry agent draft requests and resolutions: +//! `kind:44300` (agent → owner draft request) and `kind:44301` (owner → agent +//! resolution). Their content is a NIP-44 v2 ciphertext that decodes to an +//! [`AgentDraftRequestPayload`] or [`AgentDraftResolutionPayload`] JSON object. +//! +//! See `docs/nips/NIP-AD.md` for the full specification. + +use nostr::{Event, Keys, PublicKey}; +use serde::{Deserialize, Serialize}; + +use crate::observer::{decrypt_observer_payload, encrypt_observer_payload, ObserverPayloadError}; + +// Re-export for callers that only need the error type. +pub use crate::observer::ObserverPayloadError as AgentDraftError; + +/// Maximum length of a draft `displayName` (NIP-AD §Decrypted Payload). +pub const AGENT_DRAFT_MAX_DISPLAY_NAME: usize = 120; +/// Maximum length of a draft `systemPrompt` (NIP-AD §Decrypted Payload). +pub const AGENT_DRAFT_MAX_SYSTEM_PROMPT: usize = 20_000; +/// Maximum length of a resolution `reason` (NIP-AD §Decrypted Payload). +pub const AGENT_DRAFT_MAX_REASON: usize = 500; +/// The only supported payload version. Consumers MUST reject any other value. +pub const AGENT_DRAFT_VERSION: u32 = 1; + +/// The action a draft request proposes: create a new managed agent, or update +/// an existing one. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AgentDraftAction { + /// Propose creating a new managed agent. + Create, + /// Propose updating an existing managed agent. + Update, +} + +/// Who an updated agent may respond to. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum AgentDraftRespondTo { + /// Only the owner may prompt the agent. + OwnerOnly, + /// Any community member may prompt the agent. + Anyone, +} + +/// The `request` body of a `create` draft (`action == "create"`). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDraftCreateRequest { + /// Proposed display name, at most [`AGENT_DRAFT_MAX_DISPLAY_NAME`] chars. + pub display_name: String, + /// Proposed system prompt, at most [`AGENT_DRAFT_MAX_SYSTEM_PROMPT`] chars. + pub system_prompt: String, +} + +impl AgentDraftCreateRequest { + /// Validate length constraints from NIP-AD §Decrypted Payload. + pub fn validate(&self) -> Result<(), AgentDraftError> { + if self.display_name.chars().count() > AGENT_DRAFT_MAX_DISPLAY_NAME { + return Err(ObserverPayloadError::InvalidPayload(format!( + "displayName exceeds {} chars", + AGENT_DRAFT_MAX_DISPLAY_NAME + ))); + } + if self.system_prompt.chars().count() > AGENT_DRAFT_MAX_SYSTEM_PROMPT { + return Err(ObserverPayloadError::InvalidPayload(format!( + "systemPrompt exceeds {} chars", + AGENT_DRAFT_MAX_SYSTEM_PROMPT + ))); + } + Ok(()) + } +} + +/// The `request` body of an `update` draft (`action == "update"`). +/// +/// `agent_name` is REQUIRED; at least one of the remaining fields MUST be +/// present (an update with no changed field is rejected). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDraftUpdateRequest { + /// The name of the agent to update. REQUIRED. + pub agent_name: String, + /// New display name, at most [`AGENT_DRAFT_MAX_DISPLAY_NAME`] chars. + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// New system prompt, at most [`AGENT_DRAFT_MAX_SYSTEM_PROMPT`] chars. + #[serde(skip_serializing_if = "Option::is_none")] + pub system_prompt: Option, + /// New runtime identifier. + #[serde(skip_serializing_if = "Option::is_none")] + pub runtime: Option, + /// New provider identifier. + #[serde(skip_serializing_if = "Option::is_none")] + pub provider: Option, + /// New model identifier. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + /// New respond-to policy. + #[serde(skip_serializing_if = "Option::is_none")] + pub respond_to: Option, +} + +impl AgentDraftUpdateRequest { + /// Validate NIP-AD constraints: `agent_name` present, at least one changed + /// field, and length limits on any present `displayName`/`systemPrompt`. + pub fn validate(&self) -> Result<(), AgentDraftError> { + if self.agent_name.is_empty() { + return Err(ObserverPayloadError::InvalidPayload( + "update request requires agentName".to_string(), + )); + } + let has_change = self.display_name.is_some() + || self.system_prompt.is_some() + || self.runtime.is_some() + || self.provider.is_some() + || self.model.is_some() + || self.respond_to.is_some(); + if !has_change { + return Err(ObserverPayloadError::InvalidPayload( + "update request requires at least one changed field".to_string(), + )); + } + if let Some(d) = &self.display_name { + if d.chars().count() > AGENT_DRAFT_MAX_DISPLAY_NAME { + return Err(ObserverPayloadError::InvalidPayload(format!( + "displayName exceeds {} chars", + AGENT_DRAFT_MAX_DISPLAY_NAME + ))); + } + } + if let Some(s) = &self.system_prompt { + if s.chars().count() > AGENT_DRAFT_MAX_SYSTEM_PROMPT { + return Err(ObserverPayloadError::InvalidPayload(format!( + "systemPrompt exceeds {} chars", + AGENT_DRAFT_MAX_SYSTEM_PROMPT + ))); + } + } + Ok(()) + } +} + +/// The `request` field of a draft request payload — a union discriminated by +/// the top-level `action` field. `create` carries a +/// [`AgentDraftCreateRequest`]; `update` carries an [`AgentDraftUpdateRequest`]. +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(untagged)] +pub enum AgentDraftRequest { + /// A create proposal. + Create(AgentDraftCreateRequest), + /// An update proposal. + Update(AgentDraftUpdateRequest), +} + +impl<'de> Deserialize<'de> for AgentDraftRequest { + fn deserialize>(deserializer: D) -> Result { + // Disambiguate on the presence of `agentName` (only update requests + // carry it). Unknown fields are otherwise ignored for forward compat. + let value = serde_json::Value::deserialize(deserializer)?; + if value.get("agentName").is_some() { + serde_json::from_value(value) + .map(AgentDraftRequest::Update) + .map_err(serde::de::Error::custom) + } else { + serde_json::from_value(value) + .map(AgentDraftRequest::Create) + .map_err(serde::de::Error::custom) + } + } +} + +/// Decrypted payload of a `kind:44300` Agent Draft Request event. +/// +/// `version` is REQUIRED and MUST be [`AGENT_DRAFT_VERSION`]. Consumers MUST +/// ignore unknown fields, and MUST reject a payload whose `version` they do +/// not understand (fail closed). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDraftRequestPayload { + /// Payload version. MUST be [`AGENT_DRAFT_VERSION`]. + pub version: u32, + /// UUID v4 identifying this draft request. + pub request_id: String, + /// Whether this proposes creating or updating an agent. + pub action: AgentDraftAction, + /// RFC 3339 timestamp of the request. + pub timestamp: String, + /// Channel UUID the draft is scoped to. + pub channel_id: String, + /// The create or update body. + pub request: AgentDraftRequest, +} + +impl AgentDraftRequestPayload { + /// Validate NIP-AD constraints: `version` must be supported, the `action` + /// must match the `request` variant, and the request body must pass its own + /// validation. + pub fn validate(&self) -> Result<(), AgentDraftError> { + if self.version != AGENT_DRAFT_VERSION { + return Err(ObserverPayloadError::InvalidPayload(format!( + "unsupported version {} (expected {})", + self.version, AGENT_DRAFT_VERSION + ))); + } + match (&self.action, &self.request) { + (AgentDraftAction::Create, AgentDraftRequest::Create(r)) => r.validate(), + (AgentDraftAction::Update, AgentDraftRequest::Update(r)) => r.validate(), + _ => Err(ObserverPayloadError::InvalidPayload( + "action does not match request body".to_string(), + )), + } + } +} + +/// The status of a draft resolution. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AgentDraftResolutionStatus { + /// The owner accepted the draft and saved the agent. + Accepted, + /// The owner declined the draft. + Declined, + /// A newer draft superseded this one; no action was taken. + Superseded, +} + +/// Decrypted payload of a `kind:44301` Agent Draft Resolution event. +/// +/// `version` is REQUIRED and MUST be [`AGENT_DRAFT_VERSION`]. `agent_pubkey` +/// is present when `status == "accepted"`. `reason` is optional, at most +/// [`AGENT_DRAFT_MAX_REASON`] chars, and operator-visible. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDraftResolutionPayload { + /// Payload version. MUST be [`AGENT_DRAFT_VERSION`]. + pub version: u32, + /// The `requestId` of the draft this resolves (echoes the request). + pub request_id: String, + /// The resolution status. + pub status: AgentDraftResolutionStatus, + /// RFC 3339 timestamp of the resolution. + pub timestamp: String, + /// The agent the owner actually saved; present when `status == "accepted"`. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_pubkey: Option, + /// Optional operator-visible reason, at most [`AGENT_DRAFT_MAX_REASON`] chars. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +impl AgentDraftResolutionPayload { + /// Validate NIP-AD constraints: `version` must be supported, `reason` must + /// be within length limits, and `agent_pubkey` must be present when the + /// status is `accepted`. + pub fn validate(&self) -> Result<(), AgentDraftError> { + if self.version != AGENT_DRAFT_VERSION { + return Err(ObserverPayloadError::InvalidPayload(format!( + "unsupported version {} (expected {})", + self.version, AGENT_DRAFT_VERSION + ))); + } + if let Some(r) = &self.reason { + if r.chars().count() > AGENT_DRAFT_MAX_REASON { + return Err(ObserverPayloadError::InvalidPayload(format!( + "reason exceeds {} chars", + AGENT_DRAFT_MAX_REASON + ))); + } + } + if self.status == AgentDraftResolutionStatus::Accepted && self.agent_pubkey.is_none() { + return Err(ObserverPayloadError::InvalidPayload( + "accepted resolution requires agentPubkey".to_string(), + )); + } + Ok(()) + } +} + +/// Encrypt an [`AgentDraftRequestPayload`] into a NIP-44 v2 ciphertext string +/// using the agent's key pair and the owner's public key. +/// +/// This is the content field of a `kind:44300` event. +pub fn encrypt_agent_draft_request( + agent_keys: &Keys, + owner_pubkey: &PublicKey, + payload: &AgentDraftRequestPayload, +) -> Result { + payload.validate()?; + encrypt_observer_payload(agent_keys, owner_pubkey, payload) +} + +/// Decrypt and deserialize an [`AgentDraftRequestPayload`] from a `kind:44300` +/// event. `recipient_keys` is the owner's key pair. +pub fn decrypt_agent_draft_request( + recipient_keys: &Keys, + event: &Event, +) -> Result { + let payload: AgentDraftRequestPayload = decrypt_observer_payload(recipient_keys, event)?; + payload.validate()?; + Ok(payload) +} + +/// Encrypt an [`AgentDraftResolutionPayload`] into a NIP-44 v2 ciphertext +/// string using the owner's key pair and the agent's public key. +/// +/// This is the content field of a `kind:44301` event. +pub fn encrypt_agent_draft_resolution( + owner_keys: &Keys, + agent_pubkey: &PublicKey, + payload: &AgentDraftResolutionPayload, +) -> Result { + payload.validate()?; + encrypt_observer_payload(owner_keys, agent_pubkey, payload) +} + +/// Decrypt and deserialize an [`AgentDraftResolutionPayload`] from a +/// `kind:44301` event. `recipient_keys` is the agent's key pair. +pub fn decrypt_agent_draft_resolution( + recipient_keys: &Keys, + event: &Event, +) -> Result { + let payload: AgentDraftResolutionPayload = decrypt_observer_payload(recipient_keys, event)?; + payload.validate()?; + Ok(payload) +} + +#[cfg(test)] +mod tests { + use super::*; + use nostr::{EventBuilder, Kind, Tag}; + + fn sample_create_payload() -> AgentDraftRequestPayload { + AgentDraftRequestPayload { + version: 1, + request_id: "9f1c2b3a-4d5e-4f6a-8b7c-1d2e3f4a5b6c".to_string(), + action: AgentDraftAction::Create, + timestamp: "2026-08-05T12:00:00.000Z".to_string(), + channel_id: "f0347328-e105-4e62-9af8-807d20e484dd".to_string(), + request: AgentDraftRequest::Create(AgentDraftCreateRequest { + display_name: "dev-coder".to_string(), + system_prompt: "You are a coding specialist.".to_string(), + }), + } + } + + fn sample_update_payload() -> AgentDraftRequestPayload { + AgentDraftRequestPayload { + version: 1, + request_id: "7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d".to_string(), + action: AgentDraftAction::Update, + timestamp: "2026-08-05T12:10:00.000Z".to_string(), + channel_id: "f0347328-e105-4e62-9af8-807d20e484dd".to_string(), + request: AgentDraftRequest::Update(AgentDraftUpdateRequest { + agent_name: "dev-coder".to_string(), + display_name: Some("dev-coder-v2".to_string()), + system_prompt: None, + runtime: None, + provider: None, + model: None, + respond_to: None, + }), + } + } + + fn sample_resolution_payload() -> AgentDraftResolutionPayload { + AgentDraftResolutionPayload { + version: 1, + request_id: "9f1c2b3a-4d5e-4f6a-8b7c-1d2e3f4a5b6c".to_string(), + status: AgentDraftResolutionStatus::Accepted, + timestamp: "2026-08-05T12:05:00.000Z".to_string(), + agent_pubkey: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()), + reason: Some("Approved".to_string()), + } + } + + fn build_request_event(agent_keys: &Keys, owner_pubkey: &PublicKey, ciphertext: String) -> Event { + EventBuilder::new(Kind::Custom(crate::kind::KIND_AGENT_DRAFT_REQUEST as u16), ciphertext) + .tags([ + Tag::parse(["p", &owner_pubkey.to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + // The agent's own pubkey is a `p` tag; nostr's EventBuilder discards + // self-`p`-tags unless self-tagging is allowed. + .allow_self_tagging() + .sign_with_keys(agent_keys) + .expect("sign") + } + + fn build_resolution_event(owner_keys: &Keys, agent_pubkey: &PublicKey, ciphertext: String) -> Event { + EventBuilder::new( + Kind::Custom(crate::kind::KIND_AGENT_DRAFT_RESOLUTION as u16), + ciphertext, + ) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_pubkey.to_hex()]).unwrap(), + Tag::parse(["agent", &agent_pubkey.to_hex()]).unwrap(), + Tag::parse(["e", "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"]).unwrap(), + ]) + // The owner's own pubkey is a `p` tag; nostr's EventBuilder discards + // self-`p`-tags unless self-tagging is allowed. + .allow_self_tagging() + .sign_with_keys(owner_keys) + .expect("sign") + } + + #[test] + fn request_round_trip_encrypt_decrypt() { + let agent_keys = Keys::generate(); + let owner_keys = Keys::generate(); + + let payload = sample_create_payload(); + let ciphertext = + encrypt_agent_draft_request(&agent_keys, &owner_keys.public_key(), &payload) + .expect("encrypt"); + let event = build_request_event(&agent_keys, &owner_keys.public_key(), ciphertext); + let decoded = decrypt_agent_draft_request(&owner_keys, &event).expect("decrypt"); + assert_eq!(decoded, payload); + } + + #[test] + fn update_request_round_trip_encrypt_decrypt() { + let agent_keys = Keys::generate(); + let owner_keys = Keys::generate(); + + let payload = sample_update_payload(); + let ciphertext = + encrypt_agent_draft_request(&agent_keys, &owner_keys.public_key(), &payload) + .expect("encrypt"); + let event = build_request_event(&agent_keys, &owner_keys.public_key(), ciphertext); + let decoded = decrypt_agent_draft_request(&owner_keys, &event).expect("decrypt"); + assert_eq!(decoded, payload); + } + + #[test] + fn resolution_round_trip_encrypt_decrypt() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + + let payload = sample_resolution_payload(); + let ciphertext = + encrypt_agent_draft_resolution(&owner_keys, &agent_keys.public_key(), &payload) + .expect("encrypt"); + let event = build_resolution_event(&owner_keys, &agent_keys.public_key(), ciphertext); + let decoded = decrypt_agent_draft_resolution(&agent_keys, &event).expect("decrypt"); + assert_eq!(decoded, payload); + } + + #[test] + fn rejects_unsupported_version() { + let mut payload = sample_create_payload(); + payload.version = 2; + assert!( + matches!( + payload.validate(), + Err(ObserverPayloadError::InvalidPayload(_)) + ), + "version 2 must be rejected (fail closed)" + ); + + let mut resolution = sample_resolution_payload(); + resolution.version = 2; + assert!( + matches!( + resolution.validate(), + Err(ObserverPayloadError::InvalidPayload(_)) + ), + "resolution version 2 must be rejected" + ); + } + + #[test] + fn rejects_overlong_display_name() { + let mut payload = sample_create_payload(); + if let AgentDraftRequest::Create(r) = &mut payload.request { + r.display_name = "x".repeat(AGENT_DRAFT_MAX_DISPLAY_NAME + 1); + } + assert!(payload.validate().is_err()); + } + + #[test] + fn rejects_overlong_system_prompt() { + let mut payload = sample_create_payload(); + if let AgentDraftRequest::Create(r) = &mut payload.request { + r.system_prompt = "x".repeat(AGENT_DRAFT_MAX_SYSTEM_PROMPT + 1); + } + assert!(payload.validate().is_err()); + } + + #[test] + fn rejects_overlong_reason() { + let mut resolution = sample_resolution_payload(); + resolution.reason = Some("x".repeat(AGENT_DRAFT_MAX_REASON + 1)); + assert!(resolution.validate().is_err()); + } + + #[test] + fn rejects_update_with_no_changed_field() { + let payload = AgentDraftRequestPayload { + version: 1, + request_id: "7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d".to_string(), + action: AgentDraftAction::Update, + timestamp: "2026-08-05T12:10:00.000Z".to_string(), + channel_id: "f0347328-e105-4e62-9af8-807d20e484dd".to_string(), + request: AgentDraftRequest::Update(AgentDraftUpdateRequest { + agent_name: "dev-coder".to_string(), + display_name: None, + system_prompt: None, + runtime: None, + provider: None, + model: None, + respond_to: None, + }), + }; + assert!( + matches!( + payload.validate(), + Err(ObserverPayloadError::InvalidPayload(_)) + ), + "an update with no changed field must be rejected" + ); + } + + #[test] + fn rejects_action_request_mismatch() { + // action=create but request=update body. + let payload = AgentDraftRequestPayload { + version: 1, + request_id: "7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d".to_string(), + action: AgentDraftAction::Create, + timestamp: "2026-08-05T12:10:00.000Z".to_string(), + channel_id: "f0347328-e105-4e62-9af8-807d20e484dd".to_string(), + request: AgentDraftRequest::Update(AgentDraftUpdateRequest { + agent_name: "dev-coder".to_string(), + display_name: Some("x".to_string()), + system_prompt: None, + runtime: None, + provider: None, + model: None, + respond_to: None, + }), + }; + assert!(payload.validate().is_err()); + } + + #[test] + fn unknown_fields_are_ignored() { + // A future payload with extra fields must still parse (forward compat). + let json = r#"{ + "version": 1, + "requestId": "9f1c2b3a-4d5e-4f6a-8b7c-1d2e3f4a5b6c", + "action": "create", + "timestamp": "2026-08-05T12:00:00.000Z", + "channelId": "f0347328-e105-4e62-9af8-807d20e484dd", + "futureField": { "nested": true }, + "request": { + "displayName": "dev-coder", + "systemPrompt": "You are a coding specialist.", + "futurePromptField": "ignored" + } + }"#; + let payload: AgentDraftRequestPayload = serde_json::from_str(json).expect("parse"); + assert_eq!(payload.version, 1); + assert_eq!(payload.action, AgentDraftAction::Create); + assert!(payload.validate().is_ok()); + } + + #[test] + fn update_deserializes_by_agent_name_presence() { + let json = r#"{ + "version": 1, + "requestId": "7a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "action": "update", + "timestamp": "2026-08-05T12:10:00.000Z", + "channelId": "f0347328-e105-4e62-9af8-807d20e484dd", + "request": { + "agentName": "dev-coder", + "displayName": "dev-coder-v2" + } + }"#; + let payload: AgentDraftRequestPayload = serde_json::from_str(json).expect("parse"); + assert_eq!(payload.action, AgentDraftAction::Update); + assert!(matches!(payload.request, AgentDraftRequest::Update(_))); + assert!(payload.validate().is_ok()); + } + + #[test] + fn accepted_resolution_requires_agent_pubkey() { + let mut resolution = sample_resolution_payload(); + resolution.agent_pubkey = None; + assert!( + matches!( + resolution.validate(), + Err(ObserverPayloadError::InvalidPayload(_)) + ), + "accepted resolution without agentPubkey must be rejected" + ); + } +} diff --git a/crates/buzz-core/src/filter.rs b/crates/buzz-core/src/filter.rs index 1671f76224..07160b0723 100644 --- a/crates/buzz-core/src/filter.rs +++ b/crates/buzz-core/src/filter.rs @@ -12,9 +12,10 @@ pub fn filters_match(filters: &[Filter], event: &StoredEvent) -> bool { } /// Result-level read authorization for relay-signed events whose content is -/// private to a single viewer. Currently gates `KIND_DM_VISIBILITY` and -/// `KIND_AGENT_TURN_METRIC`: the reader MUST equal the event's `#p` tag -/// (owner). Returns `true` for every other kind. +/// private to a single viewer. Gates every kind in [`crate::kind::RESULT_GATED_KINDS`] +/// (currently `KIND_DM_VISIBILITY`, `KIND_AGENT_TURN_METRIC`, and the NIP-AD +/// draft kinds): the reader MUST equal one of the event's `#p` tag values. +/// Returns `true` for every other kind. /// /// This guards every delivery surface — WS historical pull (`req.rs`), HTTP /// bridge (`bridge.rs`), and live fan-out (`event.rs`) — so a query that @@ -22,7 +23,9 @@ pub fn filters_match(filters: &[Filter], event: &StoredEvent) -> bool { /// a known event id) still cannot read another user's private event. pub fn reader_authorized_for_event(event: &nostr::Event, reader_pubkey_hex: &str) -> bool { let kind = crate::kind::event_kind_u32(event); - if kind != crate::kind::KIND_DM_VISIBILITY && kind != crate::kind::KIND_AGENT_TURN_METRIC { + // The constant is the single source of truth: a kind added to + // RESULT_GATED_KINDS inherits this gate without a new branch here. + if !crate::kind::RESULT_GATED_KINDS.contains(&kind) { return true; } let p = nostr::SingleLetterTag::lowercase(nostr::Alphabet::P); @@ -297,4 +300,72 @@ mod tests { "the authoring agent must NOT be authorized to read its own metric event (owner-only)" ); } + + #[test] + fn reader_authorized_for_event_gates_agent_drafts_by_p() { + let agent_keys = Keys::generate(); + let owner = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let attacker = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + + // 44300 draft request: pubkey=agent, two p tags (owner + agent), agent tag. + let request = EventBuilder::new( + Kind::Custom(crate::kind::KIND_AGENT_DRAFT_REQUEST as u16), + "encrypted-payload", + ) + .tags([ + Tag::parse(["p", owner]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + // The agent's own pubkey is a `p` tag; nostr's EventBuilder discards + // self-`p`-tags unless self-tagging is allowed. + .allow_self_tagging() + .sign_with_keys(&agent_keys) + .expect("sign"); + + assert!( + reader_authorized_for_event(&request, owner), + "owner must be authorized to read a draft request addressed to them" + ); + assert!( + reader_authorized_for_event(&request, &agent_keys.public_key().to_hex()), + "the requesting agent must be authorized to read back its own draft" + ); + assert!( + !reader_authorized_for_event(&request, attacker), + "a third party must NOT be authorized to read a draft request" + ); + + // 44301 draft resolution: pubkey=owner, two p tags (owner + agent), agent tag. + let owner_keys = Keys::generate(); + let resolution = EventBuilder::new( + Kind::Custom(crate::kind::KIND_AGENT_DRAFT_RESOLUTION as u16), + "encrypted-payload", + ) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["e", "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"]) + .unwrap(), + ]) + // The owner's own pubkey is a `p` tag; nostr's EventBuilder discards + // self-`p`-tags unless self-tagging is allowed. + .allow_self_tagging() + .sign_with_keys(&owner_keys) + .expect("sign"); + + assert!( + reader_authorized_for_event(&resolution, &owner_keys.public_key().to_hex()), + "owner must be authorized to read a resolution they authored" + ); + assert!( + reader_authorized_for_event(&resolution, &agent_keys.public_key().to_hex()), + "the agent must be authorized to read a resolution addressed to it" + ); + assert!( + !reader_authorized_for_event(&resolution, attacker), + "a third party must NOT be authorized to read a draft resolution" + ); + } } diff --git a/crates/buzz-core/src/kind.rs b/crates/buzz-core/src/kind.rs index b1be7c5038..d50eab0b62 100644 --- a/crates/buzz-core/src/kind.rs +++ b/crates/buzz-core/src/kind.rs @@ -126,7 +126,15 @@ pub const AUTHOR_ONLY_KINDS: &[u32] = &[KIND_EVENT_REMINDER, KIND_PUSH_LEASE]; /// /// Used by `filter_can_match_result_gated_kinds` to force the per-event /// fallback path in COUNT rather than the fast SQL `count_events()`. -pub const RESULT_GATED_KINDS: &[u32] = &[KIND_DM_VISIBILITY, KIND_AGENT_TURN_METRIC]; +pub const RESULT_GATED_KINDS: &[u32] = &[ + KIND_DM_VISIBILITY, + KIND_AGENT_TURN_METRIC, + // NIP-AD: agent drafts are encrypted to the owner/agent and must not be + // readable by any third party, including via kindless `ids` filters — see + // NIP-AD §Relay Behavior. + KIND_AGENT_DRAFT_REQUEST, + KIND_AGENT_DRAFT_RESOLUTION, +]; /// Kinds whose stored events have `#p`-bound read access — readable only by /// subscribers whose pubkey appears in the event's `#p` tag. @@ -153,6 +161,11 @@ pub const P_GATED_KINDS: &[u32] = &[ // readable by any unauthenticated or non-owner party, including via `ids` // filters — see NIP-AM §Relay Behavior. KIND_AGENT_TURN_METRIC, + // NIP-AD: agent drafts are encrypted to the owner/agent and must not be + // readable by any third party, including via `ids` filters — see NIP-AD + // §Relay Behavior. + KIND_AGENT_DRAFT_REQUEST, + KIND_AGENT_DRAFT_RESOLUTION, ]; /// NIP-AP: Agent Persona (parameterized replaceable, owner-authored). @@ -531,6 +544,26 @@ pub const KIND_MEMBER_REMOVED_NOTIFICATION: u32 = 44101; /// See `docs/nips/NIP-AM.md`. pub const KIND_AGENT_TURN_METRIC: u32 = 44200; +/// NIP-AD: Agent Draft Request — durable agent→owner draft proposal (agent-authored). +/// +/// Regular stored event (append-only, never replaced). The agent proposes +/// creating or updating itself as a managed agent, NIP-44 encrypted to its +/// owner. Tags: exactly two `p` tags (owner + agent, `owner != agent`), one +/// `agent` tag (agent pubkey == event pubkey), no `h` tag. Stored globally +/// (channel_id = NULL); p-gated reads (NIP-42) for the owner and the agent. +/// See `docs/nips/NIP-AD.md`. +pub const KIND_AGENT_DRAFT_REQUEST: u32 = 44300; + +/// NIP-AD: Agent Draft Resolution — durable owner→agent draft resolution (owner-authored). +/// +/// Regular stored event (append-only, never replaced). The owner accepts, +/// declines, or supersedes a draft request, NIP-44 encrypted to the agent. +/// Tags: exactly two `p` tags (owner + agent, `owner != agent`), one `agent` +/// tag (agent pubkey), one `e` tag (the request event id), no `h` tag. Stored +/// globally (channel_id = NULL); p-gated reads (NIP-42) for the owner and the +/// agent. See `docs/nips/NIP-AD.md`. +pub const KIND_AGENT_DRAFT_RESOLUTION: u32 = 44301; + // Forum / social (45000–45999) // V1 used addressable range (30001–30003) — wrong. /// A forum post (thread root). @@ -711,6 +744,8 @@ pub const ALL_KINDS: &[u32] = &[ KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_AGENT_TURN_METRIC, + KIND_AGENT_DRAFT_REQUEST, + KIND_AGENT_DRAFT_RESOLUTION, KIND_WORKFLOW_DEF, KIND_LONG_FORM, KIND_USER_STATUS, @@ -870,6 +905,15 @@ const _: () = assert!(!is_ephemeral(KIND_AGENT_TURN_METRIC)); const _: () = assert!(!is_replaceable(KIND_AGENT_TURN_METRIC)); const _: () = assert!(!is_parameterized_replaceable(KIND_AGENT_TURN_METRIC)); const _: () = assert!(KIND_AGENT_TURN_METRIC <= u16::MAX as u32); +// Compile-time: NIP-AD draft kinds are regular stored kinds (not ephemeral, not replaceable). +const _: () = assert!(!is_ephemeral(KIND_AGENT_DRAFT_REQUEST)); +const _: () = assert!(!is_replaceable(KIND_AGENT_DRAFT_REQUEST)); +const _: () = assert!(!is_parameterized_replaceable(KIND_AGENT_DRAFT_REQUEST)); +const _: () = assert!(KIND_AGENT_DRAFT_REQUEST <= u16::MAX as u32); +const _: () = assert!(!is_ephemeral(KIND_AGENT_DRAFT_RESOLUTION)); +const _: () = assert!(!is_replaceable(KIND_AGENT_DRAFT_RESOLUTION)); +const _: () = assert!(!is_parameterized_replaceable(KIND_AGENT_DRAFT_RESOLUTION)); +const _: () = assert!(KIND_AGENT_DRAFT_RESOLUTION <= u16::MAX as u32); // Moderation kinds fit u16 and are neither replaceable nor ephemeral: // 1984 is a regular event (persisted to the queue, never fanned out); // 9040–9044 are direct commands (executed, never stored). diff --git a/crates/buzz-core/src/lib.rs b/crates/buzz-core/src/lib.rs index 66b7708f1d..57b7f72628 100644 --- a/crates/buzz-core/src/lib.rs +++ b/crates/buzz-core/src/lib.rs @@ -7,6 +7,8 @@ /// NIP-AM: Agent Turn Metric — payload type and encrypt/decrypt helpers. pub mod agent_turn_metric; +/// NIP-AD: Agent Draft — payload types and encrypt/decrypt helpers. +pub mod agent_draft; /// Channel and membership enums shared across crates. pub mod channel; /// NIP-AE Agent Engrams — slug grammar, conversation key, d-tag derivation, From 3a00282cb6cae5fd9eaed0c7850f0c45b30efcc4 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 16:17:29 -0700 Subject: [PATCH 03/20] feat(relay): NIP-AD ingest, read gate, FTS exclusion Wires kinds 44300/44301 into relay ingest: MessagesWrite scope, global-only storage (no h tag), envelope validation (two p tags, agent tag, no h, NIP-44, author direction, single e tag for 44301), and is_agent_owner ownership check. Extends the ids-exemption in p_gated_filters_authorized to derive from RESULT_GATED_KINDS. Adds migration 0027 + schema.sql to exclude both kinds from FTS. count.rs and bridge.rs inherit the gate via RESULT_GATED_KINDS / reader_authorized_for_event. Closes BrianInAz/buzz#18 (part 3/12) Signed-off-by: Brian Charbonneau --- crates/buzz-db/src/migration.rs | 15 +- crates/buzz-relay/src/handlers/ingest.rs | 523 ++++++++++++++++++++++- crates/buzz-relay/src/handlers/req.rs | 84 +++- migrations/0027_agent_draft_fts.sql | 34 ++ schema/schema.sql | 4 +- 5 files changed, 636 insertions(+), 24 deletions(-) create mode 100644 migrations/0027_agent_draft_fts.sql diff --git a/crates/buzz-db/src/migration.rs b/crates/buzz-db/src/migration.rs index 6985916bba..8d276f2e3c 100644 --- a/crates/buzz-db/src/migration.rs +++ b/crates/buzz-db/src/migration.rs @@ -561,7 +561,7 @@ mod tests { let mut migrations: Vec<_> = MIGRATOR.iter().collect(); migrations.sort_by_key(|migration| migration.version); - assert_eq!(migrations.len(), 26); + assert_eq!(migrations.len(), 27); assert_eq!(migrations[0].version, 1); assert_eq!(&*migrations[0].description, "initial schema"); assert!(migrations[0] @@ -919,6 +919,19 @@ mod tests { assert!(heartbeat.contains("epoch")); assert!(heartbeat.contains("INSERT INTO replica_heartbeat (id) VALUES (1)")); assert!(heartbeat.contains("_operator_global_tables")); + + // NIP-AD (kinds 44300/44301) FTS exclusion: additive migration, never + // folded into 0001 — same brownfield checksum rule as 0005/0014. It + // captures the current generated expression and wraps it with the new + // exclusion, so 0001 must NOT carry 44300/44301. + assert_eq!(migrations[26].version, 27); + let agent_draft_fts = migrations[26].sql.as_str(); + assert!(agent_draft_fts.contains("search_tsv")); + assert!(agent_draft_fts.contains("44300")); + assert!(agent_draft_fts.contains("44301")); + assert!(agent_draft_fts.contains("pg_get_expr")); + assert!(!migrations[0].sql.as_str().contains("44300")); + assert!(!migrations[0].sql.as_str().contains("44301")); } #[test] diff --git a/crates/buzz-relay/src/handlers/ingest.rs b/crates/buzz-relay/src/handlers/ingest.rs index fcd0d70728..2acd072101 100644 --- a/crates/buzz-relay/src/handlers/ingest.rs +++ b/crates/buzz-relay/src/handlers/ingest.rs @@ -12,14 +12,15 @@ use uuid::Uuid; use buzz_auth::Scope; use buzz_core::kind::{ event_kind_u32, is_identity_archive_request_kind, is_parameterized_replaceable, - is_relay_admin_kind, KIND_AGENT_ENGRAM, KIND_AGENT_PROFILE, KIND_AGENT_TURN_METRIC, - KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_AUTH, KIND_BOOKMARK_LIST, KIND_BOOKMARK_SET, - KIND_CANVAS, KIND_CONTACT_LIST, KIND_DELETION, KIND_DM_ADD_MEMBER, KIND_DM_HIDE, KIND_DM_OPEN, - KIND_EMOJI_LIST, KIND_EMOJI_SET, KIND_EVENT_REMINDER, KIND_FOLLOW_SET, KIND_FORUM_COMMENT, - KIND_FORUM_POST, KIND_FORUM_VOTE, KIND_GIFT_WRAP, KIND_GIT_ISSUE, KIND_GIT_PATCH, - KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_REPO_STATE, - KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, - KIND_HUDDLE_ENDED, KIND_HUDDLE_GUIDELINES, KIND_HUDDLE_PARTICIPANT_JOINED, + is_relay_admin_kind, KIND_AGENT_DRAFT_REQUEST, KIND_AGENT_DRAFT_RESOLUTION, KIND_AGENT_ENGRAM, + KIND_AGENT_PROFILE, KIND_AGENT_TURN_METRIC, KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_AUTH, + KIND_BOOKMARK_LIST, KIND_BOOKMARK_SET, KIND_CANVAS, KIND_CONTACT_LIST, KIND_DELETION, + KIND_DM_ADD_MEMBER, KIND_DM_HIDE, KIND_DM_OPEN, KIND_EMOJI_LIST, KIND_EMOJI_SET, + KIND_EVENT_REMINDER, KIND_FOLLOW_SET, KIND_FORUM_COMMENT, KIND_FORUM_POST, KIND_FORUM_VOTE, + KIND_GIFT_WRAP, KIND_GIT_ISSUE, KIND_GIT_PATCH, KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, + KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_REPO_STATE, KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, + KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, KIND_HUDDLE_ENDED, KIND_HUDDLE_GUIDELINES, + KIND_HUDDLE_PARTICIPANT_JOINED, KIND_HUDDLE_PARTICIPANT_LEFT, KIND_HUDDLE_STARTED, KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST, KIND_LONG_FORM, KIND_MANAGED_AGENT, KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_MODERATION_BAN, KIND_MODERATION_RESOLVE_REPORT, @@ -219,6 +220,9 @@ fn required_scope_for_kind(kind: u32, event: &Event) -> Result Ok(Scope::MessagesWrite), + // NIP-AD: agent draft requests/resolutions are agent/owner-authored + // global events (encrypted to the counterparty). + KIND_AGENT_DRAFT_REQUEST | KIND_AGENT_DRAFT_RESOLUTION => Ok(Scope::MessagesWrite), // NIP-56 reports are ordinary member writes into the mod-only queue. // Ingest persists them to `moderation_reports` and suppresses public // storage/fanout; reports are signals, never enforcement triggers. @@ -468,6 +472,10 @@ pub(crate) fn is_global_only_kind(kind: u32) -> bool { // NIP-AM: agent turn metrics are owner-scoped global events. // Channel identity is encrypted inside the payload — no `h` tag. | KIND_AGENT_TURN_METRIC + // NIP-AD: agent drafts are owner/agent-scoped global events. + // Channel identity is encrypted inside the payload — no `h` tag. + | KIND_AGENT_DRAFT_REQUEST + | KIND_AGENT_DRAFT_RESOLUTION // NIP-PL leases are author-owned, addressable global state. | super::push_lease::KIND_PUSH_LEASE ) @@ -1592,6 +1600,147 @@ fn validate_agent_turn_metric_envelope(event: &nostr::Event) -> Result<(), Strin Ok(()) } +/// Shared NIP-AD envelope checks for kinds 44300/44301. +/// +/// Validates (without touching the encrypted payload): +/// - No `h` tag (channel identity belongs inside the encrypted payload). +/// - Exactly two `p` tags, both 64 lowercase hex, forming the set `{owner, agent}` +/// with `owner != agent`. +/// - Exactly one `agent` tag, 64 lowercase hex, equal to one of the two `p` tags. +/// - Content syntactically resembles NIP-44 v2 ciphertext (delegated to +/// `validate_engram_nip44_content`). +/// +/// Returns `(owner_hex, agent_hex)` where `agent_hex` is the `agent` tag value +/// and `owner_hex` is the `p` tag that is not the agent. Ownership +/// (`is_agent_owner`) and the author-direction check are performed by the +/// per-kind validators and the async DB check in `ingest_event_inner`. +fn validate_agent_draft_common_envelope( + event: &nostr::Event, +) -> Result<(String, String), String> { + let mut p_tags: Vec<&str> = Vec::new(); + let mut agent_tags: Vec<&str> = Vec::new(); + let mut has_h_tag = false; + + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() < 2 { + continue; + } + match parts[0].as_str() { + "p" => p_tags.push(&parts[1]), + "agent" => agent_tags.push(&parts[1]), + "h" => has_h_tag = true, + _ => {} + } + } + + if has_h_tag { + return Err( + "agent-draft event must not have an `h` tag (channel identity belongs inside the encrypted payload)".to_string(), + ); + } + + if p_tags.len() != 2 { + return Err(format!( + "agent-draft event must have exactly two `p` tags (got {})", + p_tags.len() + )); + } + for p in &p_tags { + if p.len() != 64 + || !p + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + { + return Err("agent-draft `p` tag must be 64 lowercase hex chars".to_string()); + } + } + if p_tags[0] == p_tags[1] { + return Err("agent-draft `p` tags must be distinct (owner != agent)".to_string()); + } + + if agent_tags.len() != 1 { + return Err(format!( + "agent-draft event must have exactly one `agent` tag (got {})", + agent_tags.len() + )); + } + let agent = agent_tags[0]; + if agent.len() != 64 + || !agent + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + { + return Err("agent-draft `agent` tag must be 64 lowercase hex chars".to_string()); + } + if agent != p_tags[0] && agent != p_tags[1] { + return Err("agent-draft `agent` tag must be one of the `p` tags".to_string()); + } + + // The owner is the `p` tag that is not the agent. + let owner = if p_tags[0] == agent { p_tags[1] } else { p_tags[0] }; + + // Content must look like a NIP-44 v2 ciphertext (length, base64, version prefix). + validate_engram_nip44_content(&event.content) + .map_err(|e| e.replace("agent-engram", "agent-draft"))?; + + Ok((owner.to_string(), agent.to_string())) +} + +/// Enforces the NIP-AD kind 44300 envelope (agent → owner draft request). +/// +/// In addition to the shared checks, the authoring pubkey MUST equal the +/// `agent` tag (the agent authors the request). +fn validate_agent_draft_request_envelope(event: &nostr::Event) -> Result<(), String> { + let (owner_hex, agent_hex) = validate_agent_draft_common_envelope(event)?; + if event.pubkey.to_hex() != agent_hex { + return Err( + "agent-draft-request event must be authored by the agent (event pubkey == agent tag)" + .to_string(), + ); + } + let _ = owner_hex; + Ok(()) +} + +/// Enforces the NIP-AD kind 44301 envelope (owner → agent draft resolution). +/// +/// In addition to the shared checks, the authoring pubkey MUST equal the owner +/// (the `p` tag that is not the agent), and there MUST be exactly one `e` tag +/// of 64 lowercase hex (the request event id). +fn validate_agent_draft_resolution_envelope(event: &nostr::Event) -> Result<(), String> { + let (owner_hex, _agent_hex) = validate_agent_draft_common_envelope(event)?; + if event.pubkey.to_hex() != owner_hex { + return Err( + "agent-draft-resolution event must be authored by the owner (event pubkey == owner p tag)" + .to_string(), + ); + } + + let mut e_tags: Vec<&str> = Vec::new(); + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "e" { + e_tags.push(&parts[1]); + } + } + if e_tags.len() != 1 { + return Err(format!( + "agent-draft-resolution event must have exactly one `e` tag (got {})", + e_tags.len() + )); + } + let e = e_tags[0]; + if e.len() != 64 + || !e + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + { + return Err("agent-draft-resolution `e` tag must be 64 lowercase hex chars".to_string()); + } + Ok(()) +} + /// Parse a NIP-ER `not_before` tag value into a Unix timestamp. /// /// The value MUST be a decimal integer string containing only ASCII digits, with @@ -2400,6 +2549,60 @@ async fn ingest_event_inner( } } + if kind_u32 == KIND_AGENT_DRAFT_REQUEST || kind_u32 == KIND_AGENT_DRAFT_RESOLUTION { + if kind_u32 == KIND_AGENT_DRAFT_REQUEST { + validate_agent_draft_request_envelope(&event) + .map_err(|e| IngestError::Rejected(format!("invalid: {e}")))?; + } else { + validate_agent_draft_resolution_envelope(&event) + .map_err(|e| IngestError::Rejected(format!("invalid: {e}")))?; + } + + // Ownership check: the owner `p` tag (the one that is not the agent) + // must be the registered owner of the agent. Tag shape is already + // verified above; these extractions are infallible. + let agent_hex = event + .tags + .iter() + .find_map(|t| { + let parts = t.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "agent" { + Some(parts[1].as_str()) + } else { + None + } + }) + .expect("agent tag present (validated above)"); + let owner_hex = event + .tags + .iter() + .find_map(|t| { + let parts = t.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "p" && parts[1].as_str() != agent_hex { + Some(parts[1].as_str()) + } else { + None + } + }) + .expect("owner p tag present (validated above)"); + let agent_bytes = hex::decode(agent_hex).expect("hex validated above"); + let owner_bytes = hex::decode(owner_hex).expect("hex validated above"); + let is_owner = state + .db + .is_agent_owner(tenant.community(), &agent_bytes, &owner_bytes) + .await + .map_err(|e| { + IngestError::Internal(format!( + "error: db error checking agent-draft ownership: {e}" + )) + })?; + if !is_owner { + return Err(IngestError::AuthFailed( + "restricted: agent draft is not authorized for this agent owner".into(), + )); + } + } + if kind_u32 == KIND_EVENT_REMINDER { validate_event_reminder(&event) .map_err(|e| IngestError::Rejected(format!("invalid: {e}")))?; @@ -3267,6 +3470,8 @@ mod tests { KIND_TEAM, KIND_MANAGED_AGENT, KIND_AGENT_TURN_METRIC, + KIND_AGENT_DRAFT_REQUEST, + KIND_AGENT_DRAFT_RESOLUTION, ]; for kind in migrated { assert!( @@ -3313,6 +3518,26 @@ mod tests { ); } + #[test] + fn agent_draft_kinds_are_global_only_and_in_scope_allowlist() { + let dummy = make_dummy_event(); + for kind in [KIND_AGENT_DRAFT_REQUEST, KIND_AGENT_DRAFT_RESOLUTION] { + assert!( + is_global_only_kind(kind), + "kind:{kind} must be global-only (no h tag)" + ); + assert!( + !requires_h_channel_scope(kind), + "kind:{kind} must not require an h-tag" + ); + assert_eq!( + required_scope_for_kind(kind, &dummy).unwrap(), + Scope::MessagesWrite, + "kind:{kind} requires MessagesWrite scope" + ); + } + } + #[test] fn nip51_and_nip65_lists_are_global_only() { for kind in [ @@ -4737,6 +4962,288 @@ mod tests { assert!(err.contains("agent-turn-metric"), "got: {err}"); } + // ── NIP-AD draft envelope tests ───────────────────────────────────────── + + fn make_agent_draft( + signer: &nostr::Keys, + kind: u32, + tags: Vec, + content: &str, + ) -> nostr::Event { + nostr::EventBuilder::new(nostr::Kind::Custom(kind as u16), content) + .tags(tags) + // The author's own pubkey is one of the two `p` tags; nostr's + // EventBuilder discards self-`p`-tags unless self-tagging is allowed. + .allow_self_tagging() + .sign_with_keys(signer) + .unwrap() + } + + fn canonical_request_tags(agent: &nostr::Keys, owner_hex: &str) -> Vec { + let agent_hex = agent.public_key().to_hex(); + vec![ + nostr::Tag::parse(["p", owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + ] + } + + fn canonical_resolution_tags(owner: &nostr::Keys, agent_hex: &str) -> Vec { + let owner_hex = owner.public_key().to_hex(); + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", agent_hex]).unwrap(), + nostr::Tag::parse(["agent", agent_hex]).unwrap(), + nostr::Tag::parse([ + "e", + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + ]) + .unwrap(), + ] + } + + #[test] + fn agent_draft_request_envelope_accepts_canonical() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let tags = canonical_request_tags(&agent, &owner_hex); + let ev = make_agent_draft(&agent, KIND_AGENT_DRAFT_REQUEST, tags, &fake_nip44_v2()); + assert!(validate_agent_draft_request_envelope(&ev).is_ok()); + } + + #[test] + fn agent_draft_request_envelope_rejects_wrong_p_cardinality() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let agent_hex = agent.public_key().to_hex(); + // Only one `p` tag. + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("two `p` tags"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_uppercase_hex() { + let agent = nostr::Keys::generate(); + let owner_hex = "B".repeat(64); // uppercase + let tags = canonical_request_tags(&agent, &owner_hex); + let ev = make_agent_draft(&agent, KIND_AGENT_DRAFT_REQUEST, tags, &fake_nip44_v2()); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("lowercase hex"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_owner_equals_agent() { + let agent = nostr::Keys::generate(); + let agent_hex = agent.public_key().to_hex(); + // Both `p` tags are the agent's own pubkey. + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("distinct"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_missing_agent_tag() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let agent_hex = agent.public_key().to_hex(); + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("`agent` tag"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_duplicate_agent_tag() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let agent_hex = agent.public_key().to_hex(); + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("`agent` tag"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_mismatched_agent_tag() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let agent_hex = agent.public_key().to_hex(); + let stray_hex = "c".repeat(64); // not event.pubkey and not a `p` tag + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &stray_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("one of the `p` tags"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_h_tag() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let agent_hex = agent.public_key().to_hex(); + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + nostr::Tag::parse(["h", "some-channel-uuid"]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("`h` tag"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_bad_content() { + let agent = nostr::Keys::generate(); + let owner_hex = "b".repeat(64); + let tags = canonical_request_tags(&agent, &owner_hex); + let ev = make_agent_draft(&agent, KIND_AGENT_DRAFT_REQUEST, tags, "not-a-ciphertext"); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("agent-draft"), "got: {err}"); + } + + #[test] + fn agent_draft_request_envelope_rejects_wrong_author() { + // Signed by the owner, not the agent — event.pubkey != agent tag. + let owner = nostr::Keys::generate(); + let agent = nostr::Keys::generate(); + let owner_hex = owner.public_key().to_hex(); + let agent_hex = agent.public_key().to_hex(); + let ev = make_agent_draft( + &owner, + KIND_AGENT_DRAFT_REQUEST, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_request_envelope(&ev).unwrap_err(); + assert!(err.contains("authored by the agent"), "got: {err}"); + } + + #[test] + fn agent_draft_resolution_envelope_accepts_canonical() { + let owner = nostr::Keys::generate(); + let agent_hex = "c".repeat(64); + let tags = canonical_resolution_tags(&owner, &agent_hex); + let ev = make_agent_draft(&owner, KIND_AGENT_DRAFT_RESOLUTION, tags, &fake_nip44_v2()); + assert!(validate_agent_draft_resolution_envelope(&ev).is_ok()); + } + + #[test] + fn agent_draft_resolution_envelope_rejects_missing_e_tag() { + let owner = nostr::Keys::generate(); + let agent_hex = "c".repeat(64); + let owner_hex = owner.public_key().to_hex(); + let ev = make_agent_draft( + &owner, + KIND_AGENT_DRAFT_RESOLUTION, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_resolution_envelope(&ev).unwrap_err(); + assert!(err.contains("`e` tag"), "got: {err}"); + } + + #[test] + fn agent_draft_resolution_envelope_rejects_duplicate_e_tag() { + let owner = nostr::Keys::generate(); + let agent_hex = "c".repeat(64); + let owner_hex = owner.public_key().to_hex(); + let e = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; + let ev = make_agent_draft( + &owner, + KIND_AGENT_DRAFT_RESOLUTION, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + nostr::Tag::parse(["e", e]).unwrap(), + nostr::Tag::parse(["e", e]).unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_resolution_envelope(&ev).unwrap_err(); + assert!(err.contains("`e` tag"), "got: {err}"); + } + + #[test] + fn agent_draft_resolution_envelope_rejects_wrong_author() { + // Signed by the agent, not the owner — event.pubkey != owner p tag. + let owner = nostr::Keys::generate(); + let agent = nostr::Keys::generate(); + let owner_hex = owner.public_key().to_hex(); + let agent_hex = agent.public_key().to_hex(); + let ev = make_agent_draft( + &agent, + KIND_AGENT_DRAFT_RESOLUTION, + vec![ + nostr::Tag::parse(["p", &owner_hex]).unwrap(), + nostr::Tag::parse(["p", &agent_hex]).unwrap(), + nostr::Tag::parse(["agent", &agent_hex]).unwrap(), + nostr::Tag::parse([ + "e", + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + ]) + .unwrap(), + ], + &fake_nip44_v2(), + ); + let err = validate_agent_draft_resolution_envelope(&ev).unwrap_err(); + assert!(err.contains("authored by the owner"), "got: {err}"); + } + /// The HTTP bridge's `submit_event` 400 arm and the WS `EVENT` handler's /// reject path must land on the same counter, distinguished only by the /// `transport` label — this is what lets a dashboard tell "server got diff --git a/crates/buzz-relay/src/handlers/req.rs b/crates/buzz-relay/src/handlers/req.rs index 2aed12cd7f..7e81d69064 100644 --- a/crates/buzz-relay/src/handlers/req.rs +++ b/crates/buzz-relay/src/handlers/req.rs @@ -7,8 +7,8 @@ use tracing::{debug, warn}; use buzz_core::filter::filters_match; use buzz_core::kind::{ - is_unshared_gated_event, AUTHOR_ONLY_KINDS, KIND_AGENT_ENGRAM, KIND_AGENT_TURN_METRIC, - KIND_DM_VISIBILITY, P_GATED_KINDS, RESULT_GATED_KINDS, SHARED_GATED_KINDS, + is_unshared_gated_event, AUTHOR_ONLY_KINDS, KIND_AGENT_ENGRAM, P_GATED_KINDS, + RESULT_GATED_KINDS, SHARED_GATED_KINDS, }; use buzz_core::tenant::TenantContext; use buzz_db::EventQuery; @@ -1061,18 +1061,18 @@ pub(crate) fn p_gated_filters_authorized(filters: &[Filter], authed_pubkey_hex: // The `ids` exemption ("knowing the id implies authorization") is only // safe for kinds whose id is author-bound or whose content is encrypted. - // KIND_DM_VISIBILITY is relay-signed (id not author-bound) and exposes - // plaintext private hide choices, so its `#p` owner check MUST hold even - // when `ids` is present. KIND_AGENT_TURN_METRIC events are long-lived - // and their cleartext envelope (pubkey, agent tag, created_at) leaks - // turn-activity metadata — knowing an event id is NOT authorization - // (NIP-AM §Relay Behavior). Only filters that explicitly name the kind - // lose the exemption — a kindless `ids` lookup is unaffected. + // Result-gated kinds (KIND_DM_VISIBILITY, KIND_AGENT_TURN_METRIC, and + // the NIP-AD draft kinds) are long-lived and their cleartext envelope + // leaks private metadata — knowing an event id is NOT authorization + // (NIP-AM/NIP-AD §Relay Behavior). Only filters that explicitly name a + // result-gated kind lose the exemption — a kindless `ids` lookup is + // unaffected (it is closed at the result level by + // `reader_authorized_for_event`). The constant is the single source of + // truth: a kind added to RESULT_GATED_KINDS inherits this without a new + // branch here. let explicitly_no_ids_exemption = filter.kinds.as_ref().is_some_and(|ks| { - ks.iter().any(|kind| { - let k = kind.as_u16() as u32; - k == KIND_DM_VISIBILITY || k == KIND_AGENT_TURN_METRIC - }) + ks.iter() + .any(|kind| RESULT_GATED_KINDS.contains(&(kind.as_u16() as u32))) }); if !explicitly_no_ids_exemption && filter.ids.as_ref().is_some_and(|ids| !ids.is_empty()) { return true; @@ -1680,6 +1680,48 @@ mod tests { ); } + /// NIP-AD: kinds 44300/44301 must deny `{kinds:[...], ids:[...]}` by a + /// non-owner, mirroring the NIP-AM 44200 rule. + #[test] + fn agent_draft_kinds_require_p_tag_even_with_ids() { + let p_tag = SingleLetterTag::lowercase(Alphabet::P); + let authed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let other = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + let event_id = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + + for kind in [ + buzz_core::kind::KIND_AGENT_DRAFT_REQUEST, + buzz_core::kind::KIND_AGENT_DRAFT_RESOLUTION, + ] { + let k = nostr::Kind::Custom(kind as u16); + + // {kinds:[k], ids:[...]} — explicit kind, requires #p. + let explicit_kind_ids_only = Filter::new() + .kind(k) + .id(nostr::EventId::from_hex(event_id).unwrap()); + assert!( + !p_gated_filters_authorized(&[explicit_kind_ids_only], authed), + "kind:{kind} + ids without matching #p must be denied" + ); + + let explicit_kind_wrong_p = Filter::new() + .kind(k) + .id(nostr::EventId::from_hex(event_id).unwrap()) + .custom_tags(p_tag, [other]); + assert!( + !p_gated_filters_authorized(&[explicit_kind_wrong_p], authed), + "kind:{kind} + ids + wrong #p must be denied" + ); + + // Owner querying by #p is allowed. + let owner_by_p = Filter::new().kind(k).custom_tags(p_tag, [authed]); + assert!( + p_gated_filters_authorized(&[owner_by_p], authed), + "kind:{kind} with matching #p must be allowed" + ); + } + } + #[test] fn test_mixed_search_and_non_search_detection() { let search_filter = Filter::new().search("hello"); @@ -2031,6 +2073,22 @@ mod tests { assert!(filter_can_match_result_gated_kinds(&f)); } + #[test] + fn result_gated_explicit_44300_can_match() { + let f = Filter::new().kind(nostr::Kind::Custom( + buzz_core::kind::KIND_AGENT_DRAFT_REQUEST as u16, + )); + assert!(filter_can_match_result_gated_kinds(&f)); + } + + #[test] + fn result_gated_explicit_44301_can_match() { + let f = Filter::new().kind(nostr::Kind::Custom( + buzz_core::kind::KIND_AGENT_DRAFT_RESOLUTION as u16, + )); + assert!(filter_can_match_result_gated_kinds(&f)); + } + #[test] fn result_gated_explicit_30622_can_match() { let f = Filter::new().kind(nostr::Kind::Custom( diff --git a/migrations/0027_agent_draft_fts.sql b/migrations/0027_agent_draft_fts.sql new file mode 100644 index 0000000000..8ffaf995c0 --- /dev/null +++ b/migrations/0027_agent_draft_fts.sql @@ -0,0 +1,34 @@ +-- NIP-AD kinds 44300/44301 contain NIP-44 ciphertext and are p-gated. Exclude +-- them from full-text search without changing the search policy of existing +-- installations. In particular, migration 0008 deliberately gives only +-- empty/fresh databases the positive allowlist; populated databases retain +-- their prior expression until an operator runs the out-of-band rewrite. +-- +-- PostgreSQL cannot alter a generated expression in place. Capture the current +-- expression before replacing the column, then wrap it with the new exclusion. +-- This preserves both the fresh-install allowlist and any brownfield/operator- +-- managed expression for every kind other than 44300/44301. +DO $$ +DECLARE + existing_expression TEXT; +BEGIN + SELECT pg_get_expr(d.adbin, d.adrelid) + INTO existing_expression + FROM pg_attrdef d + JOIN pg_attribute a + ON a.attrelid = d.adrelid + AND a.attnum = d.adnum + WHERE d.adrelid = 'events'::regclass + AND a.attname = 'search_tsv'; + + IF existing_expression IS NULL THEN + RAISE EXCEPTION 'events.search_tsv generated expression not found'; + END IF; + + ALTER TABLE events DROP COLUMN search_tsv; + EXECUTE format( + 'ALTER TABLE events ADD COLUMN search_tsv TSVECTOR GENERATED ALWAYS AS (CASE WHEN kind IN (44300, 44301) THEN NULL::tsvector ELSE (%s) END) STORED', + existing_expression + ); + CREATE INDEX idx_events_search_tsv ON events USING GIN (search_tsv); +END $$; diff --git a/schema/schema.sql b/schema/schema.sql index 3c64729367..d0eb167da5 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -210,9 +210,9 @@ CREATE TABLE events ( -- Privacy: encrypted/private routing wrappers and p-gated membership notices -- must never be discoverable through NIP-50 full-text search. NULL tsvector -- never matches `@@`. - -- Keep in sync with migrations (final state: 0001 + 0005 + 0009). + -- Keep in sync with migrations (final state: 0001 + 0005 + 0009 + 0027). search_tsv TSVECTOR GENERATED ALWAYS AS ( - CASE WHEN kind IN (1059, 30300, 30350, 30622, 44100, 44101, 44200) THEN NULL::tsvector + CASE WHEN kind IN (1059, 30300, 30350, 30622, 44100, 44101, 44200, 44300, 44301) THEN NULL::tsvector ELSE to_tsvector('simple', content) END ) STORED, From fac7b6cd42d75166d616f81f546cb3c5d5e1a8ee Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 16:19:03 -0700 Subject: [PATCH 04/20] feat(sdk): NIP-AD draft request/resolution builders Adds build_agent_draft_request (44300) and build_agent_draft_resolution (44301) with the two-p-tag + agent-tag (+ e-tag for resolution) envelope, NIP-44 content check, owner!=agent and hex validation, and allow_self_tagging so the author's own pubkey survives as a p tag. Closes BrianInAz/buzz#18 (part 4/12) Signed-off-by: Brian Charbonneau --- crates/buzz-sdk/src/builders.rs | 214 ++++++++++++++++++++++++++++++-- 1 file changed, 206 insertions(+), 8 deletions(-) diff --git a/crates/buzz-sdk/src/builders.rs b/crates/buzz-sdk/src/builders.rs index 9a139f0377..39b64ef313 100644 --- a/crates/buzz-sdk/src/builders.rs +++ b/crates/buzz-sdk/src/builders.rs @@ -5,14 +5,15 @@ use buzz_core::{ kind::{ - KIND_AGENT_OBSERVER_FRAME, KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_DELETION, - KIND_DM_ADD_MEMBER, KIND_DM_OPEN, KIND_EMOJI_SET, KIND_GIT_ISSUE, KIND_GIT_PATCH, - KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, - KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, - KIND_GIT_STATUS_OPEN, KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST, - KIND_MODERATION_BAN, KIND_MODERATION_RESOLVE_REPORT, KIND_MODERATION_TIMEOUT, - KIND_MODERATION_UNBAN, KIND_MODERATION_UNTIMEOUT, KIND_PRESENCE_UPDATE, KIND_PROJECT, - KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, + KIND_AGENT_DRAFT_REQUEST, KIND_AGENT_DRAFT_RESOLUTION, KIND_AGENT_OBSERVER_FRAME, + KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_DELETION, KIND_DM_ADD_MEMBER, KIND_DM_OPEN, + KIND_EMOJI_SET, KIND_GIT_ISSUE, KIND_GIT_PATCH, KIND_GIT_PR_UPDATE, + KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_STATUS_CLOSED, + KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, + KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST, KIND_MODERATION_BAN, + KIND_MODERATION_RESOLVE_REPORT, KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN, + KIND_MODERATION_UNTIMEOUT, KIND_PRESENCE_UPDATE, KIND_PROJECT, KIND_USER_STATUS, + KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, }, observer::{ content_looks_like_nip44, OBSERVER_AGENT_TAG, OBSERVER_FRAME_CONTROL, OBSERVER_FRAME_TAG, @@ -274,6 +275,83 @@ pub fn build_agent_observer_frame( .tags(tags)) } +/// Build a NIP-AD agent draft request (kind 44300, agent → owner). +/// +/// `encrypted_content` must be NIP-44 v2 ciphertext (agent seckey → owner +/// pubkey). The envelope carries two `p` tags (owner + agent, `owner != agent`) +/// and one `agent` tag (the agent pubkey), with no `h` tag. The builder enables +/// self-tagging so the agent's own pubkey survives as a `p` tag (nostr's +/// `EventBuilder` discards self-`p`-tags by default). See `docs/nips/NIP-AD.md`. +pub fn build_agent_draft_request( + owner_pubkey: &str, + agent_pubkey: &str, + encrypted_content: &str, +) -> Result { + if !content_looks_like_nip44(encrypted_content) { + return Err(SdkError::InvalidInput( + "agent draft request content must be NIP-44 v2 ciphertext".into(), + )); + } + let owner_pubkey = check_pubkey_hex(owner_pubkey, "owner_pubkey")?; + let agent_pubkey = check_pubkey_hex(agent_pubkey, "agent_pubkey")?; + if owner_pubkey == agent_pubkey { + return Err(SdkError::InvalidInput( + "agent draft request owner and agent must differ".into(), + )); + } + let tags = vec![ + tag(&["p", &owner_pubkey])?, + tag(&["p", &agent_pubkey])?, + tag(&[OBSERVER_AGENT_TAG, &agent_pubkey])?, + ]; + Ok(EventBuilder::new( + Kind::Custom(KIND_AGENT_DRAFT_REQUEST as u16), + encrypted_content, + ) + .tags(tags) + .allow_self_tagging()) +} + +/// Build a NIP-AD agent draft resolution (kind 44301, owner → agent). +/// +/// `encrypted_content` must be NIP-44 v2 ciphertext (owner seckey → agent +/// pubkey). The envelope carries two `p` tags (owner + agent, `owner != agent`), +/// one `agent` tag (the agent pubkey), and one `e` tag (the request event id), +/// with no `h` tag. The builder enables self-tagging so the owner's own pubkey +/// survives as a `p` tag. See `docs/nips/NIP-AD.md`. +pub fn build_agent_draft_resolution( + owner_pubkey: &str, + agent_pubkey: &str, + request_event_id: &str, + encrypted_content: &str, +) -> Result { + if !content_looks_like_nip44(encrypted_content) { + return Err(SdkError::InvalidInput( + "agent draft resolution content must be NIP-44 v2 ciphertext".into(), + )); + } + let owner_pubkey = check_pubkey_hex(owner_pubkey, "owner_pubkey")?; + let agent_pubkey = check_pubkey_hex(agent_pubkey, "agent_pubkey")?; + if owner_pubkey == agent_pubkey { + return Err(SdkError::InvalidInput( + "agent draft resolution owner and agent must differ".into(), + )); + } + let request_event_id = check_hex_exact(request_event_id, 64, "request_event_id")?; + let tags = vec![ + tag(&["p", &owner_pubkey])?, + tag(&["p", &agent_pubkey])?, + tag(&[OBSERVER_AGENT_TAG, &agent_pubkey])?, + tag(&["e", &request_event_id])?, + ]; + Ok(EventBuilder::new( + Kind::Custom(KIND_AGENT_DRAFT_RESOLUTION as u16), + encrypted_content, + ) + .tags(tags) + .allow_self_tagging()) +} + /// Build a forum post thread root (kind 45001). pub fn build_forum_post( channel_id: Uuid, @@ -2292,6 +2370,126 @@ mod tests { assert!(matches!(err, SdkError::InvalidInput(_))); } + // ── NIP-AD draft builders ─────────────────────────────────────────────── + + fn fake_nip44() -> String { + // base64(b"\x02" + b"\x00" * 98) — 132 chars, decoded length 99. + let mut s = String::from("Ag"); + s.push_str(&"A".repeat(130)); + s + } + + #[test] + fn agent_draft_request_happy_path() { + let owner = keys(); + let agent = keys(); + let encrypted = fake_nip44(); + let ev = sign( + build_agent_draft_request( + &owner.public_key().to_hex(), + &agent.public_key().to_hex(), + &encrypted, + ) + .unwrap(), + ); + + assert_eq!(ev.kind.as_u16(), KIND_AGENT_DRAFT_REQUEST as u16); + assert_eq!(ev.content, encrypted); + // Exactly two `p` tags (owner + agent) and one `agent` tag, no `h`. + let p_tags = tag_values(&ev, "p"); + assert_eq!(p_tags.len(), 2, "must have exactly two p tags"); + assert!(p_tags.contains(&owner.public_key().to_hex())); + assert!(p_tags.contains(&agent.public_key().to_hex())); + assert_eq!( + tag_values(&ev, OBSERVER_AGENT_TAG), + vec![agent.public_key().to_hex()] + ); + assert!(!has_tag(&ev, "h", "")); + assert!(!ev.tags.iter().any(|t| t.as_slice().first().map(|v| v.as_str()) == Some("h"))); + } + + #[test] + fn agent_draft_resolution_happy_path() { + let owner = keys(); + let agent = keys(); + let encrypted = fake_nip44(); + let request_event_id = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; + let ev = sign( + build_agent_draft_resolution( + &owner.public_key().to_hex(), + &agent.public_key().to_hex(), + request_event_id, + &encrypted, + ) + .unwrap(), + ); + + assert_eq!(ev.kind.as_u16(), KIND_AGENT_DRAFT_RESOLUTION as u16); + assert_eq!(ev.content, encrypted); + let p_tags = tag_values(&ev, "p"); + assert_eq!(p_tags.len(), 2, "must have exactly two p tags"); + assert!(p_tags.contains(&owner.public_key().to_hex())); + assert!(p_tags.contains(&agent.public_key().to_hex())); + assert_eq!( + tag_values(&ev, OBSERVER_AGENT_TAG), + vec![agent.public_key().to_hex()] + ); + assert_eq!(tag_values(&ev, "e"), vec![request_event_id.to_string()]); + assert!(!ev.tags.iter().any(|t| t.as_slice().first().map(|v| v.as_str()) == Some("h"))); + } + + #[test] + fn agent_draft_request_rejects_plaintext_content() { + let err = build_agent_draft_request(&"a".repeat(64), &"b".repeat(64), "not encrypted") + .unwrap_err(); + assert!(matches!(err, SdkError::InvalidInput(_))); + } + + #[test] + fn agent_draft_resolution_rejects_plaintext_content() { + let err = build_agent_draft_resolution( + &"a".repeat(64), + &"b".repeat(64), + &"c".repeat(64), + "not encrypted", + ) + .unwrap_err(); + assert!(matches!(err, SdkError::InvalidInput(_))); + } + + #[test] + fn agent_draft_request_rejects_owner_equals_agent() { + let pk = "a".repeat(64); + let err = build_agent_draft_request(&pk, &pk, &fake_nip44()).unwrap_err(); + assert!(matches!(err, SdkError::InvalidInput(_))); + } + + #[test] + fn agent_draft_resolution_rejects_owner_equals_agent() { + let pk = "a".repeat(64); + let err = build_agent_draft_resolution(&pk, &pk, &"c".repeat(64), &fake_nip44()) + .unwrap_err(); + assert!(matches!(err, SdkError::InvalidInput(_))); + } + + #[test] + fn agent_draft_resolution_rejects_bad_event_id() { + let err = build_agent_draft_resolution( + &"a".repeat(64), + &"b".repeat(64), + "not-hex", + &fake_nip44(), + ) + .unwrap_err(); + assert!(matches!(err, SdkError::InvalidInput(_))); + } + + #[test] + fn agent_draft_request_rejects_bad_pubkey() { + let err = build_agent_draft_request("short", &"b".repeat(64), &fake_nip44()).unwrap_err(); + assert!(matches!(err, SdkError::InvalidInput(_))); + } + #[test] fn message_direct_reply() { let cid = uuid(); From 699c552eb2a9baa6ea4017ffb36ffb9c1ea0f794 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 16:26:40 -0700 Subject: [PATCH 05/20] feat(cli): durable NIP-AD draft publish + drafts list/status draft-create/draft-update now publish a durable kind 44300 via submit_event (was ephemeral kind 24200), building AgentDraftRequestPayload and returning event_id. Adds 'buzz agents drafts list|status' reading both 44300/44301, decrypting with the running key, and joining on requestId for pending-ness. Closes BrianInAz/buzz#18 (part 5/12) Signed-off-by: Brian Charbonneau --- crates/buzz-cli/TESTING.md | 30 +++ crates/buzz-cli/src/agent_management.rs | 187 ++++++++--------- crates/buzz-cli/src/commands/agent_drafts.rs | 209 +++++++++++++++++++ crates/buzz-cli/src/commands/agents.rs | 15 +- crates/buzz-cli/src/commands/mod.rs | 1 + crates/buzz-cli/src/lib.rs | 32 ++- 6 files changed, 367 insertions(+), 107 deletions(-) create mode 100644 crates/buzz-cli/src/commands/agent_drafts.rs diff --git a/crates/buzz-cli/TESTING.md b/crates/buzz-cli/TESTING.md index 77234b7faa..51ef7cc2ae 100644 --- a/crates/buzz-cli/TESTING.md +++ b/crates/buzz-cli/TESTING.md @@ -482,6 +482,36 @@ buzz notes get --name dco-check # exits non-zero: not found buzz notes rm --name does-not-exist # exits non-zero ``` +### 6.13 Agent Drafts (NIP-AD, kinds 44300/44301) + +`buzz agents draft-create` / `draft-update` publish a **durable** kind 44300 +request for owner review in Buzz Desktop (replacing the old ephemeral +kind-24200 path). `buzz agents drafts list|status` read them back. + +```bash +# Publish a create draft (requires BUZZ_AUTH_TAG; owner is the attested owner) +buzz agents draft-create --channel --display-name "dev-coder" \ + --system-prompt "You are a coding specialist." +# → {event_id, request_id, action, saved:false, message} + +# List pending drafts (default) — works as the agent or the owner +buzz agents drafts list --pending | jq . +# → [{request_id, event_id, action, channel_id, agent_pubkey, created_at, status}] + +# List all drafts including resolved ones +buzz agents drafts list --all | jq . + +# Filter to a single channel +buzz agents drafts list --channel | jq . + +# Status of a single draft +buzz agents drafts status --request-id | jq . +# → {request_id, event_id, action, channel_id, agent_pubkey, created_at, status, resolution?} + +# Unknown request id → exit 1 with {"error":"unknown request_id"} +buzz agents drafts status --request-id ; echo $? # 1 +``` + --- ## 7. Error Path Testing diff --git a/crates/buzz-cli/src/agent_management.rs b/crates/buzz-cli/src/agent_management.rs index ce4059f821..19c2f6e87c 100644 --- a/crates/buzz-cli/src/agent_management.rs +++ b/crates/buzz-cli/src/agent_management.rs @@ -1,68 +1,39 @@ -//! Owner-reviewed agent draft requests published through Buzz observer frames. +//! Owner-reviewed agent draft requests published as durable NIP-AD kind 44300. -use buzz_core::observer::{encrypt_observer_payload, OBSERVER_FRAME_TELEMETRY}; +use buzz_core::agent_draft::{ + encrypt_agent_draft_request, AgentDraftAction, AgentDraftCreateRequest, AgentDraftRequest, + AgentDraftRequestPayload, AgentDraftRespondTo, AgentDraftUpdateRequest, AGENT_DRAFT_VERSION, +}; use nostr::{Event, Keys, PublicKey}; -use serde::Serialize; use crate::error::CliError; -const REQUEST_KIND: &str = "agent_management_request"; const MAX_NAME_CHARS: usize = 120; const MAX_PROMPT_CHARS: usize = 20_000; -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub struct CreateAgentDraft { pub channel_id: String, pub display_name: String, pub system_prompt: String, } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub struct UpdateAgentDraft { pub channel_id: String, pub agent_name: String, - #[serde(skip_serializing_if = "Option::is_none")] pub display_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub system_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub runtime: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub provider: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub respond_to: Option, } -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ManagementRequest { - #[serde(rename = "type")] - request_type: &'static str, - action: &'static str, - request_id: String, - request: T, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ObserverEvent { - seq: u64, - timestamp: String, - kind: &'static str, - agent_index: Option, - channel_id: Option, - session_id: Option, - turn_id: Option, - payload: ManagementRequest, -} - #[derive(Debug)] pub struct BuiltDraftRequest { pub event: Event, + pub event_id: String, pub request_id: String, pub action: &'static str, } @@ -80,48 +51,46 @@ fn required(value: String, label: &str, max: usize) -> Result Ok(value.to_owned()) } -fn optional(value: Option, label: &str) -> Result, CliError> { - value.map(|value| required(value, label, 300)).transpose() +fn optional(value: Option, label: &str, max: usize) -> Result, CliError> { + value.map(|value| required(value, label, max)).transpose() } -fn build( +fn build( keys: &Keys, owner: &PublicKey, channel_id: String, - action: &'static str, - request: T, + action: AgentDraftAction, + request: AgentDraftRequest, ) -> Result { let request_id = uuid::Uuid::new_v4().to_string(); - let payload = ObserverEvent { - seq: 0, + let payload = AgentDraftRequestPayload { + version: AGENT_DRAFT_VERSION, + request_id: request_id.clone(), + action, timestamp: chrono::Utc::now().to_rfc3339(), - kind: REQUEST_KIND, - agent_index: None, - channel_id: Some(channel_id), - session_id: None, - turn_id: None, - payload: ManagementRequest { - request_type: REQUEST_KIND, - action, - request_id: request_id.clone(), - request, - }, + channel_id, + request, }; - let encrypted = encrypt_observer_payload(keys, owner, &payload) + let encrypted = encrypt_agent_draft_request(keys, owner, &payload) .map_err(|error| CliError::Other(format!("could not encrypt draft request: {error}")))?; - let event = buzz_sdk::build_agent_observer_frame( + let event = buzz_sdk::build_agent_draft_request( &owner.to_hex(), &keys.public_key().to_hex(), - OBSERVER_FRAME_TELEMETRY, &encrypted, ) .map_err(|error| CliError::Other(format!("could not build draft request: {error}")))? .sign_with_keys(keys) .map_err(|error| CliError::Other(format!("could not sign draft request: {error}")))?; + let event_id = event.id.to_hex(); + let action_str = match action { + AgentDraftAction::Create => "create", + AgentDraftAction::Update => "update", + }; Ok(BuiltDraftRequest { event, + event_id, request_id, - action, + action: action_str, }) } @@ -133,12 +102,11 @@ pub fn build_create( 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 request = CreateAgentDraft { - channel_id: channel_id.clone(), + let request = AgentDraftRequest::Create(AgentDraftCreateRequest { display_name: required(draft.display_name, "display name", MAX_NAME_CHARS)?, system_prompt: required(draft.system_prompt, "system prompt", MAX_PROMPT_CHARS)?, - }; - build(keys, owner, channel_id, "create", request) + }); + build(keys, owner, channel_id, AgentDraftAction::Create, request) } pub fn build_update( @@ -149,46 +117,54 @@ pub fn build_update( 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 respond_to = optional(draft.respond_to, "respond-to")?; - if respond_to - .as_deref() - .is_some_and(|value| value != "owner-only" && value != "anyone") - { - return Err(CliError::Usage( - "respond-to must be owner-only or anyone".into(), - )); - } - let request = UpdateAgentDraft { - channel_id: channel_id.clone(), + let respond_to = match draft.respond_to.as_deref() { + None => None, + Some("owner-only") => Some(AgentDraftRespondTo::OwnerOnly), + Some("anyone") => Some(AgentDraftRespondTo::Anyone), + Some(other) => { + return Err(CliError::Usage(format!( + "respond-to must be owner-only or anyone (got {other})" + ))) + } + }; + let request = AgentDraftRequest::Update(AgentDraftUpdateRequest { agent_name: required(draft.agent_name, "agent name", MAX_NAME_CHARS)?, - display_name: optional(draft.display_name, "display name")?, + display_name: optional(draft.display_name, "display name", MAX_NAME_CHARS)?, system_prompt: draft .system_prompt .map(|value| required(value, "system prompt", MAX_PROMPT_CHARS)) .transpose()?, - runtime: optional(draft.runtime, "runtime")?, - provider: optional(draft.provider, "provider")?, - model: optional(draft.model, "model")?, + runtime: optional(draft.runtime, "runtime", 300)?, + provider: optional(draft.provider, "provider", 300)?, + model: optional(draft.model, "model", 300)?, respond_to, - }; - if request.display_name.is_none() - && request.system_prompt.is_none() - && request.runtime.is_none() - && request.provider.is_none() - && request.model.is_none() - && request.respond_to.is_none() - { + }); + if request_has_no_change(&request) { return Err(CliError::Usage( "include at least one field to update".into(), )); } - build(keys, owner, channel_id, "update", request) + build(keys, owner, channel_id, AgentDraftAction::Update, request) +} + +fn request_has_no_change(request: &AgentDraftRequest) -> bool { + match request { + AgentDraftRequest::Update(u) => { + u.display_name.is_none() + && u.system_prompt.is_none() + && u.runtime.is_none() + && u.provider.is_none() + && u.model.is_none() + && u.respond_to.is_none() + } + AgentDraftRequest::Create(_) => false, + } } #[cfg(test)] mod tests { use super::*; - use buzz_core::observer::{decrypt_observer_payload, OBSERVER_AGENT_TAG, OBSERVER_FRAME_TAG}; + use buzz_core::agent_draft::decrypt_agent_draft_request; const CHANNEL: &str = "7c07e659-3610-42f4-9a5e-1e9973c09da9"; @@ -207,37 +183,44 @@ mod tests { ) .unwrap(); - assert_eq!(built.event.kind.as_u16(), 24_200); + assert_eq!(built.event.kind.as_u16(), 44_300); let tags: Vec> = built .event .tags .iter() .map(|tag| tag.as_slice().to_vec()) .collect(); + // Exactly two `p` tags (owner + agent) and one `agent` tag, no `h`. + let p_tags: Vec<&Vec> = tags + .iter() + .filter(|tag| tag.first().map(String::as_str) == Some("p")) + .collect(); + assert_eq!(p_tags.len(), 2, "must have exactly two p tags"); assert!(tags .iter() .any(|tag| tag == &["p", &owner.public_key().to_hex()])); assert!(tags .iter() - .any(|tag| tag == &[OBSERVER_AGENT_TAG, &agent.public_key().to_hex()])); + .any(|tag| tag == &["p", &agent.public_key().to_hex()])); assert!(tags .iter() - .any(|tag| tag == &[OBSERVER_FRAME_TAG, OBSERVER_FRAME_TELEMETRY])); + .any(|tag| tag == &["agent", &agent.public_key().to_hex()])); assert!(!tags .iter() .any(|tag| tag.first().map(String::as_str) == Some("h"))); - let payload: serde_json::Value = decrypt_observer_payload(&owner, &built.event).unwrap(); - assert_eq!(payload["kind"], REQUEST_KIND); - assert_eq!(payload["channelId"], CHANNEL); - assert_eq!(payload["payload"]["type"], REQUEST_KIND); - assert_eq!(payload["payload"]["action"], "create"); - assert_eq!( - payload["payload"]["request"]["displayName"], - "Research helper" - ); - assert!(payload["payload"]["request"].get("runtime").is_none()); - assert!(payload["payload"]["request"].get("respondTo").is_none()); + let payload = decrypt_agent_draft_request(&owner, &built.event).unwrap(); + assert_eq!(payload.version, 1); + assert_eq!(payload.request_id, built.request_id); + assert_eq!(payload.action, AgentDraftAction::Create); + assert_eq!(payload.channel_id, CHANNEL); + match payload.request { + AgentDraftRequest::Create(create) => { + assert_eq!(create.display_name, "Research helper"); + assert_eq!(create.system_prompt, "Find sources."); + } + AgentDraftRequest::Update(_) => panic!("expected create request"), + } } #[test] diff --git a/crates/buzz-cli/src/commands/agent_drafts.rs b/crates/buzz-cli/src/commands/agent_drafts.rs new file mode 100644 index 0000000000..a4f319466a --- /dev/null +++ b/crates/buzz-cli/src/commands/agent_drafts.rs @@ -0,0 +1,209 @@ +//! `buzz agents drafts` — list and inspect durable NIP-AD agent drafts. +//! +//! Both `list` and `status` work for either side of the agent↔owner +//! relationship because of the two-`p`-tag design: run as the agent it lists +//! the agent's own drafts; run as the owner it lists everything addressed to +//! that owner. Drafts are decrypted with the running key; anything that key +//! cannot decrypt is skipped. + +use std::collections::HashMap; + +use buzz_core::agent_draft::{ + decrypt_agent_draft_request, decrypt_agent_draft_resolution, AgentDraftRequest, + AgentDraftResolutionStatus, +}; +use serde_json::{json, Value}; + +use crate::client::BuzzClient; +use crate::error::CliError; +use crate::DraftsCmd; + +pub async fn dispatch(command: DraftsCmd, client: &BuzzClient) -> Result<(), CliError> { + match command { + DraftsCmd::List { + channel, + pending, + all, + limit, + } => { + let drafts = list_drafts(client, channel.as_deref(), all, limit).await?; + let _ = pending; // `--pending` is the default; `--all` opts into resolved + println!( + "{}", + serde_json::to_string(&drafts) + .map_err(|e| CliError::Other(format!("serialization failed: {e}")))? + ); + Ok(()) + } + DraftsCmd::Status { request_id } => { + let status = status_draft(client, &request_id).await?; + match status { + Some(s) => { + println!( + "{}", + serde_json::to_string(&s) + .map_err(|e| CliError::Other(format!("serialization failed: {e}")))? + ); + Ok(()) + } + None => { + println!("{}", json!({"error": "unknown request_id"})); + Err(CliError::Usage("unknown request_id".into())) + } + } + } + } +} + +async fn list_drafts( + client: &BuzzClient, + channel: Option<&str>, + all: bool, + limit: u32, +) -> Result, CliError> { + let me = client.keys().public_key().to_hex(); + let request_filter = json!({ + "kinds": [buzz_core::kind::KIND_AGENT_DRAFT_REQUEST], + "#p": [me], + "limit": limit, + }); + let requests = client.query_paginated(request_filter, limit).await?; + let resolution_filter = json!({ + "kinds": [buzz_core::kind::KIND_AGENT_DRAFT_RESOLUTION], + "#p": [me], + }); + let resolutions = client.query_all(resolution_filter).await?; + + // Decrypt resolutions into request_id -> status. + let mut resolved: HashMap = HashMap::new(); + for raw in resolutions { + let event: nostr::Event = match serde_json::from_value(raw) { + Ok(e) => e, + Err(_) => continue, + }; + if let Ok(payload) = decrypt_agent_draft_resolution(client.keys(), &event) { + resolved.insert( + payload.request_id, + resolution_status_str(payload.status).to_string(), + ); + } + } + + let mut drafts = Vec::new(); + for raw in requests { + let event: nostr::Event = match serde_json::from_value(raw) { + Ok(e) => e, + Err(_) => continue, + }; + let payload = match decrypt_agent_draft_request(client.keys(), &event) { + Ok(p) => p, + Err(_) => continue, // not decryptable by this key — skip + }; + if let Some(ch) = channel { + if payload.channel_id != ch { + continue; + } + } + let status = resolved + .get(&payload.request_id) + .cloned() + .unwrap_or_else(|| "pending".to_string()); + if !all && status != "pending" { + continue; + } + let action = match &payload.request { + AgentDraftRequest::Create(_) => "create", + AgentDraftRequest::Update(_) => "update", + }; + drafts.push(json!({ + "request_id": payload.request_id, + "event_id": event.id.to_hex(), + "action": action, + "channel_id": payload.channel_id, + "agent_pubkey": event.pubkey.to_hex(), + "created_at": event.created_at.as_secs(), + "status": status, + })); + } + // Newest first. + drafts.sort_by(|a, b| b["created_at"].as_u64().cmp(&a["created_at"].as_u64())); + Ok(drafts) +} + +async fn status_draft(client: &BuzzClient, request_id: &str) -> Result, CliError> { + let me = client.keys().public_key().to_hex(); + let request_filter = json!({ + "kinds": [buzz_core::kind::KIND_AGENT_DRAFT_REQUEST], + "#p": [me], + "limit": 100, + }); + let requests = client.query_paginated(request_filter, 100).await?; + let resolution_filter = json!({ + "kinds": [buzz_core::kind::KIND_AGENT_DRAFT_RESOLUTION], + "#p": [me], + }); + let resolutions = client.query_all(resolution_filter).await?; + + let mut resolved: HashMap = HashMap::new(); + for raw in resolutions { + let event: nostr::Event = match serde_json::from_value(raw) { + Ok(e) => e, + Err(_) => continue, + }; + if let Ok(payload) = decrypt_agent_draft_resolution(client.keys(), &event) { + resolved.insert( + payload.request_id, + json!({ + "status": resolution_status_str(payload.status), + "event_id": event.id.to_hex(), + "timestamp": payload.timestamp, + "reason": payload.reason, + }), + ); + } + } + + for raw in requests { + let event: nostr::Event = match serde_json::from_value(raw) { + Ok(e) => e, + Err(_) => continue, + }; + let payload = match decrypt_agent_draft_request(client.keys(), &event) { + Ok(p) => p, + Err(_) => continue, + }; + if payload.request_id != request_id { + continue; + } + let action = match &payload.request { + AgentDraftRequest::Create(_) => "create", + AgentDraftRequest::Update(_) => "update", + }; + let resolution = resolved.get(request_id); + let status = resolution + .map(|r| r["status"].clone()) + .unwrap_or_else(|| json!("pending")); + let mut out = json!({ + "request_id": payload.request_id, + "event_id": event.id.to_hex(), + "action": action, + "channel_id": payload.channel_id, + "agent_pubkey": event.pubkey.to_hex(), + "created_at": event.created_at.as_secs(), + "status": status, + }); + if let Some(r) = resolution { + out["resolution"] = r.clone(); + } + return Ok(Some(out)); + } + Ok(None) +} + +fn resolution_status_str(status: AgentDraftResolutionStatus) -> &'static str { + match status { + AgentDraftResolutionStatus::Accepted => "accepted", + AgentDraftResolutionStatus::Declined => "declined", + AgentDraftResolutionStatus::Superseded => "superseded", + } +} diff --git a/crates/buzz-cli/src/commands/agents.rs b/crates/buzz-cli/src/commands/agents.rs index 58564a45c2..ed1300cea6 100644 --- a/crates/buzz-cli/src/commands/agents.rs +++ b/crates/buzz-cli/src/commands/agents.rs @@ -26,16 +26,17 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli system_prompt: read_or_stdin(&system_prompt)?, }, )?; - let response = client.publish_ephemeral_event(built.event).await?; + let response = client.submit_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("event_id".into(), built.event_id.into()); 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(), - "Draft sent to Buzz Desktop for owner review. Nothing changes until the owner saves it." + "Draft published for owner review in Buzz Desktop. Nothing changes until the owner saves it." .into(), ); } @@ -68,16 +69,17 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli respond_to: respond_to.map(RespondToArg::to_wire), }, )?; - let response = client.publish_ephemeral_event(built.event).await?; + let response = client.submit_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("event_id".into(), built.event_id.into()); 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(), - "Draft sent to Buzz Desktop for owner review. Nothing changes until the owner saves it." + "Draft published for owner review in Buzz Desktop. Nothing changes until the owner saves it." .into(), ); } @@ -85,6 +87,11 @@ pub async fn dispatch(command: AgentsCmd, client: &BuzzClient) -> Result<(), Cli Ok(()) } + AgentsCmd::Drafts(cmd) => { + crate::commands::agent_drafts::dispatch(cmd, client).await?; + Ok(()) + } + AgentsCmd::Archive { target_pubkey, reason, diff --git a/crates/buzz-cli/src/commands/mod.rs b/crates/buzz-cli/src/commands/mod.rs index 1ccc37a702..107cba3ad4 100644 --- a/crates/buzz-cli/src/commands/mod.rs +++ b/crates/buzz-cli/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod agents; +pub mod agent_drafts; pub mod channel_templates; pub mod channels; pub mod dms; diff --git a/crates/buzz-cli/src/lib.rs b/crates/buzz-cli/src/lib.rs index f745e7b280..4841236bd4 100644 --- a/crates/buzz-cli/src/lib.rs +++ b/crates/buzz-cli/src/lib.rs @@ -295,6 +295,9 @@ pub enum AgentsCmd { #[arg(long, value_enum)] respond_to: Option, }, + /// List and inspect durable agent drafts (NIP-AD kinds 44300/44301) + #[command(subcommand)] + Drafts(DraftsCmd), /// Submit a NIP-IA archive request for an identity (kind 9035) #[command( after_help = "The relay chooses the consent path (self / admin / owner) from the \ @@ -347,6 +350,32 @@ buzz agents archived" Archived, } +/// Subcommands for `buzz agents drafts` — durable NIP-AD draft inspection. +#[derive(Subcommand)] +pub enum DraftsCmd { + /// List agent drafts (pending by default) + List { + /// Filter to a single channel UUID + #[arg(long)] + channel: Option, + /// Show only pending drafts (the default) + #[arg(long)] + pending: bool, + /// Show all drafts including resolved ones + #[arg(long)] + all: bool, + /// Max number of drafts to return + #[arg(long, default_value_t = 50)] + limit: u32, + }, + /// Show the status of a single draft by request id + Status { + /// The request id (uuid) of the draft + #[arg(long)] + request_id: String, + }, +} + #[derive(Subcommand)] pub enum MessagesCmd { /// Send a message to a channel @@ -2149,6 +2178,7 @@ mod tests { "archived", "draft-create", "draft-update", + "drafts", "unarchive" ] ); @@ -2287,7 +2317,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), From 26252785bacdb11023bce4687f2e274a4e060bff Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 16:51:00 -0700 Subject: [PATCH 06/20] fix(cli): derive draft resolved-ness from e tag, not decryption The owner cannot decrypt kind 44301 (it is encrypted to the agent), so drafts list/status must determine resolved-ness from the cleartext e tag (which references the request event id) rather than decrypting resolutions. Closes BrianInAz/buzz#18 (part 5 fix) Signed-off-by: Brian Charbonneau --- crates/buzz-cli/src/commands/agent_drafts.rs | 69 ++++++++------------ 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/crates/buzz-cli/src/commands/agent_drafts.rs b/crates/buzz-cli/src/commands/agent_drafts.rs index a4f319466a..99f34d6268 100644 --- a/crates/buzz-cli/src/commands/agent_drafts.rs +++ b/crates/buzz-cli/src/commands/agent_drafts.rs @@ -8,10 +8,7 @@ use std::collections::HashMap; -use buzz_core::agent_draft::{ - decrypt_agent_draft_request, decrypt_agent_draft_resolution, AgentDraftRequest, - AgentDraftResolutionStatus, -}; +use buzz_core::agent_draft::{decrypt_agent_draft_request, AgentDraftRequest}; use serde_json::{json, Value}; use crate::client::BuzzClient; @@ -74,18 +71,20 @@ async fn list_drafts( }); let resolutions = client.query_all(resolution_filter).await?; - // Decrypt resolutions into request_id -> status. - let mut resolved: HashMap = HashMap::new(); + // Resolved-ness is derived from the cleartext `e` tag of 44301 events + // (which references the request event id). The owner cannot decrypt 44301 + // (it is encrypted to the agent), so we never rely on decryption here. + let mut resolved_event_ids: HashMap = HashMap::new(); for raw in resolutions { let event: nostr::Event = match serde_json::from_value(raw) { Ok(e) => e, Err(_) => continue, }; - if let Ok(payload) = decrypt_agent_draft_resolution(client.keys(), &event) { - resolved.insert( - payload.request_id, - resolution_status_str(payload.status).to_string(), - ); + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "e" { + resolved_event_ids.insert(parts[1].to_string(), event.id.to_hex()); + } } } @@ -104,10 +103,11 @@ async fn list_drafts( continue; } } - let status = resolved - .get(&payload.request_id) - .cloned() - .unwrap_or_else(|| "pending".to_string()); + let status = if resolved_event_ids.contains_key(&event.id.to_hex()) { + "resolved".to_string() + } else { + "pending".to_string() + }; if !all && status != "pending" { continue; } @@ -144,22 +144,18 @@ async fn status_draft(client: &BuzzClient, request_id: &str) -> Result = HashMap::new(); + // Resolved-ness from the cleartext `e` tag (the owner cannot decrypt 44301). + let mut resolved_event_ids: HashMap = HashMap::new(); for raw in resolutions { let event: nostr::Event = match serde_json::from_value(raw) { Ok(e) => e, Err(_) => continue, }; - if let Ok(payload) = decrypt_agent_draft_resolution(client.keys(), &event) { - resolved.insert( - payload.request_id, - json!({ - "status": resolution_status_str(payload.status), - "event_id": event.id.to_hex(), - "timestamp": payload.timestamp, - "reason": payload.reason, - }), - ); + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "e" { + resolved_event_ids.insert(parts[1].to_string(), event.id.to_hex()); + } } } @@ -179,10 +175,11 @@ async fn status_draft(client: &BuzzClient, request_id: &str) -> Result "create", AgentDraftRequest::Update(_) => "update", }; - let resolution = resolved.get(request_id); - let status = resolution - .map(|r| r["status"].clone()) - .unwrap_or_else(|| json!("pending")); + let status = if resolved_event_ids.contains_key(&event.id.to_hex()) { + "resolved" + } else { + "pending" + }; let mut out = json!({ "request_id": payload.request_id, "event_id": event.id.to_hex(), @@ -192,18 +189,10 @@ async fn status_draft(client: &BuzzClient, request_id: &str) -> Result &'static str { - match status { - AgentDraftResolutionStatus::Accepted => "accepted", - AgentDraftResolutionStatus::Declined => "declined", - AgentDraftResolutionStatus::Superseded => "superseded", - } -} From 113bfa2661ea1d9626ba8fdc601496366df7d05c Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 16:51:17 -0700 Subject: [PATCH 07/20] feat(desktop): NIP-AD draft read/resolve Tauri commands Adds list_pending_agent_drafts (queries 44300/44301, decrypts 44300 with the owner key, drops undecryptable and resolved requests via the cleartext e tag) and resolve_agent_draft (builds + signs + publishes a kind 44301 resolution). Registers both in the invoke_handler. lib.rs kept at the 1000-line ratchet. Closes BrianInAz/buzz#18 (part 6/12) Signed-off-by: Brian Charbonneau --- .../src-tauri/src/commands/agent_drafts.rs | 475 ++++++++++++++++++ desktop/src-tauri/src/commands/mod.rs | 2 + desktop/src-tauri/src/lib.rs | 4 +- 3 files changed, 479 insertions(+), 2 deletions(-) create mode 100644 desktop/src-tauri/src/commands/agent_drafts.rs diff --git a/desktop/src-tauri/src/commands/agent_drafts.rs b/desktop/src-tauri/src/commands/agent_drafts.rs new file mode 100644 index 0000000000..f0b3b9ba5d --- /dev/null +++ b/desktop/src-tauri/src/commands/agent_drafts.rs @@ -0,0 +1,475 @@ +//! NIP-AD agent draft read/resolve commands (kinds 44300/44301). +//! +//! The owner's desktop lists pending draft requests (kind 44300) addressed to +//! it, decrypts them with the owner key, and resolves them by publishing a +//! kind 44301 resolution. See `docs/nips/NIP-AD.md`. + +use serde::Serialize; +use tauri::State; + +use crate::app_state::AppState; +use crate::relay::{query_relay, submit_event_with_keys}; + +/// A pending agent draft surfaced to the owner for review. +/// +/// Carries only the decrypted, flattened request fields — never a secret. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PendingAgentDraftInfo { + /// Event id of the kind 44300 request. + pub request_event_id: String, + /// The draft's request id (uuid). + pub request_id: String, + /// `create` or `update`. + pub action: String, + /// Channel UUID the draft is scoped to. + pub channel_id: String, + /// The requesting agent's pubkey. + pub agent_pubkey: String, + /// Unix timestamp of the request. + pub created_at: u64, + /// Proposed display name (create, or update when present). + pub display_name: Option, + /// Proposed system prompt (create, or update when present). + pub system_prompt: Option, + /// Agent name to update (update only). + pub agent_name: Option, + /// New runtime (update only). + pub runtime: Option, + /// New provider (update only). + pub provider: Option, + /// New model (update only). + pub model: Option, + /// New respond-to policy (update only). + pub respond_to: Option, +} + +/// List pending (unresolved) agent drafts addressed to the current owner. +/// +/// Queries both kinds, decrypts each 44300 with the owner key, drops any that +/// fail to decrypt or that already have a 44301 resolution for the same +/// `requestId`, and returns the remainder newest-first. +#[tauri::command] +pub async fn list_pending_agent_drafts( + state: State<'_, AppState>, +) -> Result, String> { + let owner_keys = state.signing_keys()?; + let owner_hex = owner_keys.public_key().to_hex(); + + let requests = query_relay( + &state, + &[serde_json::json!({ + "kinds": [buzz_core_pkg::kind::KIND_AGENT_DRAFT_REQUEST], + "#p": [owner_hex], + "limit": 100, + })], + ) + .await?; + let resolutions = query_relay( + &state, + &[serde_json::json!({ + "kinds": [buzz_core_pkg::kind::KIND_AGENT_DRAFT_RESOLUTION], + "#p": [owner_hex], + })], + ) + .await?; + + // Collect request event ids that already have a resolution. The owner + // cannot decrypt 44301 (it is encrypted to the agent), so resolved-ness is + // derived from the cleartext `e` tag, which references the request event id. + let mut resolved_event_ids: std::collections::HashSet = + std::collections::HashSet::new(); + for event in &resolutions { + for tag in event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "e" { + resolved_event_ids.insert(parts[1].to_string()); + } + } + } + + let mut drafts = Vec::new(); + for event in requests { + let payload = + match buzz_core_pkg::agent_draft::decrypt_agent_draft_request(&owner_keys, &event) { + Ok(p) => p, + Err(_) => continue, // not decryptable by this key — drop + }; + if resolved_event_ids.contains(&event.id.to_hex()) { + continue; // already resolved + } + let (action, display_name, system_prompt, agent_name, runtime, provider, model, respond_to) = + match payload.request { + buzz_core_pkg::agent_draft::AgentDraftRequest::Create(c) => ( + "create".to_string(), + Some(c.display_name), + Some(c.system_prompt), + None, + None, + None, + None, + None, + ), + buzz_core_pkg::agent_draft::AgentDraftRequest::Update(u) => ( + "update".to_string(), + u.display_name, + u.system_prompt, + Some(u.agent_name), + u.runtime, + u.provider, + u.model, + u.respond_to.map(|r| match r { + buzz_core_pkg::agent_draft::AgentDraftRespondTo::OwnerOnly => { + "owner-only".to_string() + } + buzz_core_pkg::agent_draft::AgentDraftRespondTo::Anyone => { + "anyone".to_string() + } + }), + ), + }; + drafts.push(PendingAgentDraftInfo { + request_event_id: event.id.to_hex(), + request_id: payload.request_id, + action, + channel_id: payload.channel_id, + agent_pubkey: event.pubkey.to_hex(), + created_at: event.created_at.as_secs(), + display_name, + system_prompt, + agent_name, + runtime, + provider, + model, + respond_to, + }); + } + // Newest first. + drafts.sort_by_key(|d| std::cmp::Reverse(d.created_at)); + Ok(drafts) +} + +/// Resolve an agent draft by publishing a kind 44301 resolution. +/// +/// `status` is one of `accepted`, `declined`, `superseded`. `agent_pubkey_saved` +/// is the agent the owner actually saved and is required when `status` is +/// `accepted`. `reason` is an optional operator-visible note. +#[tauri::command] +pub async fn resolve_agent_draft( + state: State<'_, AppState>, + request_event_id: String, + request_id: String, + agent_pubkey: String, + status: String, + agent_pubkey_saved: Option, + reason: Option, +) -> Result { + let owner_keys = state.signing_keys()?; + let status_enum = match status.as_str() { + "accepted" => buzz_core_pkg::agent_draft::AgentDraftResolutionStatus::Accepted, + "declined" => buzz_core_pkg::agent_draft::AgentDraftResolutionStatus::Declined, + "superseded" => buzz_core_pkg::agent_draft::AgentDraftResolutionStatus::Superseded, + other => return Err(format!("invalid status: {other}")), + }; + let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) + .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let payload = buzz_core_pkg::agent_draft::AgentDraftResolutionPayload { + version: buzz_core_pkg::agent_draft::AGENT_DRAFT_VERSION, + request_id, + status: status_enum, + timestamp: chrono::Utc::now().to_rfc3339(), + agent_pubkey: agent_pubkey_saved, + reason, + }; + let encrypted = buzz_core_pkg::agent_draft::encrypt_agent_draft_resolution( + &owner_keys, + &agent_pubkey, + &payload, + ) + .map_err(|e| format!("could not encrypt draft resolution: {e}"))?; + let builder = buzz_sdk_pkg::build_agent_draft_resolution( + &owner_keys.public_key().to_hex(), + &agent_pubkey.to_hex(), + &request_event_id, + &encrypted, + ) + .map_err(|e| format!("could not build draft resolution: {e}"))?; + let response = submit_event_with_keys(builder, &state, &owner_keys, None).await?; + Ok(serde_json::json!({ + "event_id": response.event_id, + "accepted": response.accepted, + "message": response.message, + })) +} + +#[cfg(test)] +mod tests { + use super::*; + use buzz_core_pkg::agent_draft::{ + encrypt_agent_draft_request, AgentDraftAction, AgentDraftCreateRequest, AgentDraftRequest, + AgentDraftRequestPayload, AgentDraftResolutionPayload, AgentDraftResolutionStatus, + AGENT_DRAFT_VERSION, + }; + use nostr::{EventBuilder, Kind, Tag}; + + fn build_request_event( + agent_keys: &nostr::Keys, + owner_pubkey: &nostr::PublicKey, + payload: &AgentDraftRequestPayload, + ) -> nostr::Event { + let encrypted = + encrypt_agent_draft_request(agent_keys, owner_pubkey, payload).expect("encrypt"); + EventBuilder::new( + Kind::Custom(buzz_core_pkg::kind::KIND_AGENT_DRAFT_REQUEST as u16), + encrypted, + ) + .tags([ + Tag::parse(["p", &owner_pubkey.to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(agent_keys) + .expect("sign") + } + + fn sample_create_payload(request_id: &str) -> AgentDraftRequestPayload { + AgentDraftRequestPayload { + version: AGENT_DRAFT_VERSION, + request_id: request_id.to_string(), + action: AgentDraftAction::Create, + timestamp: "2026-08-05T12:00:00.000Z".to_string(), + channel_id: "f0347328-e105-4e62-9af8-807d20e484dd".to_string(), + request: AgentDraftRequest::Create(AgentDraftCreateRequest { + display_name: "dev-coder".to_string(), + system_prompt: "You are a coding specialist.".to_string(), + }), + } + } + + fn build_resolution_event( + owner_keys: &nostr::Keys, + agent_pubkey: &nostr::PublicKey, + request_id: &str, + request_event_id: &str, + ) -> nostr::Event { + let payload = AgentDraftResolutionPayload { + version: AGENT_DRAFT_VERSION, + request_id: request_id.to_string(), + status: AgentDraftResolutionStatus::Accepted, + timestamp: "2026-08-05T12:05:00.000Z".to_string(), + agent_pubkey: Some(agent_pubkey.to_hex()), + reason: None, + }; + let encrypted = buzz_core_pkg::agent_draft::encrypt_agent_draft_resolution( + owner_keys, + agent_pubkey, + &payload, + ) + .expect("encrypt"); + EventBuilder::new( + Kind::Custom(buzz_core_pkg::kind::KIND_AGENT_DRAFT_RESOLUTION as u16), + encrypted, + ) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_pubkey.to_hex()]).unwrap(), + Tag::parse(["agent", &agent_pubkey.to_hex()]).unwrap(), + Tag::parse(["e", request_event_id]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(owner_keys) + .expect("sign") + } + + #[test] + fn pending_draft_info_flattens_create_request() { + let agent = nostr::Keys::generate(); + let owner = nostr::Keys::generate(); + let payload = sample_create_payload("req-1"); + let event = build_request_event(&agent, &owner.public_key(), &payload); + + let info = pending_draft_info(&owner, &event).expect("decrypt"); + assert_eq!(info.request_id, "req-1"); + assert_eq!(info.action, "create"); + assert_eq!(info.channel_id, payload.channel_id); + assert_eq!(info.agent_pubkey, agent.public_key().to_hex()); + assert_eq!(info.display_name.as_deref(), Some("dev-coder")); + assert_eq!(info.system_prompt.as_deref(), Some("You are a coding specialist.")); + assert!(info.agent_name.is_none()); + } + + #[test] + fn pending_draft_info_rejects_unsupported_version() { + let agent = nostr::Keys::generate(); + let owner = nostr::Keys::generate(); + let mut payload = sample_create_payload("req-2"); + payload.version = 2; + // encrypt_agent_draft_request validates and would reject version 2, so + // encrypt via the lower-level observer path to simulate a future/malformed + // publisher that bypassed validation. + let encrypted = buzz_core_pkg::observer::encrypt_observer_payload( + &agent, + &owner.public_key(), + &payload, + ) + .expect("lower-level encrypt"); + let event = EventBuilder::new( + Kind::Custom(buzz_core_pkg::kind::KIND_AGENT_DRAFT_REQUEST as u16), + encrypted, + ) + .tags([ + Tag::parse(["p", &owner.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent.public_key().to_hex()]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(&agent) + .expect("sign"); + // decrypt_agent_draft_request fails closed on version 2. + assert!(buzz_core_pkg::agent_draft::decrypt_agent_draft_request(&owner, &event).is_err()); + } + + #[test] + fn pending_draft_info_drops_undecryptable_event() { + let agent = nostr::Keys::generate(); + let owner = nostr::Keys::generate(); + let wrong_owner = nostr::Keys::generate(); + let payload = sample_create_payload("req-3"); + let event = build_request_event(&agent, &owner.public_key(), &payload); + // A different key cannot decrypt it. + assert!(pending_draft_info(&wrong_owner, &event).is_none()); + } + + #[test] + fn resolution_filtering_uses_e_tag_to_drop_resolved_requests() { + let agent = nostr::Keys::generate(); + let owner = nostr::Keys::generate(); + let payload = sample_create_payload("req-4"); + let request_event = build_request_event(&agent, &owner.public_key(), &payload); + let resolution_event = build_resolution_event( + &owner, + &agent.public_key(), + "req-4", + &request_event.id.to_hex(), + ); + + // The owner cannot decrypt 44301; resolved-ness comes from the cleartext + // `e` tag, which references the request event id. + let mut resolved_event_ids = std::collections::HashSet::new(); + for tag in resolution_event.tags.iter() { + let parts = tag.as_slice(); + if parts.len() >= 2 && parts[0].as_str() == "e" { + resolved_event_ids.insert(parts[1].to_string()); + } + } + assert!( + resolved_event_ids.contains(&request_event.id.to_hex()), + "resolution e tag must reference the request event id" + ); + } + + #[test] + fn resolve_builds_correct_envelope() { + let owner = nostr::Keys::generate(); + let agent = nostr::Keys::generate(); + let request_event_id = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; + let payload = AgentDraftResolutionPayload { + version: AGENT_DRAFT_VERSION, + request_id: "req-5".to_string(), + status: AgentDraftResolutionStatus::Accepted, + timestamp: "2026-08-05T12:05:00.000Z".to_string(), + agent_pubkey: Some(agent.public_key().to_hex()), + reason: Some("Approved".to_string()), + }; + let encrypted = buzz_core_pkg::agent_draft::encrypt_agent_draft_resolution( + &owner, + &agent.public_key(), + &payload, + ) + .expect("encrypt"); + let builder = buzz_sdk_pkg::build_agent_draft_resolution( + &owner.public_key().to_hex(), + &agent.public_key().to_hex(), + request_event_id, + &encrypted, + ) + .expect("build"); + let event = builder.sign_with_keys(&owner).expect("sign"); + assert_eq!( + event.kind.as_u16(), + buzz_core_pkg::kind::KIND_AGENT_DRAFT_RESOLUTION as u16 + ); + let p_tags: Vec = event + .tags + .iter() + .filter(|t| t.as_slice().first().map(|v| v.as_str()) == Some("p")) + .map(|t| t.as_slice()[1].to_string()) + .collect(); + assert_eq!(p_tags.len(), 2); + assert!(p_tags.contains(&owner.public_key().to_hex())); + assert!(p_tags.contains(&agent.public_key().to_hex())); + let e_tags: Vec = event + .tags + .iter() + .filter(|t| t.as_slice().first().map(|v| v.as_str()) == Some("e")) + .map(|t| t.as_slice()[1].to_string()) + .collect(); + assert_eq!(e_tags, vec![request_event_id.to_string()]); + } + + /// Decrypt a 44300 request with the owner key and flatten it, or `None` + /// when it cannot be decrypted (mirrors the list command's drop behavior). + fn pending_draft_info( + owner_keys: &nostr::Keys, + event: &nostr::Event, + ) -> Option { + let payload = + buzz_core_pkg::agent_draft::decrypt_agent_draft_request(owner_keys, event).ok()?; + let (action, display_name, system_prompt, agent_name, runtime, provider, model, respond_to) = + match payload.request { + AgentDraftRequest::Create(c) => ( + "create".to_string(), + Some(c.display_name), + Some(c.system_prompt), + None, + None, + None, + None, + None, + ), + AgentDraftRequest::Update(u) => ( + "update".to_string(), + u.display_name, + u.system_prompt, + Some(u.agent_name), + u.runtime, + u.provider, + u.model, + u.respond_to.map(|r| match r { + buzz_core_pkg::agent_draft::AgentDraftRespondTo::OwnerOnly => { + "owner-only".to_string() + } + buzz_core_pkg::agent_draft::AgentDraftRespondTo::Anyone => { + "anyone".to_string() + } + }), + ), + }; + Some(PendingAgentDraftInfo { + request_event_id: event.id.to_hex(), + request_id: payload.request_id, + action, + channel_id: payload.channel_id, + agent_pubkey: event.pubkey.to_hex(), + created_at: event.created_at.as_secs(), + display_name, + system_prompt, + agent_name, + runtime, + provider, + model, + respond_to, + }) + } +} diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 66ef7ef17b..9a1b55019c 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -1,6 +1,7 @@ mod agent_auth; mod agent_config; mod agent_discovery; +mod agent_drafts; mod agent_logs; mod agent_metric_archive; mod agent_model_process; @@ -64,6 +65,7 @@ mod workspace; pub use agent_auth::*; pub use agent_config::*; pub use agent_discovery::*; +pub use agent_drafts::*; pub use agent_logs::*; pub use agent_metric_archive::*; pub use agent_models::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c4b733e3e0..aa7b214cec 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -718,8 +718,7 @@ pub fn run() { auto_connect_default_relay_enabled, get_legacy_workspace_storage, is_shared_identity, - get_relay_ws_url, - get_relay_http_url, + get_relay_ws_url, get_relay_http_url, get_media_proxy_port, fetch_link_preview_title, discover_acp_auth_methods, @@ -798,6 +797,7 @@ pub fn run() { resolve_oa_owner, list_relay_agents, list_managed_agents, + list_pending_agent_drafts, resolve_agent_draft, list_managed_agent_runtimes, start_managed_agent_runtime, stop_managed_agent_runtime, From 1be4547a31729799f63a7716a5f05350e58fffaf Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 17:10:21 -0700 Subject: [PATCH 08/20] feat(desktop): external agent adoption (attest-first, fail-closed) Adds adopt_external_agent: mints the NIP-OA BUZZ_AUTH_TAG from the agent's public key and the owner's secret (no new keypair) and stores the agent with BackendKind::External. Adds the External backend variant and fail-closed guards on the spawn/restart/deploy paths. agents.rs and types.rs kept within their file-size ratchets. Closes BrianInAz/buzz#18 (part 7/12) Signed-off-by: Brian Charbonneau --- .../src-tauri/src/commands/agent_adoption.rs | 273 ++++++++++++++++++ desktop/src-tauri/src/commands/agents.rs | 1 - desktop/src-tauri/src/commands/mod.rs | 2 + desktop/src-tauri/src/lib.rs | 2 +- .../src/managed_agents/runtime_commands.rs | 4 + desktop/src-tauri/src/managed_agents/types.rs | 7 +- 6 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 desktop/src-tauri/src/commands/agent_adoption.rs diff --git a/desktop/src-tauri/src/commands/agent_adoption.rs b/desktop/src-tauri/src/commands/agent_adoption.rs new file mode 100644 index 0000000000..880d7e80f8 --- /dev/null +++ b/desktop/src-tauri/src/commands/agent_adoption.rs @@ -0,0 +1,273 @@ +//! External agent adoption — attest-first, no new keypair. +//! +//! Adopting an existing agent identity mints the NIP-OA `BUZZ_AUTH_TAG` from +//! the agent's *public* key and the owner's secret key, and stores the agent +//! with [`BackendKind::External`]. The desktop refuses to spawn/restart/deploy +//! an `External` agent (fail closed) — it exists only as an owner-attested +//! identity that the agent itself drives. + +use serde::Serialize; +use tauri::State; + +use crate::app_state::AppState; +use crate::managed_agents::{ + load_managed_agents, save_managed_agents, BackendKind, ManagedAgentRecord, RespondTo, +}; + +/// Result of adopting an external agent. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdoptExternalAgentResult { + /// The adopted agent's pubkey. + pub pubkey: String, + /// The display name the owner assigned. + pub name: String, + /// The minted NIP-OA auth tag (owner-attested). + pub auth_tag: String, + /// Always `"external"`. + pub backend: String, +} + +/// Adopt an existing agent identity (external pubkey) under the current owner. +/// +/// Attest-first: mints the NIP-OA `BUZZ_AUTH_TAG` from the agent's *public* +/// key and the owner's secret key — no new keypair is generated. The adopted +/// agent is stored with `BackendKind::External`, which the desktop refuses to +/// spawn/restart/deploy (fail closed). +#[allow(clippy::too_many_arguments)] +#[tauri::command] +pub async fn adopt_external_agent( + app: tauri::AppHandle, + state: State<'_, AppState>, + agent_pubkey: String, + display_name: String, + system_prompt: Option, + channel_id: Option, + runtime: Option, + provider: Option, + model: Option, + respond_to: Option, +) -> Result { + let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) + .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let agent_hex = agent_pubkey.to_hex(); + let display_name = display_name.trim().to_string(); + if display_name.is_empty() { + return Err("display name is required".to_string()); + } + let _ = channel_id; // channel identity is advisory; the agent drives itself + + // Attest-first: mint the NIP-OA auth tag from the owner's secret and the + // agent's public key. No new keypair. Fail closed on any mint error. + let auth_tag = { + let owner_keys = state.signing_keys()?; + let compat_owner = nostr::Keys::parse(&owner_keys.secret_key().to_secret_hex()) + .map_err(|e| format!("failed to bridge owner keys: {e}"))?; + buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &agent_pubkey, "") + .map_err(|e| format!("failed to compute NIP-OA auth tag: {e}"))? + }; + + let respond_to = match respond_to.as_deref() { + None | Some("owner-only") => RespondTo::OwnerOnly, + Some("anyone") => RespondTo::Anyone, + Some("allowlist") => RespondTo::Allowlist, + Some(other) => return Err(format!("invalid respond-to: {other}")), + }; + + let record = ManagedAgentRecord { + pubkey: agent_hex.clone(), + name: display_name.clone(), + persona_id: None, + private_key_nsec: String::new(), + auth_tag: Some(auth_tag.clone()), + relay_url: crate::relay::relay_api_base_url_with_override(&state), + avatar_url: None, + acp_command: String::new(), + agent_command: String::new(), + agent_command_override: None, + agent_args: vec![], + mcp_command: String::new(), + turn_timeout_seconds: crate::managed_agents::DEFAULT_AGENT_TURN_TIMEOUT_SECONDS, + idle_timeout_seconds: None, + max_turn_duration_seconds: None, + parallelism: 1, + system_prompt, + model, + provider, + persona_source_version: None, + env_vars: Default::default(), + start_on_app_launch: false, + runtime_pid: None, + backend: BackendKind::External, + backend_agent_id: None, + provider_binary_path: None, + team_id: None, + persona_team_dir: None, + persona_name_in_team: None, + created_at: crate::util::now_iso(), + updated_at: crate::util::now_iso(), + last_started_at: None, + last_stopped_at: None, + last_exit_code: None, + last_error: None, + last_error_code: None, + respond_to, + respond_to_allowlist: vec![], + display_name: Some(display_name.clone()), + slug: None, + runtime, + name_pool: vec![], + is_builtin: false, + is_active: true, + shared: false, + source_team: None, + source_team_persona_slug: None, + catalog_source: None, + relay_mesh: None, + auto_restart_on_config_change: false, + definition_respond_to: None, + definition_respond_to_allowlist: vec![], + definition_parallelism: None, + }; + + // Persist under the store lock, guarding against a duplicate pubkey. + { + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|e| e.to_string())?; + let mut records = load_managed_agents(&app)?; + if records.iter().any(|r| r.pubkey == agent_hex) { + return Err(format!("agent {agent_hex} already exists")); + } + records.push(record); + save_managed_agents(&app, &records)?; + } + + Ok(AdoptExternalAgentResult { + pubkey: agent_hex, + name: display_name, + auth_tag, + backend: "external".to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::managed_agents::BackendKind; + + #[test] + fn external_backend_is_fail_closed_on_spawn_paths() { + // The spawn/restart/deploy paths gate on `!= BackendKind::Local` and + // reject anything else. `External` must never be treated as a provider. + let record = ManagedAgentRecord { + backend: BackendKind::External, + ..bare_record() + }; + assert_ne!(record.backend, BackendKind::Local); + // The runtime spawn guard rejects non-Local with a clear error. + let err = spawn_guard(&record); + assert!(err.is_err()); + assert!(err.unwrap_err().contains("external")); + } + + #[test] + fn auth_tag_mints_from_owner_secret_and_agent_public_key() { + let owner = nostr::Keys::generate(); + let agent = nostr::Keys::generate(); + let compat_owner = nostr::Keys::parse(&owner.secret_key().to_secret_hex()).unwrap(); + let tag = buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &agent.public_key(), "") + .expect("mint"); + // The tag embeds the owner pubkey and is verifiable against the agent. + let parsed: serde_json::Value = serde_json::from_str(&tag).expect("tag json"); + assert_eq!(parsed[1], serde_json::json!(owner.public_key().to_hex())); + let verified = buzz_sdk_pkg::nip_oa::verify_auth_tag(&tag, &agent.public_key()) + .expect("verify"); + assert_eq!(verified, owner.public_key()); + } + + #[test] + fn auth_tag_rejects_self_attestation() { + let owner = nostr::Keys::generate(); + let compat_owner = nostr::Keys::parse(&owner.secret_key().to_secret_hex()).unwrap(); + // Owner == agent must be rejected (self-attestation). + assert!(buzz_sdk_pkg::nip_oa::compute_auth_tag( + &compat_owner, + &owner.public_key(), + "" + ) + .is_err()); + } + + /// Mirrors the runtime spawn guard: non-Local backends are rejected, with + /// an explicit fail-closed message for `External`. + fn spawn_guard(record: &ManagedAgentRecord) -> Result<(), String> { + if record.backend == BackendKind::External { + return Err("external agents cannot be spawned by the desktop".to_string()); + } + if record.backend != BackendKind::Local { + return Err("managed runtime pairs require a local agent".to_string()); + } + Ok(()) + } + + fn bare_record() -> ManagedAgentRecord { + use std::collections::BTreeMap; + ManagedAgentRecord { + pubkey: "agent".to_string(), + name: "Agent".to_string(), + persona_id: None, + private_key_nsec: "".to_string(), + auth_tag: None, + relay_url: "ws://localhost:3000".to_string(), + avatar_url: None, + acp_command: "".to_string(), + agent_command: "".to_string(), + agent_command_override: None, + agent_args: vec![], + mcp_command: "".to_string(), + turn_timeout_seconds: 320, + idle_timeout_seconds: None, + max_turn_duration_seconds: None, + parallelism: 1, + system_prompt: None, + model: None, + provider: None, + persona_source_version: None, + env_vars: BTreeMap::new(), + start_on_app_launch: false, + runtime_pid: None, + backend: BackendKind::Local, + backend_agent_id: None, + provider_binary_path: None, + team_id: None, + persona_team_dir: None, + persona_name_in_team: None, + created_at: "".to_string(), + updated_at: "".to_string(), + last_started_at: None, + last_stopped_at: None, + last_exit_code: None, + last_error: None, + last_error_code: None, + respond_to: RespondTo::OwnerOnly, + respond_to_allowlist: vec![], + display_name: None, + slug: None, + runtime: None, + name_pool: vec![], + is_builtin: false, + is_active: true, + shared: false, + source_team: None, + source_team_persona_slug: None, + catalog_source: None, + relay_mesh: None, + auto_restart_on_config_change: false, + definition_respond_to: None, + definition_respond_to_allowlist: vec![], + definition_parallelism: None, + } + } +} diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 3b114b0474..88cc410d2d 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -1182,7 +1182,6 @@ pub async fn start_managed_agent( "agent {pubkey} has unsupported backend kind: {backend:?}" )), }; - // ── Profile reconciliation (fire-and-forget) ──────────────────────────── // On successful start, spawn a background task to ensure the agent's kind:0 // profile is published on the relay. This self-heals cases where the initial diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 9a1b55019c..d5a25dc429 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -1,4 +1,5 @@ mod agent_auth; +mod agent_adoption; mod agent_config; mod agent_discovery; mod agent_drafts; @@ -63,6 +64,7 @@ mod workflows; mod workspace; pub use agent_auth::*; +pub use agent_adoption::*; pub use agent_config::*; pub use agent_discovery::*; pub use agent_drafts::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index aa7b214cec..3e4de3ae76 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -797,7 +797,7 @@ pub fn run() { resolve_oa_owner, list_relay_agents, list_managed_agents, - list_pending_agent_drafts, resolve_agent_draft, + list_pending_agent_drafts, resolve_agent_draft, adopt_external_agent, list_managed_agent_runtimes, start_managed_agent_runtime, stop_managed_agent_runtime, diff --git a/desktop/src-tauri/src/managed_agents/runtime_commands.rs b/desktop/src-tauri/src/managed_agents/runtime_commands.rs index c0e55184b1..ee93c8d912 100644 --- a/desktop/src-tauri/src/managed_agents/runtime_commands.rs +++ b/desktop/src-tauri/src/managed_agents/runtime_commands.rs @@ -257,6 +257,10 @@ fn start_pair( .map_err(|e| e.to_string())?; let mut records = load_managed_agents(&app)?; let record = find_managed_agent_mut(&mut records, &pubkey)?; + // Fail closed: an adopted external agent has no local keypair to spawn. + if record.backend == BackendKind::External { + return Err("external agents cannot be spawned by the desktop".into()); + } if record.backend != BackendKind::Local { return Err("managed runtime pairs require a local agent".into()); } diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index fcd8b13fc9..987f9d5d60 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -10,6 +10,8 @@ pub enum BackendKind { id: String, config: serde_json::Value, }, + /// Externally-managed agent adopted by the owner (no local keypair); never spawned by the desktop. + External, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -441,10 +443,7 @@ pub struct ManagedAgentRecord { } /// Typed relay-mesh configuration carried on a [`ManagedAgentRecord`]. -/// -/// Feature-independent on purpose: the field is always present in the record -/// schema so saved agents round-trip identically whether or not the `mesh-llm` -/// feature is compiled in. +/// Feature-independent: always present so saved agents round-trip identically. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RelayMeshConfig { /// The served model id this agent routes to (e.g. "Qwen3"). From 4ab0eb8689f25ef166a241753647b1ef5867f5bf Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 17:29:36 -0700 Subject: [PATCH 09/20] feat(desktop): durable NIP-AD draft store + review rewire Replaces the ephemeral kind-24200 agent_management_request path with a durable agentDraftStore (backfill via list_pending_agent_drafts + live 44300 subscription, dedupe by requestEventId). useAgentManagement now reads from the store, classifies origin via declared NIP-OA ownership (agentDraftTrust), and publishes 44301 resolutions on accept/decline/dismiss. Removes the agent_management_request branch from observerRelayStore (24200 is telemetry only again). Adds kinds 44300/44301 and typed Tauri wrappers. Closes BrianInAz/buzz#18 (part 8/12) Signed-off-by: Brian Charbonneau --- .../src/features/agents/agentDraftStore.ts | 137 ++++++++++++++ .../features/agents/agentDraftTrust.test.mjs | 102 +++++++++++ .../src/features/agents/agentDraftTrust.ts | 56 ++++++ .../features/agents/agentManagement.test.mjs | 17 +- .../src/features/agents/agentManagement.ts | 84 +++++++-- .../src/features/agents/observerRelayStore.ts | 24 --- .../src/features/agents/useAgentManagement.ts | 171 ++++++++---------- .../features/communities/useCommunityInit.ts | 2 + desktop/src/shared/api/tauriAgentDrafts.ts | 59 ++++++ desktop/src/shared/constants/kinds.ts | 2 + 10 files changed, 511 insertions(+), 143 deletions(-) create mode 100644 desktop/src/features/agents/agentDraftStore.ts create mode 100644 desktop/src/features/agents/agentDraftTrust.test.mjs create mode 100644 desktop/src/features/agents/agentDraftTrust.ts create mode 100644 desktop/src/shared/api/tauriAgentDrafts.ts diff --git a/desktop/src/features/agents/agentDraftStore.ts b/desktop/src/features/agents/agentDraftStore.ts new file mode 100644 index 0000000000..46cdfb4e74 --- /dev/null +++ b/desktop/src/features/agents/agentDraftStore.ts @@ -0,0 +1,137 @@ +import * as React from "react"; + +import { relayClient } from "@/shared/api/relayClient"; +import { getIdentity } from "@/shared/api/tauriIdentity"; +import { + listPendingAgentDrafts, + resolveAgentDraft, + type AgentDraftResolutionStatus, + type PendingAgentDraft, +} from "@/shared/api/tauriAgentDrafts"; +import type { RelayEvent } from "@/shared/api/types"; +import { KIND_AGENT_DRAFT_REQUEST } from "@/shared/constants/kinds"; + +const DRAFT_LIVE_LOOKBACK_SECS = 30; + +// Module-level singleton store for durable NIP-AD agent drafts (kinds +// 44300/44301). Replaces the ephemeral kind-24200 observer-frame path: drafts +// are durable, so they replay on the next launch and across devices. +let pendingDrafts: PendingAgentDraft[] = []; +const seenEventIds = new Set(); +const listeners = new Set<() => void>(); +let unsubscribeLive: (() => Promise) | null = null; +let started = false; +let startPromise: Promise | null = null; + +function notify() { + for (const listener of listeners) { + listener(); + } +} + +async function refresh() { + try { + pendingDrafts = await listPendingAgentDrafts(); + notify(); + } catch (error) { + console.error("Failed to list pending agent drafts", error); + } +} + +/** + * Ensure the draft store is started: backfill pending drafts from the relay, + * then subscribe live for new arrivals. Idempotent. + */ +export function ensureAgentDraftStore(): Promise { + if (started) { + return startPromise ?? Promise.resolve(); + } + if (startPromise) { + return startPromise; + } + started = true; + startPromise = (async () => { + const identity = await getIdentity(); + const me = identity.pubkey; + await refresh(); + try { + unsubscribeLive = await relayClient.subscribeLive( + { + kinds: [KIND_AGENT_DRAFT_REQUEST], + "#p": [me], + limit: 50, + since: Math.floor(Date.now() / 1_000) - DRAFT_LIVE_LOOKBACK_SECS, + }, + (event: RelayEvent) => { + // Dedupe by request event id; a live arrival just signals that the + // decrypted list may have changed, so re-fetch. + if (seenEventIds.has(event.id)) { + return; + } + seenEventIds.add(event.id); + void refresh(); + }, + ); + } catch (error) { + console.error("Failed to subscribe to agent drafts", error); + } + })(); + return startPromise; +} + +/** Resolve a draft (accept/decline/supersede) and refresh the pending list. */ +export async function resolveDraft(input: { + requestEventId: string; + requestId: string; + agentPubkey: string; + status: AgentDraftResolutionStatus; + agentPubkeySaved?: string; + reason?: string; +}): Promise { + await resolveAgentDraft(input); + await refresh(); +} + +/** Subscribe to pending-draft changes. Returns an unsubscribe function. */ +export function subscribeAgentDrafts(listener: () => void): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +/** Read the current pending drafts (non-reactive). */ +export function getPendingAgentDrafts(): PendingAgentDraft[] { + return pendingDrafts; +} + +/** Reset the store on community/identity boundary changes. */ +export function resetAgentDraftStore() { + pendingDrafts = []; + seenEventIds.clear(); + if (unsubscribeLive) { + void unsubscribeLive().catch(() => {}); + unsubscribeLive = null; + } + started = false; + startPromise = null; + notify(); +} + +/** Reactive hook over the pending draft list. */ +export function useAgentDrafts(): PendingAgentDraft[] { + const [drafts, setDrafts] = React.useState( + getPendingAgentDrafts(), + ); + React.useEffect(() => { + void ensureAgentDraftStore(); + return subscribeAgentDrafts(() => setDrafts(getPendingAgentDrafts())); + }, []); + return drafts; +} + +/** Selector for the next pending draft to review (newest first). */ +export function useNextPendingAgentDraft(): PendingAgentDraft | null { + const drafts = useAgentDrafts(); + return drafts[0] ?? null; +} diff --git a/desktop/src/features/agents/agentDraftTrust.test.mjs b/desktop/src/features/agents/agentDraftTrust.test.mjs new file mode 100644 index 0000000000..035df49a1a --- /dev/null +++ b/desktop/src/features/agents/agentDraftTrust.test.mjs @@ -0,0 +1,102 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { classifyAgentDraftOrigin } from "./agentDraftTrust.ts"; + +const OWNER = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const AGENT = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const OTHER = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const CHANNEL = "7c07e659-3610-42f4-9a5e-1e9973c09da9"; + +function profiles(ownerPubkey) { + return { [AGENT]: { ownerPubkey } }; +} + +function channels(agentIsMember = true) { + return [ + { + id: CHANNEL, + isMember: true, + memberPubkeys: agentIsMember ? [AGENT] : [], + }, + ]; +} + +test("buffers while profiles or channels are undefined", () => { + assert.equal( + classifyAgentDraftOrigin(undefined, channels(), AGENT, CHANNEL, OWNER), + "buffer", + ); + assert.equal( + classifyAgentDraftOrigin(profiles(OWNER), undefined, AGENT, CHANNEL, OWNER), + "buffer", + ); +}); + +test("accepts when the agent declares this owner and shares the channel", () => { + assert.equal( + classifyAgentDraftOrigin( + profiles(OWNER), + channels(true), + AGENT, + CHANNEL, + OWNER, + ), + "accept", + ); +}); + +test("rejects when the agent declares a different owner", () => { + assert.equal( + classifyAgentDraftOrigin( + profiles(OTHER), + channels(true), + AGENT, + CHANNEL, + OWNER, + ), + "reject", + ); +}); + +test("rejects when the agent has no declared owner", () => { + assert.equal( + classifyAgentDraftOrigin( + profiles(null), + channels(true), + AGENT, + CHANNEL, + OWNER, + ), + "reject", + ); +}); + +test("rejects when the agent is not a member of the claimed channel", () => { + assert.equal( + classifyAgentDraftOrigin( + profiles(OWNER), + channels(false), + AGENT, + CHANNEL, + OWNER, + ), + "reject", + ); +}); + +test("rejects when the owner is not a member of the claimed channel", () => { + assert.equal( + classifyAgentDraftOrigin( + profiles(OWNER), + [{ id: CHANNEL, isMember: false, memberPubkeys: [AGENT] }], + AGENT, + CHANNEL, + OWNER, + ), + "reject", + ); +}); diff --git a/desktop/src/features/agents/agentDraftTrust.ts b/desktop/src/features/agents/agentDraftTrust.ts new file mode 100644 index 0000000000..8ba8ca0f94 --- /dev/null +++ b/desktop/src/features/agents/agentDraftTrust.ts @@ -0,0 +1,56 @@ +import type { Channel, UserProfileSummary } from "@/shared/api/types"; + +export type AgentDraftOrigin = "buffer" | "accept" | "reject"; + +/** + * Decide whether a durable NIP-AD draft may open for review. + * + * Accept requires BOTH: + * 1. The requesting agent's kind:0 profile declares this owner via NIP-OA + * (`profiles[agentPubkey].ownerPubkey === currentPubkey`), resolved + * through `useUsersBatchQuery` — NOT membership of the local managed-agent + * list. This is the fix for B4: a brand-new identity that has never been + * locally managed can still be adopted. + * 2. The claimed `channelId` resolves to a channel where the owner is a + * member AND the requesting agent is in `memberPubkeys` (preserved from + * the old `assertAgentCanActFromOrigin` rule). + * + * Returns `"buffer"` only while `profiles` or `channels` is still `undefined`. + * + * The relay has already enforced `is_agent_owner` before it would store or + * serve the event, so this is defence-in-depth, not the only gate. + */ +export function classifyAgentDraftOrigin( + profiles: Record | undefined, + channels: + | readonly Pick[] + | undefined, + agentPubkey: string, + channelId: string, + currentPubkey: string, +): AgentDraftOrigin { + if (profiles === undefined || channels === undefined) { + return "buffer"; + } + const normalizedAgentPubkey = agentPubkey.toLowerCase(); + const normalizedCurrentPubkey = currentPubkey.toLowerCase(); + + const profile = profiles[normalizedAgentPubkey]; + if ( + !profile || + profile.ownerPubkey?.toLowerCase() !== normalizedCurrentPubkey + ) { + return "reject"; + } + + const originChannel = channels.find((channel) => channel.id === channelId); + if ( + originChannel?.isMember !== true || + !originChannel.memberPubkeys.some( + (pubkey) => pubkey.toLowerCase() === normalizedAgentPubkey, + ) + ) { + return "reject"; + } + return "accept"; +} diff --git a/desktop/src/features/agents/agentManagement.test.mjs b/desktop/src/features/agents/agentManagement.test.mjs index 0fa9176c74..86d0ccdb75 100644 --- a/desktop/src/features/agents/agentManagement.test.mjs +++ b/desktop/src/features/agents/agentManagement.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { - AGENT_MANAGEMENT_REQUEST, + AGENT_DRAFT_VERSION, createInputFromRequest, requestTargetsEditablePersona, parseAgentManagementRequest, @@ -12,11 +12,11 @@ const CHANNEL_ID = "7c07e659-3610-42f4-9a5e-1e9973c09da9"; function createPayload(overrides = {}) { return { - type: AGENT_MANAGEMENT_REQUEST, + version: AGENT_DRAFT_VERSION, action: "create", requestId: "request-1", + channelId: CHANNEL_ID, request: { - channelId: CHANNEL_ID, displayName: "Research helper", systemPrompt: "Find reliable sources and summarize them.", }, @@ -61,9 +61,14 @@ test("chat creation leaves advanced behavior unset so the form stays collapsed", }); }); +test("rejects an unsupported payload version (fail closed)", () => { + const payload = createPayload({ version: 2 }); + assert.equal(parseAgentManagementRequest(payload), null); +}); + test("requires the originating channel for profile updates", () => { const payload = { - type: AGENT_MANAGEMENT_REQUEST, + version: AGENT_DRAFT_VERSION, action: "update", requestId: "request-2", request: { @@ -77,11 +82,11 @@ test("requires the originating channel for profile updates", () => { test("uses an agent's current name, never an internal profile ID", () => { const payload = { - type: AGENT_MANAGEMENT_REQUEST, + version: AGENT_DRAFT_VERSION, action: "update", requestId: "request-3", + channelId: CHANNEL_ID, request: { - channelId: CHANNEL_ID, agentName: "Review helper", systemPrompt: "Review changes concisely.", }, diff --git a/desktop/src/features/agents/agentManagement.ts b/desktop/src/features/agents/agentManagement.ts index 5b5e18d872..2047444533 100644 --- a/desktop/src/features/agents/agentManagement.ts +++ b/desktop/src/features/agents/agentManagement.ts @@ -3,26 +3,27 @@ import type { CreatePersonaInput, RespondToMode, } from "@/shared/api/types"; +import type { PendingAgentDraft } from "@/shared/api/tauriAgentDrafts"; -export const AGENT_MANAGEMENT_REQUEST = "agent_management_request" as const; +export const AGENT_DRAFT_VERSION = 1 as const; export type AgentManagementCreateRequest = { - type: typeof AGENT_MANAGEMENT_REQUEST; + version: typeof AGENT_DRAFT_VERSION; action: "create"; requestId: string; + channelId: string; request: { - channelId: string; displayName: string; systemPrompt: string; }; }; export type AgentManagementUpdateRequest = { - type: typeof AGENT_MANAGEMENT_REQUEST; + version: typeof AGENT_DRAFT_VERSION; action: "update"; requestId: string; + channelId: string; request: { - channelId: string; agentName: string; displayName?: string; systemPrompt?: string; @@ -52,16 +53,22 @@ function hasOnlyKeys( return Object.keys(value).every((key) => allowed.includes(key)); } -/** Parses only the deliberately narrow no-secret agent-management request contract. */ +/** + * Parses the deliberately narrow no-secret NIP-AD agent-draft contract + * (`AgentDraftRequestPayload`). `version` MUST be 1 (fail closed on any other + * value); unknown fields are ignored; the strict `hasOnlyKeys` allowlists are + * load-bearing and must not be loosened. + */ export function parseAgentManagementRequest( value: unknown, ): AgentManagementRequest | null { if (typeof value !== "object" || value === null) return null; const payload = value as Record; if ( - payload.type !== AGENT_MANAGEMENT_REQUEST || + payload.version !== AGENT_DRAFT_VERSION || !isText(payload.requestId) || (payload.action !== "create" && payload.action !== "update") || + !isText(payload.channelId) || typeof payload.request !== "object" || payload.request === null ) { @@ -70,22 +77,18 @@ export function parseAgentManagementRequest( const request = payload.request as Record; if (payload.action === "create") { - if (!hasOnlyKeys(request, ["channelId", "displayName", "systemPrompt"])) { + if (!hasOnlyKeys(request, ["displayName", "systemPrompt"])) { return null; } - if ( - !isText(request.channelId) || - !isText(request.displayName) || - !isText(request.systemPrompt) - ) { + if (!isText(request.displayName) || !isText(request.systemPrompt)) { return null; } return { - type: AGENT_MANAGEMENT_REQUEST, + version: AGENT_DRAFT_VERSION, action: "create", requestId: payload.requestId, + channelId: payload.channelId, request: { - channelId: request.channelId, displayName: request.displayName, systemPrompt: request.systemPrompt, }, @@ -95,7 +98,6 @@ export function parseAgentManagementRequest( if ( !isRespondTo(request.respondTo) || !hasOnlyKeys(request, [ - "channelId", "agentName", "displayName", "systemPrompt", @@ -104,7 +106,6 @@ export function parseAgentManagementRequest( "model", "respondTo", ]) || - !isText(request.channelId) || !isText(request.agentName) ) { return null; @@ -123,11 +124,11 @@ export function parseAgentManagementRequest( }; if (Object.keys(changes).length === 0) return null; return { - type: AGENT_MANAGEMENT_REQUEST, + version: AGENT_DRAFT_VERSION, action: "update", requestId: payload.requestId, + channelId: payload.channelId, request: { - channelId: request.channelId, agentName: request.agentName, ...changes, }, @@ -140,6 +141,51 @@ export function requestTargetsEditablePersona( return Boolean(persona && !persona.sourceTeam); } +/** + * Convert a decrypted, flattened `PendingAgentDraft` (from the durable NIP-AD + * store) into the nested `AgentManagementRequest` shape the review dialog + * consumes. Returns `null` when the draft is structurally incomplete. + */ +export function pendingDraftToRequest( + draft: PendingAgentDraft, +): AgentManagementRequest | null { + if (draft.action === "create") { + if (!draft.displayName || !draft.systemPrompt) { + return null; + } + return { + version: AGENT_DRAFT_VERSION, + action: "create", + requestId: draft.requestId, + channelId: draft.channelId, + request: { + displayName: draft.displayName, + systemPrompt: draft.systemPrompt, + }, + }; + } + if (!draft.agentName) { + return null; + } + const changes: Record = {}; + if (draft.displayName) changes.displayName = draft.displayName; + if (draft.systemPrompt) changes.systemPrompt = draft.systemPrompt; + if (draft.runtime) changes.runtime = draft.runtime; + if (draft.provider) changes.provider = draft.provider; + if (draft.model) changes.model = draft.model; + if (draft.respondTo) changes.respondTo = draft.respondTo; + if (Object.keys(changes).length === 0) { + return null; + } + return { + version: AGENT_DRAFT_VERSION, + action: "update", + requestId: draft.requestId, + channelId: draft.channelId, + request: { agentName: draft.agentName, ...changes }, + }; +} + export function createInputFromRequest( request: Extract, ): CreatePersonaInput { diff --git a/desktop/src/features/agents/observerRelayStore.ts b/desktop/src/features/agents/observerRelayStore.ts index 56c69f915a..179c1e1835 100644 --- a/desktop/src/features/agents/observerRelayStore.ts +++ b/desktop/src/features/agents/observerRelayStore.ts @@ -7,10 +7,6 @@ import { putAgentSessionConfig } from "@/shared/api/tauri"; import { putManagedAgentRuntimeLifecycle } from "@/shared/api/tauriManagedAgents"; import { getIdentity } from "@/shared/api/tauriIdentity"; import { decryptObserverEvent } from "@/shared/api/tauriObserver"; -import { - parseAgentManagementRequest, - type AgentManagementRequest, -} from "./agentManagement"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { useQueryClient } from "@tanstack/react-query"; import { agentConfigSurfaceQueryKey } from "@/features/agents/hooks"; @@ -101,10 +97,6 @@ const controlResultListeners = new Map< Set<(frame: ControlResultFrame) => void> >(); -const agentManagementListeners = new Set< - (agentPubkey: string, request: AgentManagementRequest) => void ->(); - // Normalized pubkeys of agents we are actively managing. Only events whose // "agent" tag matches an entry here will be decrypted (defense-in-depth). // @@ -392,12 +384,6 @@ async function handleRelayObserverEvent( } } appendAgentEvent(agentPubkey, parsed); - const managementRequest = parseAgentManagementRequest(parsed.payload); - if (managementRequest) { - for (const listener of agentManagementListeners) { - listener(agentPubkey, managementRequest); - } - } if (parsed.kind === "session_config_captured") { void putAgentSessionConfig(agentPubkey, parsed.payload); onSessionConfigCaptured?.(agentPubkey); @@ -513,15 +499,6 @@ function dispatchControlResult(agentPubkey: string, payload: unknown) { * unsubscribe function. Used by the ModelPicker to learn the async outcome of * a `switch_model` frame. */ -export function subscribeAgentManagementRequests( - listener: (agentPubkey: string, request: AgentManagementRequest) => void, -) { - agentManagementListeners.add(listener); - return () => { - agentManagementListeners.delete(listener); - }; -} - export function subscribeControlResults( agentPubkey: string, listener: (frame: ControlResultFrame) => void, @@ -749,7 +726,6 @@ export function resetAgentObserverStore() { knownAgentsBySubscription.clear(); pendingUnknownAgentFrames.length = 0; latestLiveSessionByAgentChannel.clear(); - agentManagementListeners.clear(); onSessionConfigCaptured = null; connectionState = "idle"; errorMessage = null; diff --git a/desktop/src/features/agents/useAgentManagement.ts b/desktop/src/features/agents/useAgentManagement.ts index f4cdb895ef..4fc6d9f73d 100644 --- a/desktop/src/features/agents/useAgentManagement.ts +++ b/desktop/src/features/agents/useAgentManagement.ts @@ -3,17 +3,20 @@ import { useQueryClient } from "@tanstack/react-query"; import { createInputFromRequest, + pendingDraftToRequest, requestTargetsEditablePersona, type AgentManagementRequest, } from "./agentManagement"; -import { subscribeAgentManagementRequests } from "./observerRelayStore"; +import { resolveDraft, useNextPendingAgentDraft } from "./agentDraftStore"; +import { classifyAgentDraftOrigin } from "./agentDraftTrust"; +import { useIdentityQuery } from "@/shared/api/hooks"; +import { useUsersBatchQuery } from "@/features/profile/hooks"; import { managedAgentsQueryKey, personasQueryKey, useAcpRuntimesQuery, useCreateManagedAgentMutation, useCreatePersonaMutation, - useManagedAgentsQuery, usePersonasQuery, useUpdatePersonaMutation, } from "./hooks"; @@ -23,7 +26,6 @@ import { type BackendIntent, } from "./lib/instanceInputForDefinition"; import { useCreatedAgentChannelAttachment } from "./useCreatedAgentChannelAttachment"; -import { classifyAgentManagementOrigin } from "./agentManagementBuffer"; import { useChannelsQuery } from "@/features/channels/hooks"; import { resolveManagedAgentAvatarUrl } from "./ui/managedAgentAvatar"; import type { AgentCreateIntent } from "./ui/agentCreateIntent"; @@ -59,96 +61,56 @@ function updateInputFromRequest( export function useAgentManagement() { const queryClient = useQueryClient(); + const identityQuery = useIdentityQuery(); + const currentPubkey = identityQuery.data?.pubkey; const personasQuery = usePersonasQuery(); - const managedAgentsQuery = useManagedAgentsQuery(); const channelsQuery = useChannelsQuery(); const runtimesQuery = useAcpRuntimesQuery({ enabled: true }); const createPersonaMutation = useCreatePersonaMutation(); const updatePersonaMutation = useUpdatePersonaMutation(); const createAgentMutation = useCreateManagedAgentMutation(); - const [request, setRequest] = React.useState( - null, - ); const [error, setError] = React.useState(null); const createdAgentAttachment = useCreatedAgentChannelAttachment(); - const seenRequestIds = React.useRef(new Set()); - const pendingRequestId = React.useRef(null); - const sourceAgentPubkey = React.useRef(null); - const managedAgentsRef = React.useRef(managedAgentsQuery.data); - const channelsRef = React.useRef(channelsQuery.data); - const bufferedRequestsRef = React.useRef< - Array<{ agentPubkey: string; request: AgentManagementRequest }> - >([]); + // In-session guard: a draft is resolved durably via its 44301 event, so this + // set only prevents double-publishing within this session. + const resolvedRequestIds = React.useRef(new Set()); - const acceptOwnedRequest = React.useEffectEvent( - (agentPubkey: string, next: AgentManagementRequest) => { - if ( - classifyAgentManagementOrigin( - managedAgentsRef.current, - channelsRef.current, - agentPubkey, - next.request.channelId, - ) !== "accept" || - seenRequestIds.current.has(next.requestId) - ) { - return; - } - seenRequestIds.current.add(next.requestId); - setError(null); - if (pendingRequestId.current === null) { - pendingRequestId.current = next.requestId; - sourceAgentPubkey.current = agentPubkey; - setRequest(next); - } - }, - ); + const nextDraft = useNextPendingAgentDraft(); + const request = nextDraft ? pendingDraftToRequest(nextDraft) : null; + const agentPubkey = nextDraft?.agentPubkey; - React.useEffect(() => { - managedAgentsRef.current = managedAgentsQuery.data; - channelsRef.current = channelsQuery.data; - if (managedAgentsQuery.data && channelsQuery.data) { - const buffered = bufferedRequestsRef.current.splice(0); - for (const candidate of buffered) { - acceptOwnedRequest(candidate.agentPubkey, candidate.request); - } + // Resolve the requesting agent's kind:0 profile for declared NIP-OA + // ownership (defence-in-depth; the relay already enforced is_agent_owner). + const usersBatch = useUsersBatchQuery(agentPubkey ? [agentPubkey] : [], { + enabled: Boolean(agentPubkey), + }); + const profiles = usersBatch.data?.profiles; + + const origin = React.useMemo(() => { + if (!request || !agentPubkey || !currentPubkey) { + return "buffer"; } - }, [channelsQuery.data, managedAgentsQuery.data]); + return classifyAgentDraftOrigin( + profiles, + channelsQuery.data, + agentPubkey, + request.channelId, + currentPubkey, + ); + }, [request, agentPubkey, currentPubkey, profiles, channelsQuery.data]); - React.useEffect( - () => - subscribeAgentManagementRequests((agentPubkey, next) => { - // Observer frames are owner-scoped and authenticated. Any managed agent - // this Desktop owns may draft a change; defer the ownership decision - // until the managed-agent query has initialized so ephemeral requests - // cannot disappear during startup. - if ( - classifyAgentManagementOrigin( - managedAgentsRef.current, - channelsRef.current, - agentPubkey, - next.request.channelId, - ) === "buffer" - ) { - bufferedRequestsRef.current.push({ agentPubkey, request: next }); - if (bufferedRequestsRef.current.length > 100) { - bufferedRequestsRef.current.shift(); - } - return; - } - acceptOwnedRequest(agentPubkey, next); - }), - [], - ); + // Only surface the dialog for an accepted draft. + const visibleRequest = origin === "accept" ? request : null; const matchingPersonas = React.useMemo(() => { - if (request?.action !== "update") return []; - const target = request.request.agentName.trim().toLocaleLowerCase(); + if (visibleRequest?.action !== "update") return []; + const target = visibleRequest.request.agentName.trim().toLocaleLowerCase(); return (personasQuery.data ?? []).filter( (persona) => persona.displayName.trim().toLocaleLowerCase() === target && requestTargetsEditablePersona(persona), ); - }, [personasQuery.data, request]); + }, [personasQuery.data, visibleRequest]); const currentPersona = matchingPersonas.length === 1 ? matchingPersonas[0] : undefined; @@ -161,7 +123,7 @@ export function useAgentManagement() { const targetChannel = (channelsQuery.data ?? []).find( (channel) => channel.id === channelId, ); - const requestingPubkey = sourceAgentPubkey.current?.toLowerCase(); + const requestingPubkey = agentPubkey?.toLowerCase(); if ( !targetChannel?.isMember || !requestingPubkey || @@ -175,17 +137,37 @@ export function useAgentManagement() { } } + async function publishResolution( + status: "accepted" | "declined", + agentPubkeySaved?: string, + ) { + if (!nextDraft) { + return; + } + if (resolvedRequestIds.current.has(nextDraft.requestId)) { + return; + } + resolvedRequestIds.current.add(nextDraft.requestId); + await resolveDraft({ + requestEventId: nextDraft.requestEventId, + requestId: nextDraft.requestId, + agentPubkey: nextDraft.agentPubkey, + status, + agentPubkeySaved, + }); + } + async function submitCreate( input: CreatePersonaInput | UpdatePersonaInput, intent: AgentCreateIntent, backendIntent: BackendIntent | null, ): Promise { - if (request?.action !== "create" || "id" in input) { + if (visibleRequest?.action !== "create" || "id" in input) { return false; } setError(null); try { - assertAgentCanActFromOrigin(request.request.channelId); + assertAgentCanActFromOrigin(visibleRequest.channelId); const runtimes = await availableRuntimesForStart(runtimesQuery); const runtime = runtimes.find( (candidate) => candidate.id === input.runtime, @@ -215,19 +197,19 @@ export function useAgentManagement() { ); if (created.spawnError) throw new Error(created.spawnError); const targetChannel = (channelsQuery.data ?? []).find( - (channel) => channel.id === request.request.channelId, + (channel) => channel.id === visibleRequest.channelId, ); await createdAgentAttachment.presentCreatedAgent(created, { - id: request.request.channelId, + id: visibleRequest.channelId, name: targetChannel?.name ?? "this channel", }); } + await publishResolution("accepted", agentPubkey); await Promise.all([ queryClient.invalidateQueries({ queryKey: personasQueryKey }), queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey }), ]); - dismiss(); return true; } catch (cause) { setError( @@ -238,18 +220,18 @@ export function useAgentManagement() { } async function submitUpdate(input: CreatePersonaInput | UpdatePersonaInput) { - if (request?.action !== "update" || !("id" in input)) { + if (visibleRequest?.action !== "update" || !("id" in input)) { return false; } setError(null); try { - assertAgentCanActFromOrigin(request.request.channelId); + assertAgentCanActFromOrigin(visibleRequest.channelId); await updatePersonaMutation.mutateAsync(input); + await publishResolution("accepted", agentPubkey); await Promise.all([ queryClient.invalidateQueries({ queryKey: personasQueryKey }), queryClient.invalidateQueries({ queryKey: managedAgentsQueryKey }), ]); - dismiss(); return true; } catch (cause) { setError( @@ -260,28 +242,29 @@ export function useAgentManagement() { } function dismiss() { - pendingRequestId.current = null; - sourceAgentPubkey.current = null; - setRequest(null); + // Closing the dialog without accepting declines the draft durably. + void publishResolution("declined"); } const createInitialValues = React.useMemo( () => - request?.action === "create" ? createInputFromRequest(request) : null, - [request], + visibleRequest?.action === "create" + ? createInputFromRequest(visibleRequest) + : null, + [visibleRequest], ); const editInitialValues = React.useMemo(() => { - if (request?.action !== "update" || !currentPersona) return null; + if (visibleRequest?.action !== "update" || !currentPersona) return null; return updateInputFromRequest( - request, + visibleRequest, editPersonaDialogState(currentPersona) .initialValues as UpdatePersonaInput, ); - }, [currentPersona, request]); + }, [currentPersona, visibleRequest]); const editError = React.useMemo(() => { - if (request?.action !== "update") return error; + if (visibleRequest?.action !== "update") return error; if (error) return error; if (matchingPersonas.length > 1) { return "More than one personal agent has that name. Rename it in Agents, then ask the agent again."; @@ -290,10 +273,10 @@ export function useAgentManagement() { return "Agents can only update a personal agent profile by its current name."; } return null; - }, [currentPersona, error, matchingPersonas.length, request]); + }, [currentPersona, error, matchingPersonas.length, visibleRequest]); return { - request, + request: visibleRequest, createInitialValues, editInitialValues, editError, diff --git a/desktop/src/features/communities/useCommunityInit.ts b/desktop/src/features/communities/useCommunityInit.ts index afa69f913f..af144e3149 100644 --- a/desktop/src/features/communities/useCommunityInit.ts +++ b/desktop/src/features/communities/useCommunityInit.ts @@ -26,6 +26,7 @@ import { } from "@/features/agents/activeAgentTurnsStore"; import { resetAgentWorkingSignal } from "@/features/agents/agentWorkingSignal"; import { resetAgentObserverStore } from "@/features/agents/observerRelayStore"; +import { resetAgentDraftStore } from "@/features/agents/agentDraftStore"; import { resetAvatarPresentations } from "@/features/profile/avatarPresentationStore"; import { resetAvatarProfileSync } from "@/features/profile/avatarProfileSync"; import { resetSidebarRelayConnectionCardState } from "@/features/sidebar/ui/useSidebarRelayConnectionCard"; @@ -54,6 +55,7 @@ function resetCommunityState({ resetRateLimitGate(); clearAllDrafts(); resetAgentObserverStore(); + resetAgentDraftStore(); resetActiveAgentTurnsStore(); resetAgentWorkingSignal(); if (isTauri() && isMacPlatform()) { diff --git a/desktop/src/shared/api/tauriAgentDrafts.ts b/desktop/src/shared/api/tauriAgentDrafts.ts new file mode 100644 index 0000000000..89a989a22e --- /dev/null +++ b/desktop/src/shared/api/tauriAgentDrafts.ts @@ -0,0 +1,59 @@ +import { invokeTauri } from "./tauri"; + +/** A pending NIP-AD agent draft surfaced to the owner for review. */ +export type PendingAgentDraft = { + requestEventId: string; + requestId: string; + action: "create" | "update"; + channelId: string; + agentPubkey: string; + createdAt: number; + displayName?: string; + systemPrompt?: string; + agentName?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; +}; + +export type AgentDraftResolutionStatus = "accepted" | "declined" | "superseded"; + +export type ResolveAgentDraftResult = { + eventId: string; + accepted: boolean; + message: string; +}; + +/** List pending (unresolved) agent drafts addressed to the current owner. */ +export async function listPendingAgentDrafts(): Promise { + return invokeTauri("list_pending_agent_drafts"); +} + +/** Resolve an agent draft by publishing a kind 44301 resolution. */ +export async function resolveAgentDraft(input: { + requestEventId: string; + requestId: string; + agentPubkey: string; + status: AgentDraftResolutionStatus; + agentPubkeySaved?: string; + reason?: string; +}): Promise { + const response = await invokeTauri<{ + event_id: string; + accepted: boolean; + message: string; + }>("resolve_agent_draft", { + requestEventId: input.requestEventId, + requestId: input.requestId, + agentPubkey: input.agentPubkey, + status: input.status, + agentPubkeySaved: input.agentPubkeySaved, + reason: input.reason, + }); + return { + eventId: response.event_id, + accepted: response.accepted, + message: response.message, + }; +} diff --git a/desktop/src/shared/constants/kinds.ts b/desktop/src/shared/constants/kinds.ts index ef3234f4c5..e3c5ec2878 100644 --- a/desktop/src/shared/constants/kinds.ts +++ b/desktop/src/shared/constants/kinds.ts @@ -55,6 +55,8 @@ export const KIND_MANAGED_AGENT = 30177; export const KIND_USER_STATUS = 30315; export const KIND_AGENT_OBSERVER_FRAME = 24200; export const KIND_AGENT_TURN_METRIC = 44200; +export const KIND_AGENT_DRAFT_REQUEST = 44300; +export const KIND_AGENT_DRAFT_RESOLUTION = 44301; export const KIND_EVENT_REMINDER = 30300; export const KIND_REPO_ANNOUNCEMENT = 30617; export const KIND_REPO_STATE = 30618; From 1c28f720353c30a88b7a5dd8abad906edb0369bf Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 17:40:41 -0700 Subject: [PATCH 10/20] feat(desktop): adoption UI + auth-tag handoff Create drafts now review through AgentDraftAdoptDialog whose primary action is 'Adopt this identity' (adopt_external_agent, no new keypair) with a separately-confirmed 'Import key to run from this Desktop' (import_external_agent_key). After adoption the minted BUZZ_AUTH_TAG is shown with a copy affordance. Adds the import_external_agent_key Tauri command and an AGENTS.md rule for BackendKind::External. Closes BrianInAz/buzz#18 (part 9/12) Signed-off-by: Brian Charbonneau --- .../src-tauri/src/commands/agent_adoption.rs | 173 +++++++++++++---- desktop/src-tauri/src/lib.rs | 2 +- desktop/src/features/agents/AGENTS.md | 12 ++ .../agents/ui/AgentDraftAdoptDialog.tsx | 180 ++++++++++++++++++ .../agents/ui/AgentManagementDialogs.tsx | 21 +- .../src/features/agents/useAgentManagement.ts | 72 +++++++ desktop/src/shared/api/tauriAgentDrafts.ts | 72 +++++++ 7 files changed, 478 insertions(+), 54 deletions(-) create mode 100644 desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx diff --git a/desktop/src-tauri/src/commands/agent_adoption.rs b/desktop/src-tauri/src/commands/agent_adoption.rs index 880d7e80f8..a4bc30cecb 100644 --- a/desktop/src-tauri/src/commands/agent_adoption.rs +++ b/desktop/src-tauri/src/commands/agent_adoption.rs @@ -24,63 +24,39 @@ pub struct AdoptExternalAgentResult { pub name: String, /// The minted NIP-OA auth tag (owner-attested). pub auth_tag: String, - /// Always `"external"`. + /// `"external"` for adoption, `"local"` for key import. pub backend: String, } -/// Adopt an existing agent identity (external pubkey) under the current owner. -/// -/// Attest-first: mints the NIP-OA `BUZZ_AUTH_TAG` from the agent's *public* -/// key and the owner's secret key — no new keypair is generated. The adopted -/// agent is stored with `BackendKind::External`, which the desktop refuses to -/// spawn/restart/deploy (fail closed). +/// Build a managed-agent record for an existing identity (no new keypair). #[allow(clippy::too_many_arguments)] -#[tauri::command] -pub async fn adopt_external_agent( - app: tauri::AppHandle, - state: State<'_, AppState>, - agent_pubkey: String, - display_name: String, +fn build_record( + state: &AppState, + agent_pubkey: &nostr::PublicKey, + display_name: &str, system_prompt: Option, - channel_id: Option, runtime: Option, provider: Option, model: Option, - respond_to: Option, -) -> Result { - let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) - .map_err(|e| format!("invalid agent pubkey: {e}"))?; - let agent_hex = agent_pubkey.to_hex(); - let display_name = display_name.trim().to_string(); - if display_name.is_empty() { - return Err("display name is required".to_string()); - } - let _ = channel_id; // channel identity is advisory; the agent drives itself - - // Attest-first: mint the NIP-OA auth tag from the owner's secret and the - // agent's public key. No new keypair. Fail closed on any mint error. + respond_to: RespondTo, + private_key_nsec: String, + backend: BackendKind, +) -> Result { let auth_tag = { let owner_keys = state.signing_keys()?; let compat_owner = nostr::Keys::parse(&owner_keys.secret_key().to_secret_hex()) .map_err(|e| format!("failed to bridge owner keys: {e}"))?; - buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &agent_pubkey, "") + buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, agent_pubkey, "") .map_err(|e| format!("failed to compute NIP-OA auth tag: {e}"))? }; - - let respond_to = match respond_to.as_deref() { - None | Some("owner-only") => RespondTo::OwnerOnly, - Some("anyone") => RespondTo::Anyone, - Some("allowlist") => RespondTo::Allowlist, - Some(other) => return Err(format!("invalid respond-to: {other}")), - }; - - let record = ManagedAgentRecord { + let agent_hex = agent_pubkey.to_hex(); + Ok(ManagedAgentRecord { pubkey: agent_hex.clone(), - name: display_name.clone(), + name: display_name.to_string(), persona_id: None, - private_key_nsec: String::new(), + private_key_nsec, auth_tag: Some(auth_tag.clone()), - relay_url: crate::relay::relay_api_base_url_with_override(&state), + relay_url: crate::relay::relay_api_base_url_with_override(state), avatar_url: None, acp_command: String::new(), agent_command: String::new(), @@ -98,7 +74,7 @@ pub async fn adopt_external_agent( env_vars: Default::default(), start_on_app_launch: false, runtime_pid: None, - backend: BackendKind::External, + backend, backend_agent_id: None, provider_binary_path: None, team_id: None, @@ -113,7 +89,7 @@ pub async fn adopt_external_agent( last_error_code: None, respond_to, respond_to_allowlist: vec![], - display_name: Some(display_name.clone()), + display_name: Some(display_name.to_string()), slug: None, runtime, name_pool: vec![], @@ -128,8 +104,59 @@ pub async fn adopt_external_agent( definition_respond_to: None, definition_respond_to_allowlist: vec![], definition_parallelism: None, + }) +} + +/// Adopt an existing agent identity (external pubkey) under the current owner. +/// +/// Attest-first: mints the NIP-OA `BUZZ_AUTH_TAG` from the agent's *public* +/// key and the owner's secret key — no new keypair is generated. The adopted +/// agent is stored with `BackendKind::External`, which the desktop refuses to +/// spawn/restart/deploy (fail closed). +#[allow(clippy::too_many_arguments)] +#[tauri::command] +pub async fn adopt_external_agent( + app: tauri::AppHandle, + state: State<'_, AppState>, + agent_pubkey: String, + display_name: String, + system_prompt: Option, + channel_id: Option, + runtime: Option, + provider: Option, + model: Option, + respond_to: Option, +) -> Result { + let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) + .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let agent_hex = agent_pubkey.to_hex(); + let display_name = display_name.trim().to_string(); + if display_name.is_empty() { + return Err("display name is required".to_string()); + } + let _ = channel_id; // channel identity is advisory; the agent drives itself + + let respond_to = match respond_to.as_deref() { + None | Some("owner-only") => RespondTo::OwnerOnly, + Some("anyone") => RespondTo::Anyone, + Some("allowlist") => RespondTo::Allowlist, + Some(other) => return Err(format!("invalid respond-to: {other}")), }; + let record = build_record( + &state, + &agent_pubkey, + &display_name, + system_prompt, + runtime, + provider, + model, + respond_to, + String::new(), + BackendKind::External, + )?; + let auth_tag = record.auth_tag.clone().unwrap_or_default(); + // Persist under the store lock, guarding against a duplicate pubkey. { let _store_guard = state @@ -152,6 +179,68 @@ pub async fn adopt_external_agent( }) } +/// Import an existing agent's private key so the desktop can run it locally. +/// +/// The `nsec` must match `agent_pubkey`. The agent is stored with +/// `BackendKind::Local` (it can be spawned/restarted by the desktop) and the +/// NIP-OA auth tag is minted from the owner's secret and the agent's public +/// key, exactly as adoption does. +#[tauri::command] +pub async fn import_external_agent_key( + app: tauri::AppHandle, + state: State<'_, AppState>, + agent_pubkey: String, + nsec: String, + display_name: String, +) -> Result { + let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) + .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let agent_hex = agent_pubkey.to_hex(); + let display_name = display_name.trim().to_string(); + if display_name.is_empty() { + return Err("display name is required".to_string()); + } + let keys = nostr::Keys::parse(nsec.trim()) + .map_err(|e| format!("invalid nsec: {e}"))?; + if keys.public_key() != agent_pubkey { + return Err("nsec does not match the agent pubkey".to_string()); + } + + let record = build_record( + &state, + &agent_pubkey, + &display_name, + None, + None, + None, + None, + RespondTo::OwnerOnly, + nsec.trim().to_string(), + BackendKind::Local, + )?; + let auth_tag = record.auth_tag.clone().unwrap_or_default(); + + { + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|e| e.to_string())?; + let mut records = load_managed_agents(&app)?; + if records.iter().any(|r| r.pubkey == agent_hex) { + return Err(format!("agent {agent_hex} already exists")); + } + records.push(record); + save_managed_agents(&app, &records)?; + } + + Ok(AdoptExternalAgentResult { + pubkey: agent_hex, + name: display_name, + auth_tag, + backend: "local".to_string(), + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 3e4de3ae76..e758eb09bc 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -798,7 +798,7 @@ pub fn run() { list_relay_agents, list_managed_agents, list_pending_agent_drafts, resolve_agent_draft, adopt_external_agent, - list_managed_agent_runtimes, + import_external_agent_key, list_managed_agent_runtimes, start_managed_agent_runtime, stop_managed_agent_runtime, restart_managed_agent_runtime, diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index d9222c7032..ccea9b45d4 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -147,6 +147,18 @@ with a TypeScript lookup table or an id comparison in a component. themselves. Never synthesize a run location a surface doesn't have. Don't expose `respond-to`, `allowlist`, Nostr, or harness jargon in primary UI copy. +12. **`BackendKind::External` agents have no launch controls by construction.** + An adopted external agent is an existing identity the owner attests to via + a minted NIP-OA `BUZZ_AUTH_TAG` — adoption never mints a keypair, and the + desktop refuses to spawn/restart/deploy it (fail closed on every + spawn/restart/deploy path). Its launch controls are absent because the + agent runs itself; the only handoff is the `BUZZ_AUTH_TAG` shown after + adoption. Do not add a "start" affordance for an `External` agent, and do + not route a create draft through the mint-a-new-keypair + `createManagedAgent` path — the review dialog's primary action for a + create draft is "Adopt this identity" (`adopt_external_agent`), with + "Import key to run from this Desktop" (`import_external_agent_key`) as a + separately-confirmed secondary. ## The tests that enforce this diff --git a/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx b/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx new file mode 100644 index 0000000000..450102bcef --- /dev/null +++ b/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx @@ -0,0 +1,180 @@ +import { useState } from "react"; + +import type { PendingAgentDraft } from "@/shared/api/tauriAgentDrafts"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { CopyButton } from "./CopyButton"; + +/** + * Review surface for a durable NIP-AD create draft. The primary action adopts + * the requesting agent's existing identity (no new keypair); a secondary, + * separately-confirmed action imports its private key so the desktop can run + * it locally. After a successful adopt, the minted `BUZZ_AUTH_TAG` is shown + * with a copy affordance. + */ +export function AgentDraftAdoptDialog({ + draft, + error, + isPending, + adoptedAuthTag, + onAdopt, + onImportKey, + onOpenChange, +}: { + draft: PendingAgentDraft; + error: string | null; + isPending: boolean; + adoptedAuthTag: string | null; + onAdopt: () => Promise; + onImportKey: (nsec: string) => Promise; + onOpenChange: (open: boolean) => void; +}) { + const [importing, setImporting] = useState(false); + const [nsec, setNsec] = useState(""); + + const handleImport = async () => { + setImporting(true); + try { + await onImportKey(nsec.trim()); + } finally { + setImporting(false); + } + }; + + return ( + + +
+ + Adopt this agent + + {draft.displayName ?? "An agent"} is asking to be registered as + yours. Adopting it attests that you own it — no new key is + created. + + + +
+
+

+ {draft.displayName ?? "Unnamed agent"} +

+ {draft.systemPrompt ? ( +

+ {draft.systemPrompt} +

+ ) : null} + + {draft.agentPubkey} + +
+ + {adoptedAuthTag ? ( +
+
+
+

+ BUZZ_AUTH_TAG +

+

+ Add this to the agent's environment where it runs. +

+
+ +
+ + {adoptedAuthTag} + +
+ ) : null} + + {error ? ( +

+ {error} +

+ ) : null} + + {importing ? ( +
+

+ Import key to run from this Desktop +

+

+ Paste the agent's private key (nsec). This lets Buzz run + the agent locally on this machine. +

+ setNsec(event.target.value)} + placeholder="nsec1…" + type="password" + value={nsec} + /> +
+ + +
+
+ ) : null} +
+ +
+ {!adoptedAuthTag ? ( + <> + + + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/desktop/src/features/agents/ui/AgentManagementDialogs.tsx b/desktop/src/features/agents/ui/AgentManagementDialogs.tsx index 0d01cbbcd1..55576e4c55 100644 --- a/desktop/src/features/agents/ui/AgentManagementDialogs.tsx +++ b/desktop/src/features/agents/ui/AgentManagementDialogs.tsx @@ -1,6 +1,7 @@ import { useAgentManagement } from "@/features/agents/useAgentManagement"; import { AgentCardDialogs } from "./AgentCardViewerDialog"; import { AgentDialog } from "./AgentDialog"; +import { AgentDraftAdoptDialog } from "./AgentDraftAdoptDialog"; import { SecretRevealDialog } from "./SecretRevealDialog"; /** Global review surfaces opened by owned agents through the Buzz harness. */ @@ -9,20 +10,18 @@ export function AgentManagementDialogs() { return ( <> - {management.request?.action === "create" ? ( - { if (!open) management.dismiss(); }} - onSubmitDefinition={management.submitCreate} - runtimes={management.runtimes} - runtimesLoading={management.runtimesLoading} /> ) : null} {management.createdAgent ? ( diff --git a/desktop/src/features/agents/useAgentManagement.ts b/desktop/src/features/agents/useAgentManagement.ts index 4fc6d9f73d..4e938f1e00 100644 --- a/desktop/src/features/agents/useAgentManagement.ts +++ b/desktop/src/features/agents/useAgentManagement.ts @@ -9,6 +9,10 @@ import { } from "./agentManagement"; import { resolveDraft, useNextPendingAgentDraft } from "./agentDraftStore"; import { classifyAgentDraftOrigin } from "./agentDraftTrust"; +import { + adoptExternalAgent, + importExternalAgentKey, +} from "@/shared/api/tauriAgentDrafts"; import { useIdentityQuery } from "@/shared/api/hooks"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import { @@ -70,6 +74,9 @@ export function useAgentManagement() { const updatePersonaMutation = useUpdatePersonaMutation(); const createAgentMutation = useCreateManagedAgentMutation(); const [error, setError] = React.useState(null); + const [adoptedAuthTag, setAdoptedAuthTag] = React.useState( + null, + ); const createdAgentAttachment = useCreatedAgentChannelAttachment(); // In-session guard: a draft is resolved durably via its 44301 event, so this // set only prevents double-publishing within this session. @@ -246,6 +253,67 @@ export function useAgentManagement() { void publishResolution("declined"); } + /** Adopt the requesting agent's existing identity (no new keypair). */ + async function adopt(): Promise { + if (!nextDraft || nextDraft.action !== "create") { + return false; + } + setError(null); + try { + assertAgentCanActFromOrigin(nextDraft.channelId); + const result = await adoptExternalAgent({ + agentPubkey: nextDraft.agentPubkey, + displayName: nextDraft.displayName ?? "", + systemPrompt: nextDraft.systemPrompt, + channelId: nextDraft.channelId, + runtime: nextDraft.runtime, + provider: nextDraft.provider, + model: nextDraft.model, + respondTo: nextDraft.respondTo, + }); + setAdoptedAuthTag(result.authTag); + await publishResolution("accepted", nextDraft.agentPubkey); + await queryClient.invalidateQueries({ + queryKey: managedAgentsQueryKey, + }); + return true; + } catch (cause) { + setError( + cause instanceof Error ? cause.message : "Could not adopt this agent.", + ); + return false; + } + } + + /** Import the agent's private key so the desktop can run it locally. */ + async function importKey(nsec: string): Promise { + if (!nextDraft || nextDraft.action !== "create") { + return false; + } + setError(null); + try { + assertAgentCanActFromOrigin(nextDraft.channelId); + const result = await importExternalAgentKey({ + agentPubkey: nextDraft.agentPubkey, + nsec, + displayName: nextDraft.displayName ?? "", + }); + setAdoptedAuthTag(result.authTag); + await publishResolution("accepted", nextDraft.agentPubkey); + await queryClient.invalidateQueries({ + queryKey: managedAgentsQueryKey, + }); + return true; + } catch (cause) { + setError( + cause instanceof Error + ? cause.message + : "Could not import this agent key.", + ); + return false; + } + } + const createInitialValues = React.useMemo( () => visibleRequest?.action === "create" @@ -277,16 +345,20 @@ export function useAgentManagement() { return { request: visibleRequest, + nextDraft, createInitialValues, editInitialValues, editError, error, + adoptedAuthTag, ...createdAgentAttachment, isPending, runtimes: runtimesQuery.data ?? [], runtimesLoading: runtimesQuery.isLoading, submitCreate, submitUpdate, + adopt, + importKey, dismiss, }; } diff --git a/desktop/src/shared/api/tauriAgentDrafts.ts b/desktop/src/shared/api/tauriAgentDrafts.ts index 89a989a22e..8ad32603a1 100644 --- a/desktop/src/shared/api/tauriAgentDrafts.ts +++ b/desktop/src/shared/api/tauriAgentDrafts.ts @@ -57,3 +57,75 @@ export async function resolveAgentDraft(input: { message: response.message, }; } + +export type AdoptExternalAgentResult = { + pubkey: string; + name: string; + authTag: string; + backend: string; +}; + +/** + * Adopt an existing agent identity under the current owner. Attest-first: + * mints the NIP-OA `BUZZ_AUTH_TAG` from the agent's public key and the owner's + * secret — no new keypair is generated. + */ +export async function adoptExternalAgent(input: { + agentPubkey: string; + displayName: string; + systemPrompt?: string; + channelId?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; +}): Promise { + const response = await invokeTauri<{ + pubkey: string; + name: string; + auth_tag: string; + backend: string; + }>("adopt_external_agent", { + agentPubkey: input.agentPubkey, + displayName: input.displayName, + systemPrompt: input.systemPrompt, + channelId: input.channelId, + runtime: input.runtime, + provider: input.provider, + model: input.model, + respondTo: input.respondTo, + }); + return { + pubkey: response.pubkey, + name: response.name, + authTag: response.auth_tag, + backend: response.backend, + }; +} + +/** + * Import an existing agent's private key so the desktop can run it locally. + * The `nsec` must match `agentPubkey`. + */ +export async function importExternalAgentKey(input: { + agentPubkey: string; + nsec: string; + displayName: string; +}): Promise { + const response = await invokeTauri<{ + pubkey: string; + name: string; + auth_tag: string; + backend: string; + }>("import_external_agent_key", { + agentPubkey: input.agentPubkey, + nsec: input.nsec, + displayName: input.displayName, + }); + return { + pubkey: response.pubkey, + name: response.name, + authTag: response.auth_tag, + backend: response.backend, + }; +} From e5a57cd5a86eabc9e07240368dbff84bd52303c9 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 17:40:55 -0700 Subject: [PATCH 11/20] refactor(desktop): remove obsolete agentManagementBuffer classifyAgentManagementOrigin is replaced by classifyAgentDraftOrigin (agentDraftTrust.ts) which keys on declared NIP-OA ownership instead of local managed-agent membership. Closes BrianInAz/buzz#18 (part 8 cleanup) Signed-off-by: Brian Charbonneau --- .../agents/agentManagementBuffer.test.mjs | 62 ------------------- .../features/agents/agentManagementBuffer.ts | 29 --------- 2 files changed, 91 deletions(-) delete mode 100644 desktop/src/features/agents/agentManagementBuffer.test.mjs delete mode 100644 desktop/src/features/agents/agentManagementBuffer.ts diff --git a/desktop/src/features/agents/agentManagementBuffer.test.mjs b/desktop/src/features/agents/agentManagementBuffer.test.mjs deleted file mode 100644 index d676759046..0000000000 --- a/desktop/src/features/agents/agentManagementBuffer.test.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { classifyAgentManagementOrigin } from "./agentManagementBuffer.ts"; - -const AGENT = "a".repeat(64); -const CHANNEL = "channel-1"; -const OWNED_AGENT = [{ pubkey: AGENT }]; -const SHARED_CHANNEL = [ - { id: CHANNEL, isMember: true, memberPubkeys: [AGENT] }, -]; - -test("buffers a draft until ownership and channel data resolve", () => { - assert.equal( - classifyAgentManagementOrigin(undefined, SHARED_CHANNEL, AGENT, CHANNEL), - "buffer", - ); - assert.equal( - classifyAgentManagementOrigin(OWNED_AGENT, undefined, AGENT, CHANNEL), - "buffer", - ); -}); - -test("accepts an owned agent drafting from a shared channel", () => { - assert.equal( - classifyAgentManagementOrigin(OWNED_AGENT, SHARED_CHANNEL, AGENT, CHANNEL), - "accept", - ); -}); - -test("rejects a draft when the owner or agent is outside the claimed channel", () => { - assert.equal( - classifyAgentManagementOrigin( - OWNED_AGENT, - [{ id: CHANNEL, isMember: false, memberPubkeys: [AGENT] }], - AGENT, - CHANNEL, - ), - "reject", - ); - assert.equal( - classifyAgentManagementOrigin( - OWNED_AGENT, - [{ id: CHANNEL, isMember: true, memberPubkeys: [] }], - AGENT, - CHANNEL, - ), - "reject", - ); -}); - -test("rejects a draft from an agent this Desktop does not own", () => { - assert.equal( - classifyAgentManagementOrigin( - [{ pubkey: "b".repeat(64) }], - SHARED_CHANNEL, - AGENT, - CHANNEL, - ), - "reject", - ); -}); diff --git a/desktop/src/features/agents/agentManagementBuffer.ts b/desktop/src/features/agents/agentManagementBuffer.ts deleted file mode 100644 index ffb9faf156..0000000000 --- a/desktop/src/features/agents/agentManagementBuffer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Channel, ManagedAgent } from "@/shared/api/types"; - -/** - * Defers the trust decision until both ownership and channel membership have - * initialized. A draft may open only when its owned sender and the owner share - * the claimed originating channel. - */ -export function classifyAgentManagementOrigin( - agents: readonly Pick[] | undefined, - channels: - | readonly Pick[] - | undefined, - agentPubkey: string, - channelId: string, -): "buffer" | "accept" | "reject" { - if (agents === undefined || channels === undefined) return "buffer"; - const normalizedAgentPubkey = agentPubkey.toLowerCase(); - const isOwnedAgent = agents.some( - (agent) => agent.pubkey.toLowerCase() === normalizedAgentPubkey, - ); - const originChannel = channels.find((channel) => channel.id === channelId); - return isOwnedAgent && - originChannel?.isMember === true && - originChannel.memberPubkeys.some( - (pubkey) => pubkey.toLowerCase() === normalizedAgentPubkey, - ) - ? "accept" - : "reject"; -} From 8447d9c31f4153ca82d5e611539135fc176f4e43 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 17:44:01 -0700 Subject: [PATCH 12/20] fix(desktop): open observer REQ on identity, not agents.length B2 cold-start fix: useManagedAgentObserverBridge now opens the owner-global 24200 REQ whenever an identity is known, regardless of agents.length, so a newly adopted external agent's live telemetry is subscribed even when no agent existed before. knownAgentPubkeys remains the telemetry decrypt gate. Removes the now-obsolete shouldObserveManagedAgents gate and its test. Closes BrianInAz/buzz#18 (part 10/12) Signed-off-by: Brian Charbonneau --- .../src/features/agents/observerRelayStore.ts | 17 ++++++++--------- .../observerRelaySubscriptionGate.test.mjs | 15 --------------- 2 files changed, 8 insertions(+), 24 deletions(-) delete mode 100644 desktop/src/features/agents/observerRelaySubscriptionGate.test.mjs diff --git a/desktop/src/features/agents/observerRelayStore.ts b/desktop/src/features/agents/observerRelayStore.ts index 179c1e1835..679f32f22a 100644 --- a/desktop/src/features/agents/observerRelayStore.ts +++ b/desktop/src/features/agents/observerRelayStore.ts @@ -9,6 +9,7 @@ import { getIdentity } from "@/shared/api/tauriIdentity"; import { decryptObserverEvent } from "@/shared/api/tauriObserver"; import { normalizePubkey } from "@/shared/lib/pubkey"; import { useQueryClient } from "@tanstack/react-query"; +import { useIdentityQuery } from "@/shared/api/hooks"; import { agentConfigSurfaceQueryKey } from "@/features/agents/hooks"; import type { ConnectionState, @@ -567,17 +568,15 @@ export function getAgentTranscript( return state?.items ?? EMPTY_TRANSCRIPT; } -export function shouldObserveManagedAgents( - agents: readonly Pick[], -): boolean { - return agents.length > 0; -} - export function useManagedAgentObserverBridge( agents: readonly Pick[], ) { const subscriptionId = React.useId(); - const hasManagedAgent = shouldObserveManagedAgents(agents); + const identityQuery = useIdentityQuery(); + // B2 cold-start fix: open the owner-global 24200 REQ whenever an identity is + // known, regardless of `agents.length`. A newly adopted external agent's + // live telemetry must be subscribed even when no agent existed before. + const hasIdentity = Boolean(identityQuery.data?.pubkey); const agentPubkeys = React.useMemo( () => agents.map((agent) => agent.pubkey), @@ -595,11 +594,11 @@ export function useManagedAgentObserverBridge( }, [subscriptionId, agentPubkeys]); React.useEffect(() => { - if (!hasManagedAgent) { + if (!hasIdentity) { return; } void ensureRelayObserverSubscription(); - }, [hasManagedAgent]); + }, [hasIdentity]); // Wire up config-surface query invalidation when session_config_captured fires. const queryClient = useQueryClient(); diff --git a/desktop/src/features/agents/observerRelaySubscriptionGate.test.mjs b/desktop/src/features/agents/observerRelaySubscriptionGate.test.mjs deleted file mode 100644 index e776eeeb92..0000000000 --- a/desktop/src/features/agents/observerRelaySubscriptionGate.test.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { shouldObserveManagedAgents } from "./observerRelayStore.ts"; - -test("observer ingestion opens for a cold stopped managed agent", () => { - assert.equal( - shouldObserveManagedAgents([{ pubkey: "aa", status: "stopped" }]), - true, - ); -}); - -test("observer ingestion stays closed when there are no owned agents", () => { - assert.equal(shouldObserveManagedAgents([]), false); -}); From d31ebe970d22cdb5c5b65a3a294fbeda0e168433 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 17:59:08 -0700 Subject: [PATCH 13/20] test(e2e): NIP-AD agent draft relay integration Covers every read chokepoint (REQ, kindless ids, COUNT, live fan-out) for kinds 44300/44301: owner/agent read, third party closed out, ingest envelope rejections (no owner binding, wrong p cardinality, h tag, non-NIP-44, wrong author), resolution e-tag join, and FTS exclusion. Closes BrianInAz/buzz#18 (part 11/12) Signed-off-by: Brian Charbonneau --- .../buzz-test-client/tests/e2e_agent_draft.rs | 736 ++++++++++++++++++ 1 file changed, 736 insertions(+) create mode 100644 crates/buzz-test-client/tests/e2e_agent_draft.rs diff --git a/crates/buzz-test-client/tests/e2e_agent_draft.rs b/crates/buzz-test-client/tests/e2e_agent_draft.rs new file mode 100644 index 0000000000..59abef49d6 --- /dev/null +++ b/crates/buzz-test-client/tests/e2e_agent_draft.rs @@ -0,0 +1,736 @@ +//! End-to-end tests for NIP-AD agent drafts (kinds 44300/44301). +//! +//! Kinds 44300/44301 are durable, p-gated, FTS-excluded events. These tests +//! assert the wire behaviour of that gate at every read chokepoint (REQ, +//! kindless `ids` lookup, COUNT, live fan-out) plus the ingest envelope rules +//! that make the gate sound: +//! - exactly two `p` tags (owner + agent, `owner != agent`), one `agent` tag, +//! no `h` tag, NIP-44 content; +//! - 44300 authored by the agent, 44301 authored by the owner; +//! - `is_agent_owner` must hold (established via NIP-OA auth). +//! +//! # Running +//! +//! Start the relay, then run: +//! +//! ```text +//! RELAY_URL=ws://localhost:3000 cargo test --test e2e_agent_draft -- --ignored +//! ``` + +use std::time::Duration; + +use buzz_core::agent_draft::{ + encrypt_agent_draft_request, encrypt_agent_draft_resolution, AgentDraftAction, + AgentDraftCreateRequest, AgentDraftRequest, AgentDraftRequestPayload, + AgentDraftResolutionPayload, AgentDraftResolutionStatus, AGENT_DRAFT_VERSION, +}; +use buzz_sdk::nip_oa; +use buzz_test_client::{BuzzTestClient, RelayMessage}; +use nostr::{EventBuilder, Filter, Keys, Kind, Tag}; + +const DRAFT_REQUEST_KIND: u16 = 44300; +const DRAFT_RESOLUTION_KIND: u16 = 44301; + +fn relay_url() -> String { + std::env::var("RELAY_URL").unwrap_or_else(|_| "ws://localhost:3000".to_string()) +} + +fn sub_id(name: &str) -> String { + format!("e2e-agent-draft-{name}-{}", uuid::Uuid::new_v4()) +} + +/// Build a NIP-OA auth tag for `agent_keys` signed by `owner_keys`. +fn make_nip_oa_auth_tag(owner_keys: &Keys, agent_keys: &Keys) -> Tag { + let tag_json = nip_oa::compute_auth_tag(owner_keys, &agent_keys.public_key(), "") + .expect("compute_auth_tag"); + nip_oa::parse_auth_tag(&tag_json).expect("parse_auth_tag") +} + +/// Connect `agent_keys` with NIP-OA, establishing owner→agent in the DB. +async fn connect_agent_with_owner(agent_keys: &Keys, owner_keys: &Keys) -> BuzzTestClient { + let url = relay_url(); + let auth_tag = make_nip_oa_auth_tag(owner_keys, agent_keys); + let mut client = BuzzTestClient::connect_unauthenticated(&url) + .await + .expect("connect agent unauthenticated"); + client + .authenticate_with_nip_oa(agent_keys, &auth_tag) + .await + .expect("NIP-OA auth"); + client +} + +fn sample_create_payload(request_id: &str) -> AgentDraftRequestPayload { + AgentDraftRequestPayload { + version: AGENT_DRAFT_VERSION, + request_id: request_id.to_string(), + action: AgentDraftAction::Create, + timestamp: "2026-08-05T12:00:00.000Z".to_string(), + channel_id: "f0347328-e105-4e62-9af8-807d20e484dd".to_string(), + request: AgentDraftRequest::Create(AgentDraftCreateRequest { + display_name: "dev-coder".to_string(), + system_prompt: "You are a coding specialist.".to_string(), + }), + } +} + +/// Build a signed kind:44300 draft request event. +fn build_draft_request( + agent_keys: &Keys, + owner_pubkey: &nostr::PublicKey, + payload: &AgentDraftRequestPayload, +) -> nostr::Event { + let encrypted = + encrypt_agent_draft_request(agent_keys, owner_pubkey, payload).expect("encrypt request"); + EventBuilder::new(Kind::Custom(DRAFT_REQUEST_KIND), encrypted) + .tags([ + Tag::parse(["p", &owner_pubkey.to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(agent_keys) + .expect("sign request") +} + +/// Build a signed kind:44301 draft resolution event. +fn build_draft_resolution( + owner_keys: &Keys, + agent_pubkey: &nostr::PublicKey, + request_event_id: &str, + request_id: &str, +) -> nostr::Event { + let payload = AgentDraftResolutionPayload { + version: AGENT_DRAFT_VERSION, + request_id: request_id.to_string(), + status: AgentDraftResolutionStatus::Accepted, + timestamp: "2026-08-05T12:05:00.000Z".to_string(), + agent_pubkey: Some(agent_pubkey.to_hex()), + reason: None, + }; + let encrypted = + encrypt_agent_draft_resolution(owner_keys, agent_pubkey, &payload).expect("encrypt res"); + EventBuilder::new(Kind::Custom(DRAFT_RESOLUTION_KIND), encrypted) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_pubkey.to_hex()]).unwrap(), + Tag::parse(["agent", &agent_pubkey.to_hex()]).unwrap(), + Tag::parse(["e", request_event_id]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(owner_keys) + .expect("sign resolution") +} + +fn owner_p_filter(owner: &Keys) -> Filter { + Filter::new() + .kind(Kind::Custom(DRAFT_REQUEST_KIND)) + .custom_tags( + nostr::SingleLetterTag::lowercase(nostr::Alphabet::P), + [owner.public_key().to_hex()], + ) +} + +/// Expect the next message to be a CLOSED with a `restricted:` reason. +async fn expect_closed(client: &mut BuzzTestClient, sid: &str) { + match client.recv_event(Duration::from_secs(5)).await { + Ok(RelayMessage::Closed { + subscription_id, + message, + }) => { + assert_eq!(subscription_id, sid); + assert!( + message.contains("restricted:"), + "expected restricted refusal, got: {message}" + ); + } + other => panic!("expected CLOSED, got: {other:?}"), + } +} + +/// Owner reads their pending drafts via REQ. +#[tokio::test] +#[ignore] +async fn test_agent_draft_owner_reads_pending() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let event = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let event_id = event.id; + let ok = agent.send_event(event).await.expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + + let mut owner = BuzzTestClient::connect(&url, &owner_keys) + .await + .expect("connect owner"); + let sid = sub_id("owner-read"); + owner + .subscribe(&sid, vec![owner_p_filter(&owner_keys)]) + .await + .expect("subscribe"); + let events = owner + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + assert!( + events.iter().any(|e| e.id == event_id), + "owner must read their pending draft" + ); + + agent.disconnect().await.expect("disconnect agent"); + owner.disconnect().await.expect("disconnect owner"); +} + +/// The requesting agent reads back its own draft (second `p` tag). +#[tokio::test] +#[ignore] +async fn test_agent_draft_agent_reads_own() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let event = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let event_id = event.id; + let ok = agent.send_event(event).await.expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + + let sid = sub_id("agent-read"); + agent + .subscribe( + &sid, + vec![Filter::new() + .kind(Kind::Custom(DRAFT_REQUEST_KIND)) + .custom_tags( + nostr::SingleLetterTag::lowercase(nostr::Alphabet::P), + [agent_keys.public_key().to_hex()], + )], + ) + .await + .expect("subscribe"); + let events = agent + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + assert!( + events.iter().any(|e| e.id == event_id), + "agent must read back its own draft" + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// A third party gets nothing via REQ. +#[tokio::test] +#[ignore] +async fn test_agent_draft_third_party_gets_nothing() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let third_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let event = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let ok = agent.send_event(event).await.expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + agent.disconnect().await.expect("disconnect agent"); + + let mut third = BuzzTestClient::connect(&url, &third_keys) + .await + .expect("connect third"); + let sid = sub_id("third-req"); + third + .subscribe(&sid, vec![owner_p_filter(&owner_keys)]) + .await + .expect("subscribe"); + // The p-gate closes a `#p` filter that does not name the reader. + expect_closed(&mut third, &sid).await; + + third.disconnect().await.expect("disconnect third"); +} + +/// Kindless `{ids:[known]}` returns nothing to a third party. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ids_lookup_third_party_gets_nothing() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let third_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let event = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let event_id = event.id; + let ok = agent.send_event(event).await.expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + agent.disconnect().await.expect("disconnect agent"); + + let mut third = BuzzTestClient::connect(&url, &third_keys) + .await + .expect("connect third"); + let sid = sub_id("third-ids"); + third + .subscribe(&sid, vec![Filter::new().id(event_id)]) + .await + .expect("subscribe"); + let events = third + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + assert!( + events.is_empty(), + "kindless ids lookup must return nothing to a third party" + ); + + third.disconnect().await.expect("disconnect third"); +} + +/// COUNT excludes the draft for a third party. +#[tokio::test] +#[ignore] +async fn test_agent_draft_count_third_party_gets_nothing() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let third_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let ok = agent + .send_event(build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + )) + .await + .expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + agent.disconnect().await.expect("disconnect agent"); + + let mut third = BuzzTestClient::connect(&url, &third_keys) + .await + .expect("connect third"); + let sid = sub_id("third-count"); + let count_msg = serde_json::json!(["COUNT", sid, owner_p_filter(&owner_keys)]); + third.send_raw(&count_msg).await.expect("send COUNT"); + // The p-gate either closes the COUNT or returns 0 — either way the third + // party learns nothing about the draft. + match third.recv_event(Duration::from_secs(5)).await { + Ok(RelayMessage::Count { count, .. }) => { + assert_eq!(count, 0, "third party COUNT must be 0, got {count}") + } + Ok(RelayMessage::Closed { message, .. }) => { + assert!( + message.contains("restricted:"), + "expected restricted COUNT refusal, got: {message}" + ) + } + Ok(other) => panic!("unexpected COUNT message: {other:?}"), + Err(e) => panic!("COUNT error: {e}"), + } + + third.disconnect().await.expect("disconnect third"); +} + +/// Live fan-out does not deliver the draft to a third party. +#[tokio::test] +#[ignore] +async fn test_agent_draft_live_fanout_third_party_gets_nothing() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let third_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut third = BuzzTestClient::connect(&url, &third_keys) + .await + .expect("connect third"); + let sid = sub_id("third-fanout"); + third + .subscribe(&sid, vec![owner_p_filter(&owner_keys)]) + .await + .expect("subscribe"); + // The p-gate closes the subscription before any live delivery. + expect_closed(&mut third, &sid).await; + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let event = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let event_id = event.id; + let ok = agent.send_event(event).await.expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + + // No live event may reach the third party. + match third.recv_event(Duration::from_millis(750)).await { + Err(buzz_test_client::TestClientError::Timeout) => {} + Ok(RelayMessage::Event { event, .. }) if event.id == event_id => { + panic!("draft leaked to third-party live subscription") + } + Ok(_) => {} + Err(e) => panic!("unexpected fan-out error: {e}"), + } + + agent.disconnect().await.expect("disconnect agent"); + third.disconnect().await.expect("disconnect third"); +} + +/// Ingest rejects a draft with no agent-owner binding. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ingest_rejects_no_owner_binding() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + // Agent connects WITHOUT NIP-OA — no owner binding. + let mut agent = BuzzTestClient::connect(&url, &agent_keys) + .await + .expect("connect agent"); + let event = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let ok = agent.send_event(event).await.expect("send draft"); + assert!(!ok.accepted, "draft without owner binding must be rejected"); + assert!( + ok.message.contains("restricted:"), + "expected restricted refusal, got: {}", + ok.message + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// Ingest rejects wrong `p` cardinality. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ingest_rejects_wrong_p_cardinality() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let encrypted = encrypt_agent_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ) + .expect("encrypt"); + // Only one `p` tag. + let event = EventBuilder::new(Kind::Custom(DRAFT_REQUEST_KIND), encrypted) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(&agent_keys) + .expect("sign"); + let ok = agent.send_event(event).await.expect("send draft"); + assert!(!ok.accepted, "wrong p cardinality must be rejected"); + assert!( + ok.message.contains("invalid:"), + "expected invalid refusal, got: {}", + ok.message + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// Ingest rejects a draft carrying an `h` tag. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ingest_rejects_h_tag() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let encrypted = encrypt_agent_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ) + .expect("encrypt"); + let event = EventBuilder::new(Kind::Custom(DRAFT_REQUEST_KIND), encrypted) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["h", "some-channel"]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(&agent_keys) + .expect("sign"); + let ok = agent.send_event(event).await.expect("send draft"); + assert!(!ok.accepted, "draft with h tag must be rejected"); + assert!( + ok.message.contains("invalid:"), + "expected invalid refusal, got: {}", + ok.message + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// Ingest rejects non-NIP-44 content. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ingest_rejects_non_nip44_content() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let event = EventBuilder::new(Kind::Custom(DRAFT_REQUEST_KIND), "not-a-ciphertext") + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(&agent_keys) + .expect("sign"); + let ok = agent.send_event(event).await.expect("send draft"); + assert!(!ok.accepted, "non-NIP-44 content must be rejected"); + assert!( + ok.message.contains("invalid:"), + "expected invalid refusal, got: {}", + ok.message + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// Ingest rejects a 44300 authored by someone other than the agent. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ingest_rejects_wrong_author_44300() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let encrypted = encrypt_agent_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ) + .expect("encrypt"); + // Signed by the OWNER, not the agent — event.pubkey != agent tag. + let event = EventBuilder::new(Kind::Custom(DRAFT_REQUEST_KIND), encrypted) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(&owner_keys) + .expect("sign"); + let ok = agent.send_event(event).await.expect("send draft"); + assert!(!ok.accepted, "44300 authored by non-agent must be rejected"); + assert!( + ok.message.contains("invalid:"), + "expected invalid refusal, got: {}", + ok.message + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// Ingest rejects a 44301 authored by someone other than the owner. +#[tokio::test] +#[ignore] +async fn test_agent_draft_ingest_rejects_wrong_author_44301() { + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + let request_event_id = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let payload = AgentDraftResolutionPayload { + version: AGENT_DRAFT_VERSION, + request_id: request_id.clone(), + status: AgentDraftResolutionStatus::Accepted, + timestamp: "2026-08-05T12:05:00.000Z".to_string(), + agent_pubkey: Some(agent_keys.public_key().to_hex()), + reason: None, + }; + let encrypted = encrypt_agent_draft_resolution(&owner_keys, &agent_keys.public_key(), &payload) + .expect("encrypt res"); + // Signed by the AGENT, not the owner — event.pubkey != owner p tag. + let event = EventBuilder::new(Kind::Custom(DRAFT_RESOLUTION_KIND), encrypted) + .tags([ + Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["e", request_event_id]).unwrap(), + ]) + .allow_self_tagging() + .sign_with_keys(&agent_keys) + .expect("sign"); + let ok = agent.send_event(event).await.expect("send resolution"); + assert!(!ok.accepted, "44301 authored by non-owner must be rejected"); + assert!( + ok.message.contains("invalid:"), + "expected invalid refusal, got: {}", + ok.message + ); + + agent.disconnect().await.expect("disconnect agent"); +} + +/// A 44301 resolution retires the draft from a pending query. +#[tokio::test] +#[ignore] +async fn test_agent_draft_resolution_retires_draft() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let request = build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &sample_create_payload(&request_id), + ); + let request_event_id = request.id.to_hex(); + let ok = agent.send_event(request).await.expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + + let mut owner = BuzzTestClient::connect(&url, &owner_keys) + .await + .expect("connect owner"); + let resolution = build_draft_resolution( + &owner_keys, + &agent_keys.public_key(), + &request_event_id, + &request_id, + ); + let resolution_id = resolution.id; + let ok = owner.send_event(resolution).await.expect("send resolution"); + assert!(ok.accepted, "resolution rejected: {}", ok.message); + + // The owner can read the resolution back, and its `e` tag references the + // request event id — the join a client uses to retire the draft from its + // pending list. + let sid = sub_id("resolution-read"); + let resolution_filter = Filter::new() + .kind(Kind::Custom(DRAFT_RESOLUTION_KIND)) + .custom_tags( + nostr::SingleLetterTag::lowercase(nostr::Alphabet::P), + [owner_keys.public_key().to_hex()], + ); + owner + .subscribe(&sid, vec![resolution_filter]) + .await + .expect("subscribe"); + let events = owner + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + let resolution_event = events + .iter() + .find(|e| e.id == resolution_id) + .expect("owner must read the resolution"); + let e_tag = resolution_event + .tags + .iter() + .find_map(|t| { + let parts = t.as_slice(); + if parts.first().map(|p| p.as_str()) == Some("e") { + parts.get(1).map(|v| v.as_str()) + } else { + None + } + }) + .expect("resolution must carry an e tag"); + assert_eq!( + e_tag, request_event_id, + "resolution e tag must reference the request event id" + ); + + agent.disconnect().await.expect("disconnect agent"); + owner.disconnect().await.expect("disconnect owner"); +} + +/// The draft is not FTS-discoverable via a NIP-50 search filter. +#[tokio::test] +#[ignore] +async fn test_agent_draft_not_fts_discoverable() { + let url = relay_url(); + let owner_keys = Keys::generate(); + let agent_keys = Keys::generate(); + let request_id = uuid::Uuid::new_v4().to_string(); + let unique_token = format!("draftnosearch_{}", uuid::Uuid::new_v4().simple()); + + let mut agent = connect_agent_with_owner(&agent_keys, &owner_keys).await; + let mut payload = sample_create_payload(&request_id); + if let AgentDraftRequest::Create(c) = &mut payload.request { + c.system_prompt = format!("{unique_token} secret instructions"); + } + let ok = agent + .send_event(build_draft_request( + &agent_keys, + &owner_keys.public_key(), + &payload, + )) + .await + .expect("send draft"); + assert!(ok.accepted, "draft rejected: {}", ok.message); + agent.disconnect().await.expect("disconnect agent"); + + let mut owner = BuzzTestClient::connect(&url, &owner_keys) + .await + .expect("connect owner"); + let sid = sub_id("fts"); + // Scope the search to the owner's drafts so the p-gate is satisfied; the + // draft must still not surface because its `search_tsv` is NULL. + let search_filter = Filter::new() + .kind(Kind::Custom(DRAFT_REQUEST_KIND)) + .custom_tags( + nostr::SingleLetterTag::lowercase(nostr::Alphabet::P), + [owner_keys.public_key().to_hex()], + ) + .search(&unique_token); + owner + .subscribe(&sid, vec![search_filter]) + .await + .expect("subscribe"); + let events = owner + .collect_until_eose(&sid, Duration::from_secs(5)) + .await + .expect("collect"); + assert!( + events.is_empty(), + "draft must not be FTS-discoverable, got {} events", + events.len() + ); + + owner.disconnect().await.expect("disconnect owner"); +} From ba8826c94eb3856af7f90112d010d6d3bf2809f2 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 18:17:55 -0700 Subject: [PATCH 14/20] test(desktop): agent draft review Playwright spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds agent-draft-review.spec.ts (smoke project) covering: draft arrives → review dialog opens; adopt → BUZZ_AUTH_TAG shown with copy; decline → resolution published and the draft does not resurface after reload. Adds mock bridge handlers for list_pending_agent_drafts / resolve_agent_draft / adopt_external_agent, a pendingAgentDrafts seed, and the __BUZZ_E2E_EMIT_MOCK_AGENT_DRAFT__ helper. Keeps the adopt dialog mounted after adoption so the auth tag stays visible. Closes BrianInAz/buzz#18 (part 11/12) Signed-off-by: Brian Charbonneau --- desktop/playwright.config.ts | 1 + .../agents/ui/AgentDraftAdoptDialog.tsx | 30 +-- .../agents/ui/AgentManagementDialogs.tsx | 14 ++ desktop/src/shared/api/types.ts | 6 +- desktop/src/testing/e2eBridge.ts | 189 +++++++++++++++++- desktop/tests/e2e/agent-draft-review.spec.ts | 87 ++++++++ desktop/tests/helpers/bridge.ts | 16 ++ 7 files changed, 325 insertions(+), 18 deletions(-) create mode 100644 desktop/tests/e2e/agent-draft-review.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 4b7f73a8c6..5eaa9dd1a1 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -52,6 +52,7 @@ export default defineConfig({ "**/voice-settings.spec.ts", "**/agent-readiness-screenshots.spec.ts", "**/agent-error-state-screenshots.spec.ts", + "**/agent-draft-review.spec.ts", "**/edit-agent.spec.ts", "**/doctor-cta-screenshots.spec.ts", "**/pubkey-display-screenshots.spec.ts", diff --git a/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx b/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx index 450102bcef..3cd5dbb7a5 100644 --- a/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDraftAdoptDialog.tsx @@ -27,7 +27,7 @@ export function AgentDraftAdoptDialog({ onImportKey, onOpenChange, }: { - draft: PendingAgentDraft; + draft: PendingAgentDraft | null; error: string | null; isPending: boolean; adoptedAuthTag: string | null; @@ -54,26 +54,28 @@ export function AgentDraftAdoptDialog({ Adopt this agent - {draft.displayName ?? "An agent"} is asking to be registered as + {draft?.displayName ?? "An agent"} is asking to be registered as yours. Adopting it attests that you own it — no new key is created.
-
-

- {draft.displayName ?? "Unnamed agent"} -

- {draft.systemPrompt ? ( -

- {draft.systemPrompt} + {draft ? ( +

+

+ {draft.displayName ?? "Unnamed agent"}

- ) : null} - - {draft.agentPubkey} - -
+ {draft.systemPrompt ? ( +

+ {draft.systemPrompt} +

+ ) : null} + + {draft.agentPubkey} + +
+ ) : null} {adoptedAuthTag ? (
diff --git a/desktop/src/features/agents/ui/AgentManagementDialogs.tsx b/desktop/src/features/agents/ui/AgentManagementDialogs.tsx index 55576e4c55..09607df13d 100644 --- a/desktop/src/features/agents/ui/AgentManagementDialogs.tsx +++ b/desktop/src/features/agents/ui/AgentManagementDialogs.tsx @@ -23,6 +23,20 @@ export function AgentManagementDialogs() { if (!open) management.dismiss(); }} /> + ) : management.adoptedAuthTag ? ( + // Keep the dialog mounted after a successful adopt so the minted + // BUZZ_AUTH_TAG stays visible (the draft itself is now resolved). + { + if (!open) management.dismiss(); + }} + /> ) : null} {management.createdAgent ? ( }; + | { type: "provider"; id: string; config: Record } + | { type: "external" }; export type ManagedAgent = { pubkey: string; @@ -310,8 +311,7 @@ export type ManagedAgent = { personaId: string | null; /** * The record's harness/runtime id (e.g. "goose", "my-custom-harness"). - * `null` means the agent inherits its harness from the linked persona. - * Used to count agents referencing a harness definition (delete confirm). + * `null` = inherit from the linked persona; used to count harness refs. */ runtime: string | null; teamId?: string | null; diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 355ccea9fc..2cde1cde06 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -98,6 +98,22 @@ type MockManagedAgentRuntimeSeed = { lifecycle?: MockManagedAgentRuntimeRow["lifecycle"]; }; +type MockPendingAgentDraftSeed = { + requestEventId?: string; + requestId: string; + action: "create" | "update"; + channelId: string; + agentPubkey: string; + createdAt?: number; + displayName?: string; + systemPrompt?: string; + agentName?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; +}; + type MockRelayAgentSeed = { pubkey: string; name: string; @@ -249,6 +265,8 @@ type E2eConfig = { mcp?: MockCommandAvailability; }; managedAgents?: MockManagedAgentSeed[]; + /** NIP-AD pending agent drafts served by `list_pending_agent_drafts`. */ + pendingAgentDrafts?: MockPendingAgentDraftSeed[]; /** Result returned by the mocked `add_agent_to_huddle` command. */ addAgentToHuddleResult?: { ephemeral_added: boolean; @@ -807,7 +825,8 @@ type RawManagedAgent = { auto_restart_on_config_change?: boolean; backend: | { type: "local" } - | { type: "provider"; id: string; config: Record }; + | { type: "provider"; id: string; config: Record } + | { type: "external" }; backend_agent_id: string | null; respond_to: "owner-only" | "allowlist" | "anyone"; respond_to_allowlist: string[]; @@ -1196,6 +1215,21 @@ declare global { payload: unknown; }>; }) => void; + __BUZZ_E2E_EMIT_MOCK_AGENT_DRAFT__?: (draft: { + requestEventId?: string; + requestId: string; + action: "create" | "update"; + channelId: string; + agentPubkey: string; + createdAt?: number; + displayName?: string; + systemPrompt?: string; + agentName?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; + }) => void; __BUZZ_E2E_EMIT_MOCK_READ_STATE__?: (input: { clientId: string; contexts: Record; @@ -2155,6 +2189,24 @@ function resetMockRelayAgents(config?: E2eConfig) { function resetMockManagedAgents(config?: E2eConfig) { mockManagedAgents = []; + mockPendingAgentDrafts = (config?.mock?.pendingAgentDrafts ?? []).map( + (seed) => ({ + requestEventId: seed.requestEventId ?? `mock-draft-${seed.requestId}`, + requestId: seed.requestId, + action: seed.action, + channelId: seed.channelId, + agentPubkey: seed.agentPubkey, + createdAt: seed.createdAt ?? Math.floor(Date.now() / 1000), + displayName: seed.displayName, + systemPrompt: seed.systemPrompt, + agentName: seed.agentName, + runtime: seed.runtime, + provider: seed.provider, + model: seed.model, + respondTo: seed.respondTo, + }), + ); + mockResolvedDraftIds = new Set(); mockManagedAgentRuntimes = (config?.mock?.managedAgentRuntimes ?? []).map( (seed) => ({ pubkey: seed.pubkey, @@ -2874,6 +2926,27 @@ const realSockets = new Map(); let mockManagedAgents: MockManagedAgent[] = []; let mockManagedAgentRuntimes: MockManagedAgentRuntimeRow[] = []; +// NIP-AD mock draft state (kinds 44300/44301). Seeded/emitted by E2E specs and +// served by the `list_pending_agent_drafts` / `resolve_agent_draft` / +// `adopt_external_agent` mock command handlers. +type MockPendingAgentDraft = { + requestEventId: string; + requestId: string; + action: "create" | "update"; + channelId: string; + agentPubkey: string; + createdAt: number; + displayName?: string; + systemPrompt?: string; + agentName?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; +}; +let mockPendingAgentDrafts: MockPendingAgentDraft[] = []; +let mockResolvedDraftIds = new Set(); + // Mutable `save_subscriptions` table mirror — TEST-ONLY. // // Cloned from `activeConfig.mock.saveSubscriptions` at install time, then @@ -7515,6 +7588,93 @@ async function handleListManagedAgents( return mockManagedAgents.map(cloneManagedAgent); } +function handleListPendingAgentDrafts(): MockPendingAgentDraft[] { + return mockPendingAgentDrafts + .filter((draft) => !mockResolvedDraftIds.has(draft.requestId)) + .map((draft) => ({ ...draft })); +} + +function handleResolveAgentDraft(input: { + requestEventId: string; + requestId: string; + agentPubkey: string; + status: string; + agentPubkeySaved?: string; + reason?: string; +}): { event_id: string; accepted: boolean; message: string } { + mockResolvedDraftIds.add(input.requestId); + return { + event_id: `mock-resolution-${input.requestId}`, + accepted: true, + message: "ok", + }; +} + +function handleAdoptExternalAgent(input: { + agentPubkey: string; + displayName: string; + systemPrompt?: string; + channelId?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; +}): { + pubkey: string; + name: string; + auth_tag: string; + backend: string; +} { + const pubkey = input.agentPubkey.toLowerCase(); + if ( + !mockManagedAgents.some((agent) => agent.pubkey.toLowerCase() === pubkey) + ) { + mockManagedAgents.push({ + pubkey, + name: input.displayName, + persona_id: null, + runtime: input.runtime ?? null, + relay_url: "ws://localhost:3000", + acp_command: "", + agent_command: "", + agent_args: [], + mcp_command: "", + turn_timeout_seconds: 320, + idle_timeout_seconds: null, + max_turn_duration_seconds: null, + parallelism: 1, + system_prompt: input.systemPrompt ?? null, + avatar_url: null, + model: input.model ?? null, + provider: input.provider ?? null, + status: "stopped", + pid: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + last_started_at: null, + last_stopped_at: null, + last_exit_code: null, + last_error: null, + last_error_code: null, + log_path: "", + start_on_app_launch: false, + auto_restart_on_config_change: false, + backend: { type: "external" }, + backend_agent_id: null, + respond_to: "owner-only", + respond_to_allowlist: [], + private_key_nsec: "", + log_lines: [], + }); + } + return { + pubkey, + name: input.displayName, + auth_tag: `["auth","${pubkey}","","mock-signature"]`, + backend: "external", + }; +} + function isAgentMemoryListing( value: RawAgentMemoryListing | Record, ): value is RawAgentMemoryListing { @@ -9897,6 +10057,23 @@ export function maybeInstallE2eTauriMocks() { window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__ = ({ agentPubkey, events }) => { injectObserverEventsForE2E(agentPubkey, events); }; + window.__BUZZ_E2E_EMIT_MOCK_AGENT_DRAFT__ = (draft) => { + mockPendingAgentDrafts.push({ + requestEventId: draft.requestEventId ?? `mock-draft-${draft.requestId}`, + requestId: draft.requestId, + action: draft.action, + channelId: draft.channelId, + agentPubkey: draft.agentPubkey, + createdAt: draft.createdAt ?? Math.floor(Date.now() / 1000), + displayName: draft.displayName, + systemPrompt: draft.systemPrompt, + agentName: draft.agentName, + runtime: draft.runtime, + provider: draft.provider, + model: draft.model, + respondTo: draft.respondTo, + }); + }; const meshNodeStatus = ( state: "off" | "running", mode: "serve" | "client" | null, @@ -11388,6 +11565,16 @@ export function maybeInstallE2eTauriMocks() { } case "list_managed_agents": return handleListManagedAgents(activeConfig); + case "list_pending_agent_drafts": + return handleListPendingAgentDrafts(); + case "resolve_agent_draft": + return handleResolveAgentDraft( + payload as Parameters[0], + ); + case "adopt_external_agent": + return handleAdoptExternalAgent( + payload as Parameters[0], + ); case "get_agent_memory": return handleGetAgentMemory( (payload as Parameters[0]) ?? {}, diff --git a/desktop/tests/e2e/agent-draft-review.spec.ts b/desktop/tests/e2e/agent-draft-review.spec.ts new file mode 100644 index 0000000000..8d42a7af90 --- /dev/null +++ b/desktop/tests/e2e/agent-draft-review.spec.ts @@ -0,0 +1,87 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; + +const AGENT = TEST_IDENTITIES.charlie; +// A mock DM channel where charlie and the owner are both members, so the +// draft-origin trust gate (declared NIP-OA ownership + shared channel) passes. +const CHANNEL = "d1ec7000-d000-4000-8000-000000000003"; + +test("draft arrives → review dialog opens; adopt shows auth tag", async ({ + page, +}) => { + await installMockBridge(page, { + managedAgents: [ + { + pubkey: AGENT.pubkey, + name: "dev-coder", + channelIds: [CHANNEL], + }, + ], + pendingAgentDrafts: [ + { + requestId: "draft-adopt-1", + action: "create", + channelId: CHANNEL, + agentPubkey: AGENT.pubkey, + displayName: "dev-coder", + systemPrompt: "You are a coding specialist.", + }, + ], + }); + await page.goto("/"); + + // The durable backfill surfaces the pending draft and opens the review dialog. + await expect( + page.getByRole("dialog", { name: "Adopt this agent" }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Adopt this identity" }), + ).toBeVisible(); + + // Adopt → the minted BUZZ_AUTH_TAG is shown with a copy affordance. + await page.getByRole("button", { name: "Adopt this identity" }).click(); + await expect(page.getByText("BUZZ_AUTH_TAG")).toBeVisible(); + await expect(page.getByRole("button", { name: "Copy tag" })).toBeVisible(); +}); + +test("decline publishes a resolution and the draft does not reappear after reload", async ({ + page, +}) => { + await installMockBridge(page, { + managedAgents: [ + { + pubkey: AGENT.pubkey, + name: "dev-coder", + channelIds: [CHANNEL], + }, + ], + pendingAgentDrafts: [ + { + requestId: "draft-decline-1", + action: "create", + channelId: CHANNEL, + agentPubkey: AGENT.pubkey, + displayName: "dev-coder", + systemPrompt: "You are a coding specialist.", + }, + ], + }); + await page.goto("/"); + + await expect( + page.getByRole("dialog", { name: "Adopt this agent" }), + ).toBeVisible(); + + // Close the dialog (decline) — publishes a 44301 resolution. + await page.keyboard.press("Escape"); + await expect( + page.getByRole("dialog", { name: "Adopt this agent" }), + ).not.toBeVisible(); + + // Reload — the durable resolution means the draft must not resurface. + await page.reload(); + await expect( + page.getByRole("dialog", { name: "Adopt this agent" }), + ).not.toBeVisible(); +}); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 8a9ab2be11..7c4ba56c8a 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -229,6 +229,22 @@ type MockBridgeOptions = { mcp?: MockCommandAvailability; }; managedAgents?: MockManagedAgentSeed[]; + /** NIP-AD pending agent drafts served by `list_pending_agent_drafts`. */ + pendingAgentDrafts?: Array<{ + requestEventId?: string; + requestId: string; + action: "create" | "update"; + channelId: string; + agentPubkey: string; + createdAt?: number; + displayName?: string; + systemPrompt?: string; + agentName?: string; + runtime?: string; + provider?: string; + model?: string; + respondTo?: string; + }>; /** Result returned by the mocked `add_agent_to_huddle` command. */ addAgentToHuddleResult?: { ephemeral_added: boolean; From a61e3b2e1b4093e9a2b255f80321b7cc9998f343 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 19:16:36 -0700 Subject: [PATCH 15/20] docs: NIP-AD changelog and review screenshots Adds CHANGELOG + relay CHANGELOG entries for NIP-AD durable agent drafts and captures review-dialog screenshots from the Playwright spec. Closes BrianInAz/buzz#18 (part 12/12) Signed-off-by: Brian Charbonneau --- CHANGELOG.md | 11 +++++++++++ crates/buzz-relay/CHANGELOG.md | 4 ++++ desktop/tests/e2e/agent-draft-review.spec.ts | 12 ++++++++++++ 3 files changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a4bbd449..bd9c64e779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +### NIP-AD: durable agent drafts + +- **feat(nips):** add NIP-AD spec for durable agent drafts (kinds 44300/44301), replacing the ephemeral kind-24200 `agent_management_request` path. +- **feat(core):** add `KIND_AGENT_DRAFT_REQUEST` (44300) and `KIND_AGENT_DRAFT_RESOLUTION` (44301) to `P_GATED_KINDS`/`RESULT_GATED_KINDS`, plus an `agent_draft` payload module (create/update request + resolution, NIP-44 encrypt/decrypt, fail-closed version validation). +- **feat(relay):** ingest envelope validation + `is_agent_owner` for both kinds, global-only storage, and FTS exclusion (migration 0027). +- **feat(sdk):** `build_agent_draft_request` / `build_agent_draft_resolution` builders. +- **feat(cli):** `buzz agents draft-create`/`draft-update` now publish durable kind 44300; new `buzz agents drafts list|status`. +- **feat(desktop):** durable draft store + review dialog; `adopt_external_agent` (attest-first, no new keypair) and `import_external_agent_key`; `BackendKind::External` fails closed on spawn/restart/deploy. + ## v0.5.3 ### Desktop and shared changes diff --git a/crates/buzz-relay/CHANGELOG.md b/crates/buzz-relay/CHANGELOG.md index c0d6ab9ccd..f6324fac1b 100644 --- a/crates/buzz-relay/CHANGELOG.md +++ b/crates/buzz-relay/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- feat(relay): NIP-AD durable agent drafts — ingest envelope validation + `is_agent_owner` for kinds 44300/44301, global-only storage, and FTS exclusion (migration 0027). + ## relay-v0.2.0 - feat: relay invite links (mint + claim + landing page + deep link) ([#1668](https://github.com/block/buzz/pull/1668)) ([`2e529aab7`](https://github.com/block/buzz/commit/2e529aab759a18c1bb81e447f3696fe99db53a27)) diff --git a/desktop/tests/e2e/agent-draft-review.spec.ts b/desktop/tests/e2e/agent-draft-review.spec.ts index 8d42a7af90..57c68a33b4 100644 --- a/desktop/tests/e2e/agent-draft-review.spec.ts +++ b/desktop/tests/e2e/agent-draft-review.spec.ts @@ -1,11 +1,13 @@ import { expect, test } from "@playwright/test"; +import { waitForAnimations } from "../helpers/animations"; import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; const AGENT = TEST_IDENTITIES.charlie; // A mock DM channel where charlie and the owner are both members, so the // draft-origin trust gate (declared NIP-OA ownership + shared channel) passes. const CHANNEL = "d1ec7000-d000-4000-8000-000000000003"; +const SHOTS = "test-results/agent-draft-review"; test("draft arrives → review dialog opens; adopt shows auth tag", async ({ page, @@ -38,11 +40,21 @@ test("draft arrives → review dialog opens; adopt shows auth tag", async ({ await expect( page.getByRole("button", { name: "Adopt this identity" }), ).toBeVisible(); + await waitForAnimations(page); + await page.screenshot({ + path: `${SHOTS}/01-draft-review.png`, + clip: { x: 0, y: 0, width: 1280, height: 720 }, + }); // Adopt → the minted BUZZ_AUTH_TAG is shown with a copy affordance. await page.getByRole("button", { name: "Adopt this identity" }).click(); await expect(page.getByText("BUZZ_AUTH_TAG")).toBeVisible(); await expect(page.getByRole("button", { name: "Copy tag" })).toBeVisible(); + await waitForAnimations(page); + await page.screenshot({ + path: `${SHOTS}/02-auth-tag.png`, + clip: { x: 0, y: 0, width: 1280, height: 720 }, + }); }); test("decline publishes a resolution and the draft does not reappear after reload", async ({ From 8b4dc5048419f09e94ec04bdee30d925970b66ea Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 19:33:40 -0700 Subject: [PATCH 16/20] style: apply cargo fmt + biome formatting from just fix-all Closes BrianInAz/buzz#18 (part 12 fmt) Signed-off-by: Brian Charbonneau --- crates/buzz-cli/src/commands/mod.rs | 2 +- crates/buzz-core/src/agent_draft.rs | 47 +++++++++++++------ crates/buzz-core/src/filter.rs | 7 ++- crates/buzz-core/src/lib.rs | 4 +- crates/buzz-relay/src/handlers/ingest.rs | 40 ++++++++-------- crates/buzz-sdk/src/builders.rs | 28 ++++++----- .../src-tauri/src/commands/agent_adoption.rs | 24 ++++------ .../src-tauri/src/commands/agent_drafts.rs | 9 ++-- desktop/src-tauri/src/commands/mod.rs | 4 +- desktop/src-tauri/src/lib.rs | 30 ++++++------ 10 files changed, 111 insertions(+), 84 deletions(-) diff --git a/crates/buzz-cli/src/commands/mod.rs b/crates/buzz-cli/src/commands/mod.rs index 107cba3ad4..fc417fd5ca 100644 --- a/crates/buzz-cli/src/commands/mod.rs +++ b/crates/buzz-cli/src/commands/mod.rs @@ -1,5 +1,5 @@ -pub mod agents; pub mod agent_drafts; +pub mod agents; pub mod channel_templates; pub mod channels; pub mod dms; diff --git a/crates/buzz-core/src/agent_draft.rs b/crates/buzz-core/src/agent_draft.rs index e3c255c209..cebb6f47e0 100644 --- a/crates/buzz-core/src/agent_draft.rs +++ b/crates/buzz-core/src/agent_draft.rs @@ -371,26 +371,39 @@ mod tests { request_id: "9f1c2b3a-4d5e-4f6a-8b7c-1d2e3f4a5b6c".to_string(), status: AgentDraftResolutionStatus::Accepted, timestamp: "2026-08-05T12:05:00.000Z".to_string(), - agent_pubkey: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()), + agent_pubkey: Some( + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + ), reason: Some("Approved".to_string()), } } - fn build_request_event(agent_keys: &Keys, owner_pubkey: &PublicKey, ciphertext: String) -> Event { - EventBuilder::new(Kind::Custom(crate::kind::KIND_AGENT_DRAFT_REQUEST as u16), ciphertext) - .tags([ - Tag::parse(["p", &owner_pubkey.to_hex()]).unwrap(), - Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), - Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), - ]) - // The agent's own pubkey is a `p` tag; nostr's EventBuilder discards - // self-`p`-tags unless self-tagging is allowed. - .allow_self_tagging() - .sign_with_keys(agent_keys) - .expect("sign") + fn build_request_event( + agent_keys: &Keys, + owner_pubkey: &PublicKey, + ciphertext: String, + ) -> Event { + EventBuilder::new( + Kind::Custom(crate::kind::KIND_AGENT_DRAFT_REQUEST as u16), + ciphertext, + ) + .tags([ + Tag::parse(["p", &owner_pubkey.to_hex()]).unwrap(), + Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), + Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), + ]) + // The agent's own pubkey is a `p` tag; nostr's EventBuilder discards + // self-`p`-tags unless self-tagging is allowed. + .allow_self_tagging() + .sign_with_keys(agent_keys) + .expect("sign") } - fn build_resolution_event(owner_keys: &Keys, agent_pubkey: &PublicKey, ciphertext: String) -> Event { + fn build_resolution_event( + owner_keys: &Keys, + agent_pubkey: &PublicKey, + ciphertext: String, + ) -> Event { EventBuilder::new( Kind::Custom(crate::kind::KIND_AGENT_DRAFT_RESOLUTION as u16), ciphertext, @@ -399,7 +412,11 @@ mod tests { Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), Tag::parse(["p", &agent_pubkey.to_hex()]).unwrap(), Tag::parse(["agent", &agent_pubkey.to_hex()]).unwrap(), - Tag::parse(["e", "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"]).unwrap(), + Tag::parse([ + "e", + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + ]) + .unwrap(), ]) // The owner's own pubkey is a `p` tag; nostr's EventBuilder discards // self-`p`-tags unless self-tagging is allowed. diff --git a/crates/buzz-core/src/filter.rs b/crates/buzz-core/src/filter.rs index 07160b0723..54b612048e 100644 --- a/crates/buzz-core/src/filter.rs +++ b/crates/buzz-core/src/filter.rs @@ -346,8 +346,11 @@ mod tests { Tag::parse(["p", &owner_keys.public_key().to_hex()]).unwrap(), Tag::parse(["p", &agent_keys.public_key().to_hex()]).unwrap(), Tag::parse(["agent", &agent_keys.public_key().to_hex()]).unwrap(), - Tag::parse(["e", "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"]) - .unwrap(), + Tag::parse([ + "e", + "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", + ]) + .unwrap(), ]) // The owner's own pubkey is a `p` tag; nostr's EventBuilder discards // self-`p`-tags unless self-tagging is allowed. diff --git a/crates/buzz-core/src/lib.rs b/crates/buzz-core/src/lib.rs index 57b7f72628..26427fcb25 100644 --- a/crates/buzz-core/src/lib.rs +++ b/crates/buzz-core/src/lib.rs @@ -5,10 +5,10 @@ //! Provides [`StoredEvent`], filter matching, kind constants, and event //! verification. All other Buzz crates depend on this one. -/// NIP-AM: Agent Turn Metric — payload type and encrypt/decrypt helpers. -pub mod agent_turn_metric; /// NIP-AD: Agent Draft — payload types and encrypt/decrypt helpers. pub mod agent_draft; +/// NIP-AM: Agent Turn Metric — payload type and encrypt/decrypt helpers. +pub mod agent_turn_metric; /// Channel and membership enums shared across crates. pub mod channel; /// NIP-AE Agent Engrams — slug grammar, conversation key, d-tag derivation, diff --git a/crates/buzz-relay/src/handlers/ingest.rs b/crates/buzz-relay/src/handlers/ingest.rs index 2acd072101..09127af343 100644 --- a/crates/buzz-relay/src/handlers/ingest.rs +++ b/crates/buzz-relay/src/handlers/ingest.rs @@ -20,21 +20,21 @@ use buzz_core::kind::{ KIND_GIFT_WRAP, KIND_GIT_ISSUE, KIND_GIT_PATCH, KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_REPO_STATE, KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, KIND_HUDDLE_ENDED, KIND_HUDDLE_GUIDELINES, - KIND_HUDDLE_PARTICIPANT_JOINED, - KIND_HUDDLE_PARTICIPANT_LEFT, KIND_HUDDLE_STARTED, KIND_IA_ARCHIVE_REQUEST, - KIND_IA_UNARCHIVE_REQUEST, KIND_LONG_FORM, KIND_MANAGED_AGENT, KIND_MEMBER_ADDED_NOTIFICATION, - KIND_MEMBER_REMOVED_NOTIFICATION, KIND_MODERATION_BAN, KIND_MODERATION_RESOLVE_REPORT, - KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN, KIND_MODERATION_UNTIMEOUT, KIND_MUTE_LIST, - KIND_NIP29_CREATE_GROUP, KIND_NIP29_DELETE_EVENT, KIND_NIP29_DELETE_GROUP, - KIND_NIP29_EDIT_METADATA, KIND_NIP29_JOIN_REQUEST, KIND_NIP29_LEAVE_REQUEST, - KIND_NIP29_PUT_USER, KIND_NIP29_REMOVE_USER, KIND_NIP43_LEAVE_REQUEST, - KIND_NIP65_RELAY_LIST_METADATA, KIND_PERSONA, KIND_PIN_LIST, KIND_PRESENCE_UPDATE, - KIND_PRODUCT_FEEDBACK, KIND_PROFILE, KIND_PROJECT, KIND_REACTION, KIND_READ_STATE, KIND_REPORT, - KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED, KIND_STREAM_MESSAGE_DIFF, - KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED, KIND_STREAM_MESSAGE_SCHEDULED, - KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEAM, KIND_TEAM_CATALOG, KIND_TEXT_NOTE, - KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, RELAY_ADMIN_ADD_MEMBER, - RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER, RELAY_ADMIN_SET_WORKSPACE_PROFILE, + KIND_HUDDLE_PARTICIPANT_JOINED, KIND_HUDDLE_PARTICIPANT_LEFT, KIND_HUDDLE_STARTED, + KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST, KIND_LONG_FORM, KIND_MANAGED_AGENT, + KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_MODERATION_BAN, + KIND_MODERATION_RESOLVE_REPORT, KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN, + KIND_MODERATION_UNTIMEOUT, KIND_MUTE_LIST, KIND_NIP29_CREATE_GROUP, KIND_NIP29_DELETE_EVENT, + KIND_NIP29_DELETE_GROUP, KIND_NIP29_EDIT_METADATA, KIND_NIP29_JOIN_REQUEST, + KIND_NIP29_LEAVE_REQUEST, KIND_NIP29_PUT_USER, KIND_NIP29_REMOVE_USER, + KIND_NIP43_LEAVE_REQUEST, KIND_NIP65_RELAY_LIST_METADATA, KIND_PERSONA, KIND_PIN_LIST, + KIND_PRESENCE_UPDATE, KIND_PRODUCT_FEEDBACK, KIND_PROFILE, KIND_PROJECT, KIND_REACTION, + KIND_READ_STATE, KIND_REPORT, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED, + KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED, + KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEAM, + KIND_TEAM_CATALOG, KIND_TEXT_NOTE, KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, + RELAY_ADMIN_ADD_MEMBER, RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER, + RELAY_ADMIN_SET_WORKSPACE_PROFILE, }; use buzz_core::tenant::TenantContext; use buzz_core::verification::verify_event; @@ -1614,9 +1614,7 @@ fn validate_agent_turn_metric_envelope(event: &nostr::Event) -> Result<(), Strin /// and `owner_hex` is the `p` tag that is not the agent. Ownership /// (`is_agent_owner`) and the author-direction check are performed by the /// per-kind validators and the async DB check in `ingest_event_inner`. -fn validate_agent_draft_common_envelope( - event: &nostr::Event, -) -> Result<(String, String), String> { +fn validate_agent_draft_common_envelope(event: &nostr::Event) -> Result<(String, String), String> { let mut p_tags: Vec<&str> = Vec::new(); let mut agent_tags: Vec<&str> = Vec::new(); let mut has_h_tag = false; @@ -1678,7 +1676,11 @@ fn validate_agent_draft_common_envelope( } // The owner is the `p` tag that is not the agent. - let owner = if p_tags[0] == agent { p_tags[1] } else { p_tags[0] }; + let owner = if p_tags[0] == agent { + p_tags[1] + } else { + p_tags[0] + }; // Content must look like a NIP-44 v2 ciphertext (length, base64, version prefix). validate_engram_nip44_content(&event.content) diff --git a/crates/buzz-sdk/src/builders.rs b/crates/buzz-sdk/src/builders.rs index 39b64ef313..273b75fcfc 100644 --- a/crates/buzz-sdk/src/builders.rs +++ b/crates/buzz-sdk/src/builders.rs @@ -7,13 +7,13 @@ use buzz_core::{ kind::{ KIND_AGENT_DRAFT_REQUEST, KIND_AGENT_DRAFT_RESOLUTION, KIND_AGENT_OBSERVER_FRAME, KIND_APPROVAL_DENY, KIND_APPROVAL_GRANT, KIND_DELETION, KIND_DM_ADD_MEMBER, KIND_DM_OPEN, - KIND_EMOJI_SET, KIND_GIT_ISSUE, KIND_GIT_PATCH, KIND_GIT_PR_UPDATE, - KIND_GIT_PULL_REQUEST, KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_STATUS_CLOSED, - KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, - KIND_IA_ARCHIVE_REQUEST, KIND_IA_UNARCHIVE_REQUEST, KIND_MODERATION_BAN, - KIND_MODERATION_RESOLVE_REPORT, KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN, - KIND_MODERATION_UNTIMEOUT, KIND_PRESENCE_UPDATE, KIND_PROJECT, KIND_USER_STATUS, - KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, + KIND_EMOJI_SET, KIND_GIT_ISSUE, KIND_GIT_PATCH, KIND_GIT_PR_UPDATE, KIND_GIT_PULL_REQUEST, + KIND_GIT_REPO_ANNOUNCEMENT, KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, + KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN, KIND_IA_ARCHIVE_REQUEST, + KIND_IA_UNARCHIVE_REQUEST, KIND_MODERATION_BAN, KIND_MODERATION_RESOLVE_REPORT, + KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN, KIND_MODERATION_UNTIMEOUT, + KIND_PRESENCE_UPDATE, KIND_PROJECT, KIND_USER_STATUS, KIND_WORKFLOW_DEF, + KIND_WORKFLOW_TRIGGER, }, observer::{ content_looks_like_nip44, OBSERVER_AGENT_TAG, OBSERVER_FRAME_CONTROL, OBSERVER_FRAME_TAG, @@ -2405,7 +2405,10 @@ mod tests { vec![agent.public_key().to_hex()] ); assert!(!has_tag(&ev, "h", "")); - assert!(!ev.tags.iter().any(|t| t.as_slice().first().map(|v| v.as_str()) == Some("h"))); + assert!(!ev + .tags + .iter() + .any(|t| t.as_slice().first().map(|v| v.as_str()) == Some("h"))); } #[test] @@ -2435,7 +2438,10 @@ mod tests { vec![agent.public_key().to_hex()] ); assert_eq!(tag_values(&ev, "e"), vec![request_event_id.to_string()]); - assert!(!ev.tags.iter().any(|t| t.as_slice().first().map(|v| v.as_str()) == Some("h"))); + assert!(!ev + .tags + .iter() + .any(|t| t.as_slice().first().map(|v| v.as_str()) == Some("h"))); } #[test] @@ -2467,8 +2473,8 @@ mod tests { #[test] fn agent_draft_resolution_rejects_owner_equals_agent() { let pk = "a".repeat(64); - let err = build_agent_draft_resolution(&pk, &pk, &"c".repeat(64), &fake_nip44()) - .unwrap_err(); + let err = + build_agent_draft_resolution(&pk, &pk, &"c".repeat(64), &fake_nip44()).unwrap_err(); assert!(matches!(err, SdkError::InvalidInput(_))); } diff --git a/desktop/src-tauri/src/commands/agent_adoption.rs b/desktop/src-tauri/src/commands/agent_adoption.rs index a4bc30cecb..80d6be1726 100644 --- a/desktop/src-tauri/src/commands/agent_adoption.rs +++ b/desktop/src-tauri/src/commands/agent_adoption.rs @@ -127,8 +127,8 @@ pub async fn adopt_external_agent( model: Option, respond_to: Option, ) -> Result { - let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) - .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let agent_pubkey = + nostr::PublicKey::parse(&agent_pubkey).map_err(|e| format!("invalid agent pubkey: {e}"))?; let agent_hex = agent_pubkey.to_hex(); let display_name = display_name.trim().to_string(); if display_name.is_empty() { @@ -193,15 +193,14 @@ pub async fn import_external_agent_key( nsec: String, display_name: String, ) -> Result { - let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) - .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let agent_pubkey = + nostr::PublicKey::parse(&agent_pubkey).map_err(|e| format!("invalid agent pubkey: {e}"))?; let agent_hex = agent_pubkey.to_hex(); let display_name = display_name.trim().to_string(); if display_name.is_empty() { return Err("display name is required".to_string()); } - let keys = nostr::Keys::parse(nsec.trim()) - .map_err(|e| format!("invalid nsec: {e}"))?; + let keys = nostr::Keys::parse(nsec.trim()).map_err(|e| format!("invalid nsec: {e}"))?; if keys.public_key() != agent_pubkey { return Err("nsec does not match the agent pubkey".to_string()); } @@ -271,8 +270,8 @@ mod tests { // The tag embeds the owner pubkey and is verifiable against the agent. let parsed: serde_json::Value = serde_json::from_str(&tag).expect("tag json"); assert_eq!(parsed[1], serde_json::json!(owner.public_key().to_hex())); - let verified = buzz_sdk_pkg::nip_oa::verify_auth_tag(&tag, &agent.public_key()) - .expect("verify"); + let verified = + buzz_sdk_pkg::nip_oa::verify_auth_tag(&tag, &agent.public_key()).expect("verify"); assert_eq!(verified, owner.public_key()); } @@ -281,12 +280,9 @@ mod tests { let owner = nostr::Keys::generate(); let compat_owner = nostr::Keys::parse(&owner.secret_key().to_secret_hex()).unwrap(); // Owner == agent must be rejected (self-attestation). - assert!(buzz_sdk_pkg::nip_oa::compute_auth_tag( - &compat_owner, - &owner.public_key(), - "" - ) - .is_err()); + assert!( + buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &owner.public_key(), "").is_err() + ); } /// Mirrors the runtime spawn guard: non-Local backends are rejected, with diff --git a/desktop/src-tauri/src/commands/agent_drafts.rs b/desktop/src-tauri/src/commands/agent_drafts.rs index f0b3b9ba5d..c93da4036d 100644 --- a/desktop/src-tauri/src/commands/agent_drafts.rs +++ b/desktop/src-tauri/src/commands/agent_drafts.rs @@ -171,8 +171,8 @@ pub async fn resolve_agent_draft( "superseded" => buzz_core_pkg::agent_draft::AgentDraftResolutionStatus::Superseded, other => return Err(format!("invalid status: {other}")), }; - let agent_pubkey = nostr::PublicKey::parse(&agent_pubkey) - .map_err(|e| format!("invalid agent pubkey: {e}"))?; + let agent_pubkey = + nostr::PublicKey::parse(&agent_pubkey).map_err(|e| format!("invalid agent pubkey: {e}"))?; let payload = buzz_core_pkg::agent_draft::AgentDraftResolutionPayload { version: buzz_core_pkg::agent_draft::AGENT_DRAFT_VERSION, request_id, @@ -295,7 +295,10 @@ mod tests { assert_eq!(info.channel_id, payload.channel_id); assert_eq!(info.agent_pubkey, agent.public_key().to_hex()); assert_eq!(info.display_name.as_deref(), Some("dev-coder")); - assert_eq!(info.system_prompt.as_deref(), Some("You are a coding specialist.")); + assert_eq!( + info.system_prompt.as_deref(), + Some("You are a coding specialist.") + ); assert!(info.agent_name.is_none()); } diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index d5a25dc429..2eca952991 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -1,5 +1,5 @@ -mod agent_auth; mod agent_adoption; +mod agent_auth; mod agent_config; mod agent_discovery; mod agent_drafts; @@ -63,8 +63,8 @@ mod window_vibrancy; mod workflows; mod workspace; -pub use agent_auth::*; pub use agent_adoption::*; +pub use agent_auth::*; pub use agent_config::*; pub use agent_discovery::*; pub use agent_drafts::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index e758eb09bc..b07a6277c2 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -88,9 +88,9 @@ fn reveal_initial_window(window: &tauri::Window) { #[cfg(target_os = "macos")] fn set_initial_window_backing(window: &tauri::Window) { - // The window remains transparent at runtime for vibrancy. Use an opaque - // native backing only across the first visible frames so the previous app - // cannot show through before WebKit has submitted its first surface. + // The window stays transparent at runtime for vibrancy; use an opaque + // native backing only across the first frames so the prior app can't show + // through before WebKit submits its first surface. if let Err(error) = window.set_background_color(Some(tauri::window::Color(17, 21, 24, 255))) { eprintln!("buzz-desktop: failed to set initial window backing: {error}"); } @@ -115,10 +115,8 @@ async fn wait_for_stable_initial_window_geometry(window: &tau for _ in 0..MAX_POLLS { // Accept whatever geometry the window-state plugin restores — maximized // or a normal saved size. macOS applies the restore asynchronously, so - // we only need consecutive identical outer bounds to know it settled. - // Gating on `is_maximized()` here would leave `bounds` permanently - // `None` for restored non-maximized windows and stall the reveal until - // the poll timeout. + // consecutive identical outer bounds mean it settled; gating on + // `is_maximized()` would leave `bounds` None and stall the reveal. let bounds = match (window.outer_position(), window.outer_size()) { (Ok(position), Ok(size)) => Some((position.x, position.y, size.width, size.height)), _ => None, @@ -142,11 +140,9 @@ async fn wait_for_stable_initial_window_geometry(window: &tau #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - // mesh-llm's async chains (model download, node start/join) overflow - // tokio's default 2 MiB worker stacks — a stack-guard SIGABRT, not a - // panic. Upstream mesh-llm and mesh-console both run on 8 MiB worker - // stacks for this reason; give Tauri's command runtime the same headroom - // before anything else touches tauri::async_runtime. + // mesh-llm's async chains overflow tokio's default 2 MiB worker stacks + // (a stack-guard SIGABRT). Upstream runs 8 MiB workers; give Tauri's + // command runtime the same headroom before anything else touches it. #[cfg(feature = "mesh-llm")] match tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -718,7 +714,8 @@ pub fn run() { auto_connect_default_relay_enabled, get_legacy_workspace_storage, is_shared_identity, - get_relay_ws_url, get_relay_http_url, + get_relay_ws_url, + get_relay_http_url, get_media_proxy_port, fetch_link_preview_title, discover_acp_auth_methods, @@ -797,8 +794,11 @@ pub fn run() { resolve_oa_owner, list_relay_agents, list_managed_agents, - list_pending_agent_drafts, resolve_agent_draft, adopt_external_agent, - import_external_agent_key, list_managed_agent_runtimes, + list_pending_agent_drafts, + resolve_agent_draft, + adopt_external_agent, + import_external_agent_key, + list_managed_agent_runtimes, start_managed_agent_runtime, stop_managed_agent_runtime, restart_managed_agent_runtime, From 95ba0103a86c2c8b05ba19517c08d5a055cf9299 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 20:29:33 -0700 Subject: [PATCH 17/20] chore: refresh Cargo.lock to fix --locked Docker build The committed lock was stale; cargo chef prepare updates it, so the --locked relay build in the Docker image workflow failed with 'cannot update the lock file'. Regenerate the lock so the CI build is reproducible. Closes BrianInAz/buzz#18 (part 12 lock) Signed-off-by: Brian Charbonneau --- Cargo.lock | 1558 +++++++++++++++++++++++++++------------------------- 1 file changed, 795 insertions(+), 763 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 937ead564a..c5b10dc1a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -74,9 +74,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -133,9 +133,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "appattest" @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -205,9 +205,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "async-broadcast" @@ -235,9 +235,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -300,7 +300,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -329,20 +329,20 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "async-utility" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9" dependencies = [ "futures-util", "gloo-timers", @@ -417,7 +417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84790c55b5704b0d35130bf16a4ce22a8e70eb0ea773522557524d9a4852663d" dependencies = [ "nix 0.30.1", - "rand 0.9.4", + "rand 0.9.5", ] [[package]] @@ -433,14 +433,14 @@ dependencies = [ "serde", "serde_json", "url", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-creds" @@ -453,16 +453,16 @@ dependencies = [ "quick-xml 0.38.4", "rust-ini", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "url", ] [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -470,14 +470,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -486,7 +487,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "838b36c8dc927b6db1b6c6b8f5d05865f2213550b9e83bf92fa99ed6525472c0" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -516,7 +517,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1 0.10.6", + "sha1 0.10.7", "sync_wrapper", "tokio", "tokio-tungstenite 0.29.0", @@ -553,7 +554,7 @@ checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -644,20 +645,40 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + [[package]] name = "bitcoin-io" -version = "0.1.4" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] [[package]] name = "bitcoin_hashes" -version = "0.14.1" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", - "hex-conservative", + "hex-conservative 0.2.2", "serde", ] @@ -669,9 +690,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -687,9 +708,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.5" +version = "1.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77" dependencies = [ "arrayref", "arrayvec", @@ -710,9 +731,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -756,9 +777,9 @@ checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc" [[package]] name = "bon" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +checksum = "a602c73c7b0148ec6d12af6fd5cc7a46e2eacc8878271a999abac56eed12f561" dependencies = [ "bon-macros", "rustversion", @@ -766,9 +787,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.9.1" +version = "3.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +checksum = "6dee98b0db6a962de883bf5d20362dee4d7ca0d12fe39a7c6c73c844e1cd7c1f" dependencies = [ "darling 0.23.0", "ident_case", @@ -776,25 +797,25 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "buzz-acp" @@ -818,11 +839,11 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-tungstenite 0.29.0", "tokio-util", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", "tracing", "tracing-subscriber", "url", @@ -891,7 +912,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "uuid", @@ -904,11 +925,11 @@ dependencies = [ "buzz-core", "hex", "nostr", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "url", @@ -926,7 +947,7 @@ dependencies = [ "k8s-openapi", "kube", "nostr", - "rand 0.10.1", + "rand 0.10.2", "rustls", "serde", "serde_json", @@ -953,14 +974,14 @@ dependencies = [ "hex", "infer", "nostr", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rustls", "serde", "serde_json", "sha2 0.11.0", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "url", "uuid", @@ -973,7 +994,7 @@ dependencies = [ "proptest", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "uuid", ] @@ -987,12 +1008,12 @@ dependencies = [ "hmac 0.13.0", "nostr", "percent-encoding", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "sha2 0.11.0", "subtle", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", "uuid", "zeroize", @@ -1008,12 +1029,12 @@ dependencies = [ "metrics", "metrics-util", "nostr", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "sha2 0.11.0", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "uuid", @@ -1070,7 +1091,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tracing", @@ -1105,7 +1126,7 @@ dependencies = [ "hex", "nostr", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-tungstenite 0.29.0", "url", @@ -1120,7 +1141,7 @@ dependencies = [ "serde_json", "serde_yaml", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1136,7 +1157,7 @@ dependencies = [ "redis", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "uuid", @@ -1161,13 +1182,13 @@ dependencies = [ "nostr", "p256", "proptest", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "serde", "serde_json", "sha2 0.11.0", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tower", "tower-http", @@ -1219,7 +1240,7 @@ dependencies = [ "opentelemetry_sdk 0.32.1", "postcard", "pulldown-cmark", - "rand 0.10.1", + "rand 0.10.2", "redis", "reqwest 0.13.4", "rust-s3", @@ -1231,7 +1252,7 @@ dependencies = [ "sqlx", "subtle", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-tungstenite 0.29.0", "tokio-util", @@ -1261,7 +1282,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "uuid", @@ -1275,7 +1296,7 @@ dependencies = [ "nostr", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "uuid", ] @@ -1285,7 +1306,7 @@ version = "0.1.0" dependencies = [ "buzz-core", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "uuid", ] @@ -1304,7 +1325,7 @@ dependencies = [ "futures-util", "hex", "nostr", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rust-s3", "rustls", @@ -1312,7 +1333,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "sqlx", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-tungstenite 0.29.0", "tracing", @@ -1329,7 +1350,7 @@ dependencies = [ "hex", "ort", "ort-sys", - "rand 0.10.1", + "rand 0.10.2", "sentencepiece-model", "serde", "serde_json", @@ -1357,7 +1378,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "uuid", @@ -1370,7 +1391,7 @@ dependencies = [ "futures-util", "nostr", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-tungstenite 0.29.0", "tracing", @@ -1385,9 +1406,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -1403,9 +1424,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bzip2" @@ -1447,9 +1468,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1488,9 +1509,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -1505,9 +1526,9 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if 1.0.4", "cpufeatures 0.3.0", @@ -1554,9 +1575,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" dependencies = [ "clap_builder", "clap_derive", @@ -1564,9 +1585,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" dependencies = [ "anstream", "anstyle", @@ -1576,14 +1597,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -1603,9 +1624,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "cobs" @@ -1613,7 +1634,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1775,9 +1796,9 @@ dependencies = [ [[package]] name = "cordyceps" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" dependencies = [ "loom", "tracing", @@ -1900,18 +1921,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1928,18 +1949,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crossterm" @@ -1947,7 +1968,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm_winapi", "mio", "parking_lot", @@ -1963,7 +1984,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm_winapi", "derive_more", "document-features", @@ -2067,43 +2088,16 @@ dependencies = [ "phf", ] -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - [[package]] name = "ctor" -version = "0.6.3" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" dependencies = [ - "ctor-proc-macro", - "dtor", + "link-section", + "linktime-proc-macro", ] -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" - [[package]] name = "ctr" version = "0.9.2" @@ -2140,9 +2134,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "5.0.0-rc.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if 1.0.4", "cpufeatures 0.3.0", @@ -2164,7 +2158,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2187,6 +2181,16 @@ dependencies = [ "darling_macro 0.23.0", ] +[[package]] +name = "darling" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +dependencies = [ + "darling_core 0.24.0", + "darling_macro 0.24.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -2198,7 +2202,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2211,7 +2215,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.3", ] [[package]] @@ -2222,7 +2239,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2233,7 +2250,18 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +dependencies = [ + "darling_core 0.24.0", + "quote", + "syn 3.0.3", ] [[package]] @@ -2261,15 +2289,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "data-encoding-macro" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -2277,19 +2305,19 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "dbus" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "libc", "libdbus-sys", @@ -2353,9 +2381,9 @@ checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "pem-rfc7468", @@ -2368,7 +2396,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -2380,7 +2407,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2401,7 +2428,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2411,7 +2438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2433,7 +2460,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.119", "unicode-xid", ] @@ -2445,9 +2472,9 @@ checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" [[package]] name = "diffy" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05264ab2aab4fb952fc4b0f3f6eff1ddfb4563064053a4ea174d91537584a769" +checksum = "10aec8f7f9393bd6a4f2762be0ceb012d3cbe2478987258cc9960de148561914" dependencies = [ "hashbrown 0.17.1", ] @@ -2469,7 +2496,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", "crypto-common 0.2.2", "ctutils", @@ -2502,7 +2529,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -2510,13 +2537,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -2554,21 +2581,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dtor" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - [[package]] name = "dunce" version = "1.0.5" @@ -2613,7 +2625,7 @@ version = "3.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" dependencies = [ - "curve25519-dalek 5.0.0-rc.0", + "curve25519-dalek 5.0.0", "ed25519", "rand_core 0.10.1", "serde", @@ -2625,9 +2637,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" dependencies = [ "serde", ] @@ -2694,7 +2706,7 @@ checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2715,7 +2727,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -2767,11 +2779,10 @@ checksum = "b6aff27af350e7b53e82aac3e5ab6389abd8f280640ac034508dff0608c4c7e5" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -2824,17 +2835,11 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -2930,7 +2935,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin 0.9.9", ] [[package]] @@ -2941,7 +2946,7 @@ checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin 0.9.9", ] [[package]] @@ -2994,9 +2999,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -3017,14 +3022,14 @@ dependencies = [ "diatomic-waker", "futures-core", "pin-project-lite", - "spin 0.10.0", + "spin 0.10.1", ] [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -3032,15 +3037,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -3060,9 +3065,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-lite" @@ -3079,32 +3084,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -3128,9 +3133,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" dependencies = [ "cc", "cfg-if 1.0.4", @@ -3251,14 +3256,14 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "globset" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -3292,9 +3297,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -3354,9 +3359,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ "hashbrown 0.16.1", ] @@ -3394,46 +3399,61 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + [[package]] name = "hf-hub" -version = "1.0.0-rc.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89305dc8fe34e165eaf0eb12b6e294e12381d9df9a431bcc52a5809bab4319" +checksum = "e7ccb6bcc85dec15413ef5949879f9a5497ca4568ed702547eb83fac23376e5c" dependencies = [ "base64 0.22.1", "bon", "bytes", "futures", + "getrandom 0.2.17", "globset", "hf-xet", "hyper", "pathdiff", + "percent-encoding", "reqwest 0.13.4", "serde", "serde_json", "sha2 0.11.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-retry", "tokio-util", "tracing", "url", + "wasm-bindgen-futures", ] [[package]] name = "hf-xet" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b33fa84f92796d4d263070b6c0d3ca219df7b9a0e1853ee431029b1612bcd" +checksum = "c237ef4fb0ce1962a5117f8bd8c74454b41629826a9df17d14a1840ca18f0754" dependencies = [ + "anyhow", "async-trait", "bytes", "http", "more-asserts", "serde", - "thiserror 2.0.18", + "serde_json", + "thiserror 2.0.19", "tokio", "tokio-util", + "tokio_with_wasm", "tracing", "uuid", "xet-client", @@ -3461,9 +3481,9 @@ dependencies = [ "idna", "ipnet", "jni 0.22.4", - "rand 0.10.1", + "rand 0.10.2", "rustls", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tokio", "tokio-rustls", @@ -3483,9 +3503,9 @@ dependencies = [ "jni 0.22.4", "once_cell", "prefix-trie", - "rand 0.10.1", + "rand 0.10.2", "ring", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "url", @@ -3508,12 +3528,12 @@ dependencies = [ "ndk-context", "once_cell", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "resolv-conf", "rustls", "smallvec", "system-configuration", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls", "tracing", @@ -3566,9 +3586,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -3576,9 +3596,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -3586,9 +3606,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -3617,15 +3637,15 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "subtle", "typenum", @@ -3634,9 +3654,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -3669,7 +3689,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -3877,9 +3897,9 @@ dependencies = [ [[package]] name = "igd-next" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac9a3c8278f43b4cd8463380f4a25653ac843e5b177e1d3eaf849cc9ba10d4d" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" dependencies = [ "attohttpc", "bytes", @@ -3889,7 +3909,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.10.1", + "rand 0.10.2", "tokio", "url", "xmltree", @@ -3897,9 +3917,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -3985,15 +4005,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ - "darling 0.23.0", + "darling 0.24.0", "indoc", "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -4023,18 +4043,18 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" dependencies = [ "serde", ] [[package]] name = "iroh" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fca9b4b462c343ff88fc0af4096c186f939b602a0bc08723536ef2c31c93971" +checksum = "460de6bc52163b41b1646931f2897e5ab986f0966ade444467fec25024751a72" dependencies = [ "backon", "blake3", @@ -4064,7 +4084,7 @@ dependencies = [ "pin-project", "portable-atomic", "portmapper", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rustc-hash", "rustls", @@ -4083,18 +4103,18 @@ dependencies = [ [[package]] name = "iroh-base" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830a582cd54410dc1aa71d4786a82c3297d7b0165accd8b6dbbb3b240b48140d" +checksum = "6be73e16ee21c923aca9b3121aaa0db936f7c7ecc156ff47b8dac944c68d59a8" dependencies = [ - "curve25519-dalek 5.0.0-rc.0", + "curve25519-dalek 5.0.0", "data-encoding", "data-encoding-macro", "derive_more", "ed25519-dalek", "getrandom 0.4.3", "n0-error", - "rand 0.10.1", + "rand 0.10.2", "serde", "url", "zeroize", @@ -4102,9 +4122,9 @@ dependencies = [ [[package]] name = "iroh-dns" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516e4eedc38e33ab69a6bd325520332dc3d67b25454e2d590ebb84a25240dd9a" +checksum = "46f6a9b39d18e6345f5c151afd299f2488e2cb5c520fe41b107b6bd3dc4c3349" dependencies = [ "arc-swap", "cfg_aliases", @@ -4115,7 +4135,7 @@ dependencies = [ "n0-future", "ndk-context", "portable-atomic", - "rand 0.10.1", + "rand 0.10.2", "rustls", "simple-dns", "strum", @@ -4148,14 +4168,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "iroh-relay" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8149bb6a57126225a07d6928846d82dcedfd24ea0f863ef7b2eb475e1d726354" +checksum = "24bd586cf927f7b700f56ec3639b53cb5fa901ce284784051ff71092bfbf8193" dependencies = [ "blake3", "bytes", @@ -4171,7 +4191,7 @@ dependencies = [ "iroh-base", "iroh-dns", "iroh-metrics", - "lru 0.18.0", + "lru 0.18.2", "n0-error", "n0-future", "noq", @@ -4179,7 +4199,7 @@ dependencies = [ "num_enum", "pin-project", "postcard", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rustls", "rustls-pki-types", @@ -4192,8 +4212,7 @@ dependencies = [ "tokio-websockets", "tracing", "url", - "vergen-gitcl", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", "ws_stream_wasm", ] @@ -4246,7 +4265,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link 0.2.1", ] @@ -4261,7 +4280,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4289,28 +4308,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if 1.0.4", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -4334,7 +4352,7 @@ dependencies = [ "pest_derive", "regex", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4357,7 +4375,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4433,7 +4451,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tower", @@ -4455,7 +4473,7 @@ dependencies = [ "serde", "serde-value", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4472,9 +4490,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "left-right" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a" +checksum = "8bc015ded5d9b3054dbbdb63332cdd6ee42352ccef19e911e25117490e2f48ee" dependencies = [ "crossbeam-utils", "loom", @@ -4483,9 +4501,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdbus-sys" @@ -4515,18 +4533,18 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" dependencies = [ "libc", ] [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "pkg-config", "vcpkg", @@ -4534,13 +4552,25 @@ dependencies = [ [[package]] name = "line-clipping" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] +[[package]] +name = "link-section" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -4576,9 +4606,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "logos" @@ -4601,7 +4631,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4634,9 +4664,9 @@ checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" [[package]] name = "lru" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" dependencies = [ "hashbrown 0.17.1", ] @@ -4721,7 +4751,7 @@ checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -4736,9 +4766,9 @@ dependencies = [ [[package]] name = "md5" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "mdns-sd" @@ -4757,9 +4787,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmem" @@ -4783,7 +4813,7 @@ source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05 dependencies = [ "hex", "mesh-llm-client", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -4822,13 +4852,13 @@ dependencies = [ "mesh-llm-types", "model-artifact", "nostr-sdk", - "prost 0.14.3", - "rand 0.10.1", + "prost 0.14.4", + "rand 0.10.2", "rustls", "serde", "serde_json", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "uuid", @@ -4960,8 +4990,8 @@ dependencies = [ "opentelemetry 0.31.0", "opentelemetry-otlp 0.31.1", "opentelemetry_sdk 0.31.0", - "prost 0.14.3", - "rand 0.10.1", + "prost 0.14.4", + "rand 0.10.2", "regex-lite", "reqwest 0.12.28", "rmcp", @@ -4983,7 +5013,7 @@ dependencies = [ "tabwriter", "tar", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "toml 0.9.12+spec-1.1.0", @@ -5009,11 +5039,11 @@ dependencies = [ "ed25519-dalek", "hex", "keyring", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "sha2 0.10.9", - "thiserror 2.0.18", + "thiserror 2.0.19", "zeroize", ] @@ -5049,8 +5079,8 @@ source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05 dependencies = [ "anyhow", "async-trait", - "prost 0.14.3", - "prost-build 0.14.3", + "prost 0.14.4", + "prost-build 0.14.4", "protoc-bin-vendored", "rmcp", "schemars", @@ -5086,7 +5116,7 @@ dependencies = [ "anyhow", "hex", "iroh", - "prost 0.14.3", + "prost 0.14.4", "serde_json", "sha2 0.10.9", ] @@ -5229,7 +5259,7 @@ dependencies = [ "metrics-util", "quanta", "rustls", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", ] @@ -5249,7 +5279,7 @@ dependencies = [ "ordered-float 5.3.0", "quanta", "radix_trie", - "rand 0.9.4", + "rand 0.9.5", "rand_xoshiro", "rapidhash", "sketches-ddsketch", @@ -5274,7 +5304,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5326,9 +5356,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -5441,7 +5471,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5498,7 +5528,7 @@ checksum = "e2acd8b070213b0299282f884b4beba4e7b52d624fdcd504a3ad3665390c11e1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5606,9 +5636,9 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" dependencies = [ "paste", ] @@ -5619,7 +5649,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", "log", "netlink-packet-core", @@ -5627,16 +5657,17 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +checksum = "e6f7398dddf5f152d2a91a2921a134c6097056e292c0d4b9906007855e7cece6" dependencies = [ "bytes", - "futures", + "futures-channel", + "futures-util", "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -5704,7 +5735,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -5717,7 +5748,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -5729,7 +5760,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -5747,9 +5778,9 @@ dependencies = [ [[package]] name = "noq" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf95190af1bd4a00a10e8255ca0c8ddd9e9a9f5e79151d7a7eb6d56aff5dc89" +checksum = "09e4bb6601fa543c110d8957813267d5a8d775a0f8fbaccf1f615d06ba9b10da" dependencies = [ "bytes", "cfg_aliases", @@ -5760,7 +5791,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tracing", @@ -5769,9 +5800,9 @@ dependencies = [ [[package]] name = "noq-proto" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6c890013591e709a3e45dd53501351b7e27e7ff3c7e9fc3dce43e300e7e9d3" +checksum = "baa7b5ccd819a9c68a0d955e67a881032d09b1a17219b1f90b0997a0888e1a15" dependencies = [ "aes-gcm", "bytes", @@ -5780,7 +5811,7 @@ dependencies = [ "getrandom 0.4.3", "identity-hash", "lru-slab", - "rand 0.10.1", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", @@ -5788,7 +5819,7 @@ dependencies = [ "rustls-pki-types", "slab", "sorted-index-buffer", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -5796,9 +5827,9 @@ dependencies = [ [[package]] name = "noq-udp" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3137a52df66c20090a889828d1c655f21f52294cba64e5c4fbb04fc83eee7c8e" +checksum = "02bba20e097a5a16cd0ad14ec882fae1e80a092a124e9422fc4dddd92e96a647" dependencies = [ "cfg_aliases", "libc", @@ -5809,9 +5840,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.44.7" +version = "0.44.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d3d987ea7078dc36947cde532637c472a229426702e4331dd7667325378bd9" +checksum = "40ff7b77ef428b40aa2834a6acbae38a0e104c98b306208ca4b87a420d579a4b" dependencies = [ "base64 0.22.1", "bech32", @@ -5918,9 +5949,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -5949,7 +5980,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -5963,11 +5994,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -6022,7 +6052,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6043,13 +6073,24 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "dispatch2", "libc", @@ -6062,7 +6103,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -6082,7 +6123,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2", "libc", "objc2", @@ -6105,7 +6146,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -6126,7 +6167,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "libc", "objc2", @@ -6181,11 +6222,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if 1.0.4", "foreign-types", "libc", @@ -6201,7 +6242,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6212,18 +6253,18 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.6.0+3.6.2" +version = "300.6.1+3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -6242,7 +6283,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -6256,7 +6297,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -6284,9 +6325,9 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto 0.31.0", "opentelemetry_sdk 0.31.0", - "prost 0.14.3", + "prost 0.14.4", "reqwest 0.12.28", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -6299,8 +6340,8 @@ dependencies = [ "opentelemetry 0.32.0", "opentelemetry-proto 0.32.0", "opentelemetry_sdk 0.32.1", - "prost 0.14.3", - "thiserror 2.0.18", + "prost 0.14.4", + "thiserror 2.0.19", "tokio", "tonic", "tonic-types", @@ -6316,7 +6357,7 @@ dependencies = [ "const-hex", "opentelemetry 0.31.0", "opentelemetry_sdk 0.31.0", - "prost 0.14.3", + "prost 0.14.4", "serde", "serde_json", "tonic", @@ -6331,7 +6372,7 @@ checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry 0.32.0", "opentelemetry_sdk 0.32.1", - "prost 0.14.3", + "prost 0.14.4", "tonic", "tonic-prost", ] @@ -6347,8 +6388,8 @@ dependencies = [ "futures-util", "opentelemetry 0.31.0", "percent-encoding", - "rand 0.9.4", - "thiserror 2.0.18", + "rand 0.9.5", + "thiserror 2.0.19", ] [[package]] @@ -6363,8 +6404,8 @@ dependencies = [ "opentelemetry 0.32.0", "percent-encoding", "portable-atomic", - "rand 0.9.4", - "thiserror 2.0.18", + "rand 0.9.5", + "thiserror 2.0.19", "tokio", "tokio-stream", ] @@ -6464,26 +6505,35 @@ dependencies = [ [[package]] name = "palette" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" dependencies = [ "approx", - "fast-srgb8", "libm", "palette_derive", + "palette_math", ] [[package]] name = "palette_derive" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", ] [[package]] @@ -6591,9 +6641,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" dependencies = [ "memchr", "ucd-trie", @@ -6601,9 +6651,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" dependencies = [ "pest", "pest_generator", @@ -6611,22 +6661,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" dependencies = [ "pest", ] @@ -6689,7 +6739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -6702,7 +6752,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6731,7 +6781,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6769,13 +6819,13 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64 0.22.1", "indexmap", - "quick-xml 0.39.4", + "quick-xml 0.41.0", "serde", "time", ] @@ -6786,7 +6836,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -6832,9 +6882,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" dependencies = [ "serde", ] @@ -6865,7 +6915,7 @@ dependencies = [ "n0-future", "netwatch", "num_enum", - "rand 0.10.1", + "rand 0.10.2", "serde", "smallvec", "socket2", @@ -6898,7 +6948,7 @@ checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6943,7 +6993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -6984,9 +7034,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -7013,9 +7063,9 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set 0.8.0", "bit-vec 0.8.0", - "bitflags 2.13.0", + "bitflags 2.13.1", "num-traits", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -7036,12 +7086,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", - "prost-derive 0.14.3", + "prost-derive 0.14.4", ] [[package]] @@ -7060,15 +7110,15 @@ dependencies = [ "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.117", + "syn 2.0.119", "tempfile", ] [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools", @@ -7076,10 +7126,10 @@ dependencies = [ "multimap", "petgraph 0.8.3", "prettyplease", - "prost 0.14.3", - "prost-types 0.14.3", + "prost 0.14.4", + "prost-types 0.14.4", "regex", - "syn 2.0.117", + "syn 2.0.119", "tempfile", ] @@ -7093,20 +7143,20 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -7133,11 +7183,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ - "prost 0.14.3", + "prost 0.14.4", ] [[package]] @@ -7237,7 +7287,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "memchr", "pulldown-cmark-escape", "unicase", @@ -7251,9 +7301,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pxfm" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "quanta" @@ -7294,18 +7344,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -7315,7 +7365,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -7331,14 +7381,14 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand 0.10.1", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -7346,23 +7396,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -7391,9 +7441,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -7402,9 +7452,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -7412,11 +7462,11 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.0", + "chacha20 0.10.1", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -7494,9 +7544,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.2" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" dependencies = [ "rustversion", ] @@ -7523,17 +7573,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "compact_str 0.9.1", "critical-section", "hashbrown 0.17.1", "itertools", "kasuari", - "lru 0.18.0", + "lru 0.18.2", "palette", "serde", "strum", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.2", @@ -7588,7 +7638,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "hashbrown 0.17.1", "indoc", "instability", @@ -7608,7 +7658,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -7659,9 +7709,9 @@ dependencies = [ [[package]] name = "redis" -version = "1.2.4" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae41a63fd0b8a5372f82b21e810e09a316f5dd7efd96bf08e678fb240fc1918" +checksum = "3257df217f7eab0044627a268c9cc6cdb60c0c421c88f83ac41c4e31520b6b84" dependencies = [ "arc-swap", "arcstr", @@ -7694,7 +7744,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -7705,34 +7755,34 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -7742,9 +7792,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -7759,9 +7809,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -7808,7 +7858,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -7866,7 +7916,7 @@ dependencies = [ "async-trait", "http", "reqwest 0.13.4", - "thiserror 2.0.18", + "thiserror 2.0.19", "tower-service", ] @@ -7917,14 +7967,14 @@ dependencies = [ "pastey", "pin-project-lite", "process-wrap", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rmcp-macros", "schemars", "serde", "serde_json", "sse-stream", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tokio-util", @@ -7943,7 +7993,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -7994,7 +8044,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "sysinfo 0.37.2", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tokio", "tokio-stream", @@ -8003,9 +8053,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -8022,7 +8072,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -8035,7 +8085,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -8044,9 +8094,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -8060,9 +8110,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -8072,9 +8122,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -8121,9 +8171,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rusty-fork" @@ -8197,9 +8247,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "chrono", "dyn-clone", @@ -8211,14 +8261,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -8265,7 +8315,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand 0.8.6", + "rand 0.8.7", "secp256k1-sys 0.10.1", "serde", ] @@ -8277,7 +8327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" dependencies = [ "bitcoin_hashes", - "rand 0.9.4", + "rand 0.9.5", "secp256k1-sys 0.11.0", ] @@ -8321,7 +8371,7 @@ dependencies = [ "hkdf 0.12.4", "num", "once_cell", - "rand 0.8.6", + "rand 0.8.7", "serde", "sha2 0.10.9", "zbus", @@ -8333,7 +8383,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -8346,7 +8396,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -8399,9 +8449,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -8429,40 +8479,40 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -8484,13 +8534,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -8539,9 +8589,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if 1.0.4", "cpufeatures 0.2.17", @@ -8574,7 +8624,6 @@ dependencies = [ "cfg-if 1.0.4", "cpufeatures 0.2.17", "digest 0.10.7", - "sha2-asm", ] [[package]] @@ -8588,15 +8637,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "sha2-asm" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" -dependencies = [ - "cc", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -8641,9 +8681,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -8688,15 +8728,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -8710,9 +8750,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d93e861ede2e497b47833469b8ec9d5c07fa4c78ce7a00f6eb7dd8168b4b3f" +checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" dependencies = [ "bstr", ] @@ -8723,7 +8763,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -8753,7 +8793,7 @@ name = "skippy-coordinator" version = "0.74.0" source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -8774,8 +8814,8 @@ name = "skippy-protocol" version = "0.74.0" source = "git+https://github.com/Mesh-LLM/mesh-llm.git?tag=v0.74.0#e60b2fe43aa05271569fbeff2a457133aef456a1" dependencies = [ - "prost 0.14.3", - "prost-build 0.14.3", + "prost 0.14.4", + "prost-build 0.14.4", "protoc-bin-vendored", "serde", ] @@ -8840,9 +8880,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -8860,9 +8900,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -8882,23 +8922,23 @@ checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "spin" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" dependencies = [ "lock_api", ] [[package]] name = "spin" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" [[package]] name = "spki" @@ -8973,13 +9013,13 @@ dependencies = [ "serde_json", "sha2 0.10.9", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tracing", "url", "uuid", - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] @@ -8992,7 +9032,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -9015,8 +9055,8 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.117", - "thiserror 2.0.18", + "syn 2.0.119", + "thiserror 2.0.19", "tokio", "url", ] @@ -9027,7 +9067,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "byteorder", "bytes", "chrono", @@ -9044,7 +9084,7 @@ dependencies = [ "sha1 0.11.0", "sha2 0.11.0", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "uuid", ] @@ -9057,7 +9097,7 @@ checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.13.0", + "bitflags 2.13.1", "byteorder", "chrono", "crc", @@ -9073,14 +9113,14 @@ dependencies = [ "log", "md-5", "memchr", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", "sha2 0.11.0", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "uuid", "whoami", @@ -9106,7 +9146,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "url", "uuid", @@ -9114,9 +9154,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72" +checksum = "c123f296ade4ec4b8b0f6162116e6629f5146922ca5ab40ca9d3c2e73ab4761e" dependencies = [ "bytes", "futures-util", @@ -9182,7 +9222,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -9368,9 +9408,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -9394,7 +9445,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -9431,7 +9482,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -9491,7 +9542,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "parking_lot", "rustix 1.1.4", "signal-hook", @@ -9527,7 +9578,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64 0.22.1", - "bitflags 2.13.0", + "bitflags 2.13.1", "fancy-regex 0.11.0", "filedescriptor", "finl_unicode", @@ -9572,11 +9623,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -9587,37 +9638,36 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if 1.0.4", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "js-sys", "libc", "num-conv", @@ -9630,15 +9680,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -9665,9 +9715,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -9697,7 +9747,7 @@ dependencies = [ "macro_rules_attribute", "monostate", "paste", - "rand 0.9.4", + "rand 0.9.5", "rayon", "rayon-cond", "regex", @@ -9705,7 +9755,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -9713,9 +9763,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -9730,13 +9780,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -9751,12 +9801,12 @@ dependencies = [ [[package]] name = "tokio-retry" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f644c762e9d396831ae2f8935c954b0d758c4532e924bead0f666d0c1c8640" +checksum = "4a129d95275ebf4c493ec53bf0f8cd95f5ac161bc4f381700809a54f595d4470" dependencies = [ "pin-project-lite", - "rand 0.10.1", + "rand 0.10.2", "tokio", ] @@ -9784,9 +9834,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -9828,23 +9878,24 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "tokio-websockets" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" dependencies = [ "base64 0.22.1", "bytes", @@ -9853,7 +9904,7 @@ dependencies = [ "getrandom 0.4.3", "http", "httparse", - "rand 0.10.1", + "rand 0.10.2", "ring", "rustls-pki-types", "sha1_smol", @@ -9863,6 +9914,30 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio_with_wasm" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e40fbbbd95441133fe9483f522db15dbfd26dc636164ebd8f2dd28759a6aa6" +dependencies = [ + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01145a2c788d6aae4cd653afec1e8332534d7d783d01897cefcafe4428de992" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -9880,9 +9955,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -9890,7 +9965,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -9913,31 +9988,31 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tonic" @@ -9976,7 +10051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", - "prost 0.14.3", + "prost 0.14.4", "tonic", ] @@ -9986,8 +10061,8 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" dependencies = [ - "prost 0.14.3", - "prost-types 0.14.3", + "prost 0.14.4", + "prost-types 0.14.4", "tonic", ] @@ -10018,7 +10093,7 @@ checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "base64 0.22.1", - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-core", "futures-util", @@ -10072,7 +10147,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", "tracing-subscriber", ] @@ -10085,7 +10160,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -10173,11 +10248,11 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.4", + "rand 0.9.5", "rustls", "rustls-pki-types", - "sha1 0.10.6", - "thiserror 2.0.18", + "sha1 0.10.7", + "thiserror 2.0.19", "utf-8", ] @@ -10192,24 +10267,24 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.4", + "rand 0.9.5", "rustls", "rustls-pki-types", - "sha1 0.10.6", - "thiserror 2.0.18", + "sha1 0.10.7", + "thiserror 2.0.19", ] [[package]] name = "twox-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typewit" @@ -10240,7 +10315,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" dependencies = [ - "rand 0.9.4", + "rand 0.9.5", "web-time", ] @@ -10294,9 +10369,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -10410,9 +10485,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", "getrandom 0.4.3", @@ -10433,43 +10508,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vergen" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", - "vergen-lib", -] - -[[package]] -name = "vergen-gitcl" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", - "time", - "vergen", - "vergen-lib", -] - -[[package]] -name = "vergen-lib" -version = "9.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" -dependencies = [ - "anyhow", - "derive_builder", - "rustversion", -] - [[package]] name = "version_check" version = "0.9.5" @@ -10530,9 +10568,9 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] @@ -10548,9 +10586,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -10561,9 +10599,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -10571,9 +10609,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10581,22 +10619,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -10629,9 +10667,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -10649,15 +10687,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.1" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" dependencies = [ - "core-foundation 0.10.1", "jni 0.22.4", "log", "ndk-context", "objc2", + "objc2-app-kit", "objc2-foundation", "url", "web-sys", @@ -10665,9 +10703,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -10678,14 +10716,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -10917,7 +10955,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -10928,7 +10966,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -11270,9 +11308,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -11293,7 +11331,7 @@ dependencies = [ "futures", "log", "serde", - "thiserror 2.0.18", + "thiserror 2.0.19", "windows 0.62.2", "windows-core 0.62.2", ] @@ -11328,7 +11366,7 @@ dependencies = [ "pharos", "rustc_version", "send_wrapper", - "thiserror 2.0.18", + "thiserror 2.0.19", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -11356,22 +11394,20 @@ dependencies = [ [[package]] name = "xet-client" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1e496dcbe6a09017acdfaf48e1a646735e7ff5b2a49e2c7e081cca77a59bc8" +checksum = "c3b8da8cc70aa2e3c500c0400e012df82c656ab9fca47f9f939fffc5afd89aca" dependencies = [ "anyhow", "async-trait", "base64 0.22.1", "bytes", - "clap", "crc32fast", "futures", "http", "hyper", - "lazy_static", "more-asserts", - "rand 0.10.1", + "rand 0.10.2", "redb", "reqwest 0.13.4", "reqwest-middleware", @@ -11380,11 +11416,11 @@ dependencies = [ "serde_repr", "statrs", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-retry", + "tokio_with_wasm", "tracing", - "tracing-subscriber", "url", "urlencoding", "web-time", @@ -11394,33 +11430,29 @@ dependencies = [ [[package]] name = "xet-core-structures" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb838aa8eb67d730af301584cf003caad407487606058292a6750711b603fbee" +checksum = "73503c223783dccc864abde22115e09d12f190448a0baf58ab2c54bc709e2f99" dependencies = [ "async-trait", "base64 0.22.1", "blake3", "bytemuck", "bytes", - "clap", "countio", - "csv", "futures", "futures-util", "getrandom 0.4.3", "heapify", "itertools", - "lazy_static", "lz4_flex", "more-asserts", - "rand 0.10.1", + "rand 0.10.2", "regex", "safe-transmute", "serde", "static_assertions", - "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tracing", @@ -11431,32 +11463,31 @@ dependencies = [ [[package]] name = "xet-data" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fd409bef621411a9d9013798540bb8036cb2678f03ab39af89a5e88034ed8c" +checksum = "c89052ec5dec2187cad30b86af92cc24fd61c4a57a795f1ff7ff5f38d49184eb" dependencies = [ "anyhow", "async-trait", "bytes", "chrono", - "clap", "gearhash", "http", "itertools", - "lazy_static", "more-asserts", - "rand 0.10.1", + "rand 0.10.2", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "tempfile", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", + "tokio_with_wasm", "tracing", "url", "uuid", - "walkdir", + "web-time", "xet-client", "xet-core-structures", "xet-runtime", @@ -11464,9 +11495,9 @@ dependencies = [ [[package]] name = "xet-runtime" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d8f121c33866f7648b737abe70d0e2dd9c0af4ffdd7219207531d0283aa63d" +checksum = "af5c60d5eed38ab4c576f4421bae835e7bd07631fb381705605529d2015c106b" dependencies = [ "anyhow", "async-trait", @@ -11480,23 +11511,24 @@ dependencies = [ "git-version", "humantime", "konst", - "lazy_static", "libc", "more-asserts", "oneshot", "pin-project", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "serde", "serde_json", "shellexpand", "sysinfo 0.38.4", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", + "tokio_with_wasm", "tracing", "tracing-appender", "tracing-subscriber", + "web-time", "whoami", "winapi", ] @@ -11518,15 +11550,15 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.15" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -11541,7 +11573,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -11563,10 +11595,10 @@ dependencies = [ "hex", "nix 0.29.0", "ordered-stream", - "rand 0.8.6", + "rand 0.8.7", "serde", "serde_repr", - "sha1 0.10.6", + "sha1 0.10.7", "static_assertions", "tracing", "uds_windows", @@ -11586,7 +11618,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "zvariant_utils", ] @@ -11603,22 +11635,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -11638,7 +11670,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -11659,7 +11691,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -11692,7 +11724,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -11708,15 +11740,15 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror 2.0.18", + "thiserror 2.0.19", "zopfli", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" @@ -11767,7 +11799,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "zvariant_utils", ] @@ -11779,5 +11811,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] From e0f18f8bcdd55c78bf7c43f6fd7d223f8b042ba9 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 20:41:04 -0700 Subject: [PATCH 18/20] fix(desktop): gate observer REQ on archive policy resolution The B2 cold-start fix opened the owner-global 24200 REQ on identity-known, but that raced the observer-archive policy: the live filter opened before kind 24200 was guaranteed present in the subscription, breaking observer-archive-policy.spec.ts. Gate the subscription on identity-known AND observerReconciled. Closes BrianInAz/buzz#18 (part 10 fix) Signed-off-by: Brian Charbonneau --- desktop/src/app/AppShell.tsx | 10 +++++----- desktop/src/features/agents/observerRelayStore.ts | 9 ++++++--- .../src/features/agents/useAgentObserverIngestion.ts | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 4eb0a42bbe..d096038c55 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -178,17 +178,17 @@ export function AppShell() { // Owner-global observer ingestion: receives + decrypts agent observer // frames and keeps derived active-turn liveness in sync app-wide, so no // individual screen/panel has to mount its own bridge for ingestion. - // Intentionally mounted without a `startupReady`/identity guard: before - // `currentPubkey` resolves the hook ingests managed agents only, and - // relay-owned agents join automatically once identity arrives. Adding a - // guard here would drop managed-agent coverage during startup. - useAgentObserverIngestion(); // Kind 24200 is relay-ephemeral, so reconciliation runs eagerly (not // deferred) and unconditionally repairs the DB subscription on internal // builds — otherwise frames emitted before the listener opens are lost. const observerReconciled = useObserverArchiveReconciliation( identityQuery.data?.pubkey, ); + // Intentionally mounted without a `startupReady`/identity guard: before + // `currentPubkey` resolves the hook ingests managed agents only, and + // relay-owned agents join once identity arrives. The live 24200 filter + // still waits for `observerReconciled` so it never opens too early. + useAgentObserverIngestion(observerReconciled); // useArchiveSync must wait for reconciliation, or listeners could open // before kind 24200 is guaranteed present in the subscription. useArchiveSync(observerReconciled); diff --git a/desktop/src/features/agents/observerRelayStore.ts b/desktop/src/features/agents/observerRelayStore.ts index 679f32f22a..76c4f9d9c8 100644 --- a/desktop/src/features/agents/observerRelayStore.ts +++ b/desktop/src/features/agents/observerRelayStore.ts @@ -570,12 +570,15 @@ export function getAgentTranscript( export function useManagedAgentObserverBridge( agents: readonly Pick[], + observerReconciled: boolean, ) { const subscriptionId = React.useId(); const identityQuery = useIdentityQuery(); // B2 cold-start fix: open the owner-global 24200 REQ whenever an identity is // known, regardless of `agents.length`. A newly adopted external agent's - // live telemetry must be subscribed even when no agent existed before. + // live telemetry must be subscribed even when no agent existed before. Still + // gated on the observer-archive policy resolving, so the live filter never + // opens before kind 24200 is guaranteed present in the subscription. const hasIdentity = Boolean(identityQuery.data?.pubkey); const agentPubkeys = React.useMemo( @@ -594,11 +597,11 @@ export function useManagedAgentObserverBridge( }, [subscriptionId, agentPubkeys]); React.useEffect(() => { - if (!hasIdentity) { + if (!hasIdentity || !observerReconciled) { return; } void ensureRelayObserverSubscription(); - }, [hasIdentity]); + }, [hasIdentity, observerReconciled]); // Wire up config-surface query invalidation when session_config_captured fires. const queryClient = useQueryClient(); diff --git a/desktop/src/features/agents/useAgentObserverIngestion.ts b/desktop/src/features/agents/useAgentObserverIngestion.ts index 386b762142..abac2ba10c 100644 --- a/desktop/src/features/agents/useAgentObserverIngestion.ts +++ b/desktop/src/features/agents/useAgentObserverIngestion.ts @@ -73,7 +73,7 @@ export function combineObserverIngestionAgents( * Do not gate this hook on identity/startup readiness — that would drop * managed-agent observer coverage during startup. */ -export function useAgentObserverIngestion() { +export function useAgentObserverIngestion(observerReconciled: boolean) { const identityQuery = useIdentityQuery(); const currentPubkey = identityQuery.data?.pubkey; @@ -111,6 +111,6 @@ export function useAgentObserverIngestion() { ); }, [currentPubkey, managedAgents, profiles, relayAgentPubkeys]); - useManagedAgentObserverBridge(ingestionAgents); + useManagedAgentObserverBridge(ingestionAgents, observerReconciled); useActiveAgentTurnsBridge(ingestionAgents); } From a6c86d6f015e84e512428b95e22ce361750cc9a0 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 21:28:13 -0700 Subject: [PATCH 19/20] fix(docker): build relay without --locked after cargo chef cook cargo chef cook resolves workspace crates to recipe versions that diverge from the committed lock, so the --locked relay build failed with 'cannot update the lock file'. Build without --locked so the lock is refreshed to the chef resolution. Closes BrianInAz/buzz#18 (part 12 docker) Signed-off-by: Brian Charbonneau --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d883ac6b01..ac63dfe5ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,7 +67,10 @@ COPY --from=planner /build/recipe.json recipe.json # scoping to -p buzz-relay misses transitive deps and re-builds them later. RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \ +# cargo chef cook resolves workspace crates to recipe versions, which can +# diverge from the committed lock; --locked would then fail. Build without it +# so the lock is refreshed to the chef resolution. +RUN cargo build --release -p buzz-relay --bin buzz-relay \ -p buzz-admin --bin buzz-admin \ -p buzz-pair-relay --bin buzz-pair-relay From e58fcd8af6cd0bec7d6f61818a11019d3aa75723 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Wed, 5 Aug 2026 21:40:14 -0700 Subject: [PATCH 20/20] fix(docker): build push gateway without --locked after cargo chef cook Same cargo chef cook lock divergence as the relay Dockerfile; build without --locked so the lock is refreshed to the chef resolution. Closes BrianInAz/buzz#18 (part 12 docker) Signed-off-by: Brian Charbonneau --- Dockerfile.push-gateway | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile.push-gateway b/Dockerfile.push-gateway index 202262d222..2a716dc6d5 100644 --- a/Dockerfile.push-gateway +++ b/Dockerfile.push-gateway @@ -17,7 +17,9 @@ RUN apt-get update \ COPY --from=planner /build/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release --locked -p buzz-push-gateway --bin buzz-push-gateway \ +# cargo chef cook resolves workspace crates to recipe versions that diverge +# from the committed lock; --locked would then fail. Build without it. +RUN cargo build --release -p buzz-push-gateway --bin buzz-push-gateway \ && strip target/release/buzz-push-gateway FROM debian:${DEBIAN_VERSION}-slim AS runtime