Skip to content

feat(golang): every failure a guest reports is a Diagnostic that unwraps to its sentinel - #1110

Merged
coderdan merged 5 commits into
claude/gracious-einstein-ywrwim-guest-errorsfrom
claude/gracious-einstein-ywrwim-go-diagnostic
Oct 8, 2026
Merged

coderdan merged 5 commits into
claude/gracious-einstein-ywrwim-guest-errorsfrom
claude/gracious-einstein-ywrwim-go-diagnostic

Conversation

@coderdan

@coderdan coderdan commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Go callers can now inspect detailed Rust runtime errors with errors.As, while existing errors.Is checks continue to identify the same error kinds. Both encrypt and auth expose a *Diagnostic containing a stable code, message, help, structured fields, and causes. The runtime reads these details through the se_last_error export added in #1104 and falls back to the previous plain error when details are unavailable or invalid.

This is the third of three PRs for #1098, completing the path from Rust error details to the Go API.

Changes

  • Public error type: Adds shared Diagnostic and Cause types, exposed as aliases from both encrypt and auth. Error() returns the Rust message, and Unwrap() returns the existing sentinel error used by errors.Is.
  • Useful accessors: Exposes expected/found keyset identifiers and field/reason details. Structured fields decode into ordinary Go values, including nested maps and lists.
  • Runtime integration: Reads se_last_error after a nonzero status, decodes the details, and wipes both the host copy and runtime buffer. The export is optional for compatibility with older runtimes.
  • Failure handling: Missing exports, empty details, malformed data, or details without a message return the original sentinel. A trap while retrieving details also matches ErrTrap, so the client closes the runtime instance while preserving the original error kind.
  • Documentation: Adds examples of errors.Is for the kind and errors.As for details, documents allowed error contents, and removes stale comments saying only status numbers cross the boundary.
  • Upstream fix discovered by tests: A refused token exchange could echo an access key through its raw response body. The fix is in feat(stack-encrypt)!: codes, help and fields on every error in stack-encrypt, stack-kms, stack-auth and stack-profile #1103; these Go HTTP tests verify the body is absent from diagnostic details.

Verification

The existing PR description reports the following checks. This description edit did not rerun them or check current CI status.

  • Go: CGO_ENABLED=0 go test ./... from languages/golang passed against freshly built authentication and all four encryption runtimes. GOARCH=386 tests passed for internal/... and auth/...; go vet ./... and gofmt -l were clean.
  • Lint: golangci-lint run ./... with pinned version 2.14.0 reported 0 issues. A compatible binary was installed because the image's Go 1.25-built binary could not check this module.
  • Transport tests: A minimal test WebAssembly module covers all diagnostic fields, nested values, causes, keyset accessors, buffer wiping, each fallback, retrieval traps, and unknown statuses.
  • Encryption tests: HTTP outcomes (401, 403, 404, 409, 500, HTML, invalid JSON, and connection refusal) check error kinds, codes, and response-body exclusion. Publicly reachable engine failures check codes, including wrong keysets, tampering, short match text, truncated ciphertext, malformed input, calls before initialization, and untyped indexes.
  • Authentication tests: Profile-storage and authentication failures check codes and preserve existing assertions. JSON error line details are checked. All existing errors.Is assertions passed unchanged, and the README example compiled in a temporary test.
  • Reported CI at 8faaf44: Go lint, WebAssembly/runtime checks, macOS/Windows bindings, and Go live tests passed. The original description reported CI pending at rebased head d18bfbc; only two base dependency files changed. This is historical status, not a claim about current CI.
  • Skipped locally: TestLiveForeignKeysetIsRefusedBeforeRetrieval, which requires credentials. A deterministic test checks both keyset identifiers on every PR.

Related

Review notes

  • Start with internal/guest/diagnostic.go, then the integration in internal/guest/call.go.
  • Scope: Diagnostics describe failures reported by the Rust runtime. Go-side failures, such as a closed client, memory-lock failure, missing profile, rejected arguments, or a runtime trap itself, retain their existing errors. Adding codes for those would require a Go-specific namespace.
  • HTTP message suffix: Authentication retains its : HTTP 403 suffix because the Rust error does not always contain the status. Some messages therefore repeat it, such as Server error: 403: HTTP 403.
  • Field location: An empty-match-text error still has no engine-provided field. Go's existing record recheck adds the record index and field name.
  • No changeset: This PR changes only the Go module, which has no release process yet; no npm package changes.

Summary by CodeRabbit

  • New Features

    • Go authentication and encryption errors now include structured diagnostics when available, with codes, messages, help, causes, and relevant field or keyset details.
    • Standard error matching still identifies error kinds; callers can also inspect diagnostic details.
    • Guests without diagnostic support continue to return bare errors.
    • HTTP diagnostics provide available failure context without exposing response bodies. Token-source diagnostics are preserved in wrapped errors.
  • Documentation

    • Updated Go guidance on inspecting errors and diagnostics, including what diagnostic information may contain or exclude.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 58c7802

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 17917d44-08b2-441c-8f28-2dcbe59e8604
📥 Commits

Reviewing files that changed from the base of the PR and between 2ea9a96 and 58c7802.

📒 Files selected for processing (8)
  • languages/golang/auth/errors.go
  • languages/golang/auth/strategy_test.go
  • languages/golang/auth/transport.go
  • languages/golang/encrypt/README.md
  • languages/golang/encrypt/errors.go
  • languages/golang/internal/guest/diagnostic.go
  • languages/golang/internal/guest/diagnostic_test.go
  • languages/golang/internal/guest/export_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • languages/golang/auth/transport.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The shared Go guest package now retrieves optional error details after guest-call failures and returns diagnostics that unwrap to existing sentinel errors. The auth and encrypt packages expose diagnostic types. Tests and documentation cover diagnostic content, sentinel matching, and compatibility with guests that omit the export.

Changes

Guest error diagnostics

Layer / File(s) Summary
Diagnostic decoding and call integration
languages/golang/internal/guest/diagnostic.go, languages/golang/internal/guest/call.go, languages/golang/internal/guest/status.go, languages/golang/internal/guest/diagnostic_test.go
Adds diagnostic and cause types, accessors, keyset ID parsing, and decoding for guest-provided error details. Failed calls retrieve details through optional se_last_error; missing or invalid details retain the status sentinel. Tests cover decoding, fallback cases, traps, and buffer wiping.
Public diagnostic API and error propagation
languages/golang/auth/errors.go, languages/golang/auth/guest.go, languages/golang/auth/transport.go, languages/golang/encrypt/errors.go, languages/golang/encrypt/guest.go, languages/golang/encrypt/client.go, languages/golang/internal/guest/doc.go, languages/golang/internal/guest/errors.go
Auth and encrypt expose aliases for Diagnostic and Cause, and resolve the optional guest export. Encrypt places token-source errors containing diagnostics first in the wrapped error chain. Package documentation describes diagnostic details and sentinel matching.
Diagnostic behavior and compatibility validation
languages/golang/auth/*test.go, languages/golang/encrypt/*test.go, languages/golang/encrypt/README.md, languages/golang/encrypt/doc.go, languages/golang/encrypt/checker.go, languages/golang/encrypt/records.go
Tests check diagnostic codes, fields, sentinel matching, transport details, and compatibility with guests that omit the export. Documentation describes using errors.Is for error kinds and errors.As for diagnostic details.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant GuestCall as guest.Call
  participant Diagnose as Exports.diagnose
  participant LastError as se_last_error
  participant Memory as Guest memory
  GuestCall->>Diagnose: Pass PackedResult error
  Diagnose->>LastError: Request error details
  LastError-->>Diagnose: Return packed detail location and length
  Diagnose->>Memory: Copy and wipe detail bytes
  Diagnose-->>GuestCall: Return Diagnostic or status sentinel
Loading

Merge Risk: ⚪ Minimal · up to 58c78

No actionable merge-blocking issue remains in this change after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 58c78

Existing error-kind checks and compatibility with older runtimes are preserved. Diagnostic retrieval has cleanup and failure-containment controls. No sensitive-data disclosure was established, but verification of every transport-error path remains incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed exposure is guest failure metadata becoming available to Go callers and any downstream error logging. The inspected diagnostic lifecycle is instance-scoped and serialized; no new privileged operation or cross-instance diagnostic-sharing path was identified in that trace.

Trust Boundaries and Controls

  • observed — HTTP response bodies and transport failures are inputs to the guest, while producer-approved diagnostics are outputs to callers. encrypt places raw transport-error text in guest-visible buffers, but inspected Go tests assert response-body exclusions and Rust cause handling restricts foreign-error descriptions. These controls do not establish complete coverage of the unread transport response-classification path.

Resilience and Maintainability Implications

  • observed — Diagnostic cleanup and retrieval use non-cancellable cleanup semantics after the primary guest call. Missing or malformed detail cannot replace the failure with success, and retrieval traps trigger owner-level instance disposal rather than continued use of unknown guest state.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.08% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 23 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: guest-reported failures now use Diagnostic values that unwrap to their existing sentinels.
Linked Issues check ✅ Passed Issue #1101 requires shared Diagnostic and Cause types for both public packages, errors.As support, preserved errors.Is behavior, optional se_last_error decoding, buffer wiping, accessors, d…
Out of Scope Changes check ✅ Passed The reviewed changes stay within issue #1101. Runtime plumbing, public aliases, accessors, documentation, and tests directly implement or verify the diagnostic API. HTTP response-body assertions suppo…
Full details: Docstring Coverage

Explanation

Docstring coverage is 73.08% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 23 files. (1 skipped: 1 unsupported.)

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@coderdan

coderdan commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #1094 at 6efc22b, which fixes the second cipherstash-bot review there (index-only fields and nil interface passthroughs now round-trip). Two conflicts came up, in gensupport/declaration.go (#1095) and stack-encrypt/Cargo.toml (#1103, where sha2 is now gone and serde_json sits in [dependencies]). At the top of the stack the Go suite passes with all five guests built, and so do cargo nextest --workspace --all-features and clippy.

https://claude.ai/code/session_01V3WFXwax4J3uecpFEJ6yHc

@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch 4 times, most recently from 2705d06 to 20b9c50 Compare October 7, 2026 02:40
@coderdan

coderdan commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the new #1104. There was one conflict, in auth/transport.go and auth/strategy_test.go, with #1094's renames. I resolved it in this PR's own commit (20b9c50), which now uses auth.ErrTransport, ErrConfig and ErrOther and auth.WithBaseURL. Go tests and golangci-lint pass locally.

https://claude.ai/code/session_01V3WFXwax4J3uecpFEJ6yHc

@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 20b9c50 to 8e95495 Compare October 7, 2026 03:44
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 8e95495 to 8968ef1 Compare October 7, 2026 03:54
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 8968ef1 to f6bb5db Compare October 7, 2026 04:00
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch 3 times, most recently from 245bb8d to f2c025d Compare October 7, 2026 04:35
@coderdan
coderdan marked this pull request as ready for review October 7, 2026 04:52
@coderdan
coderdan requested a review from a team as a code owner October 7, 2026 04:52
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 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-10-07T04:55:46.521376Z f2c025d Draft marked ready
🔒 Security Review ✅ Completed 2026-10-07T04:57:21.268499Z f2c025d Draft marked ready
ℹ️ 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.

@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 631e755 to fbdb56f Compare October 7, 2026 17:41
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch 2 times, most recently from 05ea7c1 to d21aa96 Compare October 7, 2026 18:07
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from d21aa96 to 9e38186 Compare October 7, 2026 20:48
@coderdan

coderdan commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the new #1104 (no conflicts) and addressed the review in 9e38186. Every inline thread has a reply. The two findings in the review body that had no inline comment:

  • TestAuthTransportErrorWithoutAResponseNamesNoStatus: the comment and failure text no longer say "bare sentinel". The test now also asserts the stack_auth::request_error code.
  • Checker.Check: the doc comment now says that an engine refusal is a *Diagnostic and that Field() and Reason() name the field and the rule. stashgen still checks one field at a time, and its README describes that correctly, so I left it alone.

mise run go:test and mise run go:lint pass locally against freshly built guests.

@coderdan
coderdan requested a review from freshtonic October 7, 2026 20:52
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 9e38186 to 2ea9a96 Compare October 8, 2026 02:06

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

I examined the Go error design in this PR. The design is correct for errors.Is and errors.As. All guest calls go through guest.Call, so each status that a guest reports goes through diagnose. diagnose never returns a typed-nil *Diagnostic: it returns the *Diagnostic or the bare kind. Each %w wrap in encrypt and auth keeps the Diagnostic in the chain. I did not find a == comparison on a sentinel in non-test code.

I did not repeat the earlier comments. The author fixed those in 9e38186.

Findings:

  1. Medium (design / compatibility): Diagnostic.Error() returns only the Rust message. The kind text (cipherstash: ...) is not in the error text now. See the inline comment.
  2. Low (test gap): No test covers the context.WithoutCancel in diagnose.
  3. Nit (API): A caller outside this module cannot make a Diagnostic that unwraps to a sentinel. This makes fakes in caller tests difficult.
  4. Nit (stale comment, not in the diff): The comment on authHTTPStatus.wrap in auth/transport.go (line 61) gives the example text "cipherstash: auth transport failed: HTTP 403". The test now expects "Server error: 403: HTTP 403". Update the example.

Comment thread languages/golang/internal/guest/diagnostic.go Outdated
Comment thread languages/golang/internal/guest/diagnostic.go
Comment thread languages/golang/internal/guest/diagnostic.go
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 2ea9a96 to 968a754 Compare October 8, 2026 03:25
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 968a754 to 48b5a31 Compare October 8, 2026 03:42
@coderdan

coderdan commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

On the review's other points:

  • The stale example on authHTTPStatus.wrap in auth/transport.go now shows the new text, cipherstash: auth transport failed: Server error: 403: HTTP 403 (e5102e6).
  • The bot's two optional items were already fixed at this head. The comment and failure text of TestAuthTransportErrorWithoutAResponseNamesNoStatus now describe the guest's request error over ErrTransport. The Checker.Check doc says a refusal is a *Diagnostic with Field() and Reason().

The stack is restacked: #1096 → #1103 → #1104 → this PR.


Generated by Claude Code

@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 48b5a31 to bdd7d6d Compare October 8, 2026 03:54
@coderdan
coderdan requested a review from freshtonic October 8, 2026 04:07

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

Re-review at bdd7d6d. The branch was rebased. The PR's main commit (80d68a7) has the same Go changes as 2ea9a96. The three new Go commits fix my earlier findings.

Status of the earlier findings:

  1. Medium, Error() without the kind text: fixed (fab9419). Error() returns the sentinel text, then the Rust message. A Diagnostic with no kind returns only Message. diagnose no longer has the special case for an unknown status. TestAnUnknownStatusIsStillInternal still checks that the status number is in the text.
  2. Low, test gap for context.WithoutCancel: fixed (dd3b708). The new test uses WithCloseOnContextDone(true) and a cancelled context, and it checks for no ErrTrap.
  3. Nit, no constructor for a fake: fixed (bdd7d6d). The doc comment shows how to wrap a sentinel with a Diagnostic.
  4. Nit, stale comment on authHTTPStatus.wrap: fixed (fab9419).

New findings: two nits only. Two doc lines in encrypt still say that Error() returns Message. The auth doc was updated, but these two were not. See the inline comments. I found no new problems in the Go code.

Comment thread languages/golang/encrypt/errors.go Outdated
Comment thread languages/golang/encrypt/README.md
@freshtonic
freshtonic self-requested a review October 8, 2026 04:23
…aps to its sentinel

A Go caller could check an error's kind with errors.Is and nothing more:
the guest handed back a status number, and errors.As had no type to
target. #1100 gave both guests se_last_error, which hands over the full
error behind a failed export. This reads it.

guest.Call fetches it after any non-zero status and returns a
*Diagnostic: Code, Message, Help, URL, Severity, Fields and Causes, with
Unwrap returning the sentinel the status already mapped to, so every
errors.Is check keeps working and Error() is the Rust message. The
buffer is wiped on both sides once read. A guest with no se_last_error,
nothing recorded, or bytes that do not decode into an error with a
message all give the bare sentinel, as before; a trap fetching it also
wraps ErrTrap, so the caller closes the instance as after any trap.

encrypt and auth expose the type as Diagnostic (and Cause) by alias, as
they do the sentinels. ExpectedKeyset and FoundKeyset read a
foreign-keyset refusal's two keysets, and Field and Reason a refused
plan, record or value's field and reason.

Tests: a hand-assembled guest drives each missing or broken detail and
checks the wipe; each ZeroKMS outcome, each engine refusal reachable
through the public API, and each profile-store kind asserts its code;
a guest with se_last_error unbound fails with the bare sentinel; the
foreign-keyset refusal carries both ids, hermetically and live. The
README's Errors section explains errors.Is for the kind and errors.As for
the detail, and the rule for what an error may contain.

Closes #1101

Claude-Session: https://claude.ai/code/session_01URtfKsTToFUCRwq3g7gCUf
Error returned the Rust message alone, so a guest failure's text lost the
"cipherstash: ..." prefix it had before: a log line no longer said where
it came from, and a caller matching on the old text stopped matching.
Error now returns the sentinel's text, then the Rust message. The status
number of an unknown status is in that prefix, so diagnose no longer
wraps one specially.
The real guests close their module when the context is done. The stub
runtime now does too, and a test calls diagnose under a cancelled context:
it fails with a trap if the fetch stops using context.WithoutCancel.
@coderdan
coderdan force-pushed the claude/gracious-einstein-ywrwim-go-diagnostic branch from 325ccd4 to 58c7802 Compare October 8, 2026 04:36
@coderdan
coderdan merged commit 6f38847 into main Oct 8, 2026
32 checks passed
@coderdan
coderdan deleted the claude/gracious-einstein-ywrwim-go-diagnostic branch October 8, 2026 04:49
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.

Go: errors.As has no target — add a Diagnostic error type that unwraps to today's sentinels

4 participants