Skip to content

fix(auth): cloud connect --api-key no longer replaces the Polylane key - #104

Merged
justinhelmer merged 1 commit into
mainfrom
fix/connect-api-key-flag-collision
Sep 17, 2026
Merged

justinhelmer merged 1 commit into
mainfrom
fix/connect-api-key-flag-collision

Conversation

@justinhelmer

Copy link
Copy Markdown
Contributor

A user who ran polylane auth login and then polylane cloud connect --provider triggerdev --api-key <key> saw Error: Not signed in on 0.2.37, because the Trigger.dev key was sent to the API as their Polylane key. The connect command now keeps its own --api-key for the provider, and sign-in comes from the OAuth session, POLYLANE_API_KEY, or the config file as documented.

Why: The parser folds global and command options into one record, so the provider key given to cloud connect --api-key filled the global apiKey, and the credential resolver's raw scan of process.argv for --api-key confirmed it as a flag-supplied Polylane key; the API answered 401. The Trigger.dev flag arrived in cli#101; Render's --api-key connect path had the same defect and is fixed here too.

Where to look

  1. globalFlagsOf decides ownership by the command's option table: a flag the command declares never reaches the global layer, so the provider key stays in the command's args. ⚠ A command declaring a global flag's name now shadows it on purpose.
  2. The wiring in main hands only the global-owned flags to the config loader.
  3. The resolver reads the loader's record of where the key came from instead of scanning argv, so a --api-key anywhere on the line cannot relabel an env key as a flag key.
  4. The loader's record sets apiKeySource alongside apiKey, mirroring the precedence it already applies.
  5. auth login marks the key it just accepted as flag-sourced so the workspace picker still uses it over a stale OAuth session, which the argv scan used to guarantee.
  6. The collision test runs main's parse, split, load and resolve steps for cloud connect --provider triggerdev|render --api-key <key> with an OAuth session present and asserts the session is used.

Feedback wanted: Is an option-table split (a command's declaration shadows the global) the least surprising rule, versus scoping the global --api-key to argv positions before the subcommand? The alternative would keep polylane --api-key <polylane key> cloud connect --api-key <provider key> working; with this fix the last value wins the shared key and the Polylane one is dropped, so that shape needs the env var. A --provider-key rename would remove the collision outright and is a follow-up, not this PR, since Render users have --api-key today.

Risk: Every command's global-flag handling passes through the new split; a mistake would drop a legitimate global flag on commands whose options share a name, and today only cloud connect and auth login declare --api-key. Rollback is a revert; no data or config format changes.

Verified: npm run typecheck, npm run lint, npm run test (534 tests, 111 suites, 0 failures; 524 on main); end-to-end dev run on both heads with an unreachable domain, see Validation.

Decisions (3)
  • Ownership by declaration, not argv position. The command's option table is the one place that already says which flags it takes; a positional rule would need main's argv reshuffling (removeFirstNNonFlags) to keep track of where the subcommand ended, and would silently change meaning when a user types global flags after the subcommand, which the CLI accepts everywhere else.
  • The loader records the source; the resolver stops reading argv and env. The loader already computes flags ?? env ?? file; recording which branch won is one field, and it makes the resolver a pure function of Config, which is what every caller (HTTP client, sockets, setup, status) already passes. The argv scan was the only thing in the auth path reading the raw command line.
  • No flag rename. cloud connect --provider render --api-key has shipped for several releases; renaming it to --provider-key is a separate compatibility decision. Noted as a follow-up.
Validation (8 criteria)
Criterion Proof
Red first: on main's behavior the Trigger.dev and Render keys become the api-key credential and override POLYLANE_API_KEY New test file run with globalFlagsOf stubbed to identity on main's resolver: 4 of 5 fail (actual: 'api-key', expected: 'oauth'; actual: 'tr_prod_sk_x', expected: 'sk_from_env')
Triggerdev and Render connect with an OAuth session use the session; the provider key reaches the command's args test/connect-api-key-collision.test.ts (2 cases)
The provider key is never tried as the Polylane credential when nothing else is set (Not signed in from the local gate) test/connect-api-key-collision.test.ts
POLYLANE_API_KEY still authenticates connect, reported as source env test/connect-api-key-collision.test.ts; fails (actual: 'flag') with main's resolver restored, so the resolver change is load-bearing
polylane --api-key <polylane key> cloud list still authenticates via the flag test/connect-api-key-collision.test.ts; test/args.test.ts globalFlagsOf (3 cases)
Existing precedence tests still hold; the loader records apiKeySource for flag, env, config and none test/resolver.test.ts (6), test/loader.test.ts
End-to-end, fix head: cloud connect --provider triggerdev --api-key tr_prod_sk_x --verbose against invalid.invalid with a fake OAuth session sends Authorization: Bearer <redacted>; with no session it exits 3 Not signed in before any request dev run at 2e872bc (transcript in the agent report)
End-to-end, origin/main 006de1f: the same invocation sends x-api-key: <redacted> on both GET /v1/workspaces/plan and POST /v1/cloud_accounts, with and without the OAuth session dev run with git checkout origin/main -- src in the same worktree
For agents

Reproduce the bug on main: HOME=$(mktemp -d) npm run dev -- cloud connect --provider triggerdev --api-key tr_prod_sk_x --workspace ws_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --domain invalid.invalid --verbose --non-interactive prints > x-api-key: <redacted>; on this branch it prints Not signed in (exit 3) and makes no request. src/generated/ is produced by npm run codegen and is not part of the diff. Known limit, not addressed: polylane --api-key <polylane key> cloud connect --api-key <provider key> resolves both to the same parsed key and the later one wins; use POLYLANE_API_KEY for that shape until a --provider-key rename.

🤖 Generated with Claude Code

`polylane cloud connect --provider triggerdev --api-key <key>` (and the
Render form) failed with "Not signed in" right after a successful
`polylane auth login`: the parser folds global and command options into one
record, so the provider key landed in the global `apiKey`, the loader took it
as the Polylane key, and the resolver's raw-argv scan for `--api-key`
confirmed it. The API rejected the Trigger.dev key with a 401.

The command's declaration now decides ownership: `globalFlagsOf` strips every
flag the command declares before the globals reach the loader, so the
provider key stays in the command's args and the Polylane credential comes
from the env var, the OAuth session, or the config file. The loader records
which layer supplied `apiKey` (`apiKeySource`) and the resolver reads that
instead of scanning `process.argv`, so a `--api-key` anywhere on the line no
longer relabels an env key as a flag key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@coreplane-switchboard coreplane-switchboard 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.

LGTM: Clean ownership split of command vs global flags fixes the --api-key collision; resolver is now argv-free, precedence preserved, tests thorough — one naming nit only.

  • [nit] F1 src/commands/auth/login.ts:127 — Prompted API key labelled apiKeySource:'flag' — semantically misleading though functionally correct

Verdict submitted: approve at 2e872bc. The fix is well-scoped and correct — the option-table ownership split keeps the provider key out of the Polylane credential path, the resolver no longer scans argv, and precedence behavior is fully preserved with strong test coverage. Only finding is the F1 nit above.

@github-actions github-actions 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.

Auto-approved: coreplane-switchboard[bot] reviewed this PR and posted an LGTM verdict (see its review). A repo admin enabled this via the auto-approve workflow.

@justinhelmer
justinhelmer merged commit 8ff2670 into main Sep 17, 2026
4 checks passed
@justinhelmer
justinhelmer deleted the fix/connect-api-key-flag-collision branch September 17, 2026 18:26
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.

1 participant