mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user