fix(auth): document paste-token stdin setup (#63050)

Document that automation should pipe `models auth paste-token` credentials over stdin instead of passing token material in argv, keeping the existing secret-handling path explicit in the CLI docs.

Also include accepted auth-profile credential types in invalid-profile warning logs so malformed local auth stores are easier to repair.

Fixes #63042.

Thanks @liaoandi.
This commit is contained in:
Andi Liao
2026-05-28 01:44:44 +08:00
committed by GitHub
parent 1806b152a9
commit 085228c961
6 changed files with 25 additions and 20 deletions

View File

@@ -219,9 +219,11 @@ Notes:
method (defaulting to that provider's `setup-token` method when it exposes
one).
- `paste-token` accepts a token string generated elsewhere or from automation.
- `paste-token` requires `--provider`, prompts for the token value, and writes
it to the default profile id `<provider>:manual` unless you pass
- `paste-token` requires `--provider`, prompts for the token value by default,
and writes it to the default profile id `<provider>:manual` unless you pass
`--profile-id`.
- In automation, pipe the token on stdin instead of passing it as an argument so
provider credentials do not appear in shell history or process lists.
- `paste-token --expires-in <duration>` stores an absolute token expiry from a
relative duration such as `365d` or `12h`.
- For `openai-codex`, OpenAI API keys and ChatGPT/OAuth token material are

View File

@@ -1046,6 +1046,7 @@ describe("ensureAuthProfileStore", () => {
missing_provider: 1,
non_object: 1,
},
validTypes: ["api_key", "oauth", "token"],
keys: ["anthropic:missing-type", "openai:missing-provider", "qwen:not-object"],
},
);

View File

@@ -253,6 +253,7 @@ function warnRejectedCredentialEntries(source: string, rejected: RejectedCredent
source,
dropped: rejected.length,
reasons,
...(reasons.invalid_type ? { validTypes: [...AUTH_PROFILE_TYPES] } : {}),
keys: rejected.slice(0, 10).map((entry) => entry.key),
});
}

View File

@@ -333,10 +333,7 @@ export function registerModelsCli(program: Command) {
.option("--provider <id>", "Provider id registered by a plugin")
.option("--method <id>", "Provider auth method id")
.option("--device-code", "Use the provider device-code auth method", false)
.option(
"--profile-id <id>",
"Auth profile id override for single-profile login methods",
)
.option("--profile-id <id>", "Auth profile id override for single-profile login methods")
.option("--set-default", "Apply the provider's default model recommendation", false)
.action(async (opts, command) => {
if (opts.deviceCode && typeof opts.method === "string" && opts.method !== "device-code") {

View File

@@ -55,6 +55,7 @@ const mocks = vi.hoisted(() => ({
logConfigUpdated: vi.fn(),
openUrl: vi.fn(),
isRemoteEnvironment: vi.fn(() => false),
validateAnthropicSetupToken: vi.fn<() => string | undefined>(() => undefined),
loadAuthProfileStoreForRuntime: vi.fn(),
listProfilesForProvider: vi.fn(),
promoteAuthProfileInOrder: vi.fn(),
@@ -170,7 +171,7 @@ vi.mock("../../plugins/provider-oauth-flow.js", () => ({
}));
vi.mock("../auth-token.js", () => ({
validateAnthropicSetupToken: vi.fn(() => undefined),
validateAnthropicSetupToken: mocks.validateAnthropicSetupToken,
}));
vi.mock("../../plugins/provider-auth-choice-helpers.js", async (importOriginal) => {
@@ -356,6 +357,8 @@ describe("modelsAuthLoginCommand", () => {
mocks.clackPassword.mockReset();
mocks.clackSelect.mockReset();
mocks.clackText.mockReset();
mocks.validateAnthropicSetupToken.mockReset();
mocks.validateAnthropicSetupToken.mockReturnValue(undefined);
mocks.upsertAuthProfileWithLock.mockReset();
mocks.upsertAuthProfileWithLock.mockResolvedValue({ version: 1, profiles: {} });
mocks.promoteAuthProfileInOrder.mockReset();

View File

@@ -587,22 +587,23 @@ export async function modelsAuthPasteTokenCommand(
const profileId =
normalizeOptionalString(opts.profileId) || resolveDefaultTokenProfileId(provider);
const validateTokenInput = (value: string | undefined): string | undefined => {
const trimmed = value?.trim();
if (!trimmed) {
return "Required";
}
if (provider === "anthropic") {
return validateAnthropicSetupToken(trimmed.replaceAll(/\s+/g, ""));
}
if (isOpenAICodexProvider(provider) && looksLikeOpenAIApiKey(trimmed)) {
return `That looks like an OpenAI API key. Use ${formatCliCommand("openclaw models auth paste-api-key --provider openai-codex")} for API-key auth.`;
}
return undefined;
};
const tokenInput = await readPastedSecret({
message: `Paste token for ${provider}`,
masked: false,
validate: (value) => {
const trimmed = value?.trim();
if (!trimmed) {
return "Required";
}
if (provider === "anthropic") {
return validateAnthropicSetupToken(trimmed.replaceAll(/\s+/g, ""));
}
if (isOpenAICodexProvider(provider) && looksLikeOpenAIApiKey(trimmed)) {
return `That looks like an OpenAI API key. Use ${formatCliCommand("openclaw models auth paste-api-key --provider openai-codex")} for API-key auth.`;
}
return undefined;
},
validate: validateTokenInput,
});
const token =
provider === "anthropic"