Skip to content

Rust CLI: match Node's API-error stderr shape; scope x402 hints to payments - #333

Merged
poiley merged 7 commits into
rust-cli-portfrom
rust-cli-error-shape
Jul 22, 2026
Merged

Rust CLI: match Node's API-error stderr shape; scope x402 hints to payments#333
poiley merged 7 commits into
rust-cli-portfrom
rust-cli-error-shape

Conversation

@poiley

@poiley poiley commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Two behavioral divergences found by a live Node-vs-Rust differential against staging (same API key through both CLIs, byte-compared output; 12/17 read-only commands were already identical):

  1. API-error stderr shape. On any 4xx/5xx, the Node CLI unwraps the response envelope and prints the inner error object ({"code": ..., "message": ...}, see extractErrorPayload/formatErrorPayload in api-command.ts); the Rust port printed the raw envelope ({"error": {...}, "success": false}). Scripts that parse stderr and read .code break when switching CLIs. The parity fixtures never caught it because every 4xx/5xx case asserted contains substrings, which both shapes satisfy.
  2. Misleading x402 hint. The Rust port mapped the feature_disabled error code to the "x402 payments are not enabled" hint globally, so e.g. wake schedules list on a non-wake org got a payments hint. The Node CLI scopes that hint table to payments commands (payments-shared.ts).

What changed

  • client.rs::error_message_for_status now unwraps a non-null error field before pretty-printing, matching Node.
  • The x402 hint table moved behind a new error_for_status_with_payment_hints, used only by the three payments.rs call sites. All other friendly commands keep the generic (unauthorized) hint only.
  • Exception, matching Node: the payments path deliberately prints the whole envelope (Node's payments-shared.ts documents the envelope as the most useful payload for x402 flows), so the payments variant uses the unflattened form.
  • Four unit tests pin the flatten, the non-envelope passthrough, the hint scoping, and the unauthorized hint.
  • Three new parity fixtures assert the exact error shapes: generated-command flatten and wake-without-hint run with full Node↔Rust cross-compare; the payments case pins envelope+hint per-runner (compare: false only because Node preserves server key order while serde_json sorts keys, and the trailing hint line keeps the stream from being JSON-normalized).

Note for a possible follow-up

The key-order difference above is the last obstacle to byte-identical error output: enabling serde_json's preserve_order feature would make Rust emit fields in server order like Node everywhere. Left out of this PR since it changes output ordering globally.

Validation

  • make rust-cli-check (fmt, clippy -D warnings, all tests)
  • make cli-parity: 456/456 cases green including the three new exact-shape cases
  • Live re-check against staging: wake schedules list, emails:get-email (bad id), and invalid-key errors now byte-match Node's stderr; payments envelope+hint retained

🤖 Generated with Claude Code

etbyrd and others added 2 commits July 20, 2026 09:04
A live Node-vs-Rust differential against staging surfaced two divergences.

The Node CLI unwraps the response envelope and prints the inner error
object to stderr ({code, message, ...}); the Rust port printed the raw
{error: {...}, success: false} envelope on every 4xx/5xx, breaking any
script that parses stderr and reads .code. error_message_for_status now
unwraps a non-null `error` field before pretty-printing. Exception,
matching Node: the payments path deliberately keeps the whole envelope
(payments-shared.ts documents it as the most useful payload for x402
flows), via a raw variant.

The Rust port also mapped the feature_disabled code to the "x402 payments
are not enabled" hint globally, so wake commands on a non-wake org got a
misleading payments hint. The x402 hint table now sits behind
error_for_status_with_payment_hints, used only by payments.rs; other
commands keep the generic unauthorized hint, like Node's ERROR_CODE_HINTS.

The parity fixtures never caught the shape divergence because all 4xx/5xx
cases asserted contains substrings that both shapes satisfy. Added three
cases pinning the exact shapes: generated-command flatten and
wake-without-hint with full Node<->Rust cross-compare; payments
envelope+hint per-runner (compare off only for JSON key order: Node
preserves server order, serde_json sorts, and the trailing hint line keeps
the stream from being JSON-normalized by the harness).

Validation: make rust-cli-check, cli-parity (456 cases); live staging
re-check shows byte-identical stderr for wake, emails, and invalid-key
errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • The payment error shape matches the documented Node behavior.
  • The x402 hint path is limited to payment command helpers.

Important Files Changed

Filename Overview
cli-rust/src/client.rs Splits generic and payment error formatting so generic errors flatten while payment errors keep the envelope.
cli-rust/src/payments.rs Routes payment HTTP error handling through the payment-specific formatter.
test-fixtures/cli-parity/cases.json Adds parity coverage for flattened generic errors, wake errors without x402 hints, and payment errors with the x402 hint.
cli-rust/tests/api_commands.rs Adds bounded blocking reads to the generated API test server helper.
cli-rust/tests/config.rs Adds bounded blocking reads to the OAuth refresh test server helper.
cli-rust/tests/functions_commands.rs Adds bounded blocking reads to the functions command test server helper.
cli-rust/tests/payloads_auth.rs Adds bounded blocking reads to the payload auth test server helper.

Reviews (5): Last reviewed commit: "Merge updated rust-cli-port base" | Re-trigger Greptile

Comment thread cli-rust/src/client.rs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
poiley and others added 2 commits July 20, 2026 19:08
# Conflicts:
#	.github/workflows/rust-cli-release.yml
#	.github/workflows/sdk-checks.yml
#	Makefile
#	cli-rust/src/auth_commands.rs
#	cli-rust/src/client.rs
#	cli-rust/src/completion_commands.rs
#	cli-rust/src/payments.rs
#	cli-rust/tests/auth_commands.rs
#	scripts/run-cli-help-sweep.mjs
#	scripts/run-cli-parity.mjs
#	test-fixtures/cli-parity/cases.json
The accepted stream inherits the listener's nonblocking mode on macOS, so
an early read races the client and panics with EWOULDBLOCK (os error 35),
seen intermittently in CI (redeploy_wait_failed / deploy_wait_failed on
macos-x64 and macos-arm64). Force the accepted stream back to blocking
with a bounded read timeout in all four test servers that use the
nonblocking-accept pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@etbyrd
etbyrd force-pushed the rust-cli-port branch 5 times, most recently from 322c5b5 to 0765fa5 Compare July 21, 2026 17:25
# Conflicts:
#	.github/workflows/rust-cli-release.yml
#	.github/workflows/sdk-checks.yml
#	Makefile
#	cli-rust/src/client.rs
#	cli-rust/src/completion_commands.rs
#	cli-rust/src/friendly.rs
#	cli-rust/src/functions_commands.rs
#	cli-rust/src/help_snapshots.generated.rs
#	cli-rust/src/payments.rs
#	cli-rust/tests/api_commands.rs
#	cli-rust/tests/completion_commands.rs
#	cli-rust/tests/config.rs
#	cli-rust/tests/functions_commands.rs
#	cli-rust/tests/payloads_auth.rs
#	scripts/generate-rust-cli-help-snapshots.mjs
#	scripts/run-cli-help-sweep.mjs
#	scripts/run-cli-parity.mjs
#	test-fixtures/cli-parity/cases.json
@poiley poiley mentioned this pull request Jul 21, 2026
@poiley
poiley merged commit 7e39e2c into rust-cli-port Jul 22, 2026
19 checks passed
@poiley
poiley deleted the rust-cli-error-shape branch July 22, 2026 20:25
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