Skip to content

Bound the keyring probe on headless sessions - #581

Merged
jeremy merged 4 commits into
mainfrom
fix/bounded-keyring-probe
Jul 28, 2026
Merged

Bound the keyring probe on headless sessions#581
jeremy merged 4 commits into
mainfrom
fix/bounded-keyring-probe

Conversation

@jeremy

@jeremy jeremy commented Jul 28, 2026

Copy link
Copy Markdown
Member

Completes the #568 keychain-hang lineage: after lazy store construction (#578) fixed credential-free commands, credential-touching commands on a locked headless keychain still blocked unbounded. basecamp/cli#56 added the upstream prerequisite (StoreOptions.ProbeTimeout — a bounded probe whose darwin implementation actually kills the hung security child and falls back to file storage). This PR bumps to that credstore (pseudo-version of the merge commit; no release tag exists yet) and wires it.

Interactivity-aware bound

  • Headless (stderr not a terminal): probe bounded at 10s. A headless session can never answer a keychain unlock prompt, so the only prior outcomes were an infinite hang or the manual BASECAMP_NO_KEYRING=1 escape hatch. Now: bounded probe → file fallback with the existing plaintext warning.
  • Interactive: unbounded probe, exactly as today. A locked keychain in an interactive session raises an unlock prompt the user may legitimately take longer than any reasonable timeout to answer — cutting it off would silently degrade them to plaintext file storage. Not a regression: interactive sessions were never the incident class.
  • Stderr, not stdout, as the signal: interactive use routinely pipes stdout (| jq) while prompts still reach the terminal.

ForceFile is deliberately not wired — credstore's internal timeout-to-file fallback covers the CLI's need, and BASECAMP_NO_KEYRING already provides forced file mode.

Tests

TestEnsureBoundsProbeOnlyWhenHeadless pins both branches through the existing newCredStore seam plus a new stderrIsTerminal seam (precedent: stdoutIsTerminal in update_notice.go). Existing laziness test unchanged.

Verification

  • bin/ci green; go test -race ./internal/auth/... green.
  • Behavioral smoke on macOS: piped (headless-path) auth status probes within the bound against a healthy keychain and loads keyring credentials (expired: false; the file fallback holds a stale token, distinguishing the paths).
  • Upstream credstore at the pinned commit: eleven-round review convergence on credstore: forced file-only construction and a bounded keyring probe cli#56, macOS -race -count=2 proof at merge head.

Refs #568


Summary by cubic

Bound the keyring probe to 10s only in fully headless sessions (no TTYs and no GUI) and fall back to file storage to prevent hangs on locked keychains. Interactive or GUI-launched sessions keep the unbounded probe. Addresses #568.

  • Bug Fixes

    • Treat sessions as headless only when stdin, stdout, and stderr are all non-terminals and no GUI is available (macOS: launchctl managername != Aqua; Linux/*BSD: no DISPLAY/WAYLAND_DISPLAY; Windows: GUI check is always false since Credential Manager never prompts); set ProbeTimeout to 10s so credstore falls back to file storage.
    • Keep the probe unbounded when any stdio is a terminal or a GUI is present to allow unlock prompts.
  • Dependencies

    • Bump github.com/basecamp/cli to v0.2.2-0.20260728023309-04e401b12c6c to use credstore.StoreOptions.ProbeTimeout.

Written for commit 4eb0779. Summary will update on new commits.

Review in cubic

Bumps github.com/basecamp/cli to the post-#56 credstore
(v0.2.2-0.20260728023309-04e401b12c6c), which adds
StoreOptions.ProbeTimeout: a bounded availability probe that kills the
hung `security` child on darwin and falls back to file storage as if
the probe had failed.

The CLI wires it interactivity-aware: when stderr is not a terminal
(CI, piped installers, ssh without a TTY) the probe is bounded at ten
seconds — a headless session can never answer a keychain unlock prompt,
so hanging forever in an uncancellable child was the only alternative
(the #568 incident class, previously mitigated only by lazy
construction and the BASECAMP_NO_KEYRING escape hatch). Interactive
sessions keep the unbounded probe: a locked keychain there raises an
unlock prompt, and cutting it off mid-answer would silently degrade the
user to plaintext file storage.

ForceFile is deliberately not wired: credstore's internal
timeout-to-file fallback covers the CLI's need, and the env-var path
already provides forced file mode.
Copilot AI review requested due to automatic review settings July 28, 2026 02:41
@github-actions github-actions Bot added tests Tests (unit and e2e) auth OAuth authentication deps labels Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents credential-touching commands from hanging indefinitely on locked macOS keychains in headless sessions by bounding the keyring availability probe and relying on credstore’s timeout-to-file fallback, while preserving the existing unbounded behavior for interactive sessions.

Changes:

  • Detect “headless” via whether stderr is a TTY and set credstore.StoreOptions.ProbeTimeout to 10s only in that case.
  • Keep the interactive path unbounded to avoid cutting off legitimate unlock prompts.
  • Bump github.com/basecamp/cli to a pseudo-version that includes StoreOptions.ProbeTimeout, and add a unit test pinning both branches.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated no comments.

File Description
internal/auth/keyring.go Adds stderr-based headless detection and applies a 10s keyring probe timeout only when headless.
internal/auth/keyring_test.go Adds coverage to assert probe timeout is set only for headless sessions via a test seam.
go.mod Bumps github.com/basecamp/cli to a pseudo-version that provides ProbeTimeout support.
go.sum Updates checksums for the bumped github.com/basecamp/cli version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: edb710faf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/auth/keyring.go Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

No issues found across 4 files

Re-trigger cubic

Stderr alone misclassified routine interactive use: `2>auth.log` from
a terminal would have bounded the probe and, on a >10s unlock answer,
silently degraded to plaintext file storage with the warning hidden in
the redirected log. Headless now requires stdin, stdout, and stderr to
all be non-terminals — any attached stream means a human can answer the
unlock prompt on the controlling terminal or the GUI. Review feedback
on #581.
Copilot AI review requested due to automatic review settings July 28, 2026 02:55
@github-actions github-actions Bot added the bug Something isn't working label Jul 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2b72bfc9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/auth/keyring.go Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/auth/keyring.go
All three stdio streams being non-terminals is not proof of
headlessness: a macOS app or IDE task runner launches the CLI fully
detached while the user can still answer the keychain unlock dialog on
the WindowServer. Headless now additionally requires no GUI session —
on darwin via `launchctl managername` != Aqua (bounded, cannot touch
the keychain, failures count as no GUI so true headless keeps the
bounded probe); elsewhere via DISPLAY/WAYLAND_DISPLAY, with Windows
always false since Credential Manager never prompts. Review feedback
on #581.
Copilot AI review requested due to automatic review settings July 28, 2026 03:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.

Comment thread internal/auth/session_other.go Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread internal/auth/session_other_test.go
Comment thread internal/auth/session_other.go Outdated
The !darwin implementation's claim that Windows is always non-GUI was
false — that build returned true on any platform with DISPLAY set. A
dedicated session_windows.go hard-codes false with the rationale, the
unix implementation drops the stale claim, and the env test gains the
Wayland-only branch. Review feedback on #581.
Copilot AI review requested due to automatic review settings July 28, 2026 03:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.

@jeremy
jeremy merged commit 31c4936 into main Jul 28, 2026
28 checks passed
@jeremy
jeremy deleted the fix/bounded-keyring-probe branch July 28, 2026 04:59
jeremy added a commit that referenced this pull request Jul 28, 2026
resourceOrigin now lowercases the host (and the localhost carve-out
checks the lowered host) and strips explicit default ports, normalizing
leading zeros — the SDK binds the RFC 9728 protected-resource identifier
to this string code-point exact, so an equivalent spelling
(HTTPS://3.BasecampAPI.com, https://3.basecampapi.com:443) previously
mismatched the advertised identifier and silently soft-fell back to
Launchpad. IPv6 brackets are restored around the lowered hostname.

Also restacked onto main 31c4936, preserving #581's bounded headless
keyring probe alongside the credential resource field, and tidied the
go.sum lines the auto-merge duplicated.
jeremy added a commit that referenced this pull request Jul 28, 2026
resourceOrigin now lowercases the host (and the localhost carve-out
checks the lowered host) and strips explicit default ports, normalizing
leading zeros — the SDK binds the RFC 9728 protected-resource identifier
to this string code-point exact, so an equivalent spelling
(HTTPS://3.BasecampAPI.com, https://3.basecampapi.com:443) previously
mismatched the advertised identifier and silently soft-fell back to
Launchpad. IPv6 brackets are restored around the lowered hostname.

Also restacked onto main 31c4936, preserving #581's bounded headless
keyring probe alongside the credential resource field, and tidied the
go.sum lines the auto-merge duplicated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth OAuth authentication bug Something isn't working deps tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants