Commit 887401e
feat(auth): add xAI, GitHub Copilot, DigitalOcean, and Snowflake Cortex OAuth login providers (#215)
* docs(tasks): plan opencode auth-provider + dynamic-catalog + effort adoption
* candidate f1ff6f28-db9c-4236-8656-7ee20256fcee
* fix(auth): update models.dev catalog consumer test and genericize module header
The refactor moved the best-effort fetch/timeout into the shared models.dev
catalog module, so the provider test now asserts graceful degradation via the
shared loader instead of the removed inline timeout constant.
* docs(tasks): mark dynamic catalog phase done; generic framing
* candidate 55be2461-0989-4a1e-8602-102703271165
* fix(auth): make oauth_flows strict-clean and UTF-8 explicit
- Use utf-8 (not ascii) for the base64url decode to satisfy the explicit-encoding
static requirement (base64url output is ASCII, so equivalent).
- Cast socket getsockname() before len/index so strict pyright has a known arg type.
* docs(tasks): mark shared OAuth flow helper done; P3 sub-phases
* docs(tasks): finalize P3b GitHub Copilot design (primary-source verified)
* feat(auth): add GitHub Copilot device-code OAuth login provider
Add a 'copilot' managed provider for individual github.com accounts. Login
runs the GitHub device-code flow, exchanges the OAuth token for a short-lived
Copilot bearer via copilot_internal/v2/token, and stores the GitHub token as
the refresh credential so OAuthManager can re-exchange on expiry. Chat routes
through openai_legacy to api.githubcopilot.com with the Copilot integration
headers. Wired into shell /login /logout and CLI login/logout --copilot.
Business/Enterprise routing is out of scope (individual host only).
* docs(tasks): mark P3b GitHub Copilot delivered (pending live verify)
* feat(auth): add xAI Grok OAuth login provider
Add an 'xai' managed provider with two OAuth methods: browser loopback-PKCE
(pinned redirect 127.0.0.1:56121, plan=generic + OIDC nonce) and RFC 8628
device-code. Tokens exchange/refresh against auth.x.ai with rotating refresh
tokens persisted by OAuthManager; chat routes through openai_legacy to
api.x.ai/v1. Wired into shell /login /logout and CLI login --xai/--xai-device,
logout --xai.
* docs(tasks): mark P3c xAI Grok delivered (pending live verify)
* docs(tasks): P3d DigitalOcean full-build scope + verified constants
* chore(tasks): split P3d DigitalOcean into implicit-helper + provider lanes
* feat(auth): add run_loopback_implicit_flow OAuth helper
Add a reusable OAuth 2.0 implicit-flow loopback helper for providers whose
token arrives in the URL fragment (response_type=token). Serves an HTML
bootstrap page on GET <callback_path> whose inline JS posts the parsed
fragment to a pinned-port POST <token_path>; validates state, requires a
non-empty access_token, and coerces expires_in with a 30-day fallback.
Binds on a caller-pinned host (default localhost) and port so the redirect
URI exact-matches an upstream registration. Existing device-code and
authorization-code/PKCE helpers are unchanged.
* feat(auth): add DigitalOcean Gradient AI browser OAuth login
Add DigitalOcean as a login provider using the OAuth implicit flow. The
returned access token is stored as a bare API-key provider on
inference.do-ai.run/v1 (no refresh; re-login on ~30-day expiry), and the
model catalog is seeded dynamically from the Gradient Inference Routers
API as 'router:<name>' aliases. Login still succeeds when the router
catalog is unavailable (info event, zero models). Wires platform
registration, the managed-models refresh skip-guard, shell /login /logout
selectors, and CLI --digitalocean flags, mirroring the xAI provider.
* chore(tasks): mark P3d DigitalOcean delivered
* chore(tasks): record P3e Snowflake full account-scoped build scope
* feat(auth): add Snowflake Cortex browser OAuth login
Add Snowflake Cortex as an account-scoped browser-OAuth login provider.
Login prompts for a Snowflake account identifier (and optional role),
runs loopback-PKCE against the account's https://<account>.snowflakecomputing.com
OAuth endpoints (HTTP Basic client creds, role-scoped), and stores an
openai_legacy provider on the account's Cortex OpenAI-compatible base_url
with a curated model catalog. The account is encoded into the OAuth ref
key (oauth/snowflake-cortex/<account>) and parsed back in the refresh
dispatch to build the account-scoped token URL. Wires platform
registration, the refresh skip-guard, shell /login /logout, and CLI
--snowflake --account --role.
* chore(tasks): mark P3e Snowflake delivered; P3 provider set complete
* docs(changelog): add DigitalOcean and Snowflake Cortex login entries
* fix(auth): harden OAuth flow helpers and CLI login validation
Address review findings on the shared OAuth helpers and login command:
- poll_device_token: reject a 2xx success payload that carries no usable
access token instead of returning it as success.
- Implicit loopback callback: catch truncated request bodies
(IncompleteReadError) and fail closed with HTTP 400 rather than hanging
until timeout; scrub the bearer token from the browser URL fragment via
history.replaceState before it is posted.
- Implicit expiry: represent missing/malformed/nonpositive expires_in as
unknown (None) rather than fabricating a trusted 30-day lifetime.
- run_loopback_implicit_flow: require a genuine loopback redirect_host and a
nonzero port, failing closed on non-loopback binds.
- login: reject --account/--role unless --snowflake is selected.
Also make three implicit-callback assertions effectful to satisfy the
static analyzer.
* feat(auth): make models.dev catalog a typed, portable, provider-neutral API
Rework the shared models.dev catalog so any provider can consume it and so
degraded data is never mistaken for authoritative:
- get_models_dev_catalog now returns a typed CatalogResult (catalog + status +
source), distinguishing fresh, cached, stale, disabled, and unavailable
outcomes instead of collapsing every state into an empty dict.
- Replace the unconditional Unix-only fcntl import and blocking, uninterruptible
lock acquisition with a cross-platform (fcntl/msvcrt) non-blocking lock that
is bounded by a deadline, cancellation-safe, and never orphans a descriptor.
- Add a provider-neutral chat-model filter (text-output modality, excluding
embedding/reranker/moderation ids) and build_catalog_models() so each provider
can resolve its own catalog entries without opencode-specific coupling.
opencode_go consumes the new CatalogResult shape.
* feat(auth): drive provider models from the catalog with atomic, hardened logins
Rewire xAI, GitHub Copilot, Snowflake Cortex, and DigitalOcean onto the shared,
provider-neutral models.dev catalog and make their login/logout robust:
- Each provider now discovers its own models generically (xAI/Copilot/Snowflake
from their models.dev provider id, filtered to chat models; DigitalOcean from
its native Inference Routers API), falling back to a curated list when the
catalog is unavailable or degraded — no opencode coupling.
- Persist login/logout atomically via shared helpers: a login writes tokens,
applies config, then saves; on failure it rolls back the token and restores
the in-memory config. Logout persists the config removal before deleting
credentials so an interruption leaves recoverable creds, not orphaned config.
- xAI/Snowflake: map 400 invalid_grant to unauthorized so rejected refresh
tokens are suppressed; require a refresh token before reporting login success.
- Snowflake: validate the account locator (reject authority/path/port/userinfo/
query/fragment payloads) before building any URL; register models without
making Snowflake the default, since its Cortex chat adapter is not yet wired.
- DigitalOcean: report router-discovery outcomes (unauthorized, outage,
malformed, empty) distinctly instead of collapsing them into an empty catalog.
Expand tests to cover malformed/transport-failure responses, hostile account
input, missing refresh tokens, invalid_grant, catalog-driven registration, and
the distinct router-discovery outcomes.
Also revert an unrelated prompt.py change that had been picked up inadvertently.
* fix(auth): drop accidental prompt-subsystem sweep-in from the auth branch
An in-progress prompt_toolkit screen-mode refactor (prompt.py, config.py
sticky_input docs, and the prompt/erase-when-done tests) was captured from
the working tree by an earlier auth commit, leaving the tests ahead of the
reverted source and failing four prompt tests. Restore the entire prompt
subsystem and the config docstring to origin/main so the auth PR carries
only auth changes.
* fix(auth): preserve prior credential on re-login failure and log persistence errors
- persist_login now snapshots the existing token via load_tokens and restores
it (rather than blind-deleting) when the config save fails, so an unrelated
save error during re-login no longer destroys a still-valid credential; a
fresh login with no prior token still deletes on failure. Adds direct
regression tests for the atomicity helper (fails before, passes after).
- Log provider login/logout persistence failures across all four providers so
unexpected errors leave a diagnosable trail alongside the safe user message.
- Log the curated-model fallback in xai/copilot discovery when the models.dev
catalog is not authoritative, matching DigitalOcean's degraded-discovery signal.
- Add a success-path test proving _fetch_models_dev_metadata populates metadata
from an authoritative catalog, and a default-context fallback test for
build_catalog_models.
- Drop the changelog entry for the reverted prompt-fullscreen feature and note
the credential-preservation fix.
* docs(auth): design PR 215 review fixes
* fix(auth): namespace nested OAuth credential files
* fix(config): replace saved config atomically
* fix(auth): serialize OAuth persistence transactions
* fix(auth): validate implicit OAuth state before errors
* fix(auth): surface catalog and router degradation
* test(config): cover atomic symlink saves
* fix(auth): use UTF-8 for credential key encoding
* fix(auth): hide persistence diagnostics from events
---------
Co-authored-by: claude-architect <runtime@claude-architect.invalid>1 parent 7e41080 commit 887401e
32 files changed
Lines changed: 6374 additions & 105 deletions
File tree
- docs
- en/release-notes
- superpowers/specs
- src/pythinker_code
- auth
- cli
- tools/lsp
- ui/shell
- utils
- tasks
- tests
- auth
- cli
- core
- ui_and_conv
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
18 | 24 | | |
19 | 25 | | |
20 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
20 | 26 | | |
21 | 27 | | |
22 | 28 | | |
| |||
Lines changed: 69 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
| 16 | + | |
14 | 17 | | |
15 | 18 | | |
| 19 | + | |
16 | 20 | | |
17 | 21 | | |
18 | 22 | | |
19 | 23 | | |
20 | 24 | | |
21 | 25 | | |
22 | 26 | | |
| 27 | + | |
| 28 | + | |
23 | 29 | | |
24 | 30 | | |
25 | 31 | | |
| |||
30 | 36 | | |
31 | 37 | | |
32 | 38 | | |
| 39 | + | |
| 40 | + | |
33 | 41 | | |
34 | 42 | | |
35 | 43 | | |
0 commit comments