Skip to content

fix(nip-oa): accept raw Nostr tag form in parse_json_array - #4203

Merged
tlongwell-block merged 4 commits into
block:mainfrom
amanning3390:fix/nip-oa-accept-raw-tag-form
Aug 2, 2026
Merged

fix(nip-oa): accept raw Nostr tag form in parse_json_array#4203
tlongwell-block merged 4 commits into
block:mainfrom
amanning3390:fix/nip-oa-accept-raw-tag-form

Conversation

@amanning3390

@amanning3390 amanning3390 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What

BUZZ_AUTH_TAG stored in the raw Nostr tag form [auth,hex,,hex] (unquoted, comma-delimited — how an auth tag serializes inside a Nostr event and how .env files commonly store it) was rejected by the CLI:

BUZZ_AUTH_TAG is malformed: invalid JSON: expected value at line 1 column 2

…and even when the CLI could parse it, it forwarded the raw string as the x-auth-tag header, so the relay's verify_auth_tag (which expects JSON) rejected it with 403 relay_membership_required.

Two commits close both gaps.

Commits

1. fix(nip-oa): accept raw Nostr tag form in parse_json_array

parse_json_array (crates/buzz-sdk/src/nip_oa.rs) only accepted well-formed JSON arrays. Added a fallback: when strict JSON parsing fails and the trimmed input is bracket-delimited, split on , and treat each field as a string (empty field ,, → empty string, matching ["auth","hex","","hex"]). All consumers (parse_auth_tag, verify_auth_tag, the CLI, buzz-acp) benefit from one change at the lowest layer.

2. fix(cli): canonicalize BUZZ_AUTH_TAG to JSON before sending x-auth-tag header

The CLI stored the raw input string and sent it verbatim as the x-auth-tag header (client.rs:618). Added canonicalize_auth_tag in buzz-sdk: parse either form, re-serialize to canonical JSON. The CLI now canonicalizes before storing as auth_tag_json, so the header is always valid JSON regardless of input form.

Together: local parse + wire canonicalization means the raw form works end-to-end.

Why

The raw form [auth,hex,,hex] is exactly how an auth tag serializes inside a Nostr event. That shape leaks into .env files and shell variables because there's no canonical "stored form" outside an event. The SDK + CLI should accept it rather than push quoting/conversion logic onto every consumer (harnesses, agent shells, external tools).

Security

Both changes are purely syntactic — they only change how a 4-element string array is extracted and containerized. All downstream validation is unchanged:

  • parse_auth_tag: still checks exactly 4 elements, "auth" label, 64-char lowercase-hex pubkey, 128-char signature.
  • verify_auth_tag: still reconstructs the preimage and verifies the BIP-340 Schnorr signature against the owner pubkey.

No new attack surface — a malformed or forged tag is still rejected at the same validation points.

Tests

4 new tests in nip_oa::tests:

  • test_parse_auth_tag_raw_nostr_form — raw form with conditions + empty conditions
  • test_parse_auth_tag_raw_form_with_whitespace — raw form with surrounding whitespace
  • test_canonicalize_auth_tag_raw_to_json — raw→JSON and JSON→JSON normalization

All 25 nip_oa tests pass (21 existing + 4 new). cargo fmt --check and cargo clippy -p buzz-sdk -p buzz-cli clean.

Verification

Confirmed end-to-end against a live community relay (wss://hermesagent.communities.buzz.xyz):

  • Before: raw BUZZ_AUTH_TAG → CLI parse error, or 403 relay_membership_required if somehow parsed.
  • After: raw BUZZ_AUTH_TAG → CLI parses it, canonicalizes to JSON for the header, relay accepts via NIP-OA owner delegation, buzz channels members returns the full roster.

Context

Originated from a community investigation where agent-side relay access was failing because the harness-exported BUZZ_AUTH_TAG (raw Nostr form) was rejected by the CLI (expecting JSON). This removes the impedance mismatch at the source.

parse_json_array only accepted well-formed JSON arrays, so
parse_auth_tag / verify_auth_tag rejected BUZZ_AUTH_TAG values stored
in the raw Nostr tag form ([auth,hex,,hex]) — the unquoted,
comma-delimited serialization used inside Nostr events and commonly
written to .env files and shell variables.

This forced every consumer (buzz-acp harness, agent shells sourcing
BUZZ_AUTH_TAG from .env) to re-quote the value into JSON before the
CLI would accept it, or the CLI failed with
'BUZZ_AUTH_TAG is malformed: invalid JSON'.

Add a fallback in parse_json_array: when strict JSON parsing fails and
the trimmed input is bracket-delimited, split on commas and treat each
field as a string (empty field -> empty string, matching the JSON form
["auth","hex","","hex"]). Well-formed JSON still takes the fast path;
only non-JSON bracketed input triggers the fallback.

This is the lowest layer, so all consumers (parse_auth_tag,
verify_auth_tag, the CLI, buzz-acp) benefit from one change.

Tests: 3 new (raw form with conditions, raw form with empty conditions,
raw form with whitespace) + all 21 existing nip_oa tests pass.

Signed-off-by: amanning3390 <adam.manning@pro-serveinc.com>
@amanning3390
amanning3390 requested a review from a team as a code owner August 1, 2026 23:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53de09881f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-sdk/src/nip_oa.rs Outdated
_ => Err(SdkError::InvalidInput(
"auth tag must be a JSON array".into(),
)),
let trimmed = s.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required DCO sign-off

The reviewed commit object has no Signed-off-by trailer, so the repository's required DCO check will fail and prevent this change from merging; recreate or rebase the commit with --signoff.

AGENTS.md reference: AGENTS.md:L111-L111

Useful? React with 👍 / 👎.

Comment thread crates/buzz-sdk/src/nip_oa.rs Outdated
Comment on lines +145 to +146
if !arr.is_empty() {
return Ok(arr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Canonicalize raw tags before HTTP transport

When the newly supported raw form is supplied to buzz, parsing succeeds here, but run() still stores the original string in BuzzClient::auth_tag_json, which with_auth_tag() sends verbatim as x-auth-tag. During a client-first or rolling upgrade, relays predating this parser change still require JSON and reject every membership-scoped HTTP operation even though the CLI accepted and verified the tag locally; carry a canonical JSON serialization of arr for transport instead of the raw input.

Useful? React with 👍 / 👎.

…g header

The CLI stored the raw BUZZ_AUTH_TAG input string and sent it verbatim as
the x-auth-tag header value (client.rs:618). When the tag was in the raw
Nostr form ([auth,hex,,hex]), the relay's verify_auth_tag — which expects
JSON — rejected it with 403 relay_membership_required, even though the CLI
had parsed it locally.

Add canonicalize_auth_tag in buzz-sdk: parse either form (JSON or raw),
re-serialize to canonical JSON. The CLI now canonicalizes the tag before
storing it as auth_tag_json, so the header is always valid JSON regardless
of input form.

This closes the loop with the parse_json_array fallback: local parse +
wire canonicalization means the raw form works end-to-end.

Signed-off-by: amanning3390 <adam.manning@pro-serveinc.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d574fe4a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/buzz-sdk/src/nip_oa.rs Outdated
Comment on lines +139 to +143
if trimmed.starts_with('[') && trimmed.ends_with(']') {
let inner = &trimmed[1..trimmed.len() - 1];
let arr: Vec<Value> = inner
.split(',')
.map(|part| Value::String(part.trim().to_owned()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse the raw form in Git auth consumers

When BUZZ_AUTH_TAG uses the newly accepted raw form, agent Git operations still reject it locally: git-credential-nostr::load_auth_tag calls serde_json::from_str at crates/git-credential-nostr/src/lib.rs:84-86, and git-sign-nostr::load_auth_tag does the same at crates/git-sign-nostr/src/lib.rs:490-492. These tools receive the same harness-injected environment variable, so fetch/push and signing fail before reaching the relay even though the CLI now accepts the credential. Fresh evidence in this target is that only the CLI transport was canonicalized; either canonicalize the injected value centrally or teach both Git parsers the same raw syntax.

Useful? React with 👍 / 👎.

…ign-nostr

Address Codex review P2: git-credential-nostr::load_auth_tag and
git-sign-nostr::load_auth_tag parsed BUZZ_AUTH_TAG with serde_json::from_str
directly, so the raw Nostr tag form ([auth,hex,,hex]) newly accepted by the
SDK and CLI still failed for Git fetch/push/sign operations.

Add a parse helper in each crate that mirrors the SDK's parse_json_array
behavior: JSON fast path, then a raw-form fallback that splits on commas.
No new dependencies — each git crate keeps its existing dep set.

Also fix a clippy needless_question_mark warning in canonicalize_auth_tag.

Test: new test_load_auth_tag_accepts_raw_nostr_form covers raw form with
empty conditions, conditions, and whitespace in git-sign-nostr.

Note: test_parse_envelope_rejects_invalid_oa_pubkey is a pre-existing
failure on upstream/main (unrelated to this change) — confirmed by running
it with these commits stashed.

Signed-off-by: amanning3390 <adam.manning@pro-serveinc.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88471dd668

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/git-sign-nostr/src/lib.rs Outdated
let sig = "b".repeat(128);

// Raw form with empty conditions (`,,`).
std::env::set_var("BUZZ_AUTH_TAG", format!("[auth,{owner},,{sig}]"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize tests that mutate BUZZ_AUTH_TAG

Under Cargo's default parallel test execution, this new test can run alongside test_load_auth_tag_rejects_bad_conditions, and both mutate the same process-wide BUZZ_AUTH_TAG. Either test can therefore read or remove the other's value, causing assertions to fail nondeterministically; protect both tests with a shared mutex or test the parser directly without modifying the environment.

Useful? React with 👍 / 👎.

@tlongwell-block tlongwell-block left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for one concrete regression and one API-contract issue at 88471dd668c633deae2ef0ae5e2658e1dd70b9d7.

  1. The new test_load_auth_tag_accepts_raw_nostr_form mutates process-global BUZZ_AUTH_TAG concurrently with the existing test_load_auth_tag_rejects_bad_conditions. This makes the package suite flaky. Running cargo test -q -p git-sign-nostr --lib test_load_auth_tag -- --test-threads=2 repeatedly failed on iteration 2: the existing test expected kind = 9 to be rejected but read the new test's valid value instead. Please test a pure parsing helper, or serialize all tests that mutate this env var behind the same lock.

  2. canonicalize_auth_tag is public and documents that it errors unless its input is a valid 4-element auth tag, but the implementation only checks array length. For example, ["not-auth","x","y","z"] canonicalizes successfully. The CLI happens to call parse_auth_tag and verify_auth_tag first, so the current CLI path remains fail-closed, but the public helper does not uphold its stated contract and invites unsafe standalone use. Either make this a private serialization helper or have it validate via the same typed/validated path before returning.

The production direction is otherwise sound for the exact accepted grammar: strict JSON remains the fast path, raw fallback fields are still checked for label/count/lowercase hex/conditions/signature, CLI verification remains before header construction, and canonical JSON fixes the wire header. Splitting on commas is safe only because the current conditions grammar forbids commas; that dependency should be explicit.

Also, the repeated parser implementations in SDK, credential helper, and signer are drift-prone, and the claim that [auth,...] is how a tag serializes "inside a Nostr event" is inaccurate: an event serializes tags as JSON arrays. This may be an accepted local/env shorthand, but it is not Nostr event JSON.

…SDK and wire grammar strict

Review follow-up to the previous three commits, keeping their intent
(accept the hand-authored unquoted BUZZ_AUTH_TAG shorthand) while
narrowing where the leniency lives:

- Revert buzz-sdk parse_json_array to strict JSON. verify_auth_tag is
  the relay's x-auth-tag entry point (extract_nip_oa_owner and the
  bridge/api handlers), so the lenient grammar had widened the public
  wire format; probing that entry point base-vs-head showed raw form
  going rejected->accepted. Wire grammar is JSON per NIP-GS; it stays
  strict.
- Remove canonicalize_auth_tag. The CLI now derives the wire string
  from the already parsed-and-verified Tag via
  serde_json::to_string(tag.as_slice()) — the same shape buzz-acp's
  RestClient has always used — instead of re-parsing unverified input.
  This also removes the helper whose docs promised full validation but
  whose body only checked element count.
- Revert the hand-copied fallback parsers in git-credential-nostr and
  git-sign-nostr. The copies already diverged from the SDK (Vec<String>
  vs Value fast paths) and NIP-GS specifies the JSON form for these
  consumers. Reverting git-sign-nostr also removes the second
  BUZZ_AUTH_TAG-mutating test that raced the existing one under the
  default parallel test runner.
- Add normalize_auth_tag_input in buzz-cli: a small, explicit raw->JSON
  rewrite applied only to hand-authored configuration input, before the
  unchanged strict parse/verify path. Valid JSON passes through
  untouched; unrecognizable input is left for the strict parser to
  reject with an error about the original bytes.

Net: BUZZ_AUTH_TAG in raw form still works end-to-end via the CLI
(verified live against a relay with a real delegated credential, plus
JSON control and fail-closed garbage control), and the SDK, relay, and
git binaries keep the strict grammar they had before this PR.

Tests: buzz-cli 274, buzz-sdk 241, git-credential-nostr 8, buzz-relay
835 lib tests pass; git-sign-nostr suite no longer flakes under the
default runner (8/8 repeat runs; the one remaining failure is
test_parse_envelope_rejects_invalid_oa_pubkey, which fails identically
on base ac4fa13 on macOS and is unrelated). fmt and clippy clean.

Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
@tlongwell-block

Copy link
Copy Markdown
Collaborator

Pushed a maintainer follow-up commit (0b74eee) that keeps this PR's intent — the unquoted [auth,hex,,hex] shorthand for hand-authored BUZZ_AUTH_TAG works end-to-end via the CLI — while narrowing where the leniency lives:

  • SDK, git-credential-nostr, git-sign-nostr: reverted to base. verify_auth_tag is the relay's x-auth-tag entry point, so the lenient grammar had widened the public wire format (probing extract_nip_oa_owner base-vs-head showed raw going rejected→accepted). Wire grammar stays strict JSON per NIP-GS. Reverting git-sign-nostr also removes the new test that raced test_load_auth_tag_rejects_bad_conditions on the process-global env var under the default parallel runner.
  • canonicalize_auth_tag removed. The CLI now derives the wire string from the already parsed-and-verified Tag via serde_json::to_string(tag.as_slice()) (same shape buzz-acp's RestClient uses) instead of re-parsing unverified input.
  • The leniency lives in one place: normalize_auth_tag_input in buzz-cli, applied to the configuration input before the unchanged strict parse/verify path. Valid JSON passes through untouched; unrecognizable input is left for the strict parser to reject.

Verified locally at 0b74eee: buzz-cli 274, buzz-sdk 241, git-credential-nostr 8, buzz-relay lib 835 pass; git-sign-nostr no longer flakes across 8 consecutive default-settings runs; fmt/clippy clean; live CLI run against a relay with a real delegated credential in both JSON and raw forms, plus a fail-closed garbage control.

Thanks for the report and the repro — the shorthand support was worth having; it just needed to live at the config edge rather than in the shared grammar.

@tlongwell-block
tlongwell-block merged commit 89bf03c into block:main Aug 2, 2026
33 checks passed
tellaho added a commit that referenced this pull request Aug 2, 2026
- Incorporate upstream desktop changes required by the pre-push overlap guard
- Preserve the link preview thumbnail stabilization work on the updated base

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>

* origin/main: (26 commits)
  docs: formal spec for remote agents and their management (#3748)
  fix(nip-oa): accept raw Nostr tag form in parse_json_array (#4203)
  perf(relay): serve relay-membership checks from the read replica (#4124)
  chore(deps): bump nostr-relay-pool for RUSTSEC-2026-0224 (#4139)
  docs(nostr): document #h requirement for live reaction subscriptions (#3487)
  docs(chart): fix ArgoCD example for native OCI sources (full artifact repoURL + path) (#3426)
  docs(readme): clarify which release asset to download per platform (#3481)
  fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) (#3998)
  docs: note that addressable channel events scope by d, not h (#4103)
  docs: fix stale kind count, quick-start numbering, and empty Further Reading (#2613)
  fix(desktop): keep thread-open affordance in archived channels (#4012)
  docs: add one-click Railway deploy for a hosted relay (#2733)
  fix(desktop): point Oh My Pi preset at omp.sh (#3516)
  fix(mesh): stop restarting a busy or loading shared-compute node (#3909)
  fix(desktop): preserve first huddle speech (#3962)
  feat(desktop): Agent Trading Cards — mintable agent-snapshot card PNGs with optional NIP-44 lock (#3278)
  fix(buzz-acp): thread cache-read tokens into NIP-AM kind:44200 events (#3999)
  feat(relay): accept kind:30621 multi-repo projects at ingest (#3171)
  fix(release): preserve main in desktop PR body (#3979)
  chore(release): release Buzz Desktop version 0.5.3 (#3972)
  ...

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
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.

2 participants