You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Error handling / auth recovery — a corrupt (but present) stored token is treated as a fatal error on every command instead of degrading to a re-authentication prompt.
Summary
getTokenNoLockOptions returns a generic decode token: … error when a stored token entry is present but not decodable, and the API-client auth path (buildTokenSource) only recognizes keyring.ErrKeyNotFound as the "needs re-auth" signal. As a result, an absent token correctly maps to AuthRequiredError (re-auth), but a corrupt token falls through to a hard error — so the CLI fails on every command for that account with no actionable recovery, even though re-authenticating would fix it.
Where (code pointers, current main)
internal/secrets/token.go:188-191 — on json.Unmarshal failure the function returns a plain wrapped error:
internal/googleapi/client_auth.go:238-246 — the caller degrades to re-auth only for ErrKeyNotFound; any other error (including the corrupt-decode error above) becomes fatal:
The file backend is a first-class, documented configuration (GOG_KEYRING_BACKEND=file / gog auth keyring file), so a partially-written / corrupted token entry is a real on-disk failure mode (interrupted write, disk issue, or a stored-shape change across versions). The asymmetry is reproducible at the unit level with no binary build, using the same in-memory keyring the existing TestKeyringStore_TokenRoundTrip uses:
funcTestGetToken_CorruptEntry_NotClassifiableAsReauth(t*testing.T) {
ring:=keyring.NewArrayKeyring(nil)
store:=&KeyringStore{ring: ring}
client:=config.DefaultClientNameemail:=normalize("[email protected]")
// Present-but-corrupt token blob (e.g. interrupted write)._=ring.Set(keyring.Item{Key: tokenKey(client, email), Data: []byte("{not-json")})
_, err:=store.GetTokenNoMigrate(client, email)
// err = "decode token: invalid character 'n' looking for beginning of object key string"iferrors.Is(err, keyring.ErrKeyNotFound) { t.Fatal("would re-auth") }
// errors.Is(err, ErrKeyNotFound) == false => caller treats as FATAL, not re-auth (the bug)// Control: an ABSENT token *does* classify as ErrKeyNotFound -> re-auth._, errAbsent:=store.GetTokenNoMigrate(client, normalize("[email protected]"))
// errors.Is(errAbsent, keyring.ErrKeyNotFound) == true (correct)
}
Observed output (current main):
corrupt-token error: decode token: invalid character 'n' looking for beginning of object key string
errors.Is(err, ErrKeyNotFound) = false => caller treats as FATAL, not re-auth (the bug)
control: absent token -> ErrKeyNotFound -> re-auth (correct)
Expected vs actual
Expected: a present-but-undecodable token is unusable and equivalent to "no usable credential" → surface AuthRequiredError so the user re-authenticates (same recovery as an absent token).
Actual: every command for that account fails with get token for <email>: decode token: … and no re-auth path; the user must manually find and delete the keyring entry.
Prior art in this org (same bug-class)
openclaw/mcporter had the identical class (corrupt credential cache crashed instead of degrading) — filed as #207, fixed in #208 by degrading a corrupt token/client cache to re-auth while keeping security-gating material (OAuth state) fail-closed. That distinction matters here too (see fix options).
Proposed fix (options)
Option A (preferred, matches mcporter feat: add sheets notes command to read cell notes #208): introduce a sentinel (e.g. ErrCorruptToken) returned specifically from the json.Unmarshal failure in getTokenNoLockOptions, and have buildTokenSource treat errors.Is(err, ErrCorruptToken) like ErrKeyNotFound → AuthRequiredError. This targets only the corrupt-decode case.
Option B: same as A but also delete/quarantine the corrupt entry on read so the next auth flow starts clean.
Option C (not recommended): broadly map all non-ErrKeyNotFoundGetToken errors to re-auth. Avoid — this would mask genuinely transient/operational faults the code intentionally surfaces today (macOS Keychain locked in headless env, permission errors, wrong file-backend password — see the read-back guidance in internal/secrets/store.go). The fix must distinguish "credential is corrupt/unusable" from "the store is temporarily unavailable."
Happy to follow up with a PR (Option A) including the test above plus a caller-level assertion if a maintainer confirms the direction.
Filed with AI assistance (static analysis + a sandbox-run repro). The reproduction above was executed against current main; the production-frequency of corrupt entries is environment-dependent and not directly observed.
Bug type
Error handling / auth recovery — a corrupt (but present) stored token is treated as a fatal error on every command instead of degrading to a re-authentication prompt.
Summary
getTokenNoLockOptionsreturns a genericdecode token: …error when a stored token entry is present but not decodable, and the API-client auth path (buildTokenSource) only recognizeskeyring.ErrKeyNotFoundas the "needs re-auth" signal. As a result, an absent token correctly maps toAuthRequiredError(re-auth), but a corrupt token falls through to a hard error — so the CLI fails on every command for that account with no actionable recovery, even though re-authenticating would fix it.Where (code pointers, current
main)internal/secrets/token.go:188-191— onjson.Unmarshalfailure the function returns a plain wrapped error:internal/googleapi/client_auth.go:238-246— the caller degrades to re-auth only forErrKeyNotFound; any other error (including the corrupt-decode error above) becomes fatal:Reproduction (verified)
The file backend is a first-class, documented configuration (
GOG_KEYRING_BACKEND=file/gog auth keyring file), so a partially-written / corrupted token entry is a real on-disk failure mode (interrupted write, disk issue, or a stored-shape change across versions). The asymmetry is reproducible at the unit level with no binary build, using the same in-memory keyring the existingTestKeyringStore_TokenRoundTripuses:Observed output (current
main):Expected vs actual
AuthRequiredErrorso the user re-authenticates (same recovery as an absent token).get token for <email>: decode token: …and no re-auth path; the user must manually find and delete the keyring entry.Prior art in this org (same bug-class)
openclaw/mcporterhad the identical class (corrupt credential cache crashed instead of degrading) — filed as #207, fixed in #208 by degrading a corrupt token/client cache to re-auth while keeping security-gating material (OAuthstate) fail-closed. That distinction matters here too (see fix options).Proposed fix (options)
sheets notescommand to read cell notes #208): introduce a sentinel (e.g.ErrCorruptToken) returned specifically from thejson.Unmarshalfailure ingetTokenNoLockOptions, and havebuildTokenSourcetreaterrors.Is(err, ErrCorruptToken)likeErrKeyNotFound→AuthRequiredError. This targets only the corrupt-decode case.ErrKeyNotFoundGetTokenerrors to re-auth. Avoid — this would mask genuinely transient/operational faults the code intentionally surfaces today (macOS Keychain locked in headless env, permission errors, wrong file-backend password — see the read-back guidance ininternal/secrets/store.go). The fix must distinguish "credential is corrupt/unusable" from "the store is temporarily unavailable."Happy to follow up with a PR (Option A) including the test above plus a caller-level assertion if a maintainer confirms the direction.
Filed with AI assistance (static analysis + a sandbox-run repro). The reproduction above was executed against current
main; the production-frequency of corrupt entries is environment-dependent and not directly observed.