mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 04:31:10 +00:00
Auth: sanitize reauth commands
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import { sanitizeForLog } from "../../terminal/ansi.js";
|
||||
import { normalizeProviderId } from "../model-selection.js";
|
||||
|
||||
export type OAuthRefreshFailureReason =
|
||||
| "refresh_token_reused"
|
||||
| "invalid_grant"
|
||||
@@ -6,12 +10,21 @@ export type OAuthRefreshFailureReason =
|
||||
| "revoked";
|
||||
|
||||
const OAUTH_REFRESH_FAILURE_PROVIDER_RE = /OAuth token refresh failed for ([^:]+):/i;
|
||||
const SAFE_PROVIDER_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
||||
|
||||
export function extractOAuthRefreshFailureProvider(message: string): string | null {
|
||||
const provider = message.match(OAUTH_REFRESH_FAILURE_PROVIDER_RE)?.[1]?.trim();
|
||||
return provider && provider.length > 0 ? provider : null;
|
||||
}
|
||||
|
||||
export function sanitizeOAuthRefreshFailureProvider(
|
||||
provider: string | null | undefined,
|
||||
): string | null {
|
||||
const sanitized = provider ? sanitizeForLog(provider).replaceAll("`", "").trim() : "";
|
||||
const normalized = normalizeProviderId(sanitized);
|
||||
return normalized && SAFE_PROVIDER_ID_RE.test(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
export function classifyOAuthRefreshFailureReason(
|
||||
message: string,
|
||||
): OAuthRefreshFailureReason | null {
|
||||
@@ -42,7 +55,14 @@ export function classifyOAuthRefreshFailure(message: string): {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: extractOAuthRefreshFailureProvider(message),
|
||||
provider: sanitizeOAuthRefreshFailureProvider(extractOAuthRefreshFailureProvider(message)),
|
||||
reason: classifyOAuthRefreshFailureReason(message),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOAuthRefreshFailureLoginCommand(provider: string | null | undefined): string {
|
||||
const safeProvider = sanitizeOAuthRefreshFailureProvider(provider);
|
||||
return safeProvider
|
||||
? formatCliCommand(`openclaw models auth login --provider ${safeProvider}`)
|
||||
: formatCliCommand("openclaw models auth login");
|
||||
}
|
||||
|
||||
@@ -1000,6 +1000,46 @@ describe("runAgentTurnWithFallback", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to a generic reauth command when the provider in the OAuth error is unsafe", async () => {
|
||||
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
|
||||
new Error(
|
||||
"OAuth token refresh failed for openai-codex`\nrm -rf /: invalid_grant. Please try again or re-authenticate.",
|
||||
),
|
||||
);
|
||||
|
||||
const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
|
||||
const result = await runAgentTurnWithFallback({
|
||||
commandBody: "hello",
|
||||
followupRun: createFollowupRun(),
|
||||
sessionCtx: {
|
||||
Provider: "whatsapp",
|
||||
MessageSid: "msg",
|
||||
} as unknown as TemplateContext,
|
||||
opts: {},
|
||||
typingSignals: createMockTypingSignaler(),
|
||||
blockReplyPipeline: null,
|
||||
blockStreamingEnabled: false,
|
||||
resolvedBlockStreamingBreak: "message_end",
|
||||
applyReplyToMode: (payload) => payload,
|
||||
shouldEmitToolResult: () => true,
|
||||
shouldEmitToolOutput: () => false,
|
||||
pendingToolTasks: new Set(),
|
||||
resetSessionAfterCompactionFailure: async () => false,
|
||||
resetSessionAfterRoleOrderingConflict: async () => false,
|
||||
isHeartbeat: false,
|
||||
sessionKey: "main",
|
||||
getActiveSessionEntry: () => undefined,
|
||||
resolvedVerboseLevel: "off",
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("final");
|
||||
if (result.kind === "final") {
|
||||
expect(result.payload.text).toBe(
|
||||
"⚠️ Model login expired on the gateway. Re-auth with `openclaw models auth login`, then try again.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns a session reset hint for Bedrock tool mismatch errors on external chat channels", async () => {
|
||||
state.runEmbeddedPiAgentMock.mockRejectedValueOnce(
|
||||
new Error(
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
hasOutboundReplyContent,
|
||||
resolveSendableOutboundReplyParts,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import { classifyOAuthRefreshFailure } from "../../agents/auth-profiles/oauth-refresh-failure.js";
|
||||
import {
|
||||
buildOAuthRefreshFailureLoginCommand,
|
||||
classifyOAuthRefreshFailure,
|
||||
} from "../../agents/auth-profiles/oauth-refresh-failure.js";
|
||||
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
|
||||
import { runCliAgent } from "../../agents/cli-runner.js";
|
||||
import { getCliSessionBinding } from "../../agents/cli-session.js";
|
||||
@@ -311,9 +314,7 @@ function buildExternalRunFailureText(message: string): string {
|
||||
}
|
||||
const oauthRefreshFailure = classifyOAuthRefreshFailure(message);
|
||||
if (oauthRefreshFailure) {
|
||||
const loginCommand = oauthRefreshFailure.provider
|
||||
? `openclaw models auth login --provider ${oauthRefreshFailure.provider}`
|
||||
: "openclaw models auth login --provider <provider>";
|
||||
const loginCommand = buildOAuthRefreshFailureLoginCommand(oauthRefreshFailure.provider);
|
||||
if (oauthRefreshFailure.reason) {
|
||||
return `⚠️ Model login expired on the gateway${oauthRefreshFailure.provider ? ` for ${oauthRefreshFailure.provider}` : ""}. Re-auth with \`${loginCommand}\`, then try again.`;
|
||||
}
|
||||
|
||||
@@ -38,49 +38,6 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("buildEmbeddedRunBaseParams runtime config", () => {
|
||||
it("prefers the active runtime snapshot when queued reply config still contains SecretRefs", () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
},
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runtimeConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "resolved-runtime-key",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
|
||||
const resolved = buildEmbeddedRunBaseParams({
|
||||
run: makeRun(sourceConfig),
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
runId: "run-1",
|
||||
authProfile: resolveProviderScopedAuthProfile({
|
||||
provider: "openai",
|
||||
primaryProvider: "openai",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(resolved.config).toBe(runtimeConfig);
|
||||
});
|
||||
|
||||
it("keeps an already-resolved run config instead of reverting to a stale runtime snapshot", () => {
|
||||
const staleSnapshot: OpenClawConfig = {
|
||||
models: {
|
||||
|
||||
@@ -51,4 +51,17 @@ describe("resolveUnusableProfileHint", () => {
|
||||
"- openai-codex:default: OAuth refresh failed — Try again; if this persists, run `openclaw models auth login --provider openai-codex`.",
|
||||
);
|
||||
});
|
||||
|
||||
it("drops the provider-specific command when the parsed provider is unsafe", () => {
|
||||
expect(
|
||||
formatOAuthRefreshFailureDoctorLine({
|
||||
profileId: "openai-codex:default",
|
||||
provider: "openai-codex",
|
||||
message:
|
||||
"OAuth token refresh failed for openai-codex`\nrm -rf /: invalid_grant. Please try again or re-authenticate.",
|
||||
}),
|
||||
).toBe(
|
||||
"- openai-codex:default: re-auth required [invalid_grant] — Run `openclaw models auth login`.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,20 +12,17 @@ import {
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { formatAuthDoctorHint } from "../agents/auth-profiles/doctor.js";
|
||||
import {
|
||||
buildOAuthRefreshFailureLoginCommand,
|
||||
classifyOAuthRefreshFailure,
|
||||
type OAuthRefreshFailureReason,
|
||||
} from "../agents/auth-profiles/oauth-refresh-failure.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { resolvePluginProviders } from "../plugins/providers.runtime.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
import {
|
||||
buildProviderAuthRecoveryHint,
|
||||
resolveProviderAuthLoginCommand,
|
||||
} from "./provider-auth-guidance.js";
|
||||
import { buildProviderAuthRecoveryHint } from "./provider-auth-guidance.js";
|
||||
|
||||
const CODEX_PROVIDER_ID = "openai-codex";
|
||||
const CODEX_OAUTH_WARNING_TITLE = "Codex OAuth";
|
||||
@@ -202,10 +199,7 @@ export function formatOAuthRefreshFailureDoctorLine(params: {
|
||||
return null;
|
||||
}
|
||||
const provider = classified.provider ?? params.provider;
|
||||
const command =
|
||||
resolveProviderAuthLoginCommand({
|
||||
provider,
|
||||
}) ?? formatCliCommand(`openclaw models auth login --provider ${provider}`);
|
||||
const command = buildOAuthRefreshFailureLoginCommand(provider);
|
||||
if (classified.reason) {
|
||||
return `- ${params.profileId}: re-auth required [${formatOAuthRefreshFailureReason(classified.reason)}] — Run \`${command}\`.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user