Skip to content

Stop routing headless setup onto legacy GitHub App auth - #536

Merged
alexeyzimarev merged 2 commits into
mainfrom
headless-no-legacy-auth-fallback
Aug 12, 2026
Merged

Stop routing headless setup onto legacy GitHub App auth#536
alexeyzimarev merged 2 commits into
mainfrom
headless-no-legacy-auth-fallback

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Closes #534. Follow-on work tracked in #535. Linear auto-imports both.

Problem

A non-interactive kcap setup / kcap login with no --server-url fell back to the legacy GitHub App provider:

// OAuthLoginFlow.cs:70 (before)
return isInteractive ? AuthProvider.WorkOS : AuthProvider.GitHubApp;

The fallback existed for a real reason — GitHub has a device flow, while WorkOS authenticates through a http://127.0.0.1:{port}/callback loopback that a browser on another machine can never reach. But it dead-ended: the GitHub App branch has no provisioning path by design (it discovers orgs you already belong to), so a headless user with no workspace completed a full device-code sign-in and was then told:

"No Capacitor tenants are linked to your GitHub orgs. Ask your admin to install the Kurrent GitHub App on your org […]"

For someone evaluating kcap there is no admin. Telemetry caught one such user on 2026-08-11 who hit it, retried the entire flow, hit it again 4 minutes later, and gave up. cli_setup_workspace_offered never fired — they were never asked, they didn't decline.

Why remove the routing rather than fix the copy

CLI telemetry since the funnel went live (released builds, non-CI):

Provider Devices Session type
workos 6 all interactive
GitHubApp 1 headless

Every interactive user already lands on WorkOS. Headless routing was the only remaining source of new GitHub App sign-ins. Better copy on that dead-end would have advertised a provider we're migrating off and polished the one path still feeding it.

Change

ChooseDiscoveryProvider returns null for a non-interactive session. Both call sites report HeadlessDiscoveryUnsupportedMessage() and stop:

Setting up a new workspace needs an interactive terminal, and this session is non-interactive.
  • Create a workspace at https://capacitor.kurrent.io/signup, then run: kcap setup <slug>
  • Or point at an existing workspace: kcap setup --server-url <url>

No GitHub App mention; both routes that actually work are named. The signup URL derives from ProvisioningEndpoint.Url, so it honours the existing KCAP_SIGNUP_URL dev/preview override.

The check also moves ahead of the auth-service call in both call sites, so the run fails in ~2s instead of after a full device-code dance it can't use.

Blast radius — deliberately narrow

  • Discovery only (no --server-url).
  • Existing GitHub App workspaces unaffected: with --server-url the provider comes from that server's /auth/config (SetupCommand.cs:892).
  • --github remains an explicit escape hatch and still works headless — verified against the real binary, it proceeds to the device flow.

Tests

TDD: tests written first, watched fail, then implemented.

  • Headless_discovery_does_not_fall_back_to_legacy_github_app
  • Explicit_github_flag_still_selects_legacy_provider_when_headless
  • Headless_discovery_message_offers_signup_and_server_url_but_never_the_github_app — asserts the copy contains no GitHub App, and does contain /signup and --server-url
  • Existing ChooseDiscoveryProvider_honors_flags_and_default updated (it pinned the old fallback)

Verified against the AOT binary with SSH_CONNECTION set:

Command Result
kcap setup headless message shown, no network, exit 1
kcap login headless message shown, no network, exit 1
kcap setup --github headless reaches GitHub device flow (escape hatch intact)

Full unit suite: 56 failures here vs 57 on a clean origin/main baseline, all the known load-sensitive timing flakes. The two that differed run-to-run (Agent_finalized_before_the_bind_completes…, Wedged_initialize_reaches_LaunchFailed…) both pass in isolation and touch neither auth nor setup. AOT publish clean — no IL2026/IL3050.

Docs

README.md and help-login.txt both asserted the old fallback ("discovery falls back to GitHub Device Flow"); both now describe the interactive requirement and the two routes.

🤖 Generated with Claude Code

A non-interactive `kcap setup` / `kcap login` with no --server-url used to fall
back to the GitHub App provider, because GitHub offers a device flow and WorkOS
authenticates through a 127.0.0.1 loopback callback that a browser on another
machine can never reach.

That fallback was the only remaining source of NEW GitHub App sign-ins — CLI
telemetry since the funnel went live shows all 6 interactive devices on WorkOS
and the single GitHub App device headless — and it dead-ended. The GitHub App
branch has no provisioning path by design (it discovers orgs you already belong
to), so a headless user with no workspace completed a device-code sign-in and was
then told to ask an admin to install a GitHub App. One such user retried the whole
flow and gave up. Improving that copy would have advertised a provider we are
migrating off, so the routing goes instead.

ChooseDiscoveryProvider now returns null for a non-interactive session, and both
call sites report HeadlessDiscoveryUnsupportedMessage and stop — naming the two
routes that work (create a workspace in a browser, or pass --server-url) and no
GitHub App. The check moves ahead of the auth-service call in both, so the run
fails in ~2s instead of after a full device-code dance it cannot use.

Deliberately narrow: this affects discovery only. Existing GitHub App workspaces
still work via --server-url, where the provider comes from that server's
/auth/config, and --github remains an explicit escape hatch that still works
headless. Giving WorkOS a real headless path is issue #535.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fail fast for headless discovery; remove legacy GitHub App auth fallback

🐞 Bug fix 📝 Documentation 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Stop headless kcap setup/kcap login discovery from falling back to legacy GitHub App auth.
• Fail fast with a clear message pointing to signup or --server-url.
• Update docs and add unit tests to lock in the new behavior.
Diagram

graph TD
  A["kcap setup/login discovery"] --> B{"--github flag?"}
  B -->|"yes"| C["GitHub App discovery"] --> D["Auth proxy service"]
  B -->|"no"| E{"Interactive TTY?"}
  E -->|"yes"| F["WorkOS SSO discovery"] --> D
  E -->|"no"| G["Print headless guidance"] --> H["Exit early"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep GitHub App headless fallback + improve dead-end copy
  • ➕ No behavioral change for headless users relying on implicit GitHub device flow
  • ➕ Potentially less disruptive for unknown automation/scripts
  • ➖ Continues feeding sign-ins into a legacy provider the product is migrating away from
  • ➖ Still cannot provision a workspace; remains a dead end for new evaluators
  • ➖ Costs a full auth round trip before learning it can’t succeed
2. Implement a true headless WorkOS flow (device/out-of-band)
  • ➕ Preserves default provider while supporting headless discovery end-to-end
  • ➕ Avoids forcing users to pre-create a workspace or know a server URL
  • ➖ Larger cross-cutting change: auth UX, security review, and backend/proxy support
  • ➖ Higher risk and longer delivery; not a narrow fix
3. Provide a guided non-interactive fallback that outputs next steps + optional one-time token
  • ➕ Could allow remote browser completion while CLI stays headless
  • ➕ May reduce friction without reintroducing GitHub App defaulting
  • ➖ Still requires designing and securing an out-of-band handshake
  • ➖ More complexity than a fail-fast guard; broader testing needed

Recommendation: The PR’s approach (return null provider for non-interactive discovery and fail fast with actionable guidance) is the best narrow fix: it prevents wasted device-flow sign-ins into a legacy GitHub App path and makes the failure mode explicit before contacting the auth service. Longer-term, a proper headless-capable WorkOS flow is the strategic solution, but it’s appropriately deferred as follow-on work.

Files changed (6) +79 / -13

Bug fix (3) +44 / -8
OAuthLoginFlow.csMake discovery provider selection nullable for headless sessions +23/-5

Make discovery provider selection nullable for headless sessions

• Changes 'ChooseDiscoveryProvider' to return 'null' for non-interactive sessions unless '--github' is explicitly provided. Introduces 'HeadlessDiscoveryUnsupportedMessage()' to provide consistent, provider-agnostic guidance and avoid mentioning the legacy GitHub App path.

src/Capacitor.Cli.Core/Auth/OAuthLoginFlow.cs

SetupCommand.csFail fast before contacting auth proxy in headless discovery +11/-1

Fail fast before contacting auth proxy in headless discovery

• Moves discovery provider resolution ahead of the auth-service/proxy call. If provider selection returns null (non-interactive without '--github'), prints the shared headless guidance message and aborts discovery early.

src/Capacitor.Cli/Commands/SetupCommand.cs

Program.csPrevent headless discover-login from routing to legacy auth +10/-2

Prevent headless discover-login from routing to legacy auth

• Adds an early check in the login discovery handler to resolve the provider before querying the proxy. For non-interactive sessions without '--github', prints the shared guidance message and exits with a failure code.

src/Capacitor.Cli/Program.cs

Tests (1) +28 / -1
OAuthFlowTests.csAdd unit tests for headless discovery provider + message +28/-1

Add unit tests for headless discovery provider + message

• Updates existing provider-selection tests and adds coverage ensuring headless discovery returns null by default, '--github' still selects GitHub App explicitly, and the headless guidance message includes signup and '--server-url' without mentioning GitHub App.

test/Capacitor.Cli.Tests.Unit/OAuthFlowTests.cs

Documentation (2) +7 / -4
README.mdDocument discovery requiring an interactive terminal +3/-1

Document discovery requiring an interactive terminal

• Removes the documented headless fallback to GitHub Device Flow for discovery. Adds explicit guidance that discovery stops in headless/SSH sessions and points users to signup + 'kcap setup <tenant>' or '--server-url'.

README.md

help-login.txtUpdate login help text for headless discovery behavior +4/-3

Update login help text for headless discovery behavior

• Updates 'kcap login' help copy to reflect that discovery requires an interactive terminal and will stop in headless environments. Points users to create a workspace in a browser or pass '--server-url'.

src/Capacitor.Cli.Core/Resources/help-login.txt

@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: 080263e8c5

ℹ️ 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".

if (args.Contains("--github")) return AuthProvider.GitHubApp;

return isInteractive ? AuthProvider.WorkOS : AuthProvider.GitHubApp;
return isInteractive ? AuthProvider.WorkOS : null;

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 Honor --device before rejecting headless discovery

For a headless discovery invocation with no configured profile, such as the documented kcap login --device or kcap setup --device, ShouldDiscoverLogin routes here but this logic ignores --device and returns null, so the command exits before starting GitHub Device Flow. This regresses the CLI contract in help-login.txt:31-33, help-setup.txt:13-14, and README.md:1850, which explicitly recommends --device for SSH/headless environments; either treat --device as an explicit GitHub selection or update the accepted syntax and guidance to require --github --device.

Useful? React with 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Headless discovery docs wrong ✓ Resolved 🐞 Bug ≡ Correctness
Description
The updated README/help-login text states that discovery stops in SSH/headless environments without
noting that explicit --github discovery still works headless, and it implies --server-url is a
viable remedy from kcap login even though login doesn’t read that flag. This can misdirect
headless users away from a supported escape hatch and toward a non-functional command line.
Code

src/Capacitor.Cli.Core/Resources/help-login.txt[R10-13]

+required. Pass --github to sign in with GitHub instead. Discovery needs an
+interactive terminal, because SSO signs in through a local browser callback;
+in SSH / headless environments it stops and tells you to create a workspace
+in a browser or pass --server-url. With a server already configured, kcap logs into
Evidence
The docs now say headless discovery stops and recommend --server-url in the login help, but the
code still permits headless discovery with explicit --github, and the login command does not
read --server-url so that guidance won’t change behavior.

src/Capacitor.Cli.Core/Resources/help-login.txt[7-15]
README.md[280-282]
src/Capacitor.Cli.Core/Auth/OAuthLoginFlow.cs[75-79]
src/Capacitor.Cli/Program.cs[290-300]
src/Capacitor.Cli/Commands/SetupCommand.cs[110-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Docs/help claim discovery stops in SSH/headless environments and suggest using `--server-url` from the `kcap login` context. In reality, explicit `--github` still enables headless discovery, and `kcap login` does not consume `--server-url`, so that advice won’t work.

## Issue Context
- `OAuthLoginFlow.ChooseDiscoveryProvider` returns `GitHubApp` when `--github` is present even if non-interactive.
- `kcap login` handler does not parse `--server-url`; only setup/profile commands do.

## Fix Focus Areas
- src/Capacitor.Cli.Core/Resources/help-login.txt[7-15]
- README.md[280-282]

## Suggested changes
- Amend README/help-login wording to distinguish:
 - Default discovery (WorkOS/SSO) requires an interactive terminal; headless without `--github` stops.
 - Explicit `--github` still works headless (device flow).
- In `help-login.txt`, replace “pass --server-url” with an explicit setup command (e.g., `kcap setup --server-url <url>` or `kcap setup <slug>`), or alternatively implement real `--server-url` support for `kcap login` if that’s intended.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.Cli.Core/Resources/help-login.txt Outdated
Codex review on #536 caught a regression: dropping the implicit headless
fallback also broke `kcap login --device` / `kcap setup --device`, which three
docs (help-login.txt, help-setup.txt, README's command tour) present as THE way
to sign in from SSH or a container. ChooseDiscoveryProvider returned null for any
non-interactive session, so those documented invocations exited before starting
the device flow.

`--device` is an explicit request in the same class as `--github`, not the
implicit fallback this branch removed, and only GitHub has a device flow — so it
selects the legacy provider on the headless branch only. Deliberately not folded
into the `--github` check at the top: on a machine with a browser, `--device` has
always meant "use the device flow if the login step needs GitHub", never "take
discovery off org SSO", and routing it there would regress the interactive path
in the opposite direction. Both directions are now pinned by tests.

Docs updated to name the headless escape hatches (--device / --github /
--server-url) rather than implying discovery simply stops, per the Qodo review's
valid half.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alexeyzimarev

Copy link
Copy Markdown
Member Author

Thanks both — one real regression caught, one doc gap, one claim that doesn't hold. Addressed in c870be5.

✅ Codex: --device regression — valid, fixed

Correct and important. ChooseDiscoveryProvider returned null for any non-interactive session, which broke kcap login --device / kcap setup --device — documented as the SSH/container sign-in method in three places (help-login.txt:31-33, help-setup.txt:13-14, README.md:1850).

Reproduced against the AOT binary before the fix: kcap login --device with SSH_CONNECTION set printed the new headless message instead of starting the device flow.

--device is an explicit request in the same class as --github, not the implicit fallback this PR removes, so it now selects the legacy provider — on the headless branch only:

if (args.Contains("--github")) return AuthProvider.GitHubApp;
if (isInteractive) return AuthProvider.WorkOS;
return args.Contains("--device") ? AuthProvider.GitHubApp : null;

Deliberately not folded into the --github check at the top. On a machine with a browser, --device has always meant "use the device flow if the login step needs GitHub", never "take discovery off org SSO" — hoisting it would regress the interactive path in the opposite direction. Both directions are now pinned:

  • Explicit_device_flag_still_selects_legacy_provider_when_headless
  • Device_flag_does_not_divert_an_interactive_session_away_from_sso

Verified end-to-end: kcap login --device headless now reaches https://github.com/login/device with a real user code; plain kcap login / kcap setup headless still print the message and exit 1.

✅ Qodo, first half: docs omitted the escape hatches — valid, fixed

Right that the docs implied discovery simply stops. Both now name what still works headless: --server-url to configure a known workspace, and --device / --github to discover via GitHub Device Flow — flagged as the legacy path being phased out (#535), so readers prefer the first.

❌ Qodo, second half: "login doesn't read --server-url" — doesn't hold

it implies --server-url is a viable remedy from kcap login even though login doesn't read that flag

kcap login does read it. Program.cs:79 resolves baseUrl via AppConfig.ResolveServerUrl(args, …), which scans argv for --server-url (AppConfig.cs:94); a non-null baseUrl then makes ShouldDiscoverLogin return false, so login goes straight to that server instead of discovery.

Confirmed against the binary — it attempts the connection rather than falling into discovery:

$ kcap login --server-url https://nonexistent.invalid    # SSH_CONNECTION set
Kurrent Capacitor API cannot be reached, is it running? …
Error connecting to: https://nonexistent.invalid  nodename nor servname provided

Worth noting the message itself was never ambiguous — it says kcap setup --server-url <url>, naming the command that configures a workspace. The help-login prose was the loose part, and that's now explicit.

Verification

Full unit suite: 59 failures vs 57 on a clean origin/main baseline. The four that differ (PostOnceAsync_times_out_after_specified_duration, both Teardown_*, Wedged_initialize_*) are the known load-sensitive wall-clock-budget flakes — all four pass in isolation, and none touches auth or setup. AOT publish clean.

@alexeyzimarev
alexeyzimarev merged commit f842c50 into main Aug 12, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the headless-no-legacy-auth-fallback branch August 12, 2026 09:48
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.

Headless kcap setup routes to legacy GitHub App auth and dead-ends there

1 participant