fix(auth): env key beats stale credentials; strong signup passwords; 0600 agent configs - #100
Conversation
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
There was a problem hiding this comment.
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, andtest/resolver.test.tsproves each rung (the env test smartly sets the same key in config and assertssource: 'env', so it can't pass by accident). The test isolatesHOMEbefore the dynamic import — correct on POSIX. - Password generation (
src/auth/signup-helpers.ts):crypto.randomIntfor both picks and the Fisher–Yates shuffle — unbiased and CSPRNG-backed; guaranteed one char per class; shell/JSON-safe symbol set.promptPasswordhas no non-empty validation, so the interactive "leave empty to generate" path genuinely works, and--password ""falls through to generation viaconst generated = !password. - Signup flow: challenge check runs before
res.json();announceGeneratedPasswordfires only after a successful response (never on the error path, never with a supplied password); all fouremitResultcall sites carry the augmentedresult; the interactive verified path shows the password vianotebefore 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):writeFileSyncwithmode: 0o600applies only at creation (O_CREAT semantics), so existing files keep their modes — exactly the stated contract, andtest/setup.test.tsasserts both directions across all four writers, with a win32 skip.
Findings
- F1 (minor)
src/auth/signup-helpers.ts:45—isCloudflareChallengetreats any403 + text/html(orcf-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 exitsExitCode.USAGE(2), though the user made no usage error;GENERALwould 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.
…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
|
Both review notes folded in (a423aec):
Generated by Claude Code |
There was a problem hiding this comment.
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.ts—WEAK_PASSWORD_ERROR→ hedgedCHALLENGE_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 throwsExitCode.GENERAL(1, transient) instead ofUSAGE(2), with a "Retry in a moment" hint; a user-supplied password keeps exit 2 + the "re-run without --password" hint. ConfirmedGENERAL = 1,USAGE = 2insrc/errors/codes.ts.test/signup.test.ts— existing challenge assertions updated to the new message and now pinexitCode: 2for 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.mdandskill/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.
Requested by boris · Slack thread
Before: A stale
~/.polylane/credentials.jsonsilently outranked an exportedPOLYLANE_API_KEY, so CI runners used the wrong identity.polylane auth signupasked 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 bypolylane setupwere 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_keyin~/.polylane/config.json, pinned bytest/resolver.test.tsand documented in README and SKILL.md.auth signupgenerates a 32-character random password when--passwordis omitted, prints it once (stderr, plusgenerated_passwordin 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/isCloudflareChallengehelpers used by the signup command, and routed the registry's config writers through awritePrivateTextFilehelper whose mode applies only on creation. Verified withnpm run typecheck,npm run lint,npm test(517 passing, 13 new) andnpm run build+./dist/polylane.mjs --version.🤖 Generated with Claude Code
https://claude.ai/code/session_01U8mbVueynsz2KwmBwiLzat
Generated by Claude Code