From f6877a300725a436856ad09d5c6633e7695c6c41 Mon Sep 17 00:00:00 2001 From: Sabir Foxx Date: Tue, 5 May 2026 21:48:02 -0400 Subject: [PATCH] Add Claude subscription OAuth provider Add an optional third LLM provider for the admin and onscreen agents that authenticates with Anthropic via the user's Claude subscription instead of an API key. The browser owns the connect flow and a reusable settings-dialog block; the server owns the OAuth state cache, the encrypted token store under L2//meta/anthropic_oauth.json, silent refresh, and a dedicated streaming completions endpoint that translates between OpenAI chat-completions shape and Anthropic Messages API shape so the existing browser fetch readers stay unchanged. - New `_core/anthropic_oauth/` browser module with a connect block, request hook, and per-surface extensions for the admin and onscreen prepareXxxApiRequest seams. Subscription mode redirects API-mode requests to /api/anthropic_subscription_completions and strips the Authorization header so the bearer token never reaches the browser. - Five new server endpoints: oauth_anthropic_authorize, _callback (GET for redirect mode + POST for paste mode), _status, _disconnect, and anthropic_subscription_completions. Tokens are sealed with AES-256-GCM using a sub-key derived from the existing password seal key; refresh tokens are auto-rotated 60s before access-token expiry and on upstream 401. - Provider enums extended with `subscription` for both admin and onscreen agents; existing api/local paths and validation are untouched. The settings dialog gains a third segmented tab with a curated Claude model dropdown (Opus 4.7, Sonnet 4.6, Haiku 4.5), hides the params field for subscription mode (Anthropic's subscription tier rejects user-supplied temperature on newer models), and embeds the connect block. - Default flow mode is the manual code-paste flow because the public Claude Code OAuth client does not allowlist arbitrary localhost callbacks. Redirect mode is opt-in via ANTHROPIC_OAUTH_FLOW_MODE for deployments that ship their own registered Anthropic OAuth client. - Six new runtime params (ANTHROPIC_OAUTH_ALLOWED, _CLIENT_ID, _AUTHORIZE_URL, _TOKEN_URL, _REDIRECT_URI, _FLOW_MODE, ANTHROPIC_API_BASE_URL) with safe public defaults so the feature works out of the box; existing user secret model is reused. - meta/anthropic_oauth.json added to the L2 git-history ignore list so refresh-token rotation does not churn local-history commits. - Shared visual primitive .field now styles `select` consistently with `input` and `textarea` so future dialog selects match without per-feature CSS. - Documentation updated across AGENTS.md (root, app, server/api, server/lib/auth, admin/views/agent, onscreen_agent, visual) plus a new anthropic_oauth/AGENTS.md for the module contract. Ten focused unit tests cover endpoint defaults, runtime-param overrides, PKCE authorize URL composition, both flow modes, and the sealed-record status round trip. --- AGENTS.md | 3 +- app/AGENTS.md | 2 + .../mod/_core/admin/views/agent/AGENTS.md | 6 +- .../_all/mod/_core/admin/views/agent/api.js | 15 +- .../mod/_core/admin/views/agent/config.js | 13 +- .../mod/_core/admin/views/agent/panel.html | 31 +- .../_all/mod/_core/admin/views/agent/store.js | 54 ++ .../_all/mod/_core/anthropic_oauth/AGENTS.md | 45 ++ .../_core/anthropic_oauth/anthropic-oauth.css | 138 ++++ .../_all/mod/_core/anthropic_oauth/client.js | 71 ++ .../_core/anthropic_oauth/connect-block.html | 115 +++ .../_core/anthropic_oauth/connect-block.js | 332 +++++++++ .../end/anthropic-subscription.js | 19 + .../end/anthropic-subscription.js | 19 + .../_all/mod/_core/anthropic_oauth/request.js | 104 +++ .../_all/mod/_core/onscreen_agent/AGENTS.md | 3 +- app/L0/_all/mod/_core/onscreen_agent/api.js | 15 +- .../_all/mod/_core/onscreen_agent/config.js | 13 +- .../_all/mod/_core/onscreen_agent/panel.html | 31 +- app/L0/_all/mod/_core/onscreen_agent/store.js | 54 ++ app/L0/_all/mod/_core/visual/AGENTS.md | 1 + app/L0/_all/mod/_core/visual/forms/dialog.css | 32 +- commands/params.yaml | 42 ++ server/api/AGENTS.md | 20 + .../api/anthropic_subscription_completions.js | 473 +++++++++++++ server/api/oauth_anthropic_authorize.js | 91 +++ server/api/oauth_anthropic_callback.js | 234 ++++++ server/api/oauth_anthropic_disconnect.js | 46 ++ server/api/oauth_anthropic_status.js | 60 ++ server/lib/auth/AGENTS.md | 4 +- server/lib/auth/anthropic_oauth.js | 667 ++++++++++++++++++ server/lib/auth/service.js | 41 +- server/lib/customware/git_history.js | 3 +- server/runtime/state_areas.js | 2 + tests/anthropic_oauth_test.mjs | 211 ++++++ 35 files changed, 2978 insertions(+), 32 deletions(-) create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/AGENTS.md create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/anthropic-oauth.css create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/client.js create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/connect-block.html create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/connect-block.js create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/ext/js/_core/admin/views/agent/api.js/prepareAdminAgentApiRequest/end/anthropic-subscription.js create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/ext/js/_core/onscreen_agent/api.js/prepareOnscreenAgentApiRequest/end/anthropic-subscription.js create mode 100644 app/L0/_all/mod/_core/anthropic_oauth/request.js create mode 100644 server/api/anthropic_subscription_completions.js create mode 100644 server/api/oauth_anthropic_authorize.js create mode 100644 server/api/oauth_anthropic_callback.js create mode 100644 server/api/oauth_anthropic_disconnect.js create mode 100644 server/api/oauth_anthropic_status.js create mode 100644 server/lib/auth/anthropic_oauth.js create mode 100644 tests/anthropic_oauth_test.mjs diff --git a/AGENTS.md b/AGENTS.md index b7870f82..2b1fd3fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,7 @@ App docs: - `/app/L0/_all/mod/_core/agent/AGENTS.md` - `/app/L0/_all/mod/_core/agent-chat/AGENTS.md` - `/app/L0/_all/mod/_core/agent_prompt/AGENTS.md` +- `/app/L0/_all/mod/_core/anthropic_oauth/AGENTS.md` - `/app/L0/_all/mod/_core/dashboard/AGENTS.md` - `/app/L0/_all/mod/_core/dashboard_welcome/AGENTS.md` - `/app/L0/_all/mod/_core/documentation/AGENTS.md` @@ -197,7 +198,7 @@ Project concepts: - when `CUSTOMWARE_GIT_HISTORY` is enabled, writable `L1//` and `L2//` roots are treated as optional per-owner local Git repositories with adaptive-debounced server-side commits and rollback APIs - `USER_FOLDER_SIZE_LIMIT_BYTES` optionally caps each `L2//` folder on disk; app-file mutations are checked against a cached per-user size total and only size-reducing mutations are allowed once a user folder is already over the limit - runtime file discovery is backed by sharded `file_index` state; startup indexes `L0`, `L1`, and layer roots, auth first touch loads only the target user's auth files, and full `L2/` shards are loaded only on demand for file/module access or active mutations rather than preloaded just because their folders exist on disk -- runtime parameters are defined in `commands/params.yaml`; `node space serve` resolves them in this order: launch arguments, stored `.env` params written by `node space set`, then process environment variables, then schema defaults; `node space supervise` accepts the same runtime parameters, owns the public `HOST` and `PORT`, requires `CUSTOMWARE_PATH`, enables source auto-update by default, and passes the remaining resolved params to private `space serve` children; `WORKERS` controls clustered HTTP worker count for `serve` and `supervise`; `CUSTOMWARE_PATH` is the parent directory for writable backend `L1/` and `L2/` storage when configured and also hosts backend-owned cloud-share archives under `share/spaces/` when that feature is enabled; `CUSTOMWARE_WATCHDOG` defaults to `true` and controls live backend customware watching, config watching, and the periodic reconcile backstop without disabling L0/L1 startup indexing, on-demand L2 loading, or explicit clustered mutation sync; `GIT_BACKEND` defaults to `auto` and may force `native` or `isomorphic` for server-owned Git flows such as local history and Git-backed module operations; `LOGIN_ALLOWED` gates the password-login endpoints and login-shell form, `CLOUD_SHARE_ALLOWED` gates hosted cloud-share uploads, `CLOUD_SHARE_URL` tells browser clients which hosted share receiver to use, and page shells receive only `frontend_exposed` values as injected meta tags +- runtime parameters are defined in `commands/params.yaml`; `node space serve` resolves them in this order: launch arguments, stored `.env` params written by `node space set`, then process environment variables, then schema defaults; `node space supervise` accepts the same runtime parameters, owns the public `HOST` and `PORT`, requires `CUSTOMWARE_PATH`, enables source auto-update by default, and passes the remaining resolved params to private `space serve` children; `WORKERS` controls clustered HTTP worker count for `serve` and `supervise`; `CUSTOMWARE_PATH` is the parent directory for writable backend `L1/` and `L2/` storage when configured and also hosts backend-owned cloud-share archives under `share/spaces/` when that feature is enabled; `CUSTOMWARE_WATCHDOG` defaults to `true` and controls live backend customware watching, config watching, and the periodic reconcile backstop without disabling L0/L1 startup indexing, on-demand L2 loading, or explicit clustered mutation sync; `GIT_BACKEND` defaults to `auto` and may force `native` or `isomorphic` for server-owned Git flows such as local history and Git-backed module operations; `LOGIN_ALLOWED` gates the password-login endpoints and login-shell form, `CLOUD_SHARE_ALLOWED` gates hosted cloud-share uploads, `CLOUD_SHARE_URL` tells browser clients which hosted share receiver to use, `ANTHROPIC_OAUTH_ALLOWED` gates the Claude subscription OAuth provider for the agent surfaces and `ANTHROPIC_OAUTH_CLIENT_ID`, `ANTHROPIC_OAUTH_AUTHORIZE_URL`, `ANTHROPIC_OAUTH_TOKEN_URL`, `ANTHROPIC_OAUTH_REDIRECT_URI`, `ANTHROPIC_OAUTH_FLOW_MODE`, and `ANTHROPIC_API_BASE_URL` configure that flow with sane public defaults that work without a per-deployment Anthropic OAuth registration; `ANTHROPIC_OAUTH_FLOW_MODE` defaults to `auto` and selects a button-only redirect flow on `localhost` hosts and the manual code-paste flow elsewhere, and page shells receive only `frontend_exposed` values as injected meta tags - app file APIs use logical app-rooted paths such as `L2/alice/user.yaml` or `/app/L2/alice/user.yaml`, and supported endpoints may also accept `~` or `~/...` for the authenticated user's `L2//...`; those logical paths do not change when `CUSTOMWARE_PATH` relocates the writable backend roots - non-`/api` and non-`/mod` browser entry routes are served from `server/pages/`; `/login` and `/enter` are public and the protected page shells live behind the router-side session gate - detailed browser-runtime rules live in `/app/AGENTS.md` diff --git a/app/AGENTS.md b/app/AGENTS.md index 2930cdfe..8e9ef92f 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -53,6 +53,7 @@ Current module-local docs in the app tree: - `app/L0/_all/mod/_core/onscreen_agent/AGENTS.md` - `app/L0/_all/mod/_core/onscreen_menu/AGENTS.md` - `app/L0/_all/mod/_core/open_router/AGENTS.md` +- `app/L0/_all/mod/_core/anthropic_oauth/AGENTS.md` - `app/L1/_all/mod/metrics/posthog/AGENTS.md` - `app/L0/_admin/mod/_core/overlay_agent/AGENTS.md` @@ -133,6 +134,7 @@ Current major first-party modules under `app/L0/_all/mod/_core/`: - `onscreen_agent/`: floating routed overlay agent and the first-party user-facing chat runtime - `onscreen_menu/`: reserved routed shell header bar, Home shortcut to the empty default route, left and right shell-control seams, and `_core/onscreen_menu/items` dropdown action seam - `open_router/`: headless OpenRouter request-policy module that extends the admin and onscreen API transport seams instead of hardcoding provider-specific headers into the chat runtimes +- `anthropic_oauth/`: optional Claude subscription LLM provider; small headless helper plus a reusable connect block embedded under a third tab in the admin and onscreen settings dialogs, plus the per-surface request hooks that redirect API-mode requests to the authenticated `/api/anthropic_subscription_completions` endpoint when the user opted into subscription mode - `web_browsing/`: browser-surface module that contributes a Browser dropdown action, mounts draggable, minimizable, resizable popup browser windows, and defines placement-generic `` elements that can also live inside widgets or other screen DOM; the same element uses an iframe fallback in browser sessions and a DOM-backed desktop `` in the packaged host, supports optional `controls="true"` address-bar chrome, registers every surface under a unique `browser-N` id exposed through the numeric-id `space.browser` runtime, includes in-app interception of `_blank` or `window.open(...)` requests back into new modals, persists only popup-window geometry across reloads, tracks direct surface focus for prompt-time browser content, and gates app-side browser diagnostics through a shared browser log level that defaults to `error` - `skillset/`: first-party shared skill packs plus browser helper scripts and shared browser-side skill discovery helpers used by the onscreen and admin agents - `webllm/`: unlisted routed browser-only WebLLM test surface with a module-local worker, vendored browser runtime, compact searchable prebuilt model loading, expert-only compiled custom model loading, and simple throughput reporting diff --git a/app/L0/_all/mod/_core/admin/views/agent/AGENTS.md b/app/L0/_all/mod/_core/admin/views/agent/AGENTS.md index 705608d2..77dda2d1 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/AGENTS.md +++ b/app/L0/_all/mod/_core/admin/views/agent/AGENTS.md @@ -29,7 +29,7 @@ Current persistence paths: Current stored config fields are written in YAML as: -- `llm_provider` +- `llm_provider` (currently `api`, `subscription`, or `local`) - `local_provider` - `api_endpoint` - `api_key` @@ -76,7 +76,9 @@ Prompt rules: Current behavior: -- the LLM settings modal keeps one provider switch at the top with tabs named `API` and `Local`, and shows either the API settings fields or one `Local` section +- the LLM settings modal keeps one provider switch at the top with three tabs named `API key`, `Claude subscription`, and `Local`, and shows the API settings fields, the subscription connect block, or the `Local` section based on the active tab +- the `Claude subscription` tab is owned by `_core/anthropic_oauth/`; it mounts `connect-block.html` through `` and shows a Claude model name input alongside it +- when the active provider is `subscription`, `api.js` validation skips the `apiEndpoint` and `apiKey` checks and the `_core/anthropic_oauth/` request hook redirects the prepared fetch URL to the authenticated `/api/anthropic_subscription_completions` endpoint while stripping any `Authorization` header so the browser never sees the OAuth bearer token - the `Local` section only supports the Hugging Face browser runtime - the toolbar LLM settings button summarizes the current selection with the configured model name only; it does not prepend provider labels such as `API`, `Local`, or `Hugging Face` - the local section mounts the standalone Hugging Face config sidebar component through ``, so the admin modal and the routed testing harness share the same component file instead of maintaining duplicated local-provider markup diff --git a/app/L0/_all/mod/_core/admin/views/agent/api.js b/app/L0/_all/mod/_core/admin/views/agent/api.js index e36a2f31..ef2ad1d9 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/api.js +++ b/app/L0/_all/mod/_core/admin/views/agent/api.js @@ -457,12 +457,17 @@ export const prepareAdminAgentApiRequest = globalThis.space.extend( ); async function streamAdminAgentApiCompletion({ promptContext, settings, systemPrompt, messages, onDelta, signal }) { - if (!settings.apiEndpoint.trim()) { - throw new Error("Set an API endpoint before sending a message."); - } + const provider = config.normalizeAdminChatLlmProvider(settings?.provider); + const isSubscription = provider === config.ADMIN_CHAT_LLM_PROVIDER.SUBSCRIPTION; - if (!settings.apiKey.trim()) { - throw new Error("Set an API key before sending a message."); + if (!isSubscription) { + if (!settings.apiEndpoint.trim()) { + throw new Error("Set an API endpoint before sending a message."); + } + + if (!settings.apiKey.trim()) { + throw new Error("Set an API key before sending a message."); + } } if (!settings.model.trim()) { diff --git a/app/L0/_all/mod/_core/admin/views/agent/config.js b/app/L0/_all/mod/_core/admin/views/agent/config.js index 2a00ef4a..e413e89c 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/config.js +++ b/app/L0/_all/mod/_core/admin/views/agent/config.js @@ -5,7 +5,8 @@ export const ADMIN_CHAT_HISTORY_PATH = "~/hist/admin-chat.json"; export const DEFAULT_ADMIN_CHAT_MAX_TOKENS = 120_000; export const ADMIN_CHAT_LLM_PROVIDER = { API: "api", - LOCAL: "local" + LOCAL: "local", + SUBSCRIPTION: "subscription" }; export const ADMIN_CHAT_LOCAL_PROVIDER = { @@ -26,9 +27,13 @@ export const DEFAULT_ADMIN_CHAT_SETTINGS = { }; export function normalizeAdminChatLlmProvider(value) { - return value === ADMIN_CHAT_LLM_PROVIDER.LOCAL - ? ADMIN_CHAT_LLM_PROVIDER.LOCAL - : ADMIN_CHAT_LLM_PROVIDER.API; + if (value === ADMIN_CHAT_LLM_PROVIDER.LOCAL) { + return ADMIN_CHAT_LLM_PROVIDER.LOCAL; + } + if (value === ADMIN_CHAT_LLM_PROVIDER.SUBSCRIPTION) { + return ADMIN_CHAT_LLM_PROVIDER.SUBSCRIPTION; + } + return ADMIN_CHAT_LLM_PROVIDER.API; } export function normalizeAdminChatLocalProvider(value) { diff --git a/app/L0/_all/mod/_core/admin/views/agent/panel.html b/app/L0/_all/mod/_core/admin/views/agent/panel.html index bf67faaa..ac7a26ed 100644 --- a/app/L0/_all/mod/_core/admin/views/agent/panel.html +++ b/app/L0/_all/mod/_core/admin/views/agent/panel.html @@ -3,7 +3,9 @@ + +
@@ -186,7 +188,15 @@

Provider and model configuration

:class="{ 'is-active': $store.adminAgent.isSettingsDraftUsingApiProvider }" @click="$store.adminAgent.setSettingsProvider('api')" > - API + API key + +
+
+ + +
@@ -284,7 +311,7 @@

Provider and model configuration

System, history, and transient rebalance to 100% of max tokens. Single history message is a percentage of the history budget.

-