Skip to content

fix(auth): env key beats stale credentials; strong signup passwords; 0600 agent configs - #100

Merged
boristane merged 4 commits into
mainfrom
fix/credential-precedence-and-setup-hardening
Sep 15, 2026
Merged

boristane merged 4 commits into
mainfrom
fix/credential-precedence-and-setup-hardening

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Requested by boris · Slack thread

Before: A stale ~/.polylane/credentials.json silently outranked an exported POLYLANE_API_KEY, so CI runners used the wrong identity. polylane auth signup asked scripts to invent a password, and when the API edge challenged a weak or known-leaked value it failed with an opaque JSON parse error over a Cloudflare HTML page. Agent MCP config files created by polylane setup were written with the default umask mode even though the installer hands them the workspace API key immediately afterwards.

After: Credential precedence is --api-key > POLYLANE_API_KEY > ~/.polylane/credentials.json > api_key in ~/.polylane/config.json, pinned by test/resolver.test.ts and documented in README and SKILL.md. auth signup generates a 32-character random password when --password is omitted, prints it once (stderr, plus generated_password in the JSON envelope) with a note that password reset changes it, and turns a challenge response into "The password was rejected as weak or known-leaked" with a hint to re-run without --password; the emailed verification-code step is untouched. Every MCP config file the CLI creates (JSON, JSONC, Codex TOML, Goose YAML) is created 0600; existing files keep their mode.

Reordered the two resolver branches, added generatePassword / isCloudflareChallenge helpers used by the signup command, and routed the registry's config writers through a writePrivateTextFile helper whose mode applies only on creation. Verified with npm run typecheck, npm run lint, npm test (517 passing, 13 new) and npm run build + ./dist/polylane.mjs --version.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U8mbVueynsz2KwmBwiLzat


Generated by Claude Code

Credential precedence is now --api-key flag, POLYLANE_API_KEY, then
~/.polylane/credentials.json, then api_key in ~/.polylane/config.json.
Previously the OAuth credentials file outranked the env var, so a stale
token left on a CI runner silently won over an exported key.

Adds test/resolver.test.ts pinning the order (env over a stale
credentials.json in particular) and documents it in README and SKILL.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8mbVueynsz2KwmBwiLzat
The API edge challenges weak or known-leaked password values (Cloudflare
leaked-credentials rule): it answers with an HTML challenge page instead
of the JSON envelope, which `auth signup` surfaced as an opaque parse
error. Random passwords pass.

When --password is omitted (or left empty at the prompt) the CLI now
generates a 32-character password with letters, digits and symbols,
sends it, and prints it once on stderr after the server accepts it (plus
`generated_password` in the JSON envelope for scripts), with a note that
password reset changes it later. A supplied password that trips the
challenge (`cf-mitigated: challenge`, or a 403 HTML body) fails with
"The password was rejected as weak or known-leaked" and a hint to re-run
without --password. The emailed verification-code step is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8mbVueynsz2KwmBwiLzat
`polylane setup` writes only the MCP server URL into agent configs, but
those same files receive the workspace API key from the installer right
afterwards and hold the agents' own OAuth tokens. Files the CLI creates
(JSON, JSONC, Codex TOML, Goose YAML) are now created 0600; a file that
already exists keeps its mode, so another tool's config is never widened
or tightened behind its back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8mbVueynsz2KwmBwiLzat

@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: Correct precedence reorder, safe password generation, and create-only 0600 modes, all pinned by tests; only minor diagnostic-accuracy notes.

  • [minor] F1 src/auth/signup-helpers.ts:45 — Any 403 HTML (or cf-mitigated challenge) is diagnosed as a weak/leaked password, misattributing IP-reputation or rate-limit blocks
  • [nit] F2 src/commands/auth/signup.ts:305 — Challenge on a generated password exits USAGE (2) though the user made no usage error

Verdict: approve — reviewed head 19200d6, all 11 files (+412/−44) read in full. The change does what it says, and each of the three behaviors is pinned by new tests.

What I checked

  • Resolver reorder (src/auth/resolver.ts): env var moved above the credentials file; flag > env > credentials.json > config order is exactly what README/SKILL.md now document, and test/resolver.test.ts proves each rung (the env test smartly sets the same key in config and asserts source: 'env', so it can't pass by accident). The test isolates HOME before the dynamic import — correct on POSIX.
  • Password generation (src/auth/signup-helpers.ts): crypto.randomInt for both picks and the Fisher–Yates shuffle — unbiased and CSPRNG-backed; guaranteed one char per class; shell/JSON-safe symbol set. promptPassword has no non-empty validation, so the interactive "leave empty to generate" path genuinely works, and --password "" falls through to generation via const generated = !password.
  • Signup flow: challenge check runs before res.json(); announceGeneratedPassword fires only after a successful response (never on the error path, never with a supplied password); all four emitResult call sites carry the augmented result; the interactive verified path shows the password via note before the code loop. The non-JSON fallback error is a good catch for other Cloudflare interstitials.
  • 0600 configs (src/utils/fs.ts, src/agents/registry.ts): writeFileSync with mode: 0o600 applies only at creation (O_CREAT semantics), so existing files keep their modes — exactly the stated contract, and test/setup.test.ts asserts both directions across all four writers, with a win32 skip.

Findings

  • F1 (minor) src/auth/signup-helpers.ts:45isCloudflareChallenge treats any 403 + text/html (or cf-mitigated: challenge) as "The password was rejected as weak or known-leaked". A challenge triggered by IP reputation or rate limiting on the signup route gets the same message, telling a user with a perfectly fine password that their password is bad. The generated-password hint ("Retry; a fresh password is generated on every run") partly mitigates, but consider hedging the message ("the edge challenged the request — usually a weak or known-leaked password") or checking for the leaked-credential rule more specifically if the edge exposes it.
  • F2 (nit) src/commands/auth/signup.ts:305 — a challenge on a generated password exits ExitCode.USAGE (2), though the user made no usage error; GENERAL would fit that branch better.

No docs/reference/specs/ in this repo, so no spec check applies; test changes are purely additive (13 new, none removed or weakened). Note: PR body's password-reset guidance ("Forgot password" on the console) matches ERRORS.md — consistent.

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

…passwords

A 403 HTML / cf-mitigated challenge can also come from IP reputation or
rate limiting, so the error now reads "The sign-up request was challenged
by the edge, usually because the password is weak or known-leaked" while
keeping the re-run-without---password hint. When the challenged password
was generated by the CLI the caller made no usage error, so that branch
exits GENERAL (1) instead of USAGE (2). Tests, ERRORS.md and SKILL.md
follow the new wording.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8mbVueynsz2KwmBwiLzat
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Both review notes folded in (a423aec):

  • F1: the challenge error now reads "The sign-up request was challenged by the edge, usually because the password is weak or known-leaked", keeping the re-run-without---password hint; test, ERRORS.md and SKILL.md updated to the new wording.
  • F2: a challenge on a CLI-generated password now exits GENERAL (1) with a retry hint instead of USAGE (2); new test pins it.

Generated by Claude Code

@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: Delta since 19200d6 cleanly lands both prior notes — hedged challenge wording and exit 1 for a challenged CLI-generated password — with consistent code, docs, and tests; nothing else changed.

Approved at head a423aec — the one-commit delta since my LGTM at 19200d6 folds both notes correctly, and nothing else moved.

What the delta does (verified):

  • src/commands/auth/signup.tsWEAK_PASSWORD_ERROR → hedged CHALLENGE_ERROR ("challenged by the edge, usually because the password is weak or known-leaked"), acknowledging IP-reputation/rate-limit challenges produce the same page. A challenge on a CLI-generated password now throws ExitCode.GENERAL (1, transient) instead of USAGE (2), with a "Retry in a moment" hint; a user-supplied password keeps exit 2 + the "re-run without --password" hint. Confirmed GENERAL = 1, USAGE = 2 in src/errors/codes.ts.
  • test/signup.test.ts — existing challenge assertions updated to the new message and now pin exitCode: 2 for the supplied-password case; a new test pins exit 1 + retry hint for the generated-password case. No tests removed or weakened — one added, two tightened.
  • ERRORS.md and skill/SKILL.md — both docs updated to match the new message and the exit-1 generated-password branch, consistently with the code.

No specs directory in this repo, so no spec/coverage check applies. No findings.

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

@boristane
boristane merged commit 14c3adb into main Sep 15, 2026
4 checks passed
@boristane
boristane deleted the fix/credential-precedence-and-setup-hardening branch September 15, 2026 03: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.

2 participants