Skip to content

feat(cli): add non-mutating buzz doctor command (#3926) - #4063

Open
iroiro147 wants to merge 2 commits into
block:mainfrom
iroiro147:claude/issue3926-20260801
Open

feat(cli): add non-mutating buzz doctor command (#3926)#4063
iroiro147 wants to merge 2 commits into
block:mainfrom
iroiro147:claude/issue3926-20260801

Conversation

@iroiro147

Copy link
Copy Markdown

feat(cli): add non-mutating buzz doctor command (#3926)

Closes #3926.

What

Adds a root-level buzz doctor subcommand that runs independent local + remote readiness checks and aggregates them into a single structured JSON report, so users / agents / automation / support flows can answer "is my Buzz environment ready to use?" without --help trivia and without failing at the first problem.

Why

  • Today troubleshooting means running a real command and interpreting the first error — a miss between local config, identity, reachability, auth, and membership all blur.
  • Buzz Desktop has a Doctor surface for managed ACP runtimes, but that's a different scope and unavailable headlessly.
  • A dedicated preflight fills the gap; this is the smallest useful slice per the issue's "first version" guidance.

How

Command

buzz doctor [--offline]

The subcommand is added directly to the top-level Cmd enum (not nested under a group), matching the existing architecture.

Dispatch order

run() in crates/buzz-cli/src/lib.rs is reshaped so doctor dispatches before the credential gate. Every other command still requires BUZZ_PRIVATE_KEY; doctor must remain usable when the broken environment is the one being debugged, so missing/malformed credentials surface as a check result (status error), not as an argument-time failure.

Checks (all independent, all run; one invocation reveals multiple problems)

Local, always-on:

  • relay_url — presence, syntax, canonicalization via the same normalize_relay_url the rest of the CLI uses.
  • identityBUZZ_PRIVATE_KEY parseability. Key material is never printed; only the derived public identity (hex) is reported on success.
  • auth_tagBUZZ_AUTH_TAG parse + verification against the signing identity via buzz_sdk::nip_oa::parse_auth_tag / verify_auth_tag. The tag is never printed. When set but the identity is invalid, we degrade to warning.
  • versionenv!("CARGO_PKG_VERSION").

Remote (skipped when --offline):

  • relay_reachable — an unauthenticated GET / with Accept: application/nostr+json, just to prove DNS/TLS/reachability.
  • nip11 — fetches and parses the NIP-11 information document end-to-end. Done with a bare reqwest::Client so doctor works even when the identity is broken.
  • auth_read — bounded authenticated read (single query_paginated call with limit=1) against kinds:[39002] #p=self. This doubles as the membership probe input.
  • membership — interprets the previous call: zero events → warning ("not a member of any channel visible to this relay"), ≥1 events → ok.

--offline marks the four remote probes skipped and they do not affect the outcome.

Output

Single JSON document on stdout:

{
  "status": "ok" | "warning" | "error",
  "checks": [
    {
      "id": "relay_url" | "identity" | "auth_tag" | "version" | "relay_reachable" | "nip11" | "auth_read" | "membership",
      "status": "ok" | "warning" | "error" | "skipped",
      "message": "",
      "remediation": "" | null
    }
  ]
}

Field identifiers are stable for automation.

Exit behavior

  • 0 when every applicable check is ok (warnings allowed; skipped never affects).
  • 3 when any identity / auth_tag / auth_read check is error (auth-shaped).
  • 2 when the only errors are relay_reachable / nip11 (transport-shaped), via CliError::Relay { status: 0, .. }.

This matches the existing 0=ok 1=bad input 2=relay 3=auth 4=other 5=conflict convention.

Never exposes

  • Private key bytes, hex, bech32 — only the derived public identity.
  • Auth tag JSON, owner pubkey embedded inside the tag, or any signed request material.

Testing

  • 8 new unit tests in commands/doctor.rs covering:
    • all-okOk(()), indico exit 0
    • warning-only → Ok(()), exit 0
    • identity error → CliError::Auth → exit 3
    • auth_read error → CliError::Auth → exit 3
    • relay_reachable error → CliError::Relay{0,..} → exit 2
    • mixed errors → auth wins
    • skipped never affects outcome
    • JSON shape has status + checks array with expected keys
  • Updated command_inventory_is_stable to include doctor (22 groups).
  • Full cargo test -p buzz-cli --lib: 279 passed, 0 failed.
  • cargo clippy -p buzz-cli --all-targets -- -D warnings: clean.
  • Smoke-tested: buzz doctor --help and buzz doctor --offline print the expected shapes; the latter exits 3 cleanly when BUZZ_PRIVATE_KEY is unset.

Out of scope (per the issue's own staging)

  • WebSocket readiness probe (flagged in the issue as possibly-too-big for v1).
  • Deeper runtime diagnostics (per-channel permission probes, ACP runtime health — Desktop's Doctor surface already owns ACP).
  • Integration test against a real relay (would need a fixture relay; matches how other read-only commands are covered today).

Notes for reviewers

  • The command is intentionally bind-mounted ahead of the credential gate; that's the load-bearing design decision, called out in code comments and module docs.
  • relay_reachable and nip11 share an HTTP request path (both GET / with the metadata accept header); currently issued as two probes for clarity, easy to fuse if a reviewer prefers.
  • The existing buzz users me / whoami work in feat(cli): add buzz users me for external-agent identity #2933 (when it lands) is complementary — it would give doctor a clean way to also report the resolved user profile, but isn't required for v1.

Adds a root-level `buzz doctor` subcommand that runs independent
local + remote readiness checks and aggregates them into a single
structured JSON report. Use it to answer 'is my Buzz environment
ready to use?' without publishing events or mutating state.

Checks performed:

- relay URL presence, syntax, canonicalization
- BUZZ_PRIVATE_KEY parseability (never printed; only the derived
  public identity is reported)
- BUZZ_AUTH_TAG syntax + verification against the signing identity
  (never printed)
- CLI version
- relay unauthenticated reachability probe
- NIP-11 parseation against the relay info document
- authenticated read probe (single bounded query)
- community membership probe (kind:39002 #p=self)

`--offline` runs only local checks and marks remote probes as
skipped. Missing or malformed config surfaces as a check result
with status error, not as an argument-time failure, so doctor
remains useful when the environment being debugged is the broken
one.

Exit behavior distinguishes failed required checks from warnings:
  0  all applicable checks ok (warnings allowed)
  3  any auth/identity check failed
  2  any relay/network check failed
All applicable checks run so one invocation can reveal multiple
problems.

Tests cover the aggregator's exit-precedence (auth wins over
network, skipped never affects outcome, warnings stay zero) and
the JSON output shape.

Signed-off-by: iroiro147 <sarthak.singh@juspay.in>
@iroiro147
iroiro147 requested a review from a team as a code owner August 1, 2026 05:34
… not auth (exit 3) (block#3926)

The first cut routed every authed-read failure into the same
auth-shaped bucket. On an unreachable relay the relay_reachable
probe was reported error, then the authed read ALSO reported
auth_read error, which the aggregator mapped to exit 3 — even
though a DNS/connect failure has nothing to do with credentials.

Split the failure handling:

- Distinguish true auth rejections (Auth variant, or relay
  401/403) from transport/other relay failures. Only the former
  produces an auth_read error -> exit 3.
- Track whether the unauthenticated reachability probe already
  failed; if so, skip re-emitting the relay-side error and mark
  auth_read/membership as skipped instead.
- If reachability passed but the authed read failed for a
  transport reason, emit a relay_reachable error explaining the
  authed leg failed.

Smoke-verified: with BUZZ_RELAY_URL pointing at an unresolvable
host, exit is now 2 with relay_reachable + nip11 reporting the
transport problem and auth_read/membership cleanly skipped.

Signed-off-by: iroiro147 <sarthak.singh@juspay.in>
@iroiro147

Copy link
Copy Markdown
Author

Added a follow-up commit d576d88a that tightens the exit-code classification. The first cut treated every authed-read failure as auth (exit 3), which was wrong for unreachable relays — on a DNS/connect failure the doctor should exit 2 and report the transport problem, not blame credentials. The new commit distinguishes true auth rejections (Auth variant or relay 401/403) from transport errors, and avoids double-reporting the reachability failure when both the public and authed probes go down. All 279 buzz-cli tests still pass; clippy clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: add a non-mutating buzz doctor command

1 participant