diff --git a/CHANGELOG.md b/CHANGELOG.md index 5673d2dd5f5..78ee8ec31fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2192,6 +2192,7 @@ Docs: https://docs.openclaw.ai - Security/Exec: block grep safe-bin positional operand bypass by setting grep positional budget to zero, so `-e/--regexp` cannot smuggle bare filename reads (for example `.env`) via ambiguous positionals; safe-bin grep patterns must come from `-e/--regexp`. Thanks @athuljayaram for reporting. - Security/Gateway/Agents: remove implicit admin scopes from agent tool gateway calls by classifying methods to least-privilege operator scopes, and enforce owner-only tooling (`cron`, `gateway`, `whatsapp_login`) through centralized tool-policy wrappers plus tool metadata to prevent non-owner DM privilege escalation. Ships in the next npm release. Thanks @Adam55A-code for reporting. - Security/Gateway: centralize gateway method-scope authorization and default non-CLI gateway callers to least-privilege method scopes, with explicit CLI scope handling, full core-handler scope classification coverage, and regression guards to prevent scope drift. +- Auto-reply: show user-friendly error messages based on error type (rate limit, auth, billing, timeout) instead of exposing technical details. - Security/Net: block SSRF bypass via NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`), 6to4 (`2002::/16`), and Teredo (`2001:0000::/32`) IPv6 transition addresses, and fail closed on IPv6 parse errors. Thanks @jackhax. - Security/OTEL: sanitize OTLP endpoint URL resolution. (#13791) Thanks @vincentkoc. - Security: patch Dependabot security issues in pnpm lock. (#20832) Thanks @vincentkoc. diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts index cec3652d4a9..4ae822c7f35 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts @@ -154,13 +154,31 @@ describe("trigger handling", () => { { error: "sandbox is not defined.", expected: - "⚠️ Agent failed before reply: sandbox is not defined.\nLogs: openclaw logs --follow", + "Something unexpected happened. Try /new to start a fresh conversation, or try again in a moment.", }, { error: "Context window exceeded", expected: "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model.", }, + { + error: "rate_limit_exceeded: API rate limit exceeded", + expected: "The AI service is busy. Please wait a moment and try again.", + }, + { + error: "401 Unauthorized: Invalid API key", + expected: + "I couldn't connect to the AI service. Please verify your API key is configured correctly.", + }, + { + error: "402 Payment Required: billing limit exceeded", + expected: + "I've reached my limit with the AI service. Please check your account balance and try again.", + }, + { + error: "408 Request Timeout: connection timed out", + expected: "The request timed out. Please try again, or start a fresh session with /new.", + }, ] as const; for (const testCase of errorCases) { runEmbeddedPiAgentMock.mockClear(); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 9ebc239f7ff..7ca57369e50 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -6,13 +6,15 @@ import { getCliSessionId } from "../../agents/cli-session.js"; import { runWithModelFallback } from "../../agents/model-fallback.js"; import { isCliProvider } from "../../agents/model-selection.js"; import { - BILLING_ERROR_USER_MESSAGE, + isAuthErrorMessage, + isBillingErrorMessage, isCompactionFailureError, isContextOverflowError, - isBillingErrorMessage, isLikelyContextOverflowError, + isOverloadedErrorMessage, + isRateLimitErrorMessage, + isTimeoutErrorMessage, isTransientHttpError, - sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js"; import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import { @@ -534,6 +536,12 @@ export async function runAgentTurnWithFallback(params: { const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message); const isTransientHttp = isTransientHttpError(message); + const isRateLimit = isRateLimitErrorMessage(message); + const isAuthError = isAuthErrorMessage(message); + const isBillingError = isBillingErrorMessage(message); + const isTimeoutError = isTimeoutErrorMessage(message); + const isOverloaded = isOverloadedErrorMessage(message); + if ( isCompactionFailure && !didResetAfterCompactionFailure && @@ -620,17 +628,30 @@ export async function runAgentTurnWithFallback(params: { } defaultRuntime.error(`Embedded agent failed before reply: ${message}`); - const safeMessage = isTransientHttp - ? sanitizeUserFacingText(message, { errorContext: true }) - : message; - const trimmedMessage = safeMessage.replace(/\.\s*$/, ""); - const fallbackText = isBilling - ? BILLING_ERROR_USER_MESSAGE - : isContextOverflow - ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model." - : isRoleOrderingError - ? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session." - : `⚠️ Agent failed before reply: ${trimmedMessage}.\nLogs: openclaw logs --follow`; + + let fallbackText: string; + + if (isContextOverflow) { + fallbackText = + "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."; + } else if (isRoleOrderingError) { + fallbackText = + "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."; + } else if (isRateLimit || isOverloaded) { + fallbackText = "The AI service is busy. Please wait a moment and try again."; + } else if (isAuthError) { + fallbackText = + "I couldn't connect to the AI service. Please verify your API key is configured correctly."; + } else if (isBillingError) { + fallbackText = + "I've reached my limit with the AI service. Please check your account balance and try again."; + } else if (isTimeoutError) { + fallbackText = + "The request timed out. Please try again, or start a fresh session with /new."; + } else { + fallbackText = + "Something unexpected happened. Try /new to start a fresh conversation, or try again in a moment."; + } return { kind: "final", diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index 6bebdc6a390..36fda14def4 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -1485,7 +1485,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const res = await run(); expect(res).toMatchObject({ - text: expect.stringContaining("Agent failed before reply"), + text: "Something unexpected happened. Try /new to start a fresh conversation, or try again in a moment.", }); expect(sessionStore.main).toBeDefined(); await expect(fs.access(transcriptPath)).resolves.toBeUndefined();