Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e098874
docs(tasks): plan opencode auth-provider + dynamic-catalog + effort a…
elkaix Jul 18, 2026
ee6d838
candidate f1ff6f28-db9c-4236-8656-7ee20256fcee
Jan 1, 2000
e6adfea
fix(auth): update models.dev catalog consumer test and genericize mod…
elkaix Jul 18, 2026
1f9f9df
docs(tasks): mark dynamic catalog phase done; generic framing
elkaix Jul 18, 2026
cc59239
candidate 55be2461-0989-4a1e-8602-102703271165
Jan 1, 2000
79b64d4
fix(auth): make oauth_flows strict-clean and UTF-8 explicit
elkaix Jul 18, 2026
258c628
docs(tasks): mark shared OAuth flow helper done; P3 sub-phases
elkaix Jul 18, 2026
92fbebd
docs(tasks): finalize P3b GitHub Copilot design (primary-source verif…
elkaix Jul 18, 2026
5bd283e
feat(auth): add GitHub Copilot device-code OAuth login provider
elkaix Jul 18, 2026
f8e89da
docs(tasks): mark P3b GitHub Copilot delivered (pending live verify)
elkaix Jul 18, 2026
b586080
feat(auth): add xAI Grok OAuth login provider
elkaix Jul 18, 2026
8d59a83
docs(tasks): mark P3c xAI Grok delivered (pending live verify)
elkaix Jul 18, 2026
4e4d098
docs(tasks): P3d DigitalOcean full-build scope + verified constants
elkaix Jul 18, 2026
d046a83
chore(tasks): split P3d DigitalOcean into implicit-helper + provider …
elkaix Jul 18, 2026
48a1fdb
feat(auth): add run_loopback_implicit_flow OAuth helper
elkaix Jul 19, 2026
962ef99
feat(auth): add DigitalOcean Gradient AI browser OAuth login
elkaix Jul 19, 2026
13cf4a8
chore(tasks): mark P3d DigitalOcean delivered
elkaix Jul 19, 2026
57dc690
chore(tasks): record P3e Snowflake full account-scoped build scope
elkaix Jul 19, 2026
1847ab9
feat(auth): add Snowflake Cortex browser OAuth login
elkaix Jul 19, 2026
d2e2691
chore(tasks): mark P3e Snowflake delivered; P3 provider set complete
elkaix Jul 19, 2026
8451385
docs(changelog): add DigitalOcean and Snowflake Cortex login entries
elkaix Jul 19, 2026
c62d5e4
fix(auth): harden OAuth flow helpers and CLI login validation
elkaix Jul 19, 2026
775fede
feat(auth): make models.dev catalog a typed, portable, provider-neutr…
elkaix Jul 19, 2026
0d2e351
feat(auth): drive provider models from the catalog with atomic, harde…
elkaix Jul 19, 2026
179af96
fix(auth): drop accidental prompt-subsystem sweep-in from the auth br…
elkaix Jul 19, 2026
5a93747
fix(auth): preserve prior credential on re-login failure and log pers…
elkaix Jul 19, 2026
e731198
Merge branch 'main' into feat/auth-login-providers
elkaix Jul 19, 2026
8c3ba0b
docs(auth): design PR 215 review fixes
elkaix Jul 19, 2026
ab82f91
fix(auth): namespace nested OAuth credential files
elkaix Jul 19, 2026
f75a880
fix(config): replace saved config atomically
elkaix Jul 19, 2026
c9445a1
fix(auth): serialize OAuth persistence transactions
elkaix Jul 19, 2026
a4bcb81
fix(auth): validate implicit OAuth state before errors
elkaix Jul 19, 2026
eae9c86
fix(auth): surface catalog and router degradation
elkaix Jul 19, 2026
f2a49f3
test(config): cover atomic symlink saves
elkaix Jul 19, 2026
d810a43
fix(auth): use UTF-8 for credential key encoding
elkaix Jul 19, 2026
a5f0dbe
fix(auth): hide persistence diagnostics from events
elkaix Jul 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- Add xAI Grok OAuth login (browser loopback and device-code).
- Add GitHub Copilot device-code OAuth login for individual github.com accounts.
- Add DigitalOcean Gradient AI browser OAuth login with dynamically discovered Inference Routers; router-discovery failures (unauthorized, outage, malformed, empty) are now reported distinctly instead of silently yielding no models.
- Add Snowflake Cortex account-scoped browser OAuth login (`pythinker login --snowflake`); the account identifier is validated before any request, and models register without becoming the default until Cortex chat support ships.
- Discover each provider's models from a cached, typed, provider-neutral models.dev catalog with curated fallbacks, so xAI, GitHub Copilot, and Snowflake pick up new models automatically; the catalog is now portable across platforms (no longer Unix-only) and distinguishes fresh, cached, stale, disabled, and unavailable results.
- Persist provider login and logout atomically so a failed save never leaves orphaned credentials or a half-applied configuration; a re-login whose save fails now restores the previous credential instead of deleting it, and provider persistence failures are logged.
- Fix queued follow-up input showing a bordered ghost; pressing Enter during an active turn now shows one intentional queued row.

## 0.60.0 (2026-07-18)
Expand Down
6 changes: 6 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- Add xAI Grok OAuth login (browser loopback and device-code).
- Add GitHub Copilot device-code OAuth login for individual github.com accounts.
- Add DigitalOcean Gradient AI browser OAuth login with dynamically discovered Inference Routers; router-discovery failures (unauthorized, outage, malformed, empty) are now reported distinctly instead of silently yielding no models.
- Add Snowflake Cortex account-scoped browser OAuth login (`pythinker login --snowflake`); the account identifier is validated before any request, and models register without becoming the default until Cortex chat support ships.
- Discover each provider's models from a cached, typed, provider-neutral models.dev catalog with curated fallbacks, so xAI, GitHub Copilot, and Snowflake pick up new models automatically; the catalog is now portable across platforms (no longer Unix-only) and distinguishes fresh, cached, stale, disabled, and unavailable results.
- Persist provider login and logout atomically so a failed save never leaves orphaned credentials or a half-applied configuration; a re-login whose save fails now restores the previous credential instead of deleting it, and provider persistence failures are logged.
- Fix queued follow-up input showing a bordered ghost; pressing Enter during an active turn now shows one intentional queued row.

## 0.60.0 (2026-07-18)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# PR #215 auth review fixes design

## Goal

Resolve every validated review defect on PR #215 without broad auth or configuration refactoring. Preserve existing provider behavior except where it is unsafe, misleading, or violates the repository's failure-truthfulness and async-runtime contracts.

## Scope

1. Prevent nested OAuth references such as `oauth/snowflake-cortex/<account>` from sharing credential or lock files with flat references such as `oauth/xai`.
2. Make the new login/logout persistence unit:
- serialized across processes at the shared config-file boundary;
- executed outside the asyncio event loop;
- safe against partial config-file writes;
- explicit when rollback or credential deletion fails;
- unable to report logout success while credentials remain undeleted.
3. Validate the implicit-flow `state` before accepting either success or error payloads.
4. Keep DigitalOcean's intentional degraded credential persistence, but never claim that an unrelated or empty default model is a configured DigitalOcean model.
5. Log curated-model fallback both when catalog status is non-authoritative and when an authoritative catalog yields no usable provider models.
6. Preserve the existing OpenCode Go unavailable-catalog regression test and resolve its stale review thread with evidence.

Out of scope: provider protocol redesigns, replacing DigitalOcean's provider-required implicit grant, Snowflake Cortex chat transforms, global config merge semantics unrelated to these auth transactions, and new dependencies.

## Design

### Credential filenames

Keep the legacy filename for ordinary one-segment OAuth keys so existing credentials remain readable. Encode the complete relative key for multi-segment keys into a deterministic, filesystem-safe filename, and apply the same mapping to lock files. This makes the mapping injective without changing released flat-key paths.

### Persistence transaction

Expose async persistence helpers to provider login/logout callers. Each helper offloads one complete synchronous transaction to a worker thread. The transaction acquires a bounded, fail-closed inter-process lock derived from the default config path before it snapshots credentials/config, mutates state, writes, or rolls back.

Config writes use a temporary file in the target directory, flush and fsync it, apply private permissions, and atomically replace the destination. A failed write therefore leaves the previous config file intact.

Login order remains token write then config write. On failure, restore the previous token (or remove the new one), restore the in-memory snapshot, and re-raise. If rollback also fails, raise an explicit persistence error carrying both failure contexts and log the rollback failure without secrets.

Logout writes the config removal first, then deletes credentials. If deletion fails, restore and persist the previous config before raising. It must never emit success after failed credential deletion.

The config-scoped lock covers snapshot through rollback, so a failed concurrent login cannot restore a token observed before another successful transaction.

### OAuth callback validation

For implicit callbacks, parse `state` and compare it with the expected value before interpreting `error`. RFC 6749 requires the original state on both successful and error responses. A wrong-state error is rejected as `OAuthStateMismatch`, not accepted as a user denial.

### Provider messaging and fallback visibility

DigitalOcean continues to persist a valid OAuth token when router discovery is empty or unavailable, matching the approved PR scope. Its terminal success text distinguishes “router configured” from “credentials saved with no routers configured.”

Copilot, xAI, and Snowflake log when they use curated models because the catalog is non-authoritative or because authoritative conversion is empty. Logs include status/source only, never credentials.

## Tests

Use red-green TDD for each behavior:

- nested Snowflake/xAI credential and lock paths differ;
- concurrent failed login cannot overwrite a successful token;
- failed config save preserves prior file bytes;
- rollback/delete failures are surfaced and logout does not report success;
- async callers do not run persistence I/O on the event-loop thread;
- wrong-state implicit error yields `OAuthStateMismatch`;
- DigitalOcean degraded success does not name an unrelated model;
- empty authoritative catalogs emit fallback logs;
- unavailable OpenCode Go catalog coverage remains green.

After focused tests, run `make check-pythinker-code`, `make test-pythinker-code`, and `git diff --check`.

## GitHub review completion

Push the verified commit to `feat/auth-login-providers`. Reply inside each CodeRabbit thread with the specific fix and test evidence. For the already-satisfied OpenCode Go coverage thread, cite the existing unavailable-catalog test. Resolve threads only after the pushed head and checks reflect the fixes.
8 changes: 8 additions & 0 deletions src/pythinker_code/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@
MOONSHOT_PLATFORM_ID = "moonshot"
KIMI_PLATFORM_ID = "kimi"
DEEPSEEK_PLATFORM_ID = "deepseek"
DIGITALOCEAN_PLATFORM_ID = "digitalocean"
GITHUB_COPILOT_PLATFORM_ID = "copilot"
ANTHROPIC_PLATFORM_ID = "anthropic"
OPENROUTER_PLATFORM_ID = "openrouter"
SNOWFLAKE_CORTEX_PLATFORM_ID = "snowflake-cortex"
LM_STUDIO_PLATFORM_ID = "lm-studio"
OLLAMA_PLATFORM_ID = "ollama"
XAI_PLATFORM_ID = "xai"
ZAI_CODING_PLATFORM_ID = "z-ai-coding"
ZAI_API_PLATFORM_ID = "z-ai-api"

__all__ = [
"ALIBABA_PLATFORM_ID",
"ANTHROPIC_PLATFORM_ID",
"DEEPSEEK_PLATFORM_ID",
"DIGITALOCEAN_PLATFORM_ID",
"GITHUB_COPILOT_PLATFORM_ID",
"KIMI_PLATFORM_ID",
"LM_STUDIO_PLATFORM_ID",
"MINIMAX_PLATFORM_ID",
Expand All @@ -30,6 +36,8 @@
"OPENCODE_GO_PLATFORM_ID",
"OPENROUTER_PLATFORM_ID",
"PYTHINKER_CODE_PLATFORM_ID",
"SNOWFLAKE_CORTEX_PLATFORM_ID",
"XAI_PLATFORM_ID",
"ZAI_API_PLATFORM_ID",
"ZAI_CODING_PLATFORM_ID",
]
Loading
Loading