feat: add setup-token + token auth

This commit is contained in:
Peter Steinberger
2026-01-09 17:50:34 +01:00
parent 083877d286
commit c3083f0186
9 changed files with 275 additions and 22 deletions

View File

@@ -177,7 +177,11 @@ Options:
- `--workspace <dir>`
- `--non-interactive`
- `--mode <local|remote>`
- `--auth-choice <oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
- `--token <token>` (non-interactive; used with `--auth-choice token`)
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
- `--anthropic-api-key <key>`
- `--openai-api-key <key>`
- `--gemini-api-key <key>`

View File

@@ -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: longlived Claude Code token
Run this on the **gateway host** (the machine running the Gateway):
@@ -92,13 +105,15 @@ Use `--agent <id>` to target a specific agent; omit it to use the configured def
2. **Clawdbot** syncs those into
`~/.clawdbot/agents/<agentId>/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

View File

@@ -240,7 +240,23 @@ export function buildProgram() {
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
"--auth-choice <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 <id>",
"Token provider id (non-interactive; used with --auth-choice token)",
)
.option(
"--token <token>",
"Token value (non-interactive; used with --auth-choice token)",
)
.option(
"--token-profile-id <id>",
"Auth profile id (non-interactive; default: <provider>:manual)",
)
.option(
"--token-expires-in <duration>",
"Optional token expiry duration (e.g. 365d, 12h)",
)
.option("--anthropic-api-key <key>", "Anthropic API key")
.option("--openai-api-key <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,

View File

@@ -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)",

View File

@@ -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)" }],

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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";