## 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>
20 KiB
Local credential provider
draft
This document defines how a local process, such as the buzz CLI, asks a running
Buzz Desktop to act as one Nostr identity: to give its public key, sign events,
encrypt and decrypt with NIP-44, return NIP-FI evidence for a relay, and return
its NIP-OA auth tag. The private key and the sign-in session stay in the
desktop.
The CLI is the reference client and lives in this repository
(crates/buzz-cli). Buzz Desktop (block/buzz-app)
is the reference provider. Any client or provider that follows this document
interoperates with both.
Goals
- One identity per caller. A caller holds a token for exactly one identity. The desktop never decides on the caller's behalf which identity to use.
- An agent's process gets only that agent's token. The desktop gives a process it launches for an agent a token for that agent alone. Acting as the owner takes the owner's own approval, given when pairing.
- The desktop sees what it signs. The caller sends the complete unsigned event; the desktop classifies it into an operation and applies a policy before signing. There is no blind signing of opaque bytes.
- Old and new versions interoperate. The bundled CLI follows the desktop's pinned revision of this repository, but a user can also run a different CLI. An older client must work against a newer desktop, and the reverse.
Non-goals
- Remote signing. The transport is loopback only. NIP-46 covers remote signers.
- Windows. Version 1 has no Windows transport. See Platforms.
- Deciding which approval UI the desktop shows. This document fixes what needs approval, not how it is presented.
Terms
- Provider: the desktop process that holds the key and answers requests.
- Caller: the local process that sends requests.
- Token: an opaque bearer secret that names one identity and the relays it may act on. Tokens carry at least 128 bits of entropy.
- Instance: one installed desktop build, named by a stable identifier such as its bundle identifier. Production and staging builds are separate instances and issue separate tokens.
- Bound relay: a community relay URL a token may act on. Relay URLs are
compared whole after URL normalization:
http/httpsread asws/wss, lowercase host, no default port,/for an empty path. The path and query count, because one host can serve several communities. A caller sends the bound URL itself inpairandfederated_identity; NIP-FA audience canonicalization is the provider's job.
Discovery
A caller looks for a provider in this order. The first source that yields a token and a reachable endpoint is used.
An inherited token decides the identity. A caller that has one MUST use only it: when it is revoked, does not apply to the relay or fails, the caller fails. It MUST NOT go on to a paired token, which acts as the human. This keeps a well-behaved caller from acting as the owner by accident. It is not a boundary against an agent that can run its own commands. See Security considerations.
1. Inherited token
When the desktop launches a process for an agent, through any agent harness, it sets:
BUZZ_CREDENTIAL_PROVIDER={"url":"http://127.0.0.1:<port>/<path>","token":"<token>"}
- The token's identity is that agent. It is never the owner.
- The token MUST reach only processes that act for that agent. A harness that hosts several agents in one process MUST give each agent's tool processes their own agent's token, and MUST NOT expose one agent's token to another agent's tools.
- The desktop SHOULD revoke the token when the process it launched exits, and MUST revoke it on the triggers in Revocation.
- A caller MUST reject a
urlwhose scheme is nothttp, whose host is not exactly127.0.0.1, or that carries user information. - A caller also runs the same-user check below for an inherited token: if the desktop exits while the agent keeps running, another user may bind the freed port.
The desktop MUST set this variable only for processes it launches for an agent, and only in their own environment. It MUST NOT write the token to disk.
2. Paired token
A human's own terminal, or a service the human runs such as slack2buzz, gets a token by pairing. The caller stores the token in the OS keychain (Keychain on macOS, the Secret Service on Linux), never in a file:
- The CLI keeps one keychain entry, service
buzz-cliand accountdesktop-pairings, that maps each instance to its token, identity and bound relays. Each change reloads the entry under a short cross-process lock and touches only its own instance; the lock is never held across an approval wait. An entry that cannot be read is refused, never overwritten. - A paired caller only tries a paired token when no inherited token is set.
A desktop that answers
deniedorunavailablefails the command; only an unreachable or unconfirmed advertisement is skipped. - When no keychain is reachable, pairing MUST fail with an error that says so. A caller MUST NOT fall back to writing the token to disk.
- A paired token survives a desktop restart. The desktop keeps it until it is revoked or expires (see Revocation), so a caller pairs once.
The token does not say where the desktop is listening, because the port changes on every launch. The desktop advertises its current endpoint, which holds no secret, in:
~/.buzz-foundation/credential-providers/<instance>.json
{"instance":"<instance>","url":"http://127.0.0.1:<port>/<path>"}
- The desktop writes the file whole under a dot name and renames it into place, and removes it when it quits.
- The directory MUST be mode
0700and owned by the user, and each file MUST be owned by the user with no group or other permission bits. A caller MUST ignore any file that fails these checks, so another user cannot plant one. - A caller uses the advertisement whose
instancematches its stored token. - Same-user check. An advertised port may now belong to another user's
process, for example after the desktop crashed. Before it sends a token to an
advertised endpoint, a caller MUST connect and confirm, by numeric user ID,
that a process of the same user holds the server side of that connection
(
lsofon macOS,/proc/net/tcpon Linux). If it cannot confirm this within about one second, it MUST send nothing on that connection. This is the checkbuzz-agent-commandperforms for advertised agent sessions (block/buzz-appcrates/agent-command). - A refused connection means the desktop is not running. The caller MAY delete that advertisement.
3. No provider
Without an inherited token, if no paired token yields a provider, the desktop link in the caller's credential chain does not apply, and the caller moves on to its next source. A caller with an inherited token never falls through to a paired token: when the inherited token cannot serve, the caller fails.
Transport
- HTTP/1.1
POSTto the discoveredurl, on127.0.0.1only. The provider MUST bind only to127.0.0.1. Content-Type: application/json.Authorization: Bearer <token>on every request exceptpair.Buzz-Credential-Provider: <version>on every request. See Versions.- One request per connection is sufficient. Callers MUST NOT rely on keep-alive.
Envelope
Requests and responses use the NIP-46 message shape, unencrypted because they never leave the machine. That lets both sides reuse existing NIP-46 types.
{"id":"<random string>","method":"<method>","params":["<string>", "..."]}
{"id":"<same id>","result":"<string>"}
{"id":"<same id>","result":"","error":"<code>","message":"<human text>","retry_after":30}
paramsandresultare strings, as in NIP-46. Structured values are JSON-encoded into the string.- On failure,
erroris a machine code from Errors.messageis optional text for a person.retry_afteris present only forrate_limited. - A response is at most 12,352 KiB (status line, headers and body); larger ones are refused.
- Both sides MUST ignore fields they do not recognize.
Methods
NIP-46 methods
These have the same parameters and results as in NIP-46.
| Method | Params | Result |
|---|---|---|
get_public_key |
[] |
Hex public key of the token's identity |
sign_event |
[<unsigned event JSON>] |
Signed event JSON |
nip44_encrypt |
[<peer pubkey hex>, <plaintext>] |
NIP-44 payload |
nip44_decrypt |
[<peer pubkey hex>, <payload>] |
Plaintext |
For sign_event, the unsigned event carries kind, tags, content and
created_at. If it carries pubkey, the value MUST equal the token's identity,
or the provider MUST answer denied. The provider sets pubkey, computes id
and signs. sign_event is subject to the operation policy.
Buzz methods
| Method | Params | Result |
|---|---|---|
describe |
[] |
JSON: identity, role, bound relays and methods |
federated_identity |
[<relay URL>] |
NIP-FI assertion, or empty string |
auth_tag |
[] |
JSON-encoded NIP-OA auth tag, or empty string |
engram_d_tag |
[<peer pubkey hex>, <slug>] |
NIP-AE d tag for that slug |
pair |
[<client name>, <relay URL>] |
JSON: token and identity |
describe returns:
{
"instance": "<instance>",
"pubkey": "<hex>",
"role": "agent",
"owner_pubkey": "<hex>",
"relays": ["wss://community.example"],
"methods": ["get_public_key", "sign_event", "..."]
}
roleisagentorowner.owner_pubkeyis present only foragent.- A caller MUST NOT use the provider for a relay that is not in
relays. It treats the provider as not applicable to that relay: with a paired token it moves on to its next source; with an inherited token it fails (see Discovery).
federated_identity returns a current NIP-FI assertion
whose nostr_pubkey is the token's identity and whose audience is the given
relay. It returns an empty string when the relay does not advertise NIP-FI or
the desktop has no enterprise login for it, so a caller sends no evidence on
such relays. It answers denied for a relay that is not bound to the token,
and unavailable when the relay requires NIP-FI but the desktop's session for
it has lapsed. A caller SHOULD cache the assertion until shortly before its
exp and MUST NOT write it to disk.
auth_tag returns the agent's NIP-OA auth tag as a JSON array, for example
["auth","<owner>","<conditions>","<sig>"], or an empty string for an owner
token.
engram_d_tag returns the 64-hex d tag that NIP-AE
derives from the identity's NIP-44 conversation key with peer and the slug.
The provider MUST answer invalid_request for a slug outside NIP-AE's grammar,
so the method is not a general keyed-hash oracle. Engram content uses
nip44_encrypt and nip44_decrypt with the same peer, so no method exposes the
conversation key itself.
The provider never returns the private key or a NIP-44 conversation key. A caller feature that needs either one, rather than an operation built on it, needs an explicit key.
pair is described under Pairing.
Pairing
pair is the only method accepted without a token. The provider MUST:
- Show the human an approval that names the client and the relay, and the calling process when the provider can identify it.
- Hold the request open until the human decides, for at most 120 seconds.
- On approval, answer with
{"token":"<token>","instance":"<instance>","pubkey":"<hex>","relays":["<relay>"]}. The identity is the human's own key for that relay. - On refusal, answer
denied. On timeout, answerapproval_required.
The provider MUST rate-limit pair so that a local process cannot flood the
human with approvals. A caller MUST treat a new pairing for an instance as
replacing its old token.
Operation policy
The provider classifies each sign_event request before signing. Callers MUST
NOT rely on the policy for safety; it exists so that a token holder, such as a
prompt-injected agent, cannot do more than the identity's ordinary work without
a person noticing.
Auth proofs
These kinds are accepted only when bound to one of the token's relays, and only
when fresh: created_at within 60 seconds of the provider's clock.
| Kind | Requirement |
|---|---|
22242 NIP-42 AUTH |
Exactly one relay tag, equal to a bound relay after URL normalization, and exactly one challenge tag |
27235 NIP-98 HTTP auth |
Exactly one u tag whose origin is a bound relay's origin (wss→https, ws→http), and exactly one method tag |
24242 Blossom auth |
Exactly one server tag equal to a bound relay's host, a t tag of upload or get, and an expiration no later than created_at + 60 |
A NIP-98 event for any other origin is denied. This rules out using the desktop
to sign in to other services, including a NIP-FA adapter: a caller using the
provider gets NIP-FI evidence from federated_identity and never mints its own.
Ordinary content
Kinds an identity uses for everyday work are signed without a prompt: messages
and their edits, reactions, threads and forum posts, typing and presence, read
state and user status, the identity's own lists (mute, pin, bookmark, emoji,
follow sets), canvases, artifacts, git patches, issues and status events, DM
open and hide, archive requests, workflow triggers, jobs, agent memory
(30174) and observer frames. The provider keeps the exact table, keyed by the
constants in crates/buzz-core/src/kind.rs.
Sensitive operations
These kinds change who the identity is, what it controls or what others see
about it. For each request the provider MUST either get a person's approval,
holding the request open for at most 120 seconds, or refuse it with denied.
If it asks and gets no decision, it answers approval_required; on refusal,
denied.
| Category | Kinds |
|---|---|
| Profile and relays | 0 profile, 10002 relay list, 10100 agent profile |
| Social graph | 3 contact list |
| Deletion | 5 deletion, 9005 group delete event, 45011 artifact removal |
| Membership and admin | 9000–9009 group admin, 9021/9022 join and leave, 28936 NIP-43 leave, 41011 DM add member |
| Moderation | 9040–9044 |
| Agent and team management | 30175 persona, 30176 team, 30177 managed agent, 30179 private managed agent |
| Workflow authority | 30620 workflow definition, 46030/46031 approval grant and deny |
| Repositories and projects | 30617 repository announcement, 30618 repository state, 30621 project |
The provider MAY remember an approval for a short, visible period, such as repeated deletions in one session, provided the human can see and revoke it.
Unknown kinds
Any kind not in the provider's table is denied, with a message naming the
kind. A caller that needs it can use an explicit key instead. Adding a kind to
the table is an additive provider change and does not change the version.
Encryption
nip44_encrypt and nip44_decrypt act only as the token's identity, so an
agent token can read only what was encrypted to the agent. They are subject to
the token's rate limits. The provider MAY require approval for decryption with a
peer other than the identity itself on an owner token.
Limits
- The provider SHOULD rate-limit each token per method and answer
rate_limitedwithretry_afterin seconds. - Tokens SHOULD expire. An inherited token lives no longer than the process it was issued to. A paired token SHOULD expire after a period of disuse that the human can see.
Revocation
The provider MUST revoke tokens when:
- the human revokes the caller in the desktop,
- the owner signs out or the desktop's account changes, which revokes every token,
- an agent is deleted, which revokes that agent's tokens, or
- a token expires.
A revoked, expired or unknown token gets revoked. A caller holding a revoked
paired token SHOULD delete it and tell the human to pair again.
Errors
| Code | HTTP | Meaning | Caller behavior |
|---|---|---|---|
approval_required |
403 | A person must approve and did not decide in time | Tell the human to approve in Buzz, then retry |
denied |
403 | Policy or the human refused | Fail; do not retry |
rate_limited |
429 | Too many requests | Wait retry_after seconds |
revoked |
401 | Token revoked, expired or unknown | Drop a paired token and try the next paired one; with an inherited token, fail (see Discovery) |
unavailable |
503 | The desktop cannot act now, for example signed out or its sign-in session lapsed | Fail with the message |
unsupported_version |
400 | See Versions | Retry with a common version |
unknown_method |
400 | The provider does not implement the method | Degrade (see below) |
invalid_request |
400 | Malformed envelope or params. For nip44_decrypt, also a payload that is not valid ciphertext for this identity |
Fail; for nip44_decrypt, treat the payload as invalid (NIP-AE filters it) |
Callers MUST decide on error, not on the HTTP status, and MUST treat an
unrecognized code as denied.
sign_event, nip44_decrypt and pair can be held for up to 120 seconds, so
callers MUST allow at least 150 seconds for them. Other methods SHOULD answer
within a few seconds.
Versions
The protocol version is a positive integer. This document defines version 1.
- A caller sends the highest version it supports in
Buzz-Credential-Provider. - A provider that does not support that version answers
unsupported_versionwith aversionsfield listing the versions it supports, for example"versions":[1,2]. The caller retries with the highest version both support, or treats the provider as not applicable if there is none. - A provider MUST keep supporting every version that a CLI it bundles, or a CLI released in the previous six months, speaks.
Additive changes do not change the version: new methods, new optional fields,
new error codes (treated as denied by older callers), and new kinds in the
policy table. Removing or changing the meaning of a method, field or code does.
When a provider answers unknown_method, a caller degrades rather than fails
where it can: no auth_tag means the caller sends no auth tag, and no
federated_identity means it sends no NIP-FI evidence.
Platforms
Version 1 supports macOS and Linux. On Windows the desktop MUST NOT set
BUZZ_CREDENTIAL_PROVIDER or write advertisements, and callers MUST skip the
desktop link, because loopback has no same-user check there. A named pipe with
an access control list is the planned Windows transport.
Security considerations
- Loopback cannot tell processes apart. Tokens, not the transport, identify callers. The same-user check stops a token reaching another user's process; it does not stop another process of the same user, which already has the user's files.
- Evidence leaves the desktop. The caller talks to the relay directly, so
federated_identityhands it the assertion. NIP-FI's assertion–key binding means a leaked assertion is useless without the identity's key. - Tokens in the environment. Child processes of an agent inherit
BUZZ_CREDENTIAL_PROVIDERand act as that agent. That matches the agent's existing authority. - An agent running as its owner's user can reach the owner's paired tokens.
The no-fallback rule binds only callers that follow it. An agent that can run
commands as the same OS user can unset
BUZZ_CREDENTIAL_PROVIDER, or read the paired tokens from the OS keychain itself, and then act as the owner. On macOS the keychain item belongs to thebuzzbinary, so it opens with no prompt. On Linux, Secret Service is readable once it is unlocked. The same-user check does not help here, because the caller really is the same user. Where agents should not act as their owner, sandbox them so they cannot reach the owner's keychain, or run them as a separate OS user with their own key. The desktop serves only callers running as its own user, so a separate-user agent cannot use it. - Approval fatigue. The provider prompts only for sensitive operations, never for every signature, so prompts stay meaningful.