Skip to content

docs(speckit): human-readable wallet error messages spec (#179)#292

Merged
haseebrabbani merged 2 commits into
mainfrom
009-human-readable-errors
Jun 25, 2026
Merged

docs(speckit): human-readable wallet error messages spec (#179)#292
haseebrabbani merged 2 commits into
mainfrom
009-human-readable-errors

Conversation

@haseebrabbani

@haseebrabbani haseebrabbani commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What

Draft spec only (no implementation yet) for issue #179 — surface short, user-friendly error messages for common wallet failures instead of raw backend/transport errors.

Adds speckit/features/009-human-readable-errors/spec.md following the house spec-kit format.

Refs #179.

Approach (proposed)

Server-side single source of truth:

  • Add GuardianError::user_message() — a short, sanitized sentence per variant (no account IDs / commitments / paths / raw upstream text).
  • Surface it additively as user_message on the HTTP error envelope and the gRPC Status details (generalizing the existing GUARDIAN_ACCOUNT_PAUSED details pattern). code, error, and all status mappings stay frozen.
  • Rust + TS clients consume it; only codeless transport failures (server never reached) are classified client-side — the pattern from 0xMiden/wallet chore(release): Release v0.14.9 #273 (connectivity-classify.ts).
  • i18n stays client-side keyed on the stable code; server ships English defaults.

Anchored to the constitution: HTTP/gRPC parity (II), bottom-up propagation to Rust+TS clients (I), additive/stable boundary errors (IV). Bonus: stops the current raw error strings (which embed IDs/commitments/raw RPC text) from leaking to wallet clients.

Open questions for reviewers (@MCarlomagno @zeljkoX)

  1. Final wire field name — user_message vs hint vs display_message.
  2. Granular per-cause code for internal errors, or collapse to one internal_error.
  3. Ship the starter copy catalog now, or gate wording on UX review.
  4. Emit on all surfaces (incl. operator dashboard) vs wallet-only.

The 2026-06-18 clarifications in the spec are marked proposed — pending confirmation.

Not included

Implementation, plan.md / data-model.md / contracts/. Those follow once direction is confirmed (per AGENTS.md §4 contract-change workflow: proto + OpenAPI regen → Rust client → TS clients → multisig SDK → examples).

Summary by CodeRabbit

  • Documentation
    • Added a feature specification for clearer, user-safe error messages across wallet-facing APIs.
    • Defines a consistent error shape with stable codes and readable messages for both HTTP and gRPC responses.
    • Documents how common connectivity and internal failures should be presented, including behavior for unknown future error codes.

Draft spec for issue #179 — surface short, user-friendly error
messages for common wallet failures instead of raw backend/transport
errors. Server-side approach: add GuardianError::user_message(),
surfaced additively on the HTTP error envelope and gRPC Status
details; Rust/TS clients consume it and classify codeless transport
failures (pattern from 0xMiden/wallet #273).
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c6094fd0-e9ce-4510-a770-a63562543bf0

📥 Commits

Reviewing files that changed from the base of the PR and between 8ca8e7a and 71ea88f.

📒 Files selected for processing (1)
  • speckit/features/009-human-readable-errors/spec.md
 _________________________________________________
< My whiskers twitch when I detect a memory leak. >
 -------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 009-human-readable-errors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.57%. Comparing base (89e7f3a) to head (71ea88f).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #292      +/-   ##
==========================================
- Coverage   76.88%   76.57%   -0.31%     
==========================================
  Files         153      154       +1     
  Lines       27373    27662     +289     
==========================================
+ Hits        21045    21182     +137     
- Misses       6328     6480     +152     

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 89e7f3a...71ea88f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Speckit feature specification for surfacing short, sanitized, end-user-safe error messages to wallet clients, while keeping existing machine-readable error codes and status mappings stable and additive across HTTP and gRPC.

Changes:

  • Introduces a draft spec for adding a user_message alongside existing code/error fields.
  • Defines user stories, acceptance scenarios, and contract requirements for HTTP + gRPC parity and client consumption.
  • Proposes a starter copy catalog and outlines connectivity vs server-authored messaging responsibilities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +66
When Guardian hits an internal fault (storage error, signing error, configuration error, degraded data), the wallet user must see a single safe, generic message — never a file path, a raw RPC string, a stack trace, or a database detail. The diagnostic detail remains available to operators/logs via the unchanged `error` field, but it is never the message the wallet presents.

**Why this priority**: This is both a UX requirement and a security/disclosure requirement (Constitution Principle IV: stable boundary errors). Raw internal text leaking to an untrusted wallet client is a disclosure risk that exists today.

**Independent Test**: Trigger each internal variant (`StorageError`, `SigningError`, `ConfigurationError`, `DataUnavailable`, `NetworkError`) and assert the `user_message` equals the generic safe string, contains no path/URL/hash/raw-error fragment, while the `error` field still carries the diagnostic detail for logging.

**Acceptance Scenarios**:

1. **Given** the storage backend fails, **When** any wallet request errors, **Then** `user_message` is the generic "Something went wrong on Guardian's side. Please try again." and the diagnostic text appears only in `error`.
2. **Given** an upstream RPC returns a raw transport error, **When** Guardian maps it to `RpcUnavailable`, **Then** `user_message` is a connectivity-style sentence with the raw upstream text stripped, while `error` retains the raw text.
3. **Given** any internal variant, **When** the `user_message` is scanned, **Then** it matches none of the disallowed patterns (hex commitments/IDs, file paths, `http(s)://` URLs, "error:" fragments from upstream).

- **FR-001**: `GuardianError` MUST expose a `user_message()` accessor returning a short, plain-language, end-user-safe string for **every** variant. The set of `code()` values and their HTTP/gRPC status mappings MUST remain unchanged by this feature.
- **FR-002**: `user_message()` output MUST be safe-by-construction: it MUST NOT contain account IDs, commitments, nonces, signer IDs, file paths, URLs, raw upstream/RPC error text, or any value interpolated from the variant's payload fields. (Contrast with `Display`, which may.)
- **FR-003**: Internal and non-user-actionable variants (`StorageError`, `SigningError`, `ConfigurationError`, `DataUnavailable`, and the internal portion of `NetworkError`/`RpcUnavailable`) MUST map to a single shared generic message (e.g. "Something went wrong on Guardian's side. Please try again."). User-actionable variants MUST map to a category-appropriate message (see Key Entities catalog).

@zeljkoX zeljkoX 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.

Since Guardian is still early-phase with no external consumers locked to the current contract, I think we should treat this as a breaking change rather than add user_message additively. The additive path keeps two human-ish strings whose names don't say which is safe to show, plus a flat envelope that grows an optional field per variant. A clean reshape is cheap now.

Proposed wire shape — one object, identical on the HTTP body and the gRPC Status.details:

{
  "code": "authorization_failed",
  "message": "You're not an authorized signer for this account.",
  "meta": { "retryable": false }
}
  • code — stable machine code (existing vocabulary, unchanged). Branch key + i18n key.
  • message — user-safe sentence, always present and safe to display. Only code is stable; wording can change.
  • meta — structured machine data; holds the fields that are top-level optionals today (retryable, retry_after_secs, missing_permissions, paused_at, paused_reason). Omitted when empty.

The diagnostic string stays off the wire — server logs only. The Display text (account IDs, commitments, postgres://…, raw RPC errors) is exactly what we don't want to hand an untrusted wallet. Logging it instead of returning it removes the disclosure risk by construction. This also frees us to name the field plainly message (the old diagnostic error field is gone). We also drop success (the HTTP status is the discriminator; gRPC never had it).

Examples:

{ "code": "rate_limit_exceeded", "message": "Too many requests — please try again shortly.", "meta": { "retryable": true, "retry_after_secs": 30 } }
{ "code": "storage_error", "message": "Something went wrong on Guardian's side. Please try again.", "meta": { "retryable": true } }

gRPC: Status.code unchanged, Status.message becomes the user-safe message, and Status.details carries the same {code, message, meta} object. Details is still needed because the ~16 canonical gRPC codes can't recover our 37 stable code values (e.g. authorization_failed / signer_not_authorized both map to PERMISSION_DENIED).

Adopt @zeljkoX's breaking reshape to a single {code, message, meta}
object, identical on the HTTP body and gRPC Status.details. Drop the
diagnostic `error` field (logs only) and `success`; rename user_message
-> message; fold top-level optionals into meta.

Also split server-mapped connectivity faults (network_error,
rpc_unavailable, rpc_validation_failed) from pure internal faults,
resolving the Copilot review's FR-003/User-Story-2 inconsistency.
@haseebrabbani

Copy link
Copy Markdown
Collaborator Author

@zeljkoX Thanks for the feedback, pushed an update to reshape the spec accordingly:

  • Single { code, message, meta } object, byte-identical on the HTTP body and gRPC Status.details; Status.message = the user-safe message.
  • Diagnostic Display text is off the wire entirely (server logs only), and success/error are gone. The meta block absorbs retryable, retry_after_secs, missing_permissions, paused_at, paused_reason, omitted when empty.
  • code vocabulary and HTTP/gRPC status mappings unchanged; framed as a coordinated breaking change across server → proto/OpenAPI → Rust client → TS clients (incl. operator-client) → examples.
  • Also folded in Copilot's catch: explicit split between server-mapped connectivity faults (network_error / rpc_unavailable / rpc_validation_failed) and pure internal faults, so the requirements/catalog no longer contradict.

Three things still open in the spec if you have a preference:

  1. OQ-2 — keep granular internal codes sharing one message vs collapse to internal_error (I'm leaning keep-granular).
  2. OQ-3 — ship the copy catalog now vs gate wording on UX review.
  3. OQ-5 — populate meta.retryable for every variant vs only where non-obvious.

@zeljkoX

zeljkoX commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

LGTM

@haseebrabbani haseebrabbani marked this pull request as ready for review June 25, 2026 17:39
@haseebrabbani haseebrabbani merged commit dd66999 into main Jun 25, 2026
16 of 17 checks passed
@haseebrabbani haseebrabbani deleted the 009-human-readable-errors branch June 25, 2026 17:39
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants