Skip to content

feat(tokencache): opt-in file token storage for cgo source builds - #82

Closed
omarshahine wants to merge 1 commit into
steipete:mainfrom
omarshahine:tokencache-keyring-backend
Closed

feat(tokencache): opt-in file token storage for cgo source builds#82
omarshahine wants to merge 1 commit into
steipete:mainfrom
omarshahine:tokencache-keyring-backend

Conversation

@omarshahine

@omarshahine omarshahine commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto current main and reduced to the storage selector alone. One commit, one question.

What changed since the last review

The logout contract that shared this branch landed separately in #84, so it is gone from here. What remains is the selector.

More importantly, @steipete's finding was right and it corrected this PR's premise. I verified all three parts independently:

Claim Verified
Releases build with CGO_ENABLED=0 .goreleaser.yaml:15, covering darwin_amd64 and darwin_arm64
Keychain backend is cgo-gated keyring v1.2.2 keychain.go:1//go:build darwin && cgo
So released macOS binaries already use the file backend follows: the backend is never compiled in

The old body and README were wrong. They said the OS keyring is the default on macOS and that this option changes where release users' tokens live. It doesn't. Release users are already on the file backend, with no configuration and no opt-in. That text is replaced rather than patched.

What this option is actually for

A cgo-enabled source build: go install, or make install on a Mac with a C toolchain. There the Keychain backend is present, a rebuild invalidates the item's ACL, and an unattended host blocks on a consent dialog nobody can answer.

That narrows the security question considerably. This does not move release users from Keychain protection to filesystem protection, because they were never on Keychain protection. It adds an opt-in for source builds that currently have it.

The tradeoff, unchanged

The file backend's password is a fixed constant compiled into the binary, not a user secret, so filesystem permissions carry the boundary. Where an OS keyring backend is compiled in it remains the default and the stronger option. The README says this plainly.

Logout is not narrowed

The pin is a flag consulted only by Save and Load, via primaryOpener(). Clear() always reaches both real backends. Covered at two levels:

  • TestClearRevokesPrimaryWhenFileBackendPinned — seed primary, pin, clear, unpin, require the primary entry to be gone.
  • TestFreshClientDoesNotSendTokenRevokedWhilePinnedToFile — the same sequence against the production client, requiring a fresh client to re-authenticate rather than put the revoked token on the wire. Asserts non-vacuity first: before logout, the cached token really is sent.
  • TestPinRoutesSaveAndLoadToFileBackend — the pin never opens the OS keyring for Save or Load.

Restoring the aliasing bug fails both guards:

FAIL: logout left a usable session in the primary backend
FAIL: a fresh client sent the revoked token after logout across a backend switch

Worth weighing against this PR

A separate finding says the re-prompt's cause is the ad-hoc signature from make install: an ad-hoc designated requirement is a literal cdhash pin, and Go does not build reproducibly, so every rebuild invalidates the ACL. If that is the cause, signing local builds with a stable identity fixes it closer to the root, and this selector is a workaround. That remedy is untested — it needs a machine with a Developer ID.

Raising it because it argues against merging this as-is. I am happy to close in favor of the signing fix, or to keep this as a verified escape hatch while that is proven out. Your call.

Verification

🤖 Generated with Claude Code

https://claude.ai/code/session_01CtVNrHHZRwkbZhk34UxLi8

@clawsweeper

clawsweeper Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 5, 2026
@clawsweeper

clawsweeper Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex review: blocked before merge. Reviewed September 8, 2026, 3:17 AM ET / 07:17 UTC.

ClawSweeper review

What this changes

Adds a configuration and environment setting that directs cached-token reads and writes to file storage, documents its protection limits, and tests logout across backend switches.

Merge readiness

Blocked before merge - 5 items remain

The selector remains distinct work, with no blocking code defect found. The separately merged logout repair resolves the earlier compatibility concern; the outstanding choice is acceptance of the opt-in credential-storage boundary.

Priority: P2
Reviewed head: 4796401995c976d95f1d7d4d21ad34cc5b2d0752
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) A focused implementation with useful regression coverage and no blocking code finding; storage-policy approval remains separate from patch quality.
Proof confidence 🌊 off-meta tidepool Not applicable: The author is a COLLABORATOR, so ordinary contributor proof is exempt. Added tests exercise selector and post-logout client behavior with array stores; earlier real-file logout evidence belongs to the merged repair. The authority trace found the disclosed filesystem-access tradeoff, with no separate unresolved bypass requiring additional final-effect proof.
Patch quality 🐚 platinum hermit (4/6) Security review found an item that needs attention.

Verification

Check Result Evidence
Real behavior Not applicable Not applicable: The author is a COLLABORATOR, so ordinary contributor proof is exempt. Added tests exercise selector and post-logout client behavior with array stores; earlier real-file logout evidence belongs to the merged repair. The authority trace found the disclosed filesystem-access tradeoff, with no separate unresolved bypass requiring additional final-effect proof.
Evidence reviewed 9 items Verified scope and clean checkout: The checkout matches the pinned head. Its delta from the pinned main contains six files, 280 additions and two deletions; no Makefile or logout implementation changes remain. Final git status was clean. No AGENTS.md or maintainer-notes directory was found.
Selector preserves default and cleanup: Configuration enables the pin only for the normalized value file. Save and Load select the existing file opener, while Clear continues calling both original openers. Cache identity, serialization, expiry handling and API authorization headers are unchanged.
Current-main and release boundary: Current main has native-first storage with file fallback but no runtime selector. The v0.2.5 release configuration sets CGO_ENABLED=0, so the proposed macOS source-build selector is not a repair for published binaries.
Findings None None.
Security Needs attention File selection bypasses native application access controls: When enabled, the selector persists bearer tokens using a public encryption password. File readers can recover those credentials even if they lack native Keychain application authorization. This is disclosed and intentional, so it is a policy decision rather than an unreported implementation defect.

How this fits together

eightctl caches Eight Sleep authentication tokens so commands and scheduled actions can reuse them. The storage selector controls where tokens are saved and loaded before the client sends authenticated API requests; logout still clears both reachable stores.

flowchart TD
  A[Configuration or environment] --> B{File storage selected?}
  B -->|Yes| C[File token cache]
  B -->|No| D[Native keyring with file fallback]
  C --> E[Authenticated API client]
  D --> E
  F[Logout] --> C
  F --> D
Loading

Decision needed

Question Recommendation
Should eightctl expose file-only token storage when a native keyring is available, accepting filesystem permissions as the credential boundary? Accept the opt-in escape hatch: Approve the disclosed file-storage boundary while retaining the existing default and dual-store logout behavior.

Why: The implementation deliberately broadens credential access for opted-in users, and the repository owner has explicitly left that policy choice open.

Before merge

  • Resolve security concern: File selection bypasses native application access controls - When enabled, the selector persists bearer tokens using a public encryption password. File readers can recover those credentials even if they lack native Keychain application authorization. This is disclosed and intentional, so it is a policy decision rather than an unreported implementation defect.
  • Resolve merge risk (P1) - Opting in permits filesystem readers, including same-user processes without Keychain application authorization, to recover usable bearer tokens; maintainer acceptance remains unresolved.
  • Resolve merge risk (P1) - Stable-identity signing may address the source-build prompt without selecting file storage, but the discussion does not establish that remedy experimentally.
  • Complete next step (P2) - Obtain steipete's decision on accepting the opt-in filesystem credential boundary or deferring the selector for a signing-based remedy.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.

Findings

  • [medium] File selection bypasses native application access controls — internal/tokencache/tokencache.go:75
Agent review details

Security

Needs attention: No accidental authorization or supply-chain regression was found, but the intentional credential-storage tradeoff requires owner acceptance.

Review metrics

Metric Value Why it matters
Production and test growth production +60 net lines; tests +200; README +18 Growth supports one documented storage selector and three backend-switch regression tests.

Merge-risk options

Maintainer options:

  1. Approve the documented filesystem boundary (recommended)
    Accept file-only storage as an explicit user choice without changing default storage or logout coverage.
  2. Hold for a native-storage alternative
    Defer this option while validating stable signing as a remedy that retains Keychain protection.

Technical review

Best possible solution:

Offer the documented file-storage escape hatch only with explicit acceptance of its filesystem trust boundary, preserving native-first defaults and cleanup across both stores.

Do we have a high-confidence way to reproduce the issue?

Not applicable to the selector as a new capability. The macOS source-build prompt is described in the discussion, but this review did not reproduce it.

Is this the best way to solve the issue?

Unclear as the preferred product direction: this is a narrow reuse of the existing backend, while the proposed signing alternative remains unverified.

AGENTS.md: not found in the target repository.

Codex review notes: model internal, reasoning medium; reviewed against db84b936e0ba.

Labels

Label justifications:

  • P2: This is a bounded optional improvement for unattended source builds, with no demonstrated published-binary regression.
  • merge-risk: 🚨 security-boundary: The selector allows users to bypass an available native keyring in favor of filesystem-protected bearer tokens.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🌊 off-meta tidepool and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Not applicable: The author is a COLLABORATOR, so ordinary contributor proof is exempt. Added tests exercise selector and post-logout client behavior with array stores; earlier real-file logout evidence belongs to the merged repair. The authority trace found the disclosed filesystem-access tradeoff, with no separate unresolved bypass requiring additional final-effect proof.

Evidence

Security concerns:

  • [medium] File selection bypasses native application access controls — internal/tokencache/tokencache.go:75
    When enabled, the selector persists bearer tokens using a public encryption password. File readers can recover those credentials even if they lack native Keychain application authorization. This is disclosed and intentional, so it is a policy decision rather than an unreported implementation defect.
    Confidence: 0.99

What I checked:

  • Verified scope and clean checkout: The checkout matches the pinned head. Its delta from the pinned main contains six files, 280 additions and two deletions; no Makefile or logout implementation changes remain. Final git status was clean. No AGENTS.md or maintainer-notes directory was found. (4796401995c9)
  • Selector preserves default and cleanup: Configuration enables the pin only for the normalized value file. Save and Load select the existing file opener, while Clear continues calling both original openers. Cache identity, serialization, expiry handling and API authorization headers are unchanged. (internal/cmd/root.go:108, 4796401995c9)
  • Current-main and release boundary: Current main has native-first storage with file fallback but no runtime selector. The v0.2.5 release configuration sets CGO_ENABLED=0, so the proposed macOS source-build selector is not a repair for published binaries. (.goreleaser.yaml:15, bfc6abd5bf7d)
  • Affirmative dependency contract: The token cache directly uses github.com/99designs/keyring, pinned to v1.2.2 in go.mod. Its backend registration and filesystem behavior determine both the release claim and the selector's credential boundary. (internal/tokencache/tokencache.go:117, 4796401995c9)
  • Dependency backend and permission verification: Read keychain.go and file.go from v1.2.2 using the recorded command. Keychain registration requires darwin && cgo. File storage creates directories with mode 0700 and token files with mode 0600, encrypting with the supplied password. The GitHub tag endpoint identifies v1.2.2 as this dependency commit. (file.go, 929f178361c0)
  • Credential authority trace: The selector can write bearer tokens into the existing file backend despite an available native keyring. Its password is a public constant; a process able to read the token file can recover the credential. The client subsequently uses cached tokens in Authorization headers. This is the disclosed policy tradeoff, not a newly discovered logout bypass. (internal/tokencache/tokencache.go:133, 4796401995c9)

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • omarshahine: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Record acceptance of the documented file-storage boundary or choose the signing-based alternative.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (5 earlier review cycles)
  • reviewed 2026-09-05T20:16:59.621Z sha 2db50d9 :: needs real behavior proof before merge. :: none
  • reviewed 2026-09-05T20:24:52.753Z sha 67b13dc :: blocked before merge. :: none
  • reviewed 2026-09-05T20:43:42.248Z sha 64eab78 :: blocked before merge. :: none
  • reviewed 2026-09-05T20:47:17.361Z sha 64eab78 :: blocked before merge. :: none
  • reviewed 2026-09-08T06:37:42.180Z sha 4796401 :: blocked before merge. :: none

@omarshahine

Copy link
Copy Markdown
Collaborator Author

The security item was right, and it was against my documentation rather than the diff. Fixed in 67b13dc.

The gap. os.Remove reports permission denied as an *os.PathError, and the inherited isIgnorableLegacyKeyError treats every *os.PathError as "nothing here to revoke". A readable-but-undeletable token therefore produced a successful logout, which is precisely what the README paragraph I added claimed could not happen. The docs promised a guarantee the code did not deliver.

I confirmed it before changing anything, against the real file backend with the directory chmod'd to deny unlink:

--- FAIL: TestClearReportsPermissionDeniedFileRemoval
    logout reported success while a readable token survived a denied deletion

The fix splits the filter rather than widening the tolerance. The clear path uses a new isAbsentOrUnnameable: it keeps the legacy-key cases (colon/pipe keys that cannot name a file on Windows) and the already-gone cases, but excludes fs.ErrPermission. A token we are not allowed to delete is a token that survives. The load path keeps the old filter, where ignoring an unreadable legacy key is harmless.

Proof is no longer in-memory. Both new tests drive the production keyring.FileBackend:

  • TestClearReportsPermissionDeniedFileRemoval seeds through the real file keyring, denies unlink, and asserts the token is still loadable and that Clear reports the failure.
  • TestFileBackedTokenSurvivingDeniedDeletionIsReportedNotSilentlyReused carries it to the wire: a fresh production client does send the surviving token, which is what makes the reported failure truthful instead of cosmetic.

Both fail with the suppression restored:

FAIL: logout reported success while a readable token survived a denied deletion
FAIL: logout reported success while a readable token survived and was reused

They skip on Windows and under root, where directory permissions do not block unlink.

Two items I have not resolved, because they are yours and not a proof problem. They are now called out in the PR body:

  1. Whether to offer the tradeoff at all. Opting in replaces OS-keyring protection with filesystem permissions and a publicly known encryption password. If you do not want that in the tool, close this PR: build: add make build / make install with the required re-sign #77 does not depend on it.
  2. The compatibility change. Existing OS-first-with-file-fallback users can now get a logout error where they previously got success, when a reachable backend refuses removal even though the other was cleared. Intentional, and it touches code that predates this PR. If you would rather keep the old silence, I will scope the stricter contract to the pinned path only.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 5, 2026
@omarshahine

omarshahine commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

Tightened the logout wording in 64eab78 — the README was promising more than the code delivers.

It said logout "always clears both" stores, which reads as a guarantee that holds even for a backend that cannot be opened, and used "revoking", which reads as server-side revocation. Neither is accurate. It now states the actual contract: logout removes the local cache and does not revoke anything at the service, so an already-issued token stays valid there until it expires; it covers the stores it can reach, tolerating a backend that will not open (nothing there to clear) and reporting an error when a reachable backend refuses deletion. Clear's doc comment carries the same correction.

That was the last item on the list that wasn't a maintainer decision. What remains is yours: whether to offer the file-storage tradeoff at all, and whether to accept the stricter logout result for existing OS-first users. Both are spelled out in the PR body, and I'm happy to scope the stricter contract to the pinned path only if you'd rather not change behavior for current users.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@clawsweeper

clawsweeper Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

steipete pushed a commit that referenced this pull request Sep 7, 2026
Add make build and make install for local source development, with a configurable per-user destination, directory creation, and macOS signing and verification after replacement. Document that Keychain authorization can still be required after rebuilding. Exercise changed-content reinstallations into paths containing spaces in Linux CI.

Validated with the full Go 1.26.8 suite, 85.5% core coverage, lint, real macOS installations and signature checks, exact-head CI, and clean independent review. The originally reported exit-137 failure was not reproduced by a synthetic overwrite on this host.

Release-note credit is collected in the companion notes PR. Credential-storage policy remains separate in #82.

Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
@omarshahine

Copy link
Copy Markdown
Collaborator Author

Two updates: the logout half is split out, and I went looking for the cause of the Keychain re-prompt rather than the workaround.

Split

The logout-contract work is now #84, rebased onto current main and carrying no dependency on keyring_backend. Both defects it fixes are reachable on main today:

  • Clear folded "backend would not open" and "backend opened and refused removal" into one success path.
  • The clear path swallowed permission denied, because os.Remove reports it as *os.PathError and isIgnorableLegacyKeyError ignores every *os.PathError.

That leaves this PR as one question — the file backend — instead of a bundle.

The re-prompt looks like a signing artifact, not a Keychain limitation

This PR's premise is that a Keychain item's ACL is bound to the code identity, so rebuilding invalidates it. That is right, and it's worth naming what sets the identity, because #77 ad-hoc signs with codesign --force --sign -.

An ad-hoc signature's designated requirement is a literal cdhash pin:

$ codesign -d -r- ./eightctl
# designated => cdhash H"08d17ef0d09d2c23f6f117da1092bd98a8887d43"

And that cdhash moves on every build — not only when the source changes:

build 1                     # designated => cdhash H"08d17ef0d09d2c23f6f117da1092bd98a8887d43"
build 2, identical source   # designated => cdhash H"38985a2bace9f44ec39c2605d870ad259b14b65d"
build 3, one-line change    # designated => cdhash H"b5673ec8fd7486b53f68500688c8159984ffb201"

Go does not produce byte-identical output across rebuilds here, so an ad-hoc make install invalidates the ACL every single time. A certificate-signed binary's designated requirement is identity-based rather than cdhash-based, so it survives arbitrary code changes.

That points at a narrower fix than changing where tokens are stored: sign local builds with a stable identity, falling back to ad-hoc when none is configured. Released builds already go through GoReleaser with a real identity, so they should not hit this at all — which is worth confirming, because if it holds, this PR trades the shipped product's credential boundary for a local-development artifact.

A second, independent option: bound the Keychain call with a timeout. The failure being described is an indefinite block, and a timeout turns that into a fast, honest error, which is what a scheduler actually needs. That is useful whichever backend is in play.

What I have not proven

I verified the cdhash instability directly, but not that a certificate-signed binary's ACL survives a rebuild in practice. This host has no code-signing identity, and creating one plus triggering a real consent prompt is not something I can do safely here. So treat the mechanism as demonstrated and the remedy as untested — worth a quick check on a machine with a Developer ID before rewriting this PR around it.

If the stable-identity fix does hold, this PR may not be needed at all. If it does not, the tradeoff here is still yours to accept or reject, and #84 is unaffected either way.

@steipete

steipete commented Sep 7, 2026

Copy link
Copy Markdown
Owner

One additional finding for the storage decision: the published macOS build already excludes the native Keychain backend.

  • .goreleaser.yaml at both v0.2.4 and current main sets CGO_ENABLED=0 for release builds.
  • go version -m on an installed v0.2.4 Darwin/arm64 release binary confirms CGO_ENABLED=0.
  • The pinned github.com/99designs/keyring implementation registers its macOS Keychain backend only under the darwin && cgo build constraint. With that backend absent, eightctl's configured backend list selects the file backend on macOS.

The proposed selector therefore affects Cgo-enabled source builds on macOS; it does not fix a Keychain prompt in the currently published macOS binary. This also means the README's unconditional statement that release installations default to native Keychain storage needs care in any future storage-policy change.

The independent logout bug is now repaired and verified in #84, including read-only filesystem failures that the original error filter still hid. File-only selection remains a separate maintainer security-policy decision. No storage-policy change or closure was made in this pass.

Rebased onto current main and reduced to the storage selector alone. The logout
contract that shared this branch landed separately in steipete#84, so what remains is
one question rather than a bundle.

Corrects the premise as well. The pinned keyring library registers its macOS
Keychain backend under a "darwin && cgo" build constraint and releases are built
with CGO_ENABLED=0, so released binaries already select the file backend on
macOS with no configuration. The earlier README text claimed the OS keyring was
the default there, which is wrong. This option is for a cgo-enabled source build
(go install, or make install on a Mac with a C toolchain), where the Keychain
backend is present and a rebuild invalidates the item's ACL, leaving an
unattended host blocked on a consent dialog nobody can answer.

The pin is a flag consulted only by Save and Load, via primaryOpener(). Clear()
always reaches both real backends, so pinning where tokens are stored never
narrows what revoking them covers. Regression coverage asserts exactly that,
at the unit level and against the production client: seed the primary backend,
pin to file, log out, unpin, and require a fresh client to re-authenticate
rather than put the revoked token on the wire. Restoring the aliasing bug fails
both with "logout left a usable session in the primary backend" and "a fresh
client sent the revoked token after logout across a backend switch".

README documents the tradeoff it actually makes: the file backend's password is
a fixed public constant, so filesystem permissions carry the boundary rather
than Keychain code identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtVNrHHZRwkbZhk34UxLi8
@omarshahine
omarshahine force-pushed the tokencache-keyring-backend branch from 64eab78 to 4796401 Compare September 8, 2026 06:33
@omarshahine omarshahine changed the title feat(tokencache): opt-in file backend for unattended hosts, with the logout contract it needs feat(tokencache): opt-in file token storage for cgo source builds Sep 8, 2026
@omarshahine

Copy link
Copy Markdown
Collaborator Author

Your CGO finding was correct and it changed this PR rather than just annotating it. I verified all three parts before rewriting: .goreleaser.yaml:15 sets CGO_ENABLED=0 for both Darwin targets, keyring v1.2.2 gates keychain.go behind //go:build darwin && cgo, and the conclusion follows.

So the README claim I wrote was wrong, and wrong in the direction that mattered: it told release users the OS keyring held their token when the Keychain backend was never compiled into their binary. That text is replaced, not patched, and the PR is retitled to match what the option is actually for.

Rebased onto db84b93 and reduced to the selector. The logout work is gone from this branch since #84 landed it. Conflict resolved.

The security question is narrower than it looked: this does not move release users from Keychain protection to filesystem protection, because they were never on Keychain protection. It adds an opt-in for cgo source builds that currently have it.

One argument against merging this at all, which I would rather surface than bury: the re-prompt's cause looks like the ad-hoc signature from make install. An ad-hoc designated requirement is a literal cdhash pin and Go does not build reproducibly, so every rebuild invalidates the ACL, not just every source change. If that holds, signing local builds with a stable identity fixes the cause and this selector is a workaround. I have not been able to test that remedy — no Developer ID on this host.

Happy to close this in favor of the signing fix if you think that is the better shape. It is a real question and you have better context on it than I do.

@clawsweeper clawsweeper Bot removed merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. proof: sufficient Contributor real behavior proof is sufficient. labels Sep 8, 2026
@omarshahine

Copy link
Copy Markdown
Collaborator Author

The standing review above describes 64eab78, which no longer exists. This PR was rebased onto db84b93 and reduced to the selector alone (4796401, one commit, Makefile-unrelated); the logout work it reviewed landed separately in #84. CI is green and the branch is mergeable.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Sep 8, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@omarshahine

Copy link
Copy Markdown
Collaborator Author

Correction to my previous comment: it was wrong. ClawSweeper had already reviewed the rebuilt branch at 07:18 UTC, against 4796401 — the current head, not 64eab78. My query sorted comments by creation rather than update time and missed the edit, so the re-review request was unnecessary noise. Apologies for the churn.

The current verdict is accurate and needs nothing further from me: no blocking code defect, the compatibility concern resolved by #84 landing separately, and the remaining items reduced to the storage-policy decision plus the open question of whether stable-identity signing is the better remedy. That matches what I raised above.

@omarshahine

Copy link
Copy Markdown
Collaborator Author

Closing this. @steipete your CGO finding settled it, and it settled it against the PR.

Once it was clear that released binaries never compile in the Keychain backend, the audience for this option narrowed to people who build from source and run unattended and can't use a release build. For my own case the honest answer turned out to be simpler: install from the Homebrew tap instead of make install. That removes the Keychain from the picture entirely, with no config line and no new credential-storage boundary for anyone to accept. It also gets a properly signed binary rather than an ad-hoc one, which sidesteps the exit-137 overwrite trap that started this whole thread.

That leaves no reason to ask you to accept a weaker credential boundary for a problem the shipped product does not have.

What came out of this is already merged and worth more than the selector was:

If the source-build prompt ever needs a real fix, the better shape is the one this discussion surfaced: sign local builds with a stable identity so the Keychain ACL survives a rebuild, rather than moving the token somewhere the ACL doesn't apply. Worth a timeout on the token load too, so an unattended host gets a fast error instead of an indefinite block. Neither needs this PR.

Branch stays at omarshahine:tokencache-keyring-backend if anyone wants to revive the selector later.

@omarshahine omarshahine closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants