diff --git a/docs/cli/index.md b/docs/cli/index.md index 5abfdf62c..f8eaf41db 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -177,7 +177,11 @@ Options: - `--workspace ` - `--non-interactive` - `--mode ` -- `--auth-choice ` +- `--auth-choice ` +- `--token-provider ` (non-interactive; used with `--auth-choice token`) +- `--token ` (non-interactive; used with `--auth-choice token`) +- `--token-profile-id ` (non-interactive; default: `:manual`) +- `--token-expires-in ` (non-interactive; e.g. `365d`, `12h`) - `--anthropic-api-key ` - `--openai-api-key ` - `--gemini-api-key ` diff --git a/docs/gateway/authentication.md b/docs/gateway/authentication.md index 8c6b4d705..b591651f4 100644 --- a/docs/gateway/authentication.md +++ b/docs/gateway/authentication.md @@ -29,6 +29,19 @@ clawdbot models status clawdbot doctor ``` +Alternative: run the wrapper (also updates Clawdbot config): + +```bash +clawdbot models auth setup-token --provider anthropic +``` + +Manual token entry (any provider; writes `auth-profiles.json` + updates config): + +```bash +clawdbot models auth paste-token --provider anthropic +clawdbot models auth paste-token --provider openrouter +``` + ## Recommended: long‑lived Claude Code token Run this on the **gateway host** (the machine running the Gateway): @@ -92,13 +105,15 @@ Use `--agent ` to target a specific agent; omit it to use the configured def 2. **Clawdbot** syncs those into `~/.clawdbot/agents//agent/auth-profiles.json` when the auth store is loaded. -3. OAuth refresh happens automatically on use if a token is expired. +3. Refreshable OAuth profiles can be refreshed automatically on use. Static + token profiles (including Claude CLI setup-token) are not refreshable by + Clawdbot. ## Troubleshooting ### “No credentials found” -If the Anthropic OAuth profile is missing, run `claude setup-token` on the +If the Anthropic token profile is missing, run `claude setup-token` on the **gateway host**, then re-check: ```bash diff --git a/src/cli/program.ts b/src/cli/program.ts index 1124d7847..3b8292195 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -240,7 +240,23 @@ export function buildProgram() { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip", + "Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip", + ) + .option( + "--token-provider ", + "Token provider id (non-interactive; used with --auth-choice token)", + ) + .option( + "--token ", + "Token value (non-interactive; used with --auth-choice token)", + ) + .option( + "--token-profile-id ", + "Auth profile id (non-interactive; default: :manual)", + ) + .option( + "--token-expires-in ", + "Optional token expiry duration (e.g. 365d, 12h)", ) .option("--anthropic-api-key ", "Anthropic API key") .option("--openai-api-key ", "OpenAI API key") @@ -270,6 +286,7 @@ export function buildProgram() { mode: opts.mode as "local" | "remote" | undefined, authChoice: opts.authChoice as | "oauth" + | "setup-token" | "claude-cli" | "token" | "openai-codex" @@ -282,6 +299,10 @@ export function buildProgram() { | "minimax" | "skip" | undefined, + tokenProvider: opts.tokenProvider as string | undefined, + token: opts.token as string | undefined, + tokenProfileId: opts.tokenProfileId as string | undefined, + tokenExpiresIn: opts.tokenExpiresIn as string | undefined, anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, geminiApiKey: opts.geminiApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index c4203b5d5..96855f9e0 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -64,17 +64,23 @@ export function buildAuthChoiceOptions(params: { if (claudeCli?.type === "oauth" || claudeCli?.type === "token") { options.push({ value: "claude-cli", - label: "Anthropic OAuth (Claude CLI)", + label: "Anthropic token (Claude CLI)", hint: formatOAuthHint(claudeCli.expires), }); } else if (params.includeClaudeCliIfMissing && platform === "darwin") { options.push({ value: "claude-cli", - label: "Anthropic OAuth (Claude CLI)", + label: "Anthropic token (Claude CLI)", hint: "requires Keychain access", }); } + options.push({ + value: "setup-token", + label: "Anthropic token (run setup-token)", + hint: "Runs `claude setup-token`", + }); + options.push({ value: "token", label: "Anthropic token (paste setup-token)", diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index 897ae3003..499f332f3 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -216,7 +216,68 @@ export async function applyAuthChoice(params: { provider: "anthropic", mode: "token", }); - } else if (params.authChoice === "token" || params.authChoice === "oauth") { + } else if ( + params.authChoice === "setup-token" || + params.authChoice === "oauth" + ) { + await params.prompter.note( + [ + "This will run `claude setup-token` to create a long-lived Anthropic token.", + "Requires an interactive TTY and a Claude Pro/Max subscription.", + ].join("\n"), + "Anthropic setup-token", + ); + + if (!process.stdin.isTTY) { + await params.prompter.note( + "`claude setup-token` requires an interactive TTY.", + "Anthropic setup-token", + ); + return { config: nextConfig, agentModelOverride }; + } + + const proceed = await params.prompter.confirm({ + message: "Run `claude setup-token` now?", + initialValue: true, + }); + if (!proceed) return { config: nextConfig, agentModelOverride }; + + const res = await (async () => { + const { spawnSync } = await import("node:child_process"); + return spawnSync("claude", ["setup-token"], { stdio: "inherit" }); + })(); + if (res.error) { + await params.prompter.note( + `Failed to run claude: ${String(res.error)}`, + "Anthropic setup-token", + ); + return { config: nextConfig, agentModelOverride }; + } + if (typeof res.status === "number" && res.status !== 0) { + await params.prompter.note( + `claude setup-token failed (exit ${res.status})`, + "Anthropic setup-token", + ); + return { config: nextConfig, agentModelOverride }; + } + + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: true, + }); + if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) { + await params.prompter.note( + `No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`, + "Anthropic setup-token", + ); + return { config: nextConfig, agentModelOverride }; + } + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: CLAUDE_CLI_PROFILE_ID, + provider: "anthropic", + mode: "token", + }); + } else if (params.authChoice === "token") { const provider = (await params.prompter.select({ message: "Token provider", options: [{ value: "anthropic", label: "Anthropic (only supported)" }], diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 2ff885478..a50f6cd4f 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -352,6 +352,7 @@ async function promptAuthConfig( runtime, ) as | "oauth" + | "setup-token" | "claude-cli" | "token" | "openai-codex" @@ -403,7 +404,68 @@ async function promptAuthConfig( provider: "anthropic", mode: "token", }); - } else if (authChoice === "token" || authChoice === "oauth") { + } else if (authChoice === "setup-token" || authChoice === "oauth") { + note( + [ + "This will run `claude setup-token` to create a long-lived Anthropic token.", + "Requires an interactive TTY and a Claude Pro/Max subscription.", + ].join("\n"), + "Anthropic setup-token", + ); + + if (!process.stdin.isTTY) { + note( + "`claude setup-token` requires an interactive TTY.", + "Anthropic setup-token", + ); + return next; + } + + const runNow = guardCancel( + await confirm({ + message: "Run `claude setup-token` now?", + initialValue: true, + }), + runtime, + ); + if (!runNow) return next; + + const res = await (async () => { + const { spawnSync } = await import("node:child_process"); + return spawnSync("claude", ["setup-token"], { stdio: "inherit" }); + })(); + if (res.error) { + note( + `Failed to run claude: ${String(res.error)}`, + "Anthropic setup-token", + ); + return next; + } + if (typeof res.status === "number" && res.status !== 0) { + note( + `claude setup-token failed (exit ${res.status})`, + "Anthropic setup-token", + ); + return next; + } + + const store = ensureAuthProfileStore(undefined, { + allowKeychainPrompt: true, + }); + if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) { + note( + `No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`, + "Anthropic setup-token", + ); + return next; + } + + next = applyAuthProfileConfig(next, { + profileId: CLAUDE_CLI_PROFILE_ID, + provider: "anthropic", + mode: "token", + }); + } else if (authChoice === "token") { const provider = guardCancel( await select({ message: "Token provider", @@ -726,6 +788,7 @@ async function promptAuthConfig( : (next.agents?.defaults?.model?.primary ?? ""); const preferAnthropic = authChoice === "claude-cli" || + authChoice === "setup-token" || authChoice === "token" || authChoice === "oauth" || authChoice === "apiKey"; diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index c27f00eb3..db924e9ff 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -1,10 +1,14 @@ +import { spawnSync } from "node:child_process"; import path from "node:path"; import { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID, ensureAuthProfileStore, + upsertAuthProfile, } from "../agents/auth-profiles.js"; import { resolveEnvApiKey } from "../agents/model-auth.js"; +import { normalizeProviderId } from "../agents/model-selection.js"; +import { parseDurationMs } from "../cli/parse-duration.js"; import { type ClawdbotConfig, CONFIG_PATH_CLAWDBOT, @@ -206,18 +210,82 @@ export async function runNonInteractiveOnboarding( nextConfig = applyOpenAICodexModelDefault(nextConfig).next; } else if (authChoice === "minimax") { nextConfig = applyMinimaxConfig(nextConfig); - } else if ( - authChoice === "token" || - authChoice === "oauth" || - authChoice === "openai-codex" || - authChoice === "antigravity" - ) { + } else if (authChoice === "setup-token" || authChoice === "oauth") { + if (!process.stdin.isTTY) { + runtime.error("`claude setup-token` requires an interactive TTY."); + runtime.exit(1); + return; + } + + const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" }); + if (res.error) throw res.error; + if (typeof res.status === "number" && res.status !== 0) { + runtime.error(`claude setup-token failed (exit ${res.status})`); + runtime.exit(1); + return; + } + + const store = ensureAuthProfileStore(undefined, { + allowKeychainPrompt: true, + }); + if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) { + runtime.error( + `No Claude CLI credentials found after setup-token. Expected auth profile ${CLAUDE_CLI_PROFILE_ID}.`, + ); + runtime.exit(1); + return; + } + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: CLAUDE_CLI_PROFILE_ID, + provider: "anthropic", + mode: "token", + }); + } else if (authChoice === "token") { + const providerRaw = opts.tokenProvider?.trim(); + const tokenRaw = opts.token?.trim(); + if (!providerRaw) { + runtime.error( + "Missing --token-provider (required for --auth-choice token).", + ); + runtime.exit(1); + return; + } + if (!tokenRaw) { + runtime.error("Missing --token (required for --auth-choice token)."); + runtime.exit(1); + return; + } + + const provider = normalizeProviderId(providerRaw); + const profileId = ( + opts.tokenProfileId?.trim() || `${provider}:manual` + ).trim(); + const expires = + opts.tokenExpiresIn?.trim() && opts.tokenExpiresIn.trim().length > 0 + ? Date.now() + + parseDurationMs(String(opts.tokenExpiresIn).trim(), { + defaultUnit: "d", + }) + : undefined; + + upsertAuthProfile({ + profileId, + credential: { + type: "token", + provider, + token: tokenRaw, + ...(expires ? { expires } : {}), + }, + }); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider, + mode: "token", + }); + } else if (authChoice === "openai-codex" || authChoice === "antigravity") { const label = - authChoice === "antigravity" - ? "Antigravity" - : authChoice === "token" - ? "Token" - : "OAuth"; + authChoice === "antigravity" ? "Antigravity" : "OpenAI Codex OAuth"; runtime.error(`${label} requires interactive mode.`); runtime.exit(1); return; diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 3ebbe85a9..c52f7d99a 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -3,7 +3,9 @@ import type { GatewayDaemonRuntime } from "./daemon-runtime.js"; export type OnboardMode = "local" | "remote"; export type AuthChoice = + // Legacy alias for `setup-token` (kept for backwards CLI compatibility). | "oauth" + | "setup-token" | "claude-cli" | "token" | "openai-codex" @@ -27,6 +29,14 @@ export type OnboardOptions = { workspace?: string; nonInteractive?: boolean; authChoice?: AuthChoice; + /** Used when `authChoice=token` in non-interactive mode. */ + tokenProvider?: string; + /** Used when `authChoice=token` in non-interactive mode. */ + token?: string; + /** Used when `authChoice=token` in non-interactive mode. */ + tokenProfileId?: string; + /** Used when `authChoice=token` in non-interactive mode. */ + tokenExpiresIn?: string; anthropicApiKey?: string; openaiApiKey?: string; geminiApiKey?: string; diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index d1b55e6af..2608e80c1 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -10,6 +10,11 @@ export async function onboardCommand( runtime: RuntimeEnv = defaultRuntime, ) { assertSupportedRuntime(runtime); + const authChoice = + opts.authChoice === "oauth" ? ("setup-token" as const) : opts.authChoice; + const normalizedOpts = + authChoice === opts.authChoice ? opts : { ...opts, authChoice }; + if (process.platform === "win32") { runtime.log( [ @@ -20,12 +25,12 @@ export async function onboardCommand( ); } - if (opts.nonInteractive) { - await runNonInteractiveOnboarding(opts, runtime); + if (normalizedOpts.nonInteractive) { + await runNonInteractiveOnboarding(normalizedOpts, runtime); return; } - await runInteractiveOnboarding(opts, runtime); + await runInteractiveOnboarding(normalizedOpts, runtime); } export type { OnboardOptions } from "./onboard-types.js";