OpenProgram Docs

Self-contained auth storage#

Every provider's credentials live under ~/.openprogram, managed by OpenProgram itself, rather than being read out of other CLIs' credential files (~/.codex/auth.json, ~/.claude/.credentials.json, ~/.gemini, ~/.qwen, ~/.config/gh) on every use. One store, one login flow, consistent across CLI, web, and TUI.

The store#

openprogram/auth/store.py implements AuthStore at ~/.openprogram/auth/<provider_id>/<account_id>.json — 0600, atomic write→fsync→replace, cross-process flock, and an in-memory mtime/size watch so a file changed underneath is re-loaded. openprogram/auth/types.py defines the credential kinds:

kind secret storage
api_key copy of the key
oauth copy: access + refresh + expires_at_ms + client_id + token_endpoint
device_code copy (same shape as oauth)
cli_delegated POINTER onlystore_path + key-paths into an external file; re-read each use
credential_process argv run on demand

CredentialProvider is store-authoritative and never re-discovers: it serves whatever pool is on disk. Importing is an explicit, write-once step (cli_import, import_from_codex_file), so once a credential is copied into the store, nothing re-reads the external file. openprogram/auth/methods/cli_import.py with mode="copy" dereferences an external file once and builds a writable, store-owned oauth credential.

Where each provider's credential comes from#

provider origin self-contained?
openai-codex native PKCE OR copy of ~/.codex/auth.json; OpenProgram refreshes (_codex_refreshauth.openai.com/oauth/token, mirrors back to ~/.codex) yes (refresh works)
github-copilot native device-code → store oauth yes
openai, gemini, other key providers env → config.json["api_keys"] (NOT the pool store) key-based, but dual-stored
anthropic (API key) env/paste copy yes
anthropic (subscription) ~/.claude/.credentials.json pointer; refresh = None no
gemini-subscription ~/.gemini/oauth_creds.json pointer; refresh = None no
qwen ~/.qwen/oauth_creds.json pointer (no runtime package anyway) no

Patterns borrowed from the references#

opencode is fully self-contained with zero adoption. Even for the same OpenAI account the codex CLI uses, opencode runs its own PKCE with a public CLIENT_ID and stores the result in its own auth.json. It keys a provider → AuthHook registry where each provider declares methods[] and an authorize() returning method:"auto" (loopback/poll, no paste) or method:"code" (the user pastes). Refresh happens on demand inside the request fetch: compare expires < Date.now(), single-flight refresh, write tokens back. Notably, anthropic and google have no OAuth in opencode at all — API key only, which is their answer to the two providers that cannot be self-refreshed.

openclaw keys auth-profiles.json by <provider>:<label> profile id (multiple credentials per provider) and splits secrets from rotation/usage state into a sibling file. Its credential union is oauth | api_key | token, where token is a static non-refreshable bearer, and secrets can be inline or a SecretRef (env/file/exec/keychain). Refresh runs on demand under a cross-process lock that re-reads from disk inside the lock, so a concurrent refresh is adopted rather than clobbered — that race-safe core is the part worth copying. One shared createVpsAwareOAuthHandlers picks browser-callback versus paste-code from a remote-env flag and is reused by every OAuth provider, over one shared PKCE generator.

What openclaw does that this design does not follow: cli-credentials.ts and external-cli-sync.ts read codex/minimax/claude CLI files directly — the cross-CLI coupling this design removes.

Constraints that are not engineering gaps#

  1. gemini-subscription cannot be self-refreshed: Google's Code-Assist OAuth uses an embedded client secret OpenProgram cannot ship (google_gemini_cli/auth_adapter.py:14-21).
  2. anthropic subscription OAuth cannot be self-run: Anthropic has not published a third-party OAuth client (anthropic/auth_adapter.py:16-21).

For both, self-contained storage is achievable — copy the token into the store, stop pointing at the external file — but self-contained refresh is not. When the short-lived access token expires, OpenProgram can only ask the user to sign in again, or fall back to an API key, which is the choice opencode makes.

Design#

  1. One store, copy not pointer. Every credential is copied into ~/.openprogram/auth/<provider>/<profile>.json. cli_delegated pointers are not the default; importing is a one-time copy, after which the external file is irrelevant. A pointer link remains available as an explicit opt-in.
  2. One login registry. A provider → [auth method] table where each method is one of pkce_oauth | device_code | api_key | paste_code, backed by shared helpers (pkce_browser_flow, device_code_flow, and a browser_vs_paste chooser keyed off a remote/headless flag). It replaces the ad-hoc map in auth/cli.py::_available_login_methods as the single source of truth, and each method names a shared handler. auth/methods/{pkce_oauth,device_code, api_key_paste,cli_import}.py supply the handlers.
  3. Three surfaces drive the same registry. Web and TUI both drive it, so any provider's login works from any surface rather than the CLI being the only place native PKCE and device-code run.
  4. Refresh ownership in-house where a public client exists (codex and copilot today). The two constrained providers copy into the store and prompt re-login on expiry.
  5. One source of truth for api keys. Key providers currently resolve at runtime via env → config.json["api_keys"] rather than the pool. The pool becomes authoritative with a config.json mirror, so the dual store collapses.

For the constrained providers the accepted behaviour is explicit: copy the token into the store and re-login on expiry, stopping the pointer into ~/.gemini and ~/.claude. Since OpenProgram cannot auto-refresh these, an expired access token prompts a fresh sign-in; they rarely expire, and a rotation just means signing in again.

Build order#

  1. Login-method registry — the declarative provider → [method] table as the single source of truth, with the CLI reading from it first. A pure refactor with no behaviour change, so it is verifiable on its own.
  2. Shared login handlers — extract pkce_browser_flow, device_code_flow, and the browser_vs_paste chooser from auth/methods/* so all three surfaces call the same code.
  3. Web native login — drive the registry from the provider detail page.
  4. TUI native login — the same, from the /login panel.
  5. Copying gemini-subscription, qwen, and anthropic-subscription into the store, and collapsing the api_key dual store.

Doing the uncontroversial core first — codex and copilot fully self-contained, plus unified login across CLI/web/TUI — keeps the working codex share intact rather than changing everything at once.

Last updated · 2026-08-13