Menu

#62 provision_auth: support both OAuth and API key for all providers with DRY implementation

closed
nobody
None
2026-04-05
2026-04-04
Anonymous
No

Originally created by: kumaakh

Summary

provision_auth currently has two hard-coded paths:

  • OAuth copy — only for Claude (provider.supportsOAuthCopy() returns true only for Claude). Copies ~/.claude/.credentials.json to the remote member.
  • API key — for all providers, either via api_key param or OOB terminal prompt.

Gemini, Codex, and Copilot all have OAuth credential files too (e.g. ~/.gemini/oauth_creds.json), but the server has no mechanism to copy them the way it does for Claude. Users who authenticate their local Gemini CLI via OAuth have no automated way to propagate those credentials to remote members — they are forced to use an API key instead.

This session exposed the problem directly: deploying Gemini OAuth creds to a remote macOS member required manually reading the local file, base64-encoding its contents, and piping it via execute_command. This should be a one-line provision_auth call.

Goal

Every provider that supports OAuth should have its credentials automatically copyable via provision_auth — the same way Claude's are. The implementation should be DRY: provider-specific details (credential file path, env var name, verification method) live in the ProviderAdapter, and the orchestration logic in provision_auth is shared.

Provider Auth Matrix

Provider OAuth file API key env var Both supported?
Claude ~/.claude/.credentials.json ANTHROPIC_API_KEY Yes
Gemini ~/.gemini/oauth_creds.json GEMINI_API_KEY Yes
Codex (none / GitHub OAuth via device flow?) OPENAI_API_KEY TBD — investigate
Copilot (GitHub token file?) GITHUB_TOKEN / COPILOT_GITHUB_TOKEN TBD — investigate

The exact paths and mechanisms for Codex and Copilot need to be researched during implementation.

Proposed Design

1. Extend ProviderAdapter interface

interface ProviderAdapter {
  // existing
  readonly authEnvVar: string;
  supportsOAuthCopy(): boolean;

  // new
  oauthCredentialPath(): string | null;         // local path to OAuth creds file, null if not supported
  remoteOauthCredentialPath(): string | null;    // where to write it on the remote (may differ by OS)
  verifyOAuth(agent: Agent, envPrefix?: string): Promise<boolean>; // reuse existing verify logic
}

2. DRY provision_auth orchestration

if (input.api_key) → provisionApiKey (existing, no change)
else if provider.oauthCredentialPath() exists locally → provisionOAuthCopy (generalized from current Claude path)
else → collectOobApiKey (existing fallback)

provisionOAuthCopy(agent, provider):

  1. Read provider.oauthCredentialPath() — same logic as current readMasterCredentials()
  2. Validate token (expiry, refresh presence) — same as current validateCredentials()
  3. Write to provider.remoteOauthCredentialPath() on remote — same as current credentialFileWrite()
  4. Verify — call provider.verifyOAuth(agent)
  5. Return status message

Currently steps 1-5 are Claude-only in provisionMasterToken(). The refactor extracts them into a generic function parameterized by provider.

3. OS command layer

credentialFileWrite(content) and credentialFileRemove() in os-commands.ts are currently Claude-specific (hardcoded path). They need to accept the destination path as a parameter:

credentialFileWrite(content: string, destPath: string): string;
credentialFileRemove(destPath: string): string;

remove_member.ts calls credentialFileRemove() for cleanup — update it to pass provider.remoteOauthCredentialPath().

4. list_members / member_detail output

Surface the active auth mode (oauth vs api_key) per member so the PM can see which auth each member is using — already partially shown in member_detail (auth=OAuth credentials file vs auth=API key (env)).

Acceptance Criteria

  • provision_auth on a Gemini member (no api_key param) copies ~/.gemini/oauth_creds.json if it exists locally, exactly as it does for Claude
  • provision_auth on a Claude member is unchanged
  • If the local OAuth file does not exist for the provider, falls back to OOB API key prompt
  • remove_member cleans up the remote OAuth credential file for any provider that has one
  • All provider OAuth paths are defined in the respective ProviderAdapter — zero hardcoding in provision_auth.ts or remove_member.ts
  • Codex and Copilot OAuth support is investigated; if they have credential files, they are wired up; if not, a comment explains why

Out of scope

  • OAuth device flow / browser-based login (that's /login territory)
  • Refresh token rotation (tokens are copied as-is; expiry warnings already exist)

Related

Tickets: #63
Tickets: #64
Tickets: #65

Discussion

  • Anonymous

    Anonymous - 2026-04-04

    Originally posted by: kumaakh

    Implementation details for Gemini OAuth (3 required files, env var override issue, first-run trust prompt) are documented in [#63].

     

    Related

    Tickets: #63

  • Anonymous

    Anonymous - 2026-04-05

    Originally posted by: kumaakh

    Closed based on live testing against v0.1.3_d10302:

    • provision_auth(member_name="focus-dev2") on Gemini member with no api_key → "✅ OAuth credentials for gemini deployed to focus-dev2. Auth: verified with a successful gemini API call."
    • member_detail(member_name="focus-dev2", format="json") → auth field shows oauth
    • Claude OAuth path unchanged (focus-dev1 local sessions unaffected throughout testing)

    DRY OAuth implemented via ProviderAdapter interface: oauthCredentialFiles(), oauthSettingsMerge(), oauthEnvVarsToUnset(). Works for Gemini (3 files) and Claude. Codex/Copilot return null (no OAuth file copy supported).

     
  • Anonymous

    Anonymous - 2026-04-05

    Ticket changed by: kumaakh

    • status: open --> closed
     

Log in to post a comment.

MongoDB Logo MongoDB