mirror of
https://github.com/block/buzz.git
synced 2026-11-11 11:51:13 +00:00
52cb6818face2bcc1a2aeaceb44d183b6ff9f2e9
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e3a0f3d59d |
feat(cli): send NIP-FI evidence via credential chain and Buzz desktop (#8999)
## Summary
Relays that enforce federated identity (NIP-FI) reject every protected
`buzz` CLI request with `401`. The CLI never sends
`Nostr-Federated-Identity`. This PR gives the CLI a credential chain,
lets it present NIP-FI evidence, and lets it take its identity from Buzz
desktop. Relays without NIP-FI behave exactly as before.
**Credential chain** (`crates/buzz-cli/src/credentials.rs`)
- Raw `Keys` are replaced by `Credentials`, which provide the public
key, signing through nostr's `NostrSigner`, NIP-44, and the NIP-OA auth
tag. NIP-FI evidence comes through `evidence(relay)`.
- The explicit key (`--private-key` / `BUZZ_PRIVATE_KEY`) is the first
link. A later link can add a signer that holds no local key (a desktop
provider, for example) without reshaping callers.
- Operations that need key material rather than a signature are
purpose-bound methods instead of a key accessor:
- `engram_d_tag` (the NIP-AE HMAC over the conversation key)
- the relay-scoped GIF customer id (random and fixed for the process
when there is no local key)
- `buzz mem` now encrypts, decrypts and derives the `d` tag through the
provider. Invalid ciphertext skips an entry; a signer failure aborts, so
an unavailable credential never reads as empty memory.
`buzz_core::engram` gains the composable steps behind it: `encode_body`,
`event_builder`, `validate_envelope` and `check_d_tag`. `build_event`
and `validate_and_decrypt` are unchanged wrappers, so `buzz-acp` and
desktop callers don't move.
- Agent-management drafts (`agent_management.rs`) encrypt and sign
through the provider.
**Evidence on every protected request**
(`crates/buzz-cli/src/client.rs`)
- A single `authorize` helper gets the NIP-FI assertion first, then
signs the proof just before sending, so a slow mint cannot age it past
NIP-98's 60 s window. It sets these on every protected route:
`Authorization` (NIP-98 or Blossom kind:24242), `x-auth-tag`, and
`Nostr-Federated-Identity: Bearer …`. The routes are `/query`, `/count`,
`/events` (stored and moderation), `/upload`, `/media/upload`,
`/media/*`, `get_authed`, `post_json_authed` and
`post_json_once_authed`.
- Exempt routes (`get_public`) are unchanged.
- The WebSocket publish sends the header on the upgrade via
`connect_authenticated_with_headers`. NIP-42 AUTH signs through the
signer (`buzz-ws-client` now takes `NostrSigner`; `&Keys` callers
compile unchanged).
- An upgrade denial is an HTTP response under NIP-FI.
`WsClientError::upgrade_rejection` returns its status and body, which
map like any other relay error, so a `403` still says whether evidence
or authorization was refused. A denial after NIP-42 (such as a key
mismatch) is a 1008 close; `WsClientError::PolicyClose` keeps its reason
and the CLI reports it as an `auth_error`.
- The WebSocket publish caps each network phase as a whole, writes
included: connect, AUTH challenge (with any Pongs), AUTH send plus OK,
EVENT send plus OK, and a best-effort close. NIP-42 signing between
phases is bounded by the signer. The relay drops a socket not signed in
within 5 s, so a sign-in that is throttled or comes back after the
socket closed reconnects and signs the new challenge, at most 3
connections. Each connection fetches current NIP-FI evidence for its
upgrade, and a throttled sign-in is waited out between connections, so a
dead challenge is never signed. EVENT is signed before connecting and
sent once, after AUTH succeeds. Running out is `unavailable` (exit 2,
`retryable:true`).
- Relay traffic follows no redirects: reqwest strips `Authorization`
across origins but not `x-auth-tag` or `Nostr-Federated-Identity`, so a
3xx is returned as a relay error. Credential header values are marked
sensitive.
**Headless evidence source**
(`crates/buzz-cli/src/federated_identity.rs`)
- Set `BUZZ_FEDERATED_IDENTITY_TOKEN` (your own NIP-FA sign-in session:
a human session for your key, not an agent grant) and
`BUZZ_FEDERATED_IDENTITY_ADAPTER` (the adapter base) to enable it. Both
are env only: command lines are visible to other local users.
- The CLI requests an assertion and the adapter mints it, through `POST
{adapter}/v1/identity/assertions` with a NIP-98 proof in
`Nostr-Authorization` (NIP-FA §Relay assertion).
- It checks the returned pubkey and expiry, parses the assertion as a
compact JWS (three base64url segments, JSON header with `alg`, JSON
payload), and requires its `exp` to equal `expires_at`. It caches one
assertion per process, bound to the relay when credentials are resolved,
and re-mints at the earlier of half-life and 30 s before expiry; the
desktop link caches and refreshes its assertion by the same rule.
Concurrent callers share one mint. An HTTP proof is signed again, after
current evidence, when the assertion changed while it was signed or the
evidence check aged it 30 s or more, up to 3 rounds, then fails as
`unavailable` (retryable).
- Compared with the separate rules before: a 300 s assertion now
refreshes at 150 s (was 270 s) on both links, a 40 s adapter assertion
at 10 s (was 20 s), and a 20 s desktop assertion is kept for 10 s
instead of fetched on every lookup. Desktop assertions now need a
readable JWS header with `alg` and a non-empty signature segment. An
unsupported headless relay URL now fails when credentials are resolved.
- The token goes only to the adapter origin (https, or http on
loopback), never follows a redirect, and never appears in errors or
`Debug`.
- A mint retries network errors, `429` and any `5xx` up to three
attempts with jittered backoff (or the adapter's `Retry-After` on
`429`/`503`, as delta-seconds or any HTTP-date form, capped at 30 s), on
every path (reads, post-once, moderation, stored events, WebSocket). It
then fails as `CliError::Adapter` (`adapter_error`, exit 2,
`retryable:true`). Relay retry loops never repeat it. A stored write
keeps `delivery_unknown` when any earlier attempt may have been
delivered, so a later adapter failure never invites a re-sign. Relay
mutation retries are otherwise unchanged.
- An explicit port 80 relay URL (read numerically from the authority, on
any scheme) is a usage error: `wss://host` means port 443, and NIP-FA
has no relay on port 80.
- A refusal is never retried: a non-transient status with an unreadable
or truncated body is still a refusal.
- Each NIP-FA denial code maps to an `auth_error` with a specific
message.
- These settings belong to the explicit key: without one they are not
read, and when the desktop link wins a warning names them.
- If an enforcing relay answers `401` and no assertion was sent, the CLI
returns an `auth_error` that names both settings, or, for a desktop
user, says to sign in to the community in Buzz. A desktop user's `403`
hint no longer mentions `BUZZ_AUTH_TAG`.
**What got simpler**
- About ten request sites built `Authorization` and `x-auth-tag`
themselves. Five copies of the 401/403 error-mapping block are now one
`relay_error`.
- The `sign_nip98` / `sign_blossom_*` helpers are gone. Proofs are a
three-case `Proof` enum consumed by `authorize`.
- `BuzzClient::keys()` (raw secret access from every command) is
removed. Commands use `public_key()`, `sign_event` or a purpose-bound
credentials method.
**Desktop credential provider**
(`crates/buzz-cli/src/desktop_provider.rs`,
`docs/local-credential-provider.md`)
Buzz desktop is a credential provider in the chain, after an explicit
key:
1. An inherited `BUZZ_CREDENTIAL_PROVIDER` token (set by the desktop for
the agents it launches). It decides the identity: when it is revoked,
not bound to the relay or unreachable, the command fails. A paired
token, which acts as the human, is never tried. That keeps a
well-behaved CLI from acting as the owner by accident; it is not a
boundary against an agent that can run its own commands as the owner's
OS user, and the contract says so with the mitigations.
2. Otherwise, a paired token stored in the OS keychain (`keyring`; macOS
native keychain, Linux Secret Service; no file fallback). A desktop that
answers `denied` or `unavailable` fails the command; only an unreachable
or unconfirmed advertisement is skipped. Revoked entries are forgotten.
New commands: `buzz auth desktop pair` and `buzz auth desktop unpair`.
The help no longer marks `BUZZ_PRIVATE_KEY` as required.
The CLI talks to the desktop over the loopback contract in
`docs/local-credential-provider.md` (NIP-46-shaped envelopes,
`Buzz-Credential-Provider` version header, same-user check on the
listening socket for paired and inherited tokens, via `/usr/sbin/lsof`
on macOS, given 5 s; a check that runs out of time is a retryable
`unavailable`, a wrong owner is refused). Every response's `id` must
match its request, error envelopes included. Paired tokens live only in
the keychain, survive desktop restarts, and are harness-neutral for
agents.
- **Relay binding:** tokens bind the whole relay URL (`http`/`https`
read as `ws`/`wss`, lowercase host, no default port, `/` for an empty
path; path and query count). The CLI sends that URL in `pair` and
`federated_identity`; NIP-FA audience canonicalization is the desktop's.
- **Rate limits:** a `rate_limited` answer refuses before acting, so the
CLI re-sends after `retry_after` (capped at 30 s), up to four times,
except a NIP-42 sign-in, whose wait goes to the WebSocket reconnect. A
re-sent NIP-42, NIP-98 or Blossom proof is re-dated (new id, Blossom
`expiration` moved with it); content events are re-sent exactly. If it
is still throttled, the command fails as `rate_limited` (exit 2,
`retryable:true`), not `auth_error`, whether the throttled call was
NIP-98, event signing, NIP-42 or encryption. Transport failures are
never re-sent, and relay commands are never replayed. A `buzz mem` list
that is throttled partway through completes.
- **Memory:** a `nip44_decrypt` answered with `invalid_request` is
invalid ciphertext, so `buzz mem` skips that entry (NIP-AE filtering);
any other provider failure aborts.
- **Keychain changes:** each change reloads the map under a short
`flock` on `~/.buzz-foundation/cli-pairings.lock` (directory forced to
`0700`, lock opened with `O_NOFOLLOW`) and changes only its own
instance; the lock is never held across the approval wait. An unreadable
entry is refused, never overwritten; `unpair` with no instance clears
it.
### Related
Related:
[block/buzz-app#2260](https://github.com/block/buzz-app/pull/2260) (the
desktop side of the provider contract)
---------
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Will Pfleger <wpfleger@squareup.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
b0f68a06ad |
feat(cli): user status expiry and status read (#8777)
> Replaces #8768, which was opened from a fork. ## Summary This change adds two things to `buzz-cli`. `buzz users set-status` now accepts `--expires`. The status then clears itself at that time. `buzz users status` is a new command. It reads the visible status of one or more users. ## Design `--expires` takes a unix timestamp or a duration. A duration is a number with a unit: `s`, `m`, `h`, `d`, or `w`. A bare number is a unix timestamp. The CLI had no duration parser to reuse. The only one is private to `buzz-workflow`. A small parser now lives in `users.rs`. The CLI rejects an expiry that is not in the future. It does this before it signs anything. The error text says that a bare number is a timestamp. `--expires` cannot combine with `--clear`. A status with no text and no emoji cannot have an expiry. The SDK rejects that shape. `buzz-sdk` has a new builder, `build_user_status_expiring`. It adds `["expiration", ts]` after the emoji tag. This is the tag order desktop uses. `build_user_status` now calls the new builder with no expiry. Its output is unchanged. Existing callers need no change. `users status` queries kind 30315 with `#d: general`. It picks the newest event for each author. A higher `created_at` wins. On a tie, the lower event id wins. Desktop uses the same rule. The CLI then drops a user when the newest event has no text and no emoji. It also drops a user when the status has expired. An expired status means `expiration <= now`. An older status never shows behind a newer cleared or expired one. The expiration tag is read like desktop's `parseInt`. A value such as `90abc` reads as 90. A value such as `soon` reads as no expiry. The output is an array: `[{pubkey, text, emoji, expires_at, updated_at}]`. `emoji` is an empty string when there is none. `expires_at` is `null` when the status does not expire. A user with no visible status has no row. `--pubkey` takes hex, npub, or an exact display name. It reuses the name resolver of `messages`. It is repeatable. The limit is 100, because each name costs one profile search. With no `--pubkey`, the command reads your own status. A relay response that is not valid JSON is an error. It is never an empty result. ## Testing What ran: - `cargo test -p buzz-cli -p buzz-sdk` passes. - `cargo clippy -p buzz-cli -p buzz-sdk --all-targets -- -D warnings` passes. - `cargo fmt --all --check` passes. - `just file-size-check` passes. The CLI tests use an axum fake relay. They call the production functions `publish_status` and `read_statuses`. These are the same functions that the commands call. The tests cover these cases: - One publish carries the right tags. - Bad expiries cause no publish. - Expired, cleared, and malformed statuses. - Newest-event choice and tie-break. - Hex, npub, and name input. - The 100-pubkey limit. - An invalid relay response. - A publish followed by a read. Interop evidence: I ran the real `buzz` binary against a local fake relay. No live relay was used. I fed the published event to desktop's own `parseUserStatusEvent` and `fetchUserStatusLookup` with node. Desktop showed the same text, emoji, and `expiresAt` that `buzz users status` printed. The check printed `INTEROP OK`. Mutation checks: I broke each guard once. Each break made a test fail. The guards were: - the expiry comparison, `<=` changed to `<` - the future-expiry check - the `d=general` filter - the newest-first order - the tie-break - the 100-pubkey limit - the author de-duplication - the invalid-response error - the hide-cleared-status check - the duration overflow check - the SDK expiration tag - the SDK empty-status check I restored each guard after the check. One note on process: I wrote the code and the tests in the same pass. I did not write the tests first. The mutation checks cover the gap. ## Manual test steps Use a test identity on a relay that you may write to. Set `BUZZ_RELAY_URL` and `BUZZ_PRIVATE_KEY` first. 1. Set a status that expires in two minutes. ```bash buzz users set-status --text "testing expiry" --emoji "🧪" --expires 2m ``` Success: the output has `"accepted":true`. 2. Read the status. ```bash buzz users status ``` Success: the output is one row. `text` is `testing expiry`. `emoji` is `🧪`. `expires_at` is about two minutes from now. 3. Open the desktop app as the same user. Success: your status shows on your profile. 4. Wait for the expiry. Read again. ```bash buzz users status ``` Success: the output is `[]`. Desktop also hides the status. 5. Try a bad expiry. ```bash buzz users set-status --text x --expires 3600 ``` Success: exit code 1. The error says that the value must be in the future. Nothing is published. 6. Try the clear conflict. ```bash buzz users set-status --clear --expires 1h ``` Success: exit code 1. The error says `--clear` cannot be used with `--expires`. 7. Read another user. ```bash buzz users status --pubkey <hex|npub|name> ``` Success: you get that user's visible status. You get `[]` if they have none. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: bmcanally <bmcanally@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
76d792f288 |
BUZZ-357: feat(cli): token-lean agent output format for event reads (#8234)
### Why?
Agents read channel history with `buzz messages get/thread`. Every
returned event includes its 128-hex `sig`. Every agent-authored message
also carries a NIP-OA `auth` tag, which is another owner pubkey and a
128-hex signature. An agent can't use either of these, but pays for them
on every history read. Measured with Claude's tokenizer, that is about
88 tokens per human message and about 180 per agent message, so a
history read shrinks 33% for short human messages and 40% once agent
messages are in the mix (details in the comment below). The existing
`compact` format is fixed at `{id, content, created_at}` (#6884), which
drops the author, tags and reply target, so agents can't use it.
### What?
- **New `--format agent` for event reads.** Applies to `messages
get/thread/search` and `feed get`.
- Drops `sig`, and trims the NIP-OA `auth` tag to `["auth", <owner>]` so
agents can still tell an author is an agent and who owns it.
- Folds the NIP-10 `root`/`reply` tags into a `reply_to` field, using
the same resolver as relay ingest, plus `thread_root` on nested replies.
That's the value `messages send --reply-to` takes.
- Keeps every other tag exactly as `json` output shows it: channel,
mentions, edit targets, diff metadata.
- Non-event reads return the same output as `json`.
- **Default for agents.** Without `--format`, the CLI falls back to
`BUZZ_OUTPUT_FORMAT`; an empty or unknown value means `json` with a
warning, so a harness/CLI version mismatch never breaks commands.
buzz-acp sets it to `agent` for every agent runtime through
`default_agent_env` and forwards the effective value in the explicit MCP
server environment, so operator or persona env still wins, and `--format
json` still returns full signed events. buzz-agent adds the variable to
its MCP env allowlist.
- **Docs and cleanup.** The managed-agent skill doc (version bumped to
7) and `AGENTS.md` describe the new default. The two duplicate
`format_events` copies in `messages.rs` and `feed.rs` are now one
`commands/event_output` module, which also does the NIP-10 tag parsing.
The `json` and `compact` output contracts are unchanged.
### Example output
https://github.com/block/buzz/pull/8234#issuecomment-6259679347
### Testing
Started `buzz-acp run` with the deterministic task-agent fixture, then
passed its `session/new` MCP environment to Codex 0.163.0 (`codex
app-server`). With no format override the MCP child received `agent`;
operator overrides of `json` and `compact` reached the MCP child
unchanged. The probes used temporary local sessions and made no model
requests.
### Related
Same change on the other agent read surfaces: each drops `sig`, trims
`auth` to `["auth", <owner>]` and keeps thread context.
- block/buzz-app#1388: buzz-app MCP read lines (text, `claimed
owner`/`reply_to`)
- squareup/cash-server#139132: hosted agents' read tools (JSON events,
raw NIP-10 tags)
Generated with Codex
---------
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
0808ab485c |
fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825)
## Problem
`buzz channels create --template` fails with a bare-pubkey
duplicate-instance error when a persona has more than one live instance.
When duplicate instances share the same display name and avatar, the
error gives the operator no signal to identify which instance is stale.
## What changed
**`crates/buzz-cli/src/commands/channels.rs`** (boundary) + one
visibility line in **`crates/buzz-cli/src/commands/users.rs`** (`fn
presence_subject` → `pub(crate)`), plus a relay behavior change in
**`crates/buzz-relay/src/api/bridge.rs`** and a supporting test in
**`crates/buzz-pubsub/src/presence.rs`**.
### Relay: presence-lookup failure is now surfaced as a failure
`synthesize_presence` previously collapsed a Redis `get_presence_bulk`
error into `unwrap_or_default()` → HTTP 200 `[]`, making a backend
outage indistinguishable from an authoritative all-offline snapshot. It
now returns `Option<Result<Vec<Value>, (StatusCode, Json<Value>)>>`:
`None` means the request is not a presence query (fall through),
`Some(Ok)` is a real snapshot (an empty vec is an authoritative
all-offline result), and `Some(Err)` is a backend failure surfaced as a
non-2xx response. The event tag-parse and sign paths were converted from
`.ok()?` fall-throughs to explicit `Some(Err(...))` for the same reason
— a silent fall-through would have reintroduced the fake-empty-success
anti-pattern. The function takes `pubsub` and `relay_keypair` directly
rather than the whole `AppState`, which keeps the Redis-error seam
unit-testable without live infrastructure; its single production caller
was updated accordingly, and no other caller consumes
`synthesize_presence`.
### Hint fetch architecture
**`assemble_roster_resolution<F, Fut>`** — post-fetch stage extracted
from `build_roster_resolution` as an injectable-fetcher async generic.
It contains: archive-filter-based duplicate detection, the conditional
`fetch_hints` call, and `finalize_roster_resolution` delegation.
Accepting the fetcher as a closure makes the wiring directly testable
without a relay.
`build_roster_resolution` reduces to: gather slugs → `tokio::join!(scan,
archive)` → delegate with the real `fetch_candidate_hints` closure.
**Zero hint queries on the happy path**: `assemble_roster_resolution`
only invokes the fetcher when duplicate live instances exist after
archive filtering. Untrusted archive snapshot (`Err`) conservatively
treats all found instances as live.
When duplicates exist, `fetch_candidate_hints` runs the presence
(kind:40902) and profile (kind:0) queries concurrently, bounding **each
independently** through `join_bounded_queries`: a per-query 3-second
timeout maps to `Err`, so a lookup that completes is never discarded
because its sibling hung. On total failure or timeout, bare pubkeys are
printed promptly.
### Trusted-snapshot boundary — `hints_from_results`
The relay drops the Redis presence key when an identity goes offline, so
the bulk presence snapshot never returns an event for an offline
instance — precisely the stale duplicate an operator needs flagged.
`hints_from_results` therefore treats a *trusted* presence snapshot as a
**complete snapshot** for the requested pubkeys: it seeds all of them
`offline`, then overlays returned statuses.
A response is trusted only when `trusted_presence_snapshot` validates
**every** array element as the actual snapshot contract: each must parse
as a complete signed `nostr::Event` of the presence-update kind carrying
exactly one `p` tag whose subject is one of the requested pubkeys.
Requiring the *sole* `p`-tag subject — the exact value `build_hint_map`
later reads via `presence_subject` — is what stops a mixed-tag event
(`[["p","<unrequested>"],["p","<requested>"]]`) from passing the gate
yet overlaying a different subject downstream. Any element that is not
such an event — a vacuous object like
`{"pubkey":"…","content":"online"}`, `[{}]`, `[null]`, an event of the
wrong kind, one for an unrequested subject, or one carrying more than
one `p` tag — makes presence enrichment untrusted: nothing is seeded
`offline`, so absence is never falsely inferred as offline. A failed,
timed-out, or non-array presence response is likewise untrusted. In
every untrusted case the successful profile sibling still contributes
its hints.
### Response-to-map conversion — `build_hint_map`
Sync production function taking the offline seed plus raw
presence/profile event slices. Relay-signed presence events carry the
agent pubkey in the `p` tag (not the event author); `build_hint_map`
calls `presence_subject` from `users.rs` (now `pub(crate)`) to resolve
the correct key.
### Hint display — `format_candidate`
Appends `[online/offline, profile updated YYYY-MM-DD]` (or subset) to
each candidate pubkey in the error. `profile_updated_at` names the field
correctly: kind:0 is replaceable state that desktop republishes on
rename and profile reconciliation, so `created_at` reflects the last
profile update, not provisioning time. Date formatted via
`chrono::DateTime::from_timestamp`; out-of-range timestamps omit the
date. Missing entries fall back to bare pubkeys.
### Cardinality rule
`apply_cardinality_rule` remains pure and relay-free; zero/one/many
semantics unchanged.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
|
||
|
|
262f2392e3 |
fix(cli): resolve agents from owner records (#3178)
## Context
`buzz users get --name Honey` searches relay-wide profiles and can
return an identically named agent owned by someone else. This caused
agents from the wrong owner to be added to a channel.
## Summary
This bug fix scopes exact-name agent lookup to owner-authored
managed-agent records, then cryptographically verifies each returned
profile's NIP-OA `auth` tag before asserting ownership. The relay and
database contracts remain unchanged.
### Related issue
None found.
## Changes
- Adds `buzz users get --name Honey --owner me|<hex>|<npub>`.
- Resolves `me` to the NIP-OA owner when the CLI runs as an agent,
otherwise to the CLI identity.
- Matches kind `30177` managed-agent record names exactly and
case-insensitively under the requested owner.
- Requires exactly one valid NIP-OA `auth` tag whose verified owner
equals the requested owner and whose `kind` and `created_at` conditions
apply to the profile event before returning `owner_pubkey` or
`owned_by_me: true`.
- Keeps missing, malformed, stale, condition-mismatched, or unverifiable
owner-record candidates visible with `owned_by_me: false` and an
explicit `verification` value.
- Returns every same-name record for the owner so callers can require
explicit selection when duplicates remain.
- Preserves the existing output shape and client-side name filter for
unscoped searches.
- Documents the distinct owner-scoped managed-agent lookup and unscoped
NIP-50 lookup modes.
### Testing
The reviewer-reproducible red and green commands below exercise the
ownership bug against the target branch and this branch.
## Screenshots
Not applicable. This is a CLI-only change.
## Reviewer-reproducible examples
The lookups below were run against the live relay from `main` and this
branch.
### Red: unscoped lookup returns the 100-profile relay-wide cap and
excludes John's agents
On `main`:
```bash
cargo run -q -p buzz-cli -- users get --name Honey \
| jq '{count: length, first_three: .[:3] | map(.pubkey), johns_agents: map(select(.pubkey == "31b29bcbe69d6716fbb7ba33602b89200bfc9ddfdabcfd1ea6fbfa70b816dfc7" or .pubkey == "4597ac725bba33fc7dd0454c1e2316a5ed770426acf667837d46f6553b3fcf54"))}'
```
Observed output:
```json
{
"count": 100,
"first_three": [
"20d27fc6c0ab4f50b66d1a32a64c5ca1fb985254143ce911f61ab7733333c3d7",
"00644478cdd9032c563ddc712b3687d8345d948945aab3c18bab95afbf6f519a",
"93c16697d0e58007bc11fb953208bc6b1cff387b2dee094abc10bf82dfee5424"
],
"johns_agents": []
}
```
`main` also rejects the owner-scoped command:
```bash
cargo run -q -p buzz-cli -- users get --name Honey --owner me
```
```text
error: unexpected argument '--owner' found
Usage: buzz users get --name <NAME>
```
### Green: owner-scoped lookup distinguishes verified and unresolved
records
On this branch:
```bash
cargo run -q -p buzz-cli -- users get --name Honey --owner me \
| jq 'map({pubkey,display_name,owner_pubkey,owned_by_me,verification})'
```
Observed output:
```json
[
{
"pubkey": "0ca77314d7ac8b3fcf6c647cc8cb9c3afd840db3b2a8ff2079f09a168de1827e",
"display_name": null,
"owner_pubkey": null,
"owned_by_me": false,
"verification": "missing_profile"
},
{
"pubkey": "31b29bcbe69d6716fbb7ba33602b89200bfc9ddfdabcfd1ea6fbfa70b816dfc7",
"display_name": "Honey",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"owned_by_me": true,
"verification": "verified"
},
{
"pubkey": "4597ac725bba33fc7dd0454c1e2316a5ed770426acf667837d46f6553b3fcf54",
"display_name": "Honey",
"owner_pubkey": "67252b09c31a995daa63aada26569fbc6a3d12f573113f001ce7432f870da820",
"owned_by_me": true,
"verification": "verified"
}
]
```
Only the two profiles with valid NIP-OA proofs assert ownership. The
owner-authored record whose profile is absent remains visible but cannot
be selected as verified ownership.
---------
Signed-off-by: npub1qye6rec0htgg3np8yt6plpyyg8cyffaq66emt3kmk05eylckkzhq0hnf2k <0133a1e70fbad088cc2722f41f848441f044a7a0d6b3b5c6dbb3e9927f16b0ae@buzz.block.builderlab.xyz>
Co-authored-by: npub1qye6rec0htgg3np8yt6plpyyg8cyffaq66emt3kmk05eylckkzhq0hnf2k <0133a1e70fbad088cc2722f41f848441f044a7a0d6b3b5c6dbb3e9927f16b0ae@buzz.block.builderlab.xyz>
|
||
|
|
60158fce3e |
feat(cli): add users set-status command for NIP-38 profile status (#3253)
## Summary The desktop client renders a persistent user status (NIP-38 kind:30315, `d:general`) as the status line on profiles, but the CLI had no way to set it — only ephemeral presence (`set-presence`, kind:20001). Integrations that want a scriptable, durable status line (for example a now-playing music bridge that shows the current TIDAL track on a profile) had no entry point. ## Screenshots <img width="1455" height="960" alt="1" src="https://github.com/user-attachments/assets/f1669ec6-212b-4f6e-ad53-07df9aacffc9" /> <img width="1455" height="960" alt="2" src="https://github.com/user-attachments/assets/5bf70f47-e5b5-4eb0-a426-b5f1ef90d2ec" /> This adds: ```bash buzz users set-status --text "Working on the relay" --emoji "🔧" buzz users set-status --text "" --emoji "🎶" # intentional emoji-only status buzz users set-status --clear # removes the status ``` - Signs and submits the replaceable kind:30315 event via the HTTP bridge (no WS needed — unlike presence, user status is a stored event). - Uses the `d:general` coordinate the desktop client already reads for the profile status line, and the same `emoji` tag shape `SetStatusDialog` publishes. - Event construction lives in `buzz_sdk::build_user_status()`, keyed off `buzz_core::kind::KIND_USER_STATUS`, so the CLI command is a thin sign/submit wrapper. Text and emoji are trimmed; a blank emoji is omitted rather than emitted as an empty tag. - Clearing is the explicit `--clear` flag, mutually exclusive with `--text`/`--emoji`. It publishes an empty-content event carrying only `d:general`, which the desktop treats as no status. `--text ""` with an `--emoji` is an emoji-only status, not a clear. --------- Signed-off-by: Kagan Yaldizkaya <kagan@squareup.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
a041e2d21e |
Revert "fix(cli,relay): resolve agents by verified owner" (#3168)
Reverts block/buzz#2615 |
||
|
|
c3084b36d9 |
fix(cli,relay): resolve agents by verified owner (#2615)
## Context
`buzz users get --name Honey` searches relay-wide profiles and returns
up to 100 identically named results without verified ownership metadata.
An agent resolving “my Honey” cannot distinguish the requesting human's
agent from another owner's agent, and the owned match can be excluded by
the result limit. This caused the wrong Honey and Bumble pubkeys to be
added to a channel.
## Summary
This bug fix makes personal-agent resolution owner-aware. Callers can
filter profiles by a verified owner identity before result limits are
applied, and all profile results expose enough ownership context to
diagnose duplicate names.
## Changes
- Adds `buzz users get --owner me|<hex>|<npub>` for name and pubkey
lookups.
- Resolves `me` to the NIP-OA owner identity when the CLI runs as an
agent.
- Filters profiles by the relay's verified `agent_owner_pubkey`
relationship before applying the result limit.
- Returns `owner_pubkey`, `owner_display_name`, and client-relative
`owned_by_me` in compact and JSON output.
- Returns an empty result when no owned profile matches instead of
removing the ownership constraint.
- Rejects malformed owner values instead of silently running an unscoped
query; explicit `null` remains equivalent to no owner filter for
ordinary CLI lookups.
- Rejects owner constraints on specialized channel-window, feed, and
thread filters that cannot enforce author filtering.
- Scopes owner filtering and enrichment to the active community.
- Adds a partial `(community_id, agent_owner_pubkey)` index for owner
lookups.
- Documents the safe `users get --name Honey --owner me` lookup.
## Reviewer-reproducible examples
The relay-backed test creates two same-name agents with different
verified owners, queries through the HTTP `/query` route, verifies only
the selected owner's agent is returned with verified owner metadata, and
verifies a missing owner returns `[]`.
```bash
cargo test -p buzz-relay query_agent_owner_returns_only_verified_owner_matches --lib -- --ignored
```
The owner/author intersection and unsupported-specialized-filter
contracts also have infrastructure-free relay tests:
```bash
cargo test -p buzz-relay agent_owner --lib
```
The CLI surface is visible in command help:
```bash
cargo run -q -p buzz-cli -- users get --help | grep -- --owner
```
```text
--owner <OWNER> Filter agents by verified owner (`me`, 64-char hex, or npub)
```
## Validation
- `cargo test -p buzz-cli` (252 passed)
- `cargo test -p buzz-db` (84 passed, 122 infrastructure tests ignored)
- `cargo test -p buzz-relay --lib` (owner-filter tests pass; the full
local suite is blocked by unrelated Postgres pool timeouts in
media/admin tests)
- `cargo test -p buzz-relay
query_agent_owner_returns_only_verified_owner_matches --lib --
--ignored` (passed)
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- Pre-push Rust, Desktop, and Desktop Tauri suites passed
- Pre-push mobile suite could not start because `flutter` is not
installed
- `pnpm check:file-sizes` (passed after rebasing onto current `main`)
---------
Signed-off-by: npub1qye6rec0htgg3np8yt6plpyyg8cyffaq66emt3kmk05eylckkzhq0hnf2k <0133a1e70fbad088cc2722f41f848441f044a7a0d6b3b5c6dbb3e9927f16b0ae@buzz.block.builderlab.xyz>
Co-authored-by: npub1qye6rec0htgg3np8yt6plpyyg8cyffaq66emt3kmk05eylckkzhq0hnf2k <0133a1e70fbad088cc2722f41f848441f044a7a0d6b3b5c6dbb3e9927f16b0ae@buzz.block.builderlab.xyz>
|
||
|
|
5c359deb9b |
fix(cli): report presence subject (#2104)
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> |
||
|
|
73cc31cc52 |
chore: remove LLM-slop comments across the codebase (#1277)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Quinn <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> |
||
|
|
d99ad131f1 |
refactor: rename sprout backend to buzz (#958)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger96@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Will Pfleger <wpfleger96@gmail.com> |