Skip to content

feat: improve OS keychain/DPAPI access error handling for sandbox environments#173

Merged
JackZhao10086 merged 6 commits intomainfrom
feat/adapt_to_sandbox_mode
Apr 1, 2026
Merged

feat: improve OS keychain/DPAPI access error handling for sandbox environments#173
JackZhao10086 merged 6 commits intomainfrom
feat/adapt_to_sandbox_mode

Conversation

@JackZhao10086
Copy link
Copy Markdown
Collaborator

@JackZhao10086 JackZhao10086 commented Apr 1, 2026

Summary

This PR improves the cross-platform keychain error handling mechanism to better support restricted environments like Sandboxes or CI. It ensures keychain access errors are properly propagated and provides actionable troubleshooting hints when permissions are denied or blocked.

Changes

  • Refactored keychain.Get and platformGet API signatures to return (string, error) for proper error propagation.
  • Updated getMasterKey with an allowCreate flag to prevent regenerating master keys during read-only operations.
  • Introduced wrapError to wrap underlying keychain failures into user-friendly ExitErrors with sandbox/CI permission hints.
  • Improved macOS (Darwin) keychain access to properly catch and report timeout errors when users ignore or deny permission prompts.
  • Adapted callers in config.go and token_store.go to handle and correctly render the new keychain errors.

Test Plan

  • Unit tests passed.
  • Manually verified basic commands (e.g., lark auth login) work normally across different platforms (macOS/Windows/Linux).
  • Manually verified that running in a restricted environment (or denying the macOS keychain prompt) correctly displays the new error hint.

Related Issues

Summary by CodeRabbit

  • Bug Fixes
    • Keychain operations now surface clear, actionable errors with troubleshooting hints instead of failing silently.
    • Platform key storage handles missing, corrupted, or blocked master keys more robustly and only creates keys during writes.
    • Token retrieval treats keychain access errors as a missing token to avoid returning invalid data.
    • Configuration resolution preserves explicit exit errors while improving overall config error reporting.

…tforms

- Change platformGet to return error instead of empty string
- Add proper error wrapping for keychain operations
- Make master key creation conditional in getMasterKey
- Improve error messages and handling for keychain access
- Update dependent code to handle new error returns
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

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
📝 Walkthrough

Walkthrough

Keychain APIs were changed to return and normalize errors; platform implementations now distinguish missing, corrupted, and blocked states and allow conditional master-key creation for writes. Call sites (token lookup and config secret resolution) were updated to handle or propagate these errors.

Changes

Cohort / File(s) Summary
Keychain core & default
internal/keychain/keychain.go, internal/keychain/default.go
Get now returns (string, error) and uses wrapError(op, err) to convert platform errors into output.ExitError (with hints). defaultKeychain.Get delegates directly to package Get. Set/Remove wrap platform errors.
Platform: macOS
internal/keychain/keychain_darwin.go
Added getMasterKey(service, allowCreate) to allow conditional master-key creation. platformGet/platformSet now return (string, error); reads use allowCreate=false, writes allowCreate=true. Errors for missing key, corruption, blocked access, I/O, and decryption are propagated.
Platform: file-based (other OS)
internal/keychain/keychain_other.go
getMasterKey signature changed to accept allowCreate and conditionally create master key with atomic temp-file write and retry on rename. platformGet(string, error) returns ("", nil) for missing account and propagates read/decrypt/master-key errors. platformSet allows master-key creation.
Platform: Windows
internal/keychain/keychain_windows.go
platformGet signature changed to (string, error); registry reads now explicitly return ("", nil) for not-found or (value, nil) for found. Added doc comments for helpers; runtime logic largely preserved.
Callers / Consumers
internal/auth/token_store.go, internal/core/config.go
token_store.GetStoredToken now checks and treats a non-nil keychain.Get error (or empty string) as missing token. RequireConfig imports errors and output; it preserves *output.ExitError from ResolveSecretInput and wraps other errors in ConfigError.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant KeychainCore
  participant Platform
  participant Storage

  Caller->>KeychainCore: Get(service, account)
  KeychainCore->>Platform: platformGet(service, account) (allowCreate=false for reads)
  Platform->>Storage: read encrypted account / registry / file
  alt account not found
    Storage-->>Platform: not found
    Platform-->>KeychainCore: "", nil
  else value or error
    Storage-->>Platform: value OR io/crypto error OR blocked
    Platform-->>KeychainCore: value, error
  end
  KeychainCore->>KeychainCore: wrapError(op, err) -> possibly ExitError (with hint)
  KeychainCore-->>Caller: (value, error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled keys beneath the moonlit loam,

I stitched lost secrets safely back to home.
I guarded tokens, fixed each lock and tune,
Now vaults hum softly under carrot moon.
Hop, hop — the keychain's singing soon 🥕🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main focus of the PR: improving keychain/DPAPI error handling for sandbox environments, which aligns with the primary change across multiple files.
Description check ✅ Passed The description covers all required template sections: Summary explains the motivation, Changes lists the main modifications, Test Plan documents verification steps, and Related Issues is included.
Docstring Coverage ✅ Passed Docstring coverage is 97.06% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adapt_to_sandbox_mode

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/auth/token_store.go (1)

40-51: ⚠️ Potential issue | 🟠 Major

Keychain errors are silently suppressed, defeating the PR's intent.

GetStoredToken swallows the keychain error and returns nil, which callers (e.g., GetValidAccessToken in uat_client.go:66-99) interpret as "token not found" and trigger re-authentication. This means:

  1. If the OS keychain is locked or permission is denied, users see a misleading "please re-login" flow instead of the actionable hint about keychain/sandbox permissions.
  2. The wrapError improvements in this PR become ineffective for token retrieval paths.

Consider changing the signature to GetStoredToken(appId, userOpenId string) (*StoredUAToken, error) and propagating the keychain error to callers so they can distinguish "not found" from "access denied".

Proposed signature change
-// GetStoredToken reads the stored UAT for a given (appId, userOpenId) pair.
-func GetStoredToken(appId, userOpenId string) *StoredUAToken {
-	jsonStr, err := keychain.Get(keychain.LarkCliService, accountKey(appId, userOpenId))
-	if err != nil || jsonStr == "" {
-		return nil
-	}
+// GetStoredToken reads the stored UAT for a given (appId, userOpenId) pair.
+// Returns (nil, nil) if no token exists; returns (nil, err) on keychain access failure.
+func GetStoredToken(appId, userOpenId string) (*StoredUAToken, error) {
+	jsonStr, err := keychain.Get(keychain.LarkCliService, accountKey(appId, userOpenId))
+	if err != nil {
+		// Distinguish "not found" from actual access errors
+		// Platform implementations return specific errors for "not found"
+		return nil, err
+	}
+	if jsonStr == "" {
+		return nil, nil
+	}
 	var token StoredUAToken
 	if err := json.Unmarshal([]byte(jsonStr), &token); err != nil {
-		return nil
+		return nil, nil // Treat corrupted data as missing
 	}
-	return &token
+	return &token, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/token_store.go` around lines 40 - 51, GetStoredToken currently
swallows keychain.Get errors and returns nil, which prevents callers like
GetValidAccessToken from distinguishing "not found" vs keychain/access errors;
change the signature of GetStoredToken(appId, userOpenId string) to return
(*StoredUAToken, error), have it return (nil, nil) only when jsonStr=="" (not
when err != nil), propagate and wrap keychain.Get errors (use wrapError) and
return (nil, err) on JSON unmarshal failures or keychain failures, then update
callers such as GetValidAccessToken in uat_client.go to handle the error path
and show actionable messages for keychain access issues.
🧹 Nitpick comments (4)
internal/keychain/keychain_darwin.go (2)

73-77: Same issue: "keychain is corrupted" is misleading for missing master key.

When keyring.ErrNotFound occurs and allowCreate is false (read operation), returning "keychain is corrupted" suggests data corruption rather than "entry not initialized". This is the same issue as in keychain_other.go.

Suggested fix
 		if !allowCreate {
-			// Creation not allowed (e.g., during Get operation), return error
-			resCh <- result{key: nil, err: errors.New("keychain is corrupted")}
+			// Creation not allowed (e.g., during Get operation), return not-found error
+			resCh <- result{key: nil, err: errors.New("keychain entry not found")}
 			return
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_darwin.go` around lines 73 - 77, The error
returned when allowCreate is false currently uses a misleading "keychain is
corrupted" message; update the branch in the function that sends to resCh (when
!allowCreate) to return a clearer error indicating the master key/entry is
missing/not initialized (e.g., "master key not initialized" or wrap
keyring.ErrNotFound) instead of "keychain is corrupted", matching the behavior
used in keychain_other.go; adjust the error value sent via resCh's result{key:
nil, err: ...} accordingly.

53-54: Silent recover() may mask goroutine panics.

The defer func() { recover() }() swallows any panic without logging, which could hide bugs in the keychain access logic. Consider at minimum logging the panic or removing the recover if panics here indicate programming errors that should propagate.

Suggested improvement
 	go func() {
-		defer func() { recover() }()
+		defer func() {
+			if r := recover(); r != nil {
+				resCh <- result{key: nil, err: fmt.Errorf("keychain panic: %v", r)}
+			}
+		}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_darwin.go` around lines 53 - 54, The anonymous
goroutine in keychain_darwin.go uses defer func() { recover() }() which silently
swallows panics; update the deferred recover to capture the recovered value and
surface it (e.g., log the panic with context via log.Printf or
processLogger.Errorf) or remove the recover entirely if a panic should crash to
surface programming errors; specifically modify the goroutine's deferred
function in the anonymous func() to check r := recover(); if r != nil { log the
error and stack (runtime/debug.Stack()) with a clear message including
"keychain_darwin goroutine" or the surrounding function name } so panics are not
silently ignored.
internal/keychain/keychain.go (1)

41-46: Stale comment: Get now returns an error, not empty string.

The comment on line 42 says "Returns empty string if the entry does not exist" but the function now returns (string, error). The error path handles the "not found" case.

Suggested fix
 // Get retrieves a value from the keychain.
-// Returns empty string if the entry does not exist.
+// Returns an error if the entry does not exist or cannot be accessed.
 func Get(service, account string) (string, error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain.go` around lines 41 - 46, Update the stale doc
comment for the Get function in keychain.go: change "Returns empty string if the
entry does not exist" to reflect that Get now returns (string, error) and that
missing entries are reported via the returned error (wrapped by wrapError in
platformGet path); ensure the top comment describes returned values and the
error semantics for not-found and other failures.
internal/keychain/keychain_other.go (1)

53-55: Error message "keychain is corrupted" is misleading for missing master key.

When a user runs a Get operation before any Set has occurred (e.g., fresh install, querying non-existent entry), the master key won't exist. Returning "keychain is corrupted" suggests data corruption when the actual state is "not initialized" or "entry not found".

Consider a more accurate message:

Suggested fix
 	if !allowCreate {
-		return nil, errors.New("keychain is corrupted")
+		return nil, errors.New("keychain not initialized or entry not found")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_other.go` around lines 53 - 55, The error message
returned when allowCreate is false is misleading; update the return in the
branch that currently does `return nil, errors.New("keychain is corrupted")` to
a clearer message indicating missing master key or uninitialized keychain (for
example `errors.New("keychain not initialized: master key missing")` or
`errors.New("entry not found or keychain not initialized")`). Locate the branch
that checks `if !allowCreate` in the Get (or related) method in
keychain_other.go and replace the error text accordingly so callers can
distinguish "not initialized / missing entry" from actual corruption.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/auth/token_store.go`:
- Around line 40-51: GetStoredToken currently swallows keychain.Get errors and
returns nil, which prevents callers like GetValidAccessToken from distinguishing
"not found" vs keychain/access errors; change the signature of
GetStoredToken(appId, userOpenId string) to return (*StoredUAToken, error), have
it return (nil, nil) only when jsonStr=="" (not when err != nil), propagate and
wrap keychain.Get errors (use wrapError) and return (nil, err) on JSON unmarshal
failures or keychain failures, then update callers such as GetValidAccessToken
in uat_client.go to handle the error path and show actionable messages for
keychain access issues.

---

Nitpick comments:
In `@internal/keychain/keychain_darwin.go`:
- Around line 73-77: The error returned when allowCreate is false currently uses
a misleading "keychain is corrupted" message; update the branch in the function
that sends to resCh (when !allowCreate) to return a clearer error indicating the
master key/entry is missing/not initialized (e.g., "master key not initialized"
or wrap keyring.ErrNotFound) instead of "keychain is corrupted", matching the
behavior used in keychain_other.go; adjust the error value sent via resCh's
result{key: nil, err: ...} accordingly.
- Around line 53-54: The anonymous goroutine in keychain_darwin.go uses defer
func() { recover() }() which silently swallows panics; update the deferred
recover to capture the recovered value and surface it (e.g., log the panic with
context via log.Printf or processLogger.Errorf) or remove the recover entirely
if a panic should crash to surface programming errors; specifically modify the
goroutine's deferred function in the anonymous func() to check r := recover();
if r != nil { log the error and stack (runtime/debug.Stack()) with a clear
message including "keychain_darwin goroutine" or the surrounding function name }
so panics are not silently ignored.

In `@internal/keychain/keychain_other.go`:
- Around line 53-55: The error message returned when allowCreate is false is
misleading; update the return in the branch that currently does `return nil,
errors.New("keychain is corrupted")` to a clearer message indicating missing
master key or uninitialized keychain (for example `errors.New("keychain not
initialized: master key missing")` or `errors.New("entry not found or keychain
not initialized")`). Locate the branch that checks `if !allowCreate` in the Get
(or related) method in keychain_other.go and replace the error text accordingly
so callers can distinguish "not initialized / missing entry" from actual
corruption.

In `@internal/keychain/keychain.go`:
- Around line 41-46: Update the stale doc comment for the Get function in
keychain.go: change "Returns empty string if the entry does not exist" to
reflect that Get now returns (string, error) and that missing entries are
reported via the returned error (wrapped by wrapError in platformGet path);
ensure the top comment describes returned values and the error semantics for
not-found and other failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1010b21-42be-4df9-be04-703dd96f6153

📥 Commits

Reviewing files that changed from the base of the PR and between 4c51a98 and 833dc62.

📒 Files selected for processing (7)
  • internal/auth/token_store.go
  • internal/core/config.go
  • internal/keychain/default.go
  • internal/keychain/keychain.go
  • internal/keychain/keychain_darwin.go
  • internal/keychain/keychain_other.go
  • internal/keychain/keychain_windows.go

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@0b8e1f9883312254503e81cd09ee5af8984ff678

🧩 Skill update

npx skills add larksuite/cli#feat/adapt_to_sandbox_mode -y -g

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

This PR improves cross-platform keychain error handling so that access failures in sandboxed or CI environments surface as actionable ExitErrors with troubleshooting hints rather than silently returning empty strings. The changes include refactoring Get/platformGet to return (string, error), adding an allowCreate flag to getMasterKey to avoid creating keys during read-only operations, and a wrapError helper that provides platform-appropriate guidance.

Key changes:

  • keychain.go: New wrapError helper wraps all platform errors into output.ExitError with a sandbox/CI hint; errNotInitialized sentinel gets a more specific "run lark-cli config init" hint.
  • keychain_darwin.go: File-existence check now short-circuits before triggering the macOS keychain dialog; timeout cases surface as "keychain access blocked" rather than silently returning \"\".
  • keychain_other.go (Linux): Real I/O errors (permission denied, etc.) are now distinguished from "not found" and propagated correctly.
  • keychain_windows.go: platformGet updated to return (string, error), but all registryGet failures — including genuine DPAPI access failures — are still mapped to (\"\", nil), so sandbox/CI restrictions on Windows remain silently swallowed.
  • config.go: RequireConfig correctly threads ExitError through without double-wrapping it.
  • token_store.go / default.go: Call sites updated for the new Get signature; keychain errors in GetStoredToken are still treated as "no token stored" and not surfaced to callers.

Confidence Score: 4/5

Safe to merge on macOS/Linux with noted caveats; Windows DPAPI errors and missing-secret scenarios on all platforms remain silent

Several issues raised in prior review threads remain unresolved: Windows platformGet silently maps all registryGet failures (including real DPAPI sandbox errors) to ("", nil); GetStoredToken in token_store.go treats keychain errors the same as "no token"; and defaultKeychain.Get no longer returns an error for missing entries. A new P2 finding is also present: decryption failures in platformGet (Darwin and Linux) get the wrong hint. The macOS and Linux happy-path and permission-denied paths are meaningfully improved.

internal/keychain/keychain_windows.go (DPAPI errors silently swallowed), internal/keychain/default.go (not-found no longer errors), internal/auth/token_store.go (errors discarded)

Important Files Changed

Filename Overview
internal/keychain/keychain.go Adds wrapError helper to surface platform errors as ExitError with actionable hints; changes Get signature to return (string, error); sentinel errNotInitialized provides a specific hint for missing master keys
internal/keychain/keychain_darwin.go Added allowCreate flag to getMasterKey, file-existence check before triggering keychain dialog, timeout mapped to "keychain access blocked"; decryption failures are still wrapped with the generic (wrong) sandbox/permissions hint
internal/keychain/keychain_other.go Linux file-based backend gains allowCreate flag, real I/O errors propagated, ErrNotExist short-circuited; same decryption-failure/misleading-hint issue as Darwin
internal/keychain/keychain_windows.go Windows platformGet maps all registryGet failures (including real DPAPI sandbox/access failures) to ("", nil), silently swallowing them rather than surfacing the actionable hint this PR introduces
internal/keychain/default.go Simplified to delegate to package-level Get; removes the previous not-found sentinel, so callers must distinguish empty-string from error themselves
internal/core/config.go Correctly unwraps ExitError from keychain to preserve user-facing hints while wrapping unknown errors as ConfigError
internal/auth/token_store.go Updated call site for new Get signature; keychain errors are still silently treated as "no token" rather than surfaced to callers

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["keychain.Get(service, account)"] --> B["platformGet(service, account)"]

    subgraph Darwin_Linux["Darwin / Linux"]
        B --> C["os.ReadFile(enc file)"]
        C -->|ErrNotExist| D["return empty, nil"]
        C -->|other error| E["return err"]
        C -->|success| F["getMasterKey(false)"]
        F -->|ErrNotFound + no allowCreate| G["return errNotInitialized"]
        F -->|access blocked or timeout| H["return keychain access blocked"]
        F -->|corrupted key| I["return keychain is corrupted"]
        F -->|success| J["decryptData(data, key)"]
        J -->|fail| K["return err — wrong hint applied"]
        J -->|success| L["return plaintext"]
    end

    subgraph Windows_Backend["Windows"]
        B --> W["registryGet(service, account)"]
        W -->|ok=true| X["return value, nil"]
        W -->|ok=false| Y["return empty, nil — silent"]
    end

    E --> Z["wrapError('Get', err)"]
    G --> Z
    H --> Z
    I --> Z
    K --> Z
    Z --> AA["ExitError with hint"]
    AA -->|errNotInitialized| AB["hint: run lark-cli config init"]
    AA -->|all other errors| AC["hint: check locked/accessible"]
Loading

Reviews (6): Last reviewed commit: "fix(keychain): handle I/O errors when re..." | Re-trigger Greptile

Comment thread internal/keychain/keychain_darwin.go
Comment thread internal/keychain/keychain_darwin.go Outdated
Comment thread internal/keychain/keychain_windows.go
Comment thread internal/auth/token_store.go
Add detailed doc comments for all platform-specific keychain functions to clarify their purpose and behavior. Also enhance the error hint message to include a suggestion for reconfiguring the CLI when keychain access fails.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/keychain/keychain_darwin.go`:
- Around line 154-162: In platformGet, don't call getMasterKey before checking
for the ciphertext file: first attempt to read the file at
filepath.Join(StorageDir(service), safeFileName(account)), and if
os.IsNotExist(err) return "", nil; only when the ciphertext file exists should
you call getMasterKey(service, false) and proceed with decryption; update error
handling so missing files produce the documented ("", nil) and corrupted key
errors still surface when getMasterKey or decryption fails.
- Around line 75-95: There is a race in getMasterKey when allowCreate==true:
concurrent callers can both observe ErrNotFound, generate different master keys,
and both call keyring.Set (upserting) so the last write wins and breaks existing
encrypted data; fix by adding a service-scoped file lock (e.g., based on the
service string) around the creation path in getMasterKey (or invoked from
platformSet) so only one goroutine can perform generation+set at a time, and
after acquiring the lock re-check the keyring for an existing "master.key"
(handle ErrNotFound vs found) before generating a new random key, then only call
keyring.Set if still missing; ensure the lock is released on all code paths and
propagate any errors from rand.Read or keyring.Set as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb859568-85ca-49ef-aeb2-126dc6b589b7

📥 Commits

Reviewing files that changed from the base of the PR and between 833dc62 and d0980ac.

📒 Files selected for processing (4)
  • internal/keychain/keychain.go
  • internal/keychain/keychain_darwin.go
  • internal/keychain/keychain_other.go
  • internal/keychain/keychain_windows.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/keychain/keychain_windows.go
  • internal/keychain/keychain.go
  • internal/keychain/keychain_other.go

Comment thread internal/keychain/keychain_darwin.go
Comment thread internal/keychain/keychain_darwin.go
…r handling

Check for file existence before attempting to read and get master key
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
internal/keychain/keychain_darwin.go (2)

66-72: Non-sentinel errors prevent reliable error classification by callers.

The errors "keychain is corrupted" and "keychain access blocked" are created with errors.New(), making them unexported string-based errors. Callers (like ResolveSecretInput in internal/core/secret_resolve.go) cannot use errors.Is() or errors.As() to distinguish between recoverable and unrecoverable errors.

Consider defining exported sentinel errors for programmatic error handling:

Example sentinel error definitions
// In keychain package
var (
    ErrKeychainCorrupted = errors.New("keychain is corrupted")
    ErrKeychainBlocked   = errors.New("keychain access blocked")
)

Then callers can check:

if errors.Is(err, keychain.ErrKeychainBlocked) {
    // Handle sandbox/CI environment
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_darwin.go` around lines 66 - 72, Define exported
sentinel errors in the keychain package (e.g., ErrKeychainCorrupted and
ErrKeychainBlocked) and replace the inline errors.New(...) creations in
keychain_darwin.go with those exported variables where the code currently sends
result{..., err: errors.New("keychain is corrupted")} and result{..., err:
errors.New("keychain access blocked")}; update callers (such as
ResolveSecretInput in internal/core/secret_resolve.go) to use errors.Is(err,
keychain.ErrKeychainBlocked) / errors.Is(err, keychain.ErrKeychainCorrupted) for
programmatic classification.

66-79: Misleading error message: "keychain is corrupted" for missing key.

Line 78 returns "keychain is corrupted" when allowCreate=false and the key simply doesn't exist (ErrNotFound). This conflates two distinct scenarios:

  1. Key exists but is invalid/corrupted (lines 66-68)
  2. Key was never created (line 78)

For a fresh install where no Set has occurred, a Get operation will return "keychain is corrupted" which is misleading to users. Consider using distinct error messages like "keychain not initialized" for the ErrNotFound case.

Suggested differentiation
 		// If ErrNotFound, check if we are allowed to create a new key
 		if !allowCreate {
 			// Creation not allowed (e.g., during Get operation), return error
-			resCh <- result{key: nil, err: errors.New("keychain is corrupted")}
+			resCh <- result{key: nil, err: errors.New("keychain not initialized")}
 			return
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_darwin.go` around lines 66 - 79, The error message
conflates a missing key (ErrNotFound) with a corrupted key; update the branch
that handles ErrNotFound when allowCreate is false to return a distinct error
(e.g., errors.New("keychain not initialized") or similar) instead of "keychain
is corrupted". Locate the block that checks errors.Is(err, keyring.ErrNotFound)
and the subsequent if !allowCreate branch that sends on resCh with result{key:
nil, err: ...} and change that error text; keep the existing handling for the
corrupted-key branch that sends "keychain is corrupted". Ensure callers still
receive the new, clearer error when no key exists and creation is not allowed.
internal/keychain/keychain_other.go (2)

26-35: Warning message printed even when home directory is valid.

Lines 28-32 print a warning when err != nil || home == "", but line 33 continues to use home even when it might be empty. If home == "" (but err == nil), the warning mentions %v with a nil error, producing output like:

warning: unable to determine home directory: <nil>

Consider only printing the warning when there's an actual error:

Suggested fix
 func StorageDir(service string) string {
 	home, err := os.UserHomeDir()
-	if err != nil || home == "" {
-		// If home is missing, fallback to relative path and print warning.
-		// This matches the behavior in internal/core/config.go.
-		fmt.Fprintf(os.Stderr, "warning: unable to determine home directory: %v\n", err)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "warning: unable to determine home directory: %v\n", err)
+	}
+	if home == "" {
+		// Fallback handled below - xdgData will be ".local/share"
 	}
 	xdgData := filepath.Join(home, ".local", "share")
 	return filepath.Join(xdgData, service)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_other.go` around lines 26 - 35, In StorageDir, the
current check prints a warning when home=="" even if err==nil and then still
uses home; change the logic so you only call fmt.Fprintf when err != nil (to
avoid "nil" in the log) and separately handle an empty home by falling back to a
relative path (e.g. set home = ".") before computing xdgData and returning
filepath.Join(xdgData, service); update the checks around the variables home and
err accordingly so StorageDir never uses an empty home and never prints a nil
error.

50-57: Same misleading error issue: "keychain is corrupted" for missing key.

Similar to keychain_darwin.go, when the master key file doesn't exist and allowCreate=false, returning "keychain is corrupted" is misleading. This scenario occurs on a fresh install when platformGet is called before any platformSet.

Lines 50-53 check if the key exists and is valid. If the file doesn't exist or has wrong length, and allowCreate=false, line 56 returns "keychain is corrupted" even though nothing is actually corrupted—the keychain was simply never initialized.

Suggested fix
 	if !allowCreate {
-		return nil, errors.New("keychain is corrupted")
+		return nil, errors.New("keychain not initialized")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_other.go` around lines 50 - 57, The error message
"keychain is corrupted" in keychain_other.go is misleading when the master key
file is simply missing; update the logic in the code handling key loading (the
block reading keyPath and the callers platformGet/platformSet) to distinguish
file-not-found from a bad-length file: if os.ReadFile returns an error and
os.IsNotExist(err) (or equivalent) then, when allowCreate==false, return a clear
"keychain not initialized" (or "master key not found") error; if the file exists
but len(key) != masterKeyBytes then return "keychain is corrupted" as before.
Ensure this change touches the read/validate flow around keyPath and the code
paths used by platformGet/platformSet to preserve behavior when allowCreate is
true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/keychain/keychain_darwin.go`:
- Around line 66-72: Define exported sentinel errors in the keychain package
(e.g., ErrKeychainCorrupted and ErrKeychainBlocked) and replace the inline
errors.New(...) creations in keychain_darwin.go with those exported variables
where the code currently sends result{..., err: errors.New("keychain is
corrupted")} and result{..., err: errors.New("keychain access blocked")}; update
callers (such as ResolveSecretInput in internal/core/secret_resolve.go) to use
errors.Is(err, keychain.ErrKeychainBlocked) / errors.Is(err,
keychain.ErrKeychainCorrupted) for programmatic classification.
- Around line 66-79: The error message conflates a missing key (ErrNotFound)
with a corrupted key; update the branch that handles ErrNotFound when
allowCreate is false to return a distinct error (e.g., errors.New("keychain not
initialized") or similar) instead of "keychain is corrupted". Locate the block
that checks errors.Is(err, keyring.ErrNotFound) and the subsequent if
!allowCreate branch that sends on resCh with result{key: nil, err: ...} and
change that error text; keep the existing handling for the corrupted-key branch
that sends "keychain is corrupted". Ensure callers still receive the new,
clearer error when no key exists and creation is not allowed.

In `@internal/keychain/keychain_other.go`:
- Around line 26-35: In StorageDir, the current check prints a warning when
home=="" even if err==nil and then still uses home; change the logic so you only
call fmt.Fprintf when err != nil (to avoid "nil" in the log) and separately
handle an empty home by falling back to a relative path (e.g. set home = ".")
before computing xdgData and returning filepath.Join(xdgData, service); update
the checks around the variables home and err accordingly so StorageDir never
uses an empty home and never prints a nil error.
- Around line 50-57: The error message "keychain is corrupted" in
keychain_other.go is misleading when the master key file is simply missing;
update the logic in the code handling key loading (the block reading keyPath and
the callers platformGet/platformSet) to distinguish file-not-found from a
bad-length file: if os.ReadFile returns an error and os.IsNotExist(err) (or
equivalent) then, when allowCreate==false, return a clear "keychain not
initialized" (or "master key not found") error; if the file exists but len(key)
!= masterKeyBytes then return "keychain is corrupted" as before. Ensure this
change touches the read/validate flow around keyPath and the code paths used by
platformGet/platformSet to preserve behavior when allowCreate is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 025a78a9-0aea-4ad3-89c8-e9b480345536

📥 Commits

Reviewing files that changed from the base of the PR and between d0980ac and 811a9b7.

📒 Files selected for processing (2)
  • internal/keychain/keychain_darwin.go
  • internal/keychain/keychain_other.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/keychain/keychain_other.go (1)

50-57: ⚠️ Potential issue | 🟡 Minor

Inconsistent handling of corrupted master key vs Darwin.

When the master key file exists but has invalid length:

  • Darwin: Explicitly returns errors.New("keychain is corrupted") (line 66-68 in darwin file)
  • Linux: Falls through to return errNotInitialized when allowCreate=false

This means Linux users with a corrupted key see "keychain not initialized" instead of "keychain is corrupted", which is less accurate.

Suggested fix for consistency
 func getMasterKey(service string, allowCreate bool) ([]byte, error) {
 	dir := StorageDir(service)
 	keyPath := filepath.Join(dir, "master.key")
 
 	key, err := os.ReadFile(keyPath)
 	if err == nil && len(key) == masterKeyBytes {
 		return key, nil
 	}
+	if err == nil && len(key) != masterKeyBytes {
+		// Key file exists but is corrupted
+		return nil, errors.New("keychain is corrupted")
+	}
 
 	if !allowCreate {
 		return nil, errNotInitialized
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_other.go` around lines 50 - 57, When a master key
file is present but has the wrong length (os.ReadFile succeeded but len(key) !=
masterKeyBytes) the code currently falls through and returns errNotInitialized
for Linux; change this to return a consistent "keychain is corrupted" error like
Darwin does: either return errors.New("keychain is corrupted") in that branch
(when allowCreate is false) or better, introduce a shared sentinel (e.g.,
ErrKeychainCorrupted) and return it instead of errNotInitialized so the
corruption case (checked where keyPath, masterKeyBytes and allowCreate are
evaluated) is reported consistently across platforms.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/keychain/keychain_windows.go`:
- Around line 110-117: platformGet currently swallows all errors by treating
registryGet failures as "not found"; change registryGet to return (string,
error) and update platformGet to propagate real errors instead of returning ("",
nil). Specifically, modify registryGet to return an error for
DPAPI/registry/base64/decryption failures and return ("", nil) only when the key
truly does not exist, then update platformGet (and any callers) to check the
error from registryGet and return ("", err) on failure so access/permission
errors surface (refer to function names platformGet and registryGet for locating
the changes).

---

Outside diff comments:
In `@internal/keychain/keychain_other.go`:
- Around line 50-57: When a master key file is present but has the wrong length
(os.ReadFile succeeded but len(key) != masterKeyBytes) the code currently falls
through and returns errNotInitialized for Linux; change this to return a
consistent "keychain is corrupted" error like Darwin does: either return
errors.New("keychain is corrupted") in that branch (when allowCreate is false)
or better, introduce a shared sentinel (e.g., ErrKeychainCorrupted) and return
it instead of errNotInitialized so the corruption case (checked where keyPath,
masterKeyBytes and allowCreate are evaluated) is reported consistently across
platforms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69b976a6-2354-4df9-b9a3-57aa450acd9a

📥 Commits

Reviewing files that changed from the base of the PR and between 811a9b7 and fc2b96c.

📒 Files selected for processing (4)
  • internal/keychain/keychain.go
  • internal/keychain/keychain_darwin.go
  • internal/keychain/keychain_other.go
  • internal/keychain/keychain_windows.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/keychain/keychain.go

Comment thread internal/keychain/keychain_windows.go
Comment thread internal/keychain/default.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/keychain/keychain_other.go (1)

189-196: Consider using errors.Is for consistency.

platformGet uses errors.Is(err, os.ErrNotExist) (line 142), while this function uses os.IsNotExist(err). Both work, but errors.Is is the modern Go idiom and better handles wrapped errors.

♻️ Suggested change for consistency
 func platformRemove(service, account string) error {
 	err := os.Remove(filepath.Join(StorageDir(service), safeFileName(account)))
-	if err != nil && !os.IsNotExist(err) {
+	if err != nil && !errors.Is(err, os.ErrNotExist) {
 		return err
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/keychain/keychain_other.go` around lines 189 - 196, The
platformRemove function uses os.IsNotExist(err) while platformGet uses
errors.Is(err, os.ErrNotExist); update platformRemove (function platformRemove)
to check errors.Is(err, os.ErrNotExist) instead of os.IsNotExist to match the
modern Go idiom and handle wrapped errors, and ensure the package imports the
errors package if not already imported.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/keychain/keychain_other.go`:
- Around line 50-61: The code fails to propagate non-ErrNotExist read errors
from os.ReadFile(keyPath) and wrongly treats them as "not initialized"; update
the error handling around the os.ReadFile call in the function that uses
keyPath, masterKeyBytes and allowCreate so that if err != nil you first check
errors.Is(err, os.ErrNotExist) and only convert that case to errNotInitialized
(or proceed to key creation when allowCreate is true); for any other err
(permission/I/O/etc.) return that error immediately instead of falling through,
keeping the existing logic for the happy-path when len(key) == masterKeyBytes
and the corrupted-key branch.

---

Nitpick comments:
In `@internal/keychain/keychain_other.go`:
- Around line 189-196: The platformRemove function uses os.IsNotExist(err) while
platformGet uses errors.Is(err, os.ErrNotExist); update platformRemove (function
platformRemove) to check errors.Is(err, os.ErrNotExist) instead of os.IsNotExist
to match the modern Go idiom and handle wrapped errors, and ensure the package
imports the errors package if not already imported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3d57d8a-8164-421c-a4b4-07a46d6f1770

📥 Commits

Reviewing files that changed from the base of the PR and between fc2b96c and 43eb017.

📒 Files selected for processing (1)
  • internal/keychain/keychain_other.go

Comment thread internal/keychain/keychain_other.go
@JackZhao10086 JackZhao10086 merged commit d4c051d into main Apr 1, 2026
11 checks passed
@JackZhao10086 JackZhao10086 deleted the feat/adapt_to_sandbox_mode branch April 1, 2026 09:58
tuxedomm pushed a commit that referenced this pull request Apr 3, 2026
…ironments (#173)

* refactor(keychain): improve error handling and consistency across platforms

- Change platformGet to return error instead of empty string
- Add proper error wrapping for keychain operations
- Make master key creation conditional in getMasterKey
- Improve error messages and handling for keychain access
- Update dependent code to handle new error returns

* docs(keychain): improve function documentation and error message

Add detailed doc comments for all platform-specific keychain functions to clarify their purpose and behavior. Also enhance the error hint message to include a suggestion for reconfiguring the CLI when keychain access fails.

* refactor(keychain): reorder operations in platformGet for better error handling

Check for file existence before attempting to read and get master key

* fix(keychain): improve error handling and consistency across platforms.

* fix(keychain): handle corrupted master key case

* fix(keychain): handle I/O errors when reading master key
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