Skip to content

fix: error taxonomy — every failure is a fact plus one next step (N3) - #78

Merged
frahlg merged 1 commit into
mainfrom
77-error-taxonomy
Aug 29, 2026
Merged

fix: error taxonomy — every failure is a fact plus one next step (N3)#78
frahlg merged 1 commit into
mainfrom
77-error-taxonomy

Conversation

@frahlg

@frahlg frahlg commented Aug 29, 2026

Copy link
Copy Markdown
Member

Closes #77. The beta gate this serves: zero raw errors reach a user.

The inventory

Swept every user-facing failure path in go/internal/cli (+ what bubbles up from internal/client) and web/src. Paths already at the bar (U1/U2/N1/N2/R1 copy, refusals whose wording is pinned for semantics) were left alone. Rewritten:

Path Before After The one next step
mir attach/run, machine off error: signaling: agent unavailable / locator chain machine "box" is unreachable — … (cause: …) start it with mir up, or check mir list
any command, relay down error: dial signaling: Post "https://…": connection refused the relay is unreachable — … (cause: …) check your connection, then mir doctor
mir pair <code>, wrong/expired error: pairing handshake failed (wrong code?): … pairing failed — the code is wrong or expired (codes last 5 minutes) … (cause: …) get a fresh code from the machine
mir self-update error: github releases: 403 Forbidden could not update — … (cause: …) retry later, or download from the releases page
mir list, relay down silent local-only list (reads as live) stderr: note: the relay is unreachable — showing saved machines only; discovery resumes when you are back online
unknown machine while relay down unknown machine "x" — neither paired locally nor online in your encrypted registry (misleading) adds: the relay was unreachable so the registry could not be checked get back online and retry
keychain read fails macOS Keychain: owner secret "…" is unavailable adds unlock hint unlock the keychain; mir doctor checks this
SPA rename validation/seal failure browser alert() in-app notice sheet, fact + step try a plainer name / reload and retry
SPA list, relay down w/ saved machines silent, stale-as-live ⚠ The relay is unreachable — showing saved machines; discovery resumes when you are back online. (cleared by the next good fetch)
SPA security check failed bare exception text adds what it means and the way forward reload; if persistent, sign out and back in
SPA QR scan, no camera camera unavailable: NotAllowedError… The camera is unavailable — … or type the code instead. allow camera access, or type the code

Doctor

  • New: clock-skew warning against the relay's Date header (>5 min). Renames and revocations resolve last-writer-wins on timestamps, so a skewed clock silently loses every merge — now doctor says so.
  • Keychain-verification failure carries the unlock hint. (Keychain reachability itself was already covered by the identity verification check.)

Decisions

  • One funnel: go/internal/cli/failure.go holds the rewrites as pure functions; call sites stay one-line wraps. Unknown causes pass through untouched — the taxonomy rewrites known causes, it does not blanket-wrap.
  • Wording only. Every fail-closed refusal (non-interactive pairing, SAS mismatch, root shell, recovery-phrase handling) keeps its exact semantics; strings pinned for security semantics were not touched.
  • The cause stays visible, in parentheses at the end — honest failure, not hidden failure.

Tests

  • Go: failure_test.go pins the rewrites (table) + passthrough + clockSkew table; full go test ./... green, gofmt -l clean.
  • Web: errors-copy.test.js pins: zero alert( in app.js, the discovery-paused notice + its clearing, rename/security/camera copy. 152/152 green.
  • No testdata/ vectors touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y


Note

Low Risk
Changes are wording and UX around existing error paths; security semantics and fail-closed refusals are explicitly preserved, with tests pinning the new messages.

Overview
Introduces a shared N3 error taxonomy: user-facing failures become a plain fact, one next step, and (cause: …) at the end—without changing fail-closed behavior.

CLI (failure.go) centralizes rewrites for attach/run (machine unreachable vs relay down), pair (wrong/expired code), and self-update. Attach, run, and pair call sites route errors through these helpers; unknown errors still pass through unchanged.

Discovery honesty when the relay is down: mir list prints a stderr note that only saved machines are shown; resolveMachines distinguishes “unknown machine” from “registry couldn’t be checked offline.” The web app mirrors this with a discoveryPaused banner that clears on the next successful fetch.

Doctor adds a clock-skew warning (>5 min vs relay Date) for timestamp-based rename/revocation merges, plus keychain unlock hints (also on keychain read failures).

Web SPA replaces alert() with in-app noticeSheet for rename validation/failures, expands security-check and camera-denied copy, and adds source tests pinning the copy.

Reviewed by Cursor Bugbot for commit c7467eb. Bugbot is set up for automated code reviews on this repo. Configure here.

Sweeps the user-facing failure paths in both clients into house copy:
a plain sentence stating the fact, one next step, and the cause in
parentheses — never a bare wrapped chain.

CLI: a new failure.go rewrites the high-traffic causes (machine off,
relay down, wrong/expired pairing code, self-update); `mir list` and
machine resolution say when the registry could not be checked instead
of degrading silently; keychain and doctor failures carry an unlock
hint. Doctor gains a clock-skew warning against the relay's Date
header, since renames and revocations resolve last-writer-wins.

SPA: the last browser alert() calls become in-app notice sheets; a
failed registry fetch renders a one-line "showing saved machines"
notice cleared by the next good fetch; the failed security check and
the camera error now state the way forward.

Wording only: every fail-closed path stays fail-closed.

Closes #77

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T11:44:49.988331Z c7467eb PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: c7467eb041

ℹ️ 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 web/src/app.js
} // not signed in / relay unreachable — keep the local list
// Relay unreachable: keep the local list, but say so — a silently stale
// list would read as live. pollForMachine keeps retrying either way.
if (local.length && !discoveryPaused) { discoveryPaused = true; renderMachines(root, local, []); }

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 Guard failed registry refreshes before remounting the list

When saved machines exist and the registry request fails after the user has already opened a terminal or pairing screen, this catch path calls renderMachines without checking mountGen, replacing the newer view with the machine list. The polling path already uses a generation check for this reason; capture and verify the current mount before rendering the failure notice as well.

Useful? React with 👍 / 👎.

sessions, cleanup, err := client.AttachAll(ctx, resolved, idn, iceList, *relayOnly)
if err != nil {
return err
return humanAttachErr(a.binary, strings.Join(names, ", "), err)

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 Report the machine that actually failed in multi-attach

For mir attach a b, if only b is unavailable, AttachAll returns an error containing attach b, but this wrapper supplies the joined string and reports machine "a, b" is unreachable. That incorrectly identifies both machines as one failed machine and obscures which target needs mir up; preserve the failing machine name from AttachAll or avoid the singular-machine rewrite here.

Useful? React with 👍 / 👎.

Comment thread go/internal/cli/pair.go
Comment on lines 172 to +174
mc, closeConn, err := pairing.DialPair(ctx, signalURL, pairing.RoomID(token))
if err != nil {
return err
return humanRelayErr(a.binary, err)

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 Distinguish pairing admission errors from relay outages

When /pair rejects the WebSocket upgrade for a reachable-relay condition such as the repository's HTTP 429 rate limit (which includes Retry-After: 60), DialPair returns an error here and the new wrapper says the relay is unreachable and recommends mir doctor. That advice cannot resolve the failure and hides the actionable wait period; inspect the HTTP response/status before classifying every dial error as a connectivity outage.

Useful? React with 👍 / 👎.

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c7467eb. Configure here.

Comment thread web/src/app.js
// list would read as live. pollForMachine keeps retrying either way.
if (local.length && !discoveryPaused) { discoveryPaused = true; renderMachines(root, local, []); }
if (!local.length) pollForMachine(root);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Failed fetch stomps the current view

Medium Severity

The new viewMachines failure path calls renderMachines without checking mountGen. renderMachines remounts the root, so a late registry or revocation failure replaces pairing or a live terminal if the user left the list after the first paint.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c7467eb. Configure here.

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

Left a non-blocking comment: Cursor Bugbot finished as skipped and reported one unresolved finding, so this PR is not auto-approved. Reviewers were assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot requested review from miravoss26 and wachtelhund August 29, 2026 11:45
@frahlg
frahlg merged commit 4adbcc6 into main Aug 29, 2026
5 checks passed
frahlg added a commit that referenced this pull request Aug 29, 2026
…opy (N3) (#81)

BETA.md was written while both slices were still in flight; #76 and #78
landed the guided retirement flow and the failure-path pass.


Claude-Session: https://claude.ai/code/session_01KeiotDVE94wEzvc7wcvm1y

Co-authored-by: Claude Fable 5 <noreply@anthropic.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.

N3: error taxonomy — every user-facing failure is a plain sentence + one next step

1 participant