feat(providers): anthropic subscription oauth#2285
Conversation
|
All contributors have signed the DCO ✍️ ✅ |
|
I have read the DCO document and I hereby sign the DCO. |
|
recheck |
|
Personally super excited that you're tackling this! It was on my list of annoyances to fix once I had a bit more understanding of the provider system. Note that As noted in #620 you can create a provider based on it which is recognized by openshell provider create --name anthropic --type claude --credential CLAUDE_CODE_OAUTH_TOKEN
openshell sandbox create --name claude-is-sad --provider anthropic -- claude
# ... and then ignore the Claude setup screen which will want you to pick a login method
# instead, in another shell:
openshell sandbox exec --name claude-is-sad -- bash -lc 'echo $(claude auth status)'
# returns: { "loggedIn": true, "authMethod": "oauth_token", "apiProvider": "firstParty" }Looking forward to this! |
Thanks @purp! Will play around with this secret. Ideally if it works it could shrink this PR by removing some redundant code for configuring the provider. |
|
I've investigated #620 and my findings were consistent trying to use |
BlockedGator is blocked because PR #2285 currently has merge conflicts with Head SHA: Next action: @cjfit, please update the branch from |
Is this necessary? The future of |
Add an `anthropic-oauth` provider type (alias `claude-plan`) so sandboxes can run Claude Code on an Anthropic Pro/Max subscription. The OAuth token is harvested, stored, and refreshed entirely outside the sandbox and injected only at the egress boundary; it is never written into the sandbox environment, filesystem, or logs. - inference: new anthropic-oauth profile (Authorization: Bearer plus a mandatory anthropic-beta: oauth-2025-04-20 default header) - router: merge the mandatory anthropic-beta flag with any client-sent value so the sandbox cannot drop it - server: mark ANTHROPIC_OAUTH_TOKEN non-injectable (proxy-only) - cli: add `provider create --from-claude-login` to read the local Claude Code login (macOS Keychain or ~/.claude/.credentials.json) and wire the gateway's OAuth2 background refresh - docs: add the Anthropic Subscription provider page and update gateway architecture and provider tables The macOS path (Keychain) is validated end-to-end. Linux reads the same ~/.claude/.credentials.json file and should work unchanged, but has not yet been validated on Linux. Signed-off-by: Cedric Fitzgerald <soulcedric2@gmail.com>
Project ANTHROPIC_BASE_URL and a placeholder ANTHROPIC_AUTH_TOKEN into sandboxes bound to an anthropic-oauth provider so agent CLIs reach the gateway inference endpoint without manual configuration, login flows, or API-key confirmation prompts. The auth token is a non-secret constant; inference.local replaces caller auth with the real subscription token at the egress boundary. Resolve ANTHROPIC_BASE_URL to its real value in the sandbox env: the credential pipeline placeholderizes all provider env values, and a placeholderized URL cannot be parsed by SDKs at process startup. Auth values stay egress-time placeholders. Renames child_env_with_gcp_resolved to child_env_with_static_config_resolved since it now covers non-GCP static config. Signed-off-by: Cedric Fitzgerald <soulcedric2@gmail.com>
Make --name and --type optional for provider create with --from-claude-login, defaulting to claude-subscription/anthropic-oauth. After creating the provider, configure the user inference route automatically when none is set (best-effort, unverified), removing the separate inference set step. When sandbox create hits a missing anthropic-oauth provider, stop offering env-credential discovery (subscription login material lives in the host keychain and cannot be configured from inside a sandbox) and point at provider create --from-claude-login instead. Signed-off-by: Cedric Fitzgerald <soulcedric2@gmail.com>
When the anthropic-oauth token endpoint rejects the refresh grant with HTTP 4xx (revoked login, lapsed plan), append recovery guidance to the stored refresh error so provider refresh status tells the operator to re-authenticate on the host and recreate the provider. Transient network errors and 5xx responses are excluded. Signed-off-by: Cedric Fitzgerald <soulcedric2@gmail.com>
…d recovery Describe the ANTHROPIC_AUTH_TOKEN env preset and why an API key is not injected, note that the API-usage billing banner in Claude Code is cosmetic while billing goes to the subscription, and add recovery steps for a rejected refresh token. Update the gateway architecture doc to match the env projection behavior. Signed-off-by: Cedric Fitzgerald <soulcedric2@gmail.com>
195c591 to
c2d3218
Compare
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Validation: This providers-v2 contribution implements the maintainer-welcomed capability in #1925 while preserving the intended gateway-side credential boundary.
Head SHA: c2d3218e2bbd220eb4a55195e8bec60305e1e814
Review findings: Seven actionable findings are attached inline. The main blockers are cluster-wide subscription authorization and ownership of Claude Code's rotating refresh token. Integration coverage is also needed for the import/refresh/rollback path and for route authorization.
Docs: The new provider guide and gateway architecture update cover the user-facing flow, and docs/index.yml already includes the provider folder. Two accuracy fixes are requested inline.
Thanks @johntmyers. I checked your question about avoiding inference.local: the current implementation needs that route because the real token is excluded from sandbox environment injection and added by the egress router. Mapping directly to Anthropic would require a different endpoint-bound authentication mechanism. Because this PR bakes inference.local into provider injection, automatic cluster routing, architecture, and published UX, its future is a maintainer design decision that should be resolved before merge.
Next state: gator:in-review
| } | ||
|
|
||
| match client | ||
| .set_cluster_inference(SetClusterInferenceRequest { |
There was a problem hiding this comment.
gator-agent
High: Creating this provider automatically installs its subscription credential as the cluster-wide inference route. GetInferenceBundle returns that route to every authenticated sandbox supervisor without checking attached providers, so a sandbox without claude-subscription can consume the user's plan. Either filter bundles by sandbox provider bindings, or remove automatic route creation and require an explicit, clearly documented gateway-wide grant.
| "client_id".to_string(), | ||
| ANTHROPIC_OAUTH_CLIENT_ID.to_string(), | ||
| ); | ||
| material.insert("refresh_token".to_string(), creds.refresh_token); |
There was a problem hiding this comment.
gator-agent
High: This copies Claude Code's rotating refresh token into a second independent consumer. The background worker will eventually rotate it, leaving Claude Code's stored copy stale; if Claude Code rotates first, OpenShell becomes stale instead. Acquire a distinct OAuth grant for OpenShell, or make this an explicit ownership-transfer flow rather than duplicating one rotating token.
| "!".yellow(), | ||
| ); | ||
| eprintln!(); | ||
| return Ok(()); |
There was a problem hiding this comment.
gator-agent
Warning: A missing anthropic-oauth provider prints “create the provider first, then retry” but returns success. The caller then continues sandbox creation without adding the requested provider, which can launch Claude into an in-sandbox authentication path. Return an error here and add coverage proving sandbox creation stops.
| }; | ||
| state.status = "error".to_string(); | ||
| state.last_error = err.message().to_string(); | ||
| state.next_refresh_at_ms = |
There was a problem hiding this comment.
gator-agent
Warning: Anthropic 4xx grant rejections are classified as requiring re-login, but this still schedules the terminal credential for retry every 60 seconds. A revoked or stale token will hammer the endpoint indefinitely. Persist a reauth_required or disabled state that the worker skips while retaining an explicit manual retry path.
| None | ||
| }; | ||
|
|
||
| if from_claude_login && provider_type != ANTHROPIC_OAUTH_PROVIDER_TYPE { |
There was a problem hiding this comment.
gator-agent
Low: The advertised claude-subscription provider-type alias is rejected by --from-claude-login because this compares the raw type to the canonical spelling. Validate through normalize_inference_provider_type and consistently pass or store the canonical type.
| </Note> | ||
|
|
||
| <Warning> | ||
| The access token is marked non-injectable: it is stored only as gateway refresh state and is never exported into sandboxes. Sandboxes reach the model through `inference.local`, where the gateway adds the `Authorization: Bearer` header and the required `anthropic-beta: oauth-2025-04-20` flag at the egress boundary. |
There was a problem hiding this comment.
gator-agent
Low: The access token is not stored “only as gateway refresh state”; it is stored in the provider credential record, while the refresh token is stored in refresh state. Please describe both gateway-side storage locations accurately.
|
|
||
| The subscription OAuth token represents your whole plan, so it is shared across every sandbox bound to the provider, and revoking it is provider-wide. Anthropic rotates the refresh token on each refresh; the gateway persists the rotated token so refresh keeps working. The long-lived refresh material lives only in gateway state, never in a sandbox. | ||
|
|
||
| To rotate or revoke, delete and recreate the provider, or rotate the credential through `openshell provider refresh`. |
There was a problem hiding this comment.
gator-agent
Low: Deleting and recreating the OpenShell provider removes OpenShell's copy but does not revoke the OAuth session at Anthropic. Separate local deletion from upstream revocation and point users to Anthropic's supported session-revocation mechanism.
Summary
Adds an anthropic-oauth provider type (alias: claude-subscription) that lets sandboxed agents run Claude Code against an Anthropic Pro/Max subscription instead of an API key. The OAuth access token is held gateway-side and injected only at the egress boundary (inference.local) and it never enters the sandbox environment, filesystem, or logs.
Related Issue
Closes #1925
Human Notes
These screenshots show how the new provider works in a new sandbox.
oslis an alias for my locally-compiled OpenShell executable. Providers v2 must be enabled first:osl settings set --global --key providers_v2_enabled --value true --yes.Changes
Testing
Local test checklist
Prereqs: Docker running, a Claude Pro/Max subscription with
claude logincompleted on the host.cargo build -p openshell-cliandmise run gateway:docker(rebuilds the gateway + supervisor image — required, the env injection changed)mise run pre-commitmise run test(note:ssh::tests::wait_for_forward_listener_rejects_missing_listenerand sbomtest_same_domain_requests_are_spacedare pre-existing flakes under parallel load; both pass isolated)mise run e2eopenshell provider create --from-claude-login— succeeds without--name/--type, prints the createdclaude-subscriptionprovider and default inference routeopenshell sandbox create --provider claude-subscriptionthen run bareclaudeinside — no login prompt, no "Do you want to use this API key?" prompt, responds to a messageecho $ANTHROPIC_AUTH_TOKENshows anopenshell:resolve:env:...placeholder (never a real token);echo $ANTHROPIC_BASE_URLshowshttps://inference.local;ANTHROPIC_API_KEYis unsetopenshell provider refresh statusshows the token rotatingopenshell sandbox create --provider claude-subscriptionbefore creating the provider — error poim-claude-login`One caveat for macOS reviewers: a pre-existing bash 3.2 empty-array bug in e2e/mcp-conformance.sh (unrelated to this PR)
Checklist