Skip to content

Refuse browser and pasted-callback logins under BASECAMP_NONINTERACTIVE - #693

Open
jeremy wants to merge 4 commits into
mainfrom
login-noninteractive-gate
Open

Refuse browser and pasted-callback logins under BASECAMP_NONINTERACTIVE#693
jeremy wants to merge 4 commits into
mainfrom
login-noninteractive-gate

Conversation

@jeremy

@jeremy jeremy commented Sep 9, 2026

Copy link
Copy Markdown
Member

#681 gated interactive login on the explicit machine-output flags and left the environment half of #669 open: BASECAMP_NONINTERACTIVE=1 basecamp login still opened a browser (or, under SSH, printed a URL and read stdin for a pasted callback) and sat there for up to five minutes. The repo's own contract for the variable — "the CLI must not show interactive prompts regardless of TTY detection; they become actionable errors" (config.NonInteractiveEnv, SKILL.md) — is what this applies to login.

What

  • auth login / login refuse under BASECAMP_NONINTERACTIVE unless --device-code is given. The check sits after the machine-output gate and before any network call, and its hint names the two headless shapes: --device-code, which prints a code to approve from any device and asks nothing of the terminal (the exception Import personal access tokens with auth login --with-token #681 argued for), and --with-token, plus auth status for a check. Same usage-error shape as the flag gate.
  • The pasted-callback prompt refuses under the variable too (internal/auth). A device login on a Launchpad-backed host — every production account until the BC5 authorization server ships — falls back to remote mode, whose "Paste the callback URL" read is the one place a login consumes stdin. That prompt now returns the same actionable error before printing instructions, so the --device-code exemption cannot route a harness into a stdin wait.
  • SKILL.md's auth block gains the one-line rule; no surface change.

Verification

  • Failing first: on main, TestAuthLoginRefusesNonInteractiveEnvWithoutDeviceCode (default, --remote, --local, --no-browser) and TestLoginRemoteModeRefusesNonInteractiveEnv do not fail fast — they hang on the callback wait until the harness timeout, which is the reported behavior. With the gates they refuse with usage, name BASECAMP_NONINTERACTIVE, --device-code and --with-token, and the identity server sees no request; the remote test also asserts the paste prompt was never logged.
  • TestAuthLoginDeviceCodeRunsUnderNonInteractiveEnv proves the exemption end-to-end: a pinned-issuer device grant under the variable prints the code and completes.
  • e2e: auth.bats case for the env refusal (exit non-zero, hint names both flags).
  • bin/ci green on Linux (thelio, Go 1.26.7): fmt, vet, lint, unit, e2e, naming, surface, skill drift, bare groups, provenance, tidy.

Not doing here

  • Gating on a non-terminal stdin without the variable: the remote paste read already errors at EOF, and the browser flows never read stdin.
  • Bounding the device flow's poll with signal.NotifyContext: separate from this gate.

Fixes #669


Summary by cubic

Fixes #669: basecamp auth login and basecamp profile create now refuse browser and pasted-callback logins under BASECAMP_NONINTERACTIVE instead of opening a browser or waiting up to five minutes on a pasted callback. --device-code remains the exception, but only where the server offers the device flow — Launchpad does not.

Changes

  • The gate now runs at the command layer for both auth login and profile create, plus again in the auth layer for every converging Launchpad shape, so the refusal happens before any network call, stdin read, or in-memory profile entry.
  • LoginOptions.defaults treats the variable as --no-browser for every flow, so no login shape can launch a browser under it.
  • The usage error names --device-code and --with-token; SKILL.md documents the rule.

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

Review in cubic

The machine-output flags already turn an interactive login into a usage
error; the environment variable that says the same thing did not, so a
harness that set it and ran the login sat on a browser callback for five
minutes, or on a pasted-URL prompt nobody would answer. The gate refuses
those flows up front, before discovery, and names the two headless shapes:
--device-code, which prints a code to approve from any device and asks
nothing of the terminal, and --with-token.

A device login on a Launchpad-backed host falls back to the pasted
callback, so that prompt refuses under the variable too, where it would
otherwise read stdin.

Fixes #669
Copilot AI balanced review requested due to automatic review settings September 9, 2026 23:30
@github-actions github-actions Bot added commands CLI command implementations tests Tests (unit and e2e) skills Agent skills auth OAuth authentication labels Sep 9, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T23:45:13.154476Z 4c767d3 New commits
ℹ️ About Codex in GitHub

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

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

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI 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.

🟡 Changes recommended

Local Launchpad login remains interactive through other callers, and device-code guidance overstates current provider support.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds non-interactive safeguards intended to prevent OAuth login flows from waiting for browser or pasted-callback input.

Changes:

  • Gates interactive login under BASECAMP_NONINTERACTIVE.
  • Adds unit and end-to-end coverage.
  • Documents the device-code exception.

[!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.

File summaries
File Description
internal/commands/auth.go Adds the command-level non-interactive gate.
internal/auth/auth.go Refuses remote callback prompting.
internal/commands/auth_login_test.go Tests refusal and device-code behavior.
internal/auth/auth_test.go Tests remote-mode refusal.
e2e/auth.bats Adds CLI-level refusal coverage.
skills/basecamp/SKILL.md Documents non-interactive authentication.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/auth/auth.go Outdated
Comment thread internal/commands/auth.go Outdated
Comment thread skills/basecamp/SKILL.md Outdated

@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: 9c80845f7f

ℹ️ 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/auth.go Outdated
Comment on lines +530 to +531
if opts.Remote {
if config.NonInteractiveEnv() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate local OAuth initiated by profile creation

When BASECAMP_NONINTERACTIVE=1 is used with basecamp profile create <name> without --remote, newProfileCreateCmd calls Manager.Login with Remote: false, so this remote-only guard is skipped and the Launchpad branch still opens a browser and waits up to five minutes for a loopback callback. This leaves a public login entry point violating the noninteractive guarantee; gate every Launchpad browser/callback flow in the auth layer, or add the equivalent check to profile creation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same finding as Copilot's above, and it was right: d5af034 moves the refusal to the top of loginLaunchpad so every Launchpad shape refuses under the variable for every caller, and 4b0fecd/4c767d3 add the command-level gate to profile create as well.

Comment thread skills/basecamp/SKILL.md Outdated
basecamp auth login --scope full # Full access (the default; ignored by Launchpad)
basecamp auth login --scope read # Read-only access (ignored by Launchpad)
basecamp auth login --device-code # Headless authentication with manual browser instructions
BASECAMP_NONINTERACTIVE=1 basecamp auth login --device-code # The only OAuth login that runs under BASECAMP_NONINTERACTIVE; browser and pasted-callback flows refuse

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Qualify the device-code recommendation by OAuth provider

When OAuth discovery falls back to Launchpad, --device-code is converted to Remote: true in buildLoginCmd, after which the new guard in loginLaunchpad rejects it because Launchpad's implementation is still the pasted-callback flow. Consequently this newly documented command fails on Launchpad-backed accounts rather than running as claimed; describe it as available only with a BC5 device-flow provider and recommend --with-token for noninteractive Launchpad authentication.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in d5af034: the skill line says the device login runs under the variable only where the server offers the device flow (Launchpad does not) and points automation at --with-token; the command hint says the same.

…the pasted callback

profile create reaches Manager.Login without the login command's gate, so
the local Launchpad branch still opened a browser and waited on the
loopback callback under the variable. Both Launchpad shapes wait on a
person, and every entry point converges on loginLaunchpad, so that is
where the refusal lives now. The hints and the skill stop promising
--device-code unconditionally: Launchpad has no device flow.
Copilot AI review requested due to automatic review settings September 9, 2026 23:35

Copilot AI 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.

🟡 Changes recommended

BC5-backed profile creation can still open a browser under BASECAMP_NONINTERACTIVE without an explicit device-code request.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread internal/auth/auth.go

@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: d5af0342e9

ℹ️ 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/auth.go
// browser someone signs into, the remote one on a pasted redirect URL.
// Every login entry point converges here, so this is where the
// environment's word that nobody is at the terminal is final.
if config.NonInteractiveEnv() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate BC5 profile creation under noninteractive mode

When BASECAMP_NONINTERACTIVE=1 is used with basecamp profile create <name> against a BC5 issuer without --device-code, profile creation calls Manager.Login with browser-enabled defaults, and the BC5 branch returns through loginDevice before reaching this Launchpad-only guard; that flow can open a browser and poll until the device code expires. The fresh evidence after the earlier Launchpad report is the unguarded BC5 return at auth.go:480-482, combined with profile creation having no equivalent !deviceCode check. Add the same explicit device-code gate to profile creation, or propagate that intent into Manager.Login, so this entry point cannot wedge noninteractive automation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes — this was the remaining path. 4b0fecd (gate) and 4c767d3 (placed before the in-memory profile entry is added) give profile create the same refuseNonInteractiveLogin gate as login, so a BC5 profile create refuses under the variable without --device-code before discovery; and LoginOptions.defaults now treats the variable as --no-browser for every flow, so even a --device-code login under it prints the code rather than launching a browser. Covered by TestProfileCreateRefusesNonInteractiveEnv and TestLoginDefaultsNeverOpenABrowserUnderNonInteractiveEnv.

…variable

profile create runs the same OAuth flows as login without the command
gate, so on a BC5 issuer it launched a browser to the approval page under
BASECAMP_NONINTERACTIVE. Both commands now share one refusal, and
LoginOptions.defaults treats the variable as --no-browser for every flow.
Copilot AI review requested due to automatic review settings September 9, 2026 23:41

Copilot AI 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.

🔵 Needs a closer look

The profile gate returns after mutating in-memory configuration, bypassing rollback and failing the new regression test.

Review details

Suppressed comments (1)

internal/commands/profile.go:269

  • This returns after the pending profile has already been inserted and ActiveProfile/BaseURL changed, bypassing the rollback used for login errors below. A refused default profile create therefore leaves the in-memory config mutated (and makes the new assert.Empty(t, cfg.Profiles) case fail). Move this gate before the temporary mutation.
			app.Config.Profiles[name] = profileCfg
			app.Config.ActiveProfile = name
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 9, 2026 23:43

Copilot AI 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.

🟢 Approval recommended

The gates cover all production login entry points and are backed by focused unit and end-to-end tests.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth OAuth authentication commands CLI command implementations skills Agent skills tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

basecamp login blocks and emits prose under machine output / non-interactive environments

2 participants