Skip to content

Commit 1c5edaf

Browse files
committed
refactor(agents): centralize auth error preview suppression
1 parent 3e0f59e commit 1c5edaf

11 files changed

Lines changed: 83 additions & 86 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Docs: https://docs.openclaw.ai
5252
- Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.
5353
- Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.
5454
- Codex/failover: classify `deactivated_workspace` as a permanent auth failure so configured fallback models can advance when a Codex workspace is deactivated. (#55893) Thanks @litang9.
55-
- Agents/embedded runner: classify HTML 401 provider responses as `auth_html_401` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce 401 HTML bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
55+
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
5656

5757
## 2026.5.20
5858

src/agents/pi-embedded-error-observation.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
buildApiErrorObservationFields,
55
buildTextObservationFields,
66
sanitizeForConsole,
7+
shouldSuppressRawErrorConsoleSuffix,
78
} from "./pi-embedded-error-observation.js";
89

910
const OBSERVATION_BEARER_TOKEN = "sk-redact-test-token";
@@ -182,6 +183,14 @@ describe("buildApiErrorObservationFields", () => {
182183
expect(observed.httpCode).toBe("401");
183184
expect(observed.providerRuntimeFailureKind).toBe("unclassified");
184185
});
186+
187+
it("centralizes raw console suffix suppression for auth failures", () => {
188+
expect(shouldSuppressRawErrorConsoleSuffix("auth_html")).toBe(true);
189+
expect(shouldSuppressRawErrorConsoleSuffix("auth_scope")).toBe(true);
190+
expect(shouldSuppressRawErrorConsoleSuffix("auth_refresh")).toBe(true);
191+
expect(shouldSuppressRawErrorConsoleSuffix("timeout")).toBe(false);
192+
expect(shouldSuppressRawErrorConsoleSuffix(undefined)).toBe(false);
193+
});
185194
});
186195

187196
describe("sanitizeForConsole", () => {

src/agents/pi-embedded-error-observation.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const OBSERVATION_EXTRA_REDACT_PATTERNS = [
2222
String.raw`"(?:api[-_]?key|api_key)"\s*:\s*"([^"]+)"`,
2323
String.raw`(?:\bCookie\b\s*[:=]\s*[^;=\s]+=|;\s*[^;=\s]+=)([^;\s\r\n]+)`,
2424
];
25+
const RAW_ERROR_CONSOLE_SUPPRESSED_FAILURE_KINDS = new Set<ProviderRuntimeFailureKind>([
26+
"auth_html",
27+
"auth_refresh",
28+
"auth_scope",
29+
]);
2530

2631
function resolveConfiguredRedactPatterns(): string[] {
2732
const configured = readLoggingConfig()?.redactPatterns;
@@ -76,6 +81,14 @@ function redactObservationText(text: string | undefined): string | undefined {
7681
});
7782
}
7883

84+
export function shouldSuppressRawErrorConsoleSuffix(
85+
providerRuntimeFailureKind?: ProviderRuntimeFailureKind,
86+
): boolean {
87+
return providerRuntimeFailureKind
88+
? RAW_ERROR_CONSOLE_SUPPRESSED_FAILURE_KINDS.has(providerRuntimeFailureKind)
89+
: false;
90+
}
91+
7992
function buildObservationFingerprint(params: {
8093
raw: string;
8194
requestId?: string;

src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -342,27 +342,17 @@ describe("formatAssistantErrorText", () => {
342342
);
343343
});
344344

345-
it("returns an HTML-401 auth message for HTML provider 401 auth failures", () => {
346-
// Cloudflare Access login pages, nginx basic-auth challenges, and
347-
// gateway login walls all return 401 with an HTML body. Before this fix
348-
// these were classified as `upstream_html` ("CDN blocked — retry"),
349-
// sending the wrong remediation to the user.
345+
it("returns re-authentication copy for HTML provider 401 auth failures", () => {
350346
const msg = makeAssistantError("401 <!DOCTYPE html><html><body>Unauthorized</body></html>");
351347
expect(formatAssistantErrorText(msg)).toBe(
352-
"Authentication failed with an HTML 401 response from the provider. Re-authenticate and verify your provider credentials.",
348+
"Authentication failed at the provider. Re-authenticate and verify your provider credentials and account access.",
353349
);
354350
});
355351

356-
it("does not return CDN-blocked copy for HTML 401 auth failures", () => {
357-
const msg = makeAssistantError("401 <!DOCTYPE html><html><body>Unauthorized</body></html>");
358-
expect(formatAssistantErrorText(msg)).not.toContain("CDN");
359-
expect(formatAssistantErrorText(msg)).not.toContain("blocked the request");
360-
});
361-
362352
it("returns an HTML-403 auth message for HTML provider auth failures", () => {
363353
const msg = makeAssistantError("403 <!DOCTYPE html><html><body>Access denied</body></html>");
364354
expect(formatAssistantErrorText(msg)).toBe(
365-
"Authentication failed with an HTML 403 response from the provider. Re-authenticate and verify your provider account access.",
355+
"Authentication failed at the provider. Re-authenticate and verify your provider credentials and account access.",
366356
);
367357
});
368358

src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1510,7 +1510,15 @@ describe("classifyProviderRuntimeFailureKind", () => {
15101510
classifyProviderRuntimeFailureKind(
15111511
"403 <!DOCTYPE html><html><body>Access denied</body></html>",
15121512
),
1513-
).toBe("auth_html_403");
1513+
).toBe("auth_html");
1514+
});
1515+
1516+
it("classifies HTML 401 auth failures", () => {
1517+
expect(
1518+
classifyProviderRuntimeFailureKind(
1519+
"401 <!DOCTYPE html><html><body>Unauthorized</body></html>",
1520+
),
1521+
).toBe("auth_html");
15141522
});
15151523

15161524
it("classifies proxy, dns, timeout, schema, sandbox, and replay failures", () => {

src/agents/pi-embedded-helpers/errors.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,7 @@ export type ProviderRuntimeFailureKind =
259259
| "refresh_contention"
260260
| "callback_timeout"
261261
| "callback_validation"
262-
| "auth_html_403"
263-
| "auth_html_401"
262+
| "auth_html"
264263
| "upstream_html"
265264
| "proxy"
266265
| "rate_limit"
@@ -977,7 +976,7 @@ export function classifyProviderRuntimeFailureKind(
977976
return "proxy";
978977
}
979978
if (message && isHtmlErrorResponse(message, status)) {
980-
return status === 401 ? "auth_html_401" : status === 403 ? "auth_html_403" : "upstream_html";
979+
return status === 401 || status === 403 ? "auth_html" : "upstream_html";
981980
}
982981
const failoverClassification = classifyFailoverSignal({
983982
...normalizedSignal,
@@ -1091,17 +1090,10 @@ export function formatAssistantErrorText(
10911090
);
10921091
}
10931092

1094-
if (providerRuntimeFailureKind === "auth_html_401") {
1093+
if (providerRuntimeFailureKind === "auth_html") {
10951094
return (
1096-
"Authentication failed with an HTML 401 response from the provider. " +
1097-
"Re-authenticate and verify your provider credentials."
1098-
);
1099-
}
1100-
1101-
if (providerRuntimeFailureKind === "auth_html_403") {
1102-
return (
1103-
"Authentication failed with an HTML 403 response from the provider. " +
1104-
"Re-authenticate and verify your provider account access."
1095+
"Authentication failed at the provider. " +
1096+
"Re-authenticate and verify your provider credentials and account access."
11051097
);
11061098
}
11071099

src/agents/pi-embedded-helpers/provider-error-patterns.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ describe("Cloudflare / CDN HTML error page classification (#67517)", () => {
210210
);
211211
});
212212

213-
it("classifies 403 HTML runtime failures as auth_html_403", () => {
213+
it("classifies 403 HTML runtime failures as auth_html", () => {
214214
expect(classifyProviderRuntimeFailureKind({ status: 403, message: html403 })).toBe(
215-
"auth_html_403",
215+
"auth_html",
216216
);
217217
});
218218

src/agents/pi-embedded-runner/run/failover-observation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ describe("createFailoverDecisionLogger", () => {
159159
logDecision("rotate_profile");
160160

161161
const observation = firstWarnDetails(warnSpy);
162-
expect(observation.providerRuntimeFailureKind).toBe("auth_html_401");
162+
expect(observation.providerRuntimeFailureKind).toBe("auth_html");
163163
expect(observation.rawErrorPreview).toBe(
164164
"401 <!DOCTYPE html><html><body>Unauthorized</body></html>",
165165
);

src/agents/pi-embedded-runner/run/failover-observation.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AuthProfileFailureReason } from "../../auth-profiles.js";
33
import {
44
buildApiErrorObservationFields,
55
sanitizeForConsole,
6+
shouldSuppressRawErrorConsoleSuffix,
67
} from "../../pi-embedded-error-observation.js";
78
import type { FailoverReason } from "../../pi-embedded-helpers.js";
89
import { log } from "../logger.js";
@@ -58,13 +59,9 @@ export function createFailoverDecisionLogger(
5859
return (decision, extra) => {
5960
const observedError = buildApiErrorObservationFields(normalizedBase.rawError);
6061
const safeRawErrorPreview = sanitizeForConsole(observedError.rawErrorPreview);
61-
const shouldSuppressRawErrorConsoleSuffix =
62-
observedError.providerRuntimeFailureKind === "auth_html_401" ||
63-
observedError.providerRuntimeFailureKind === "auth_html_403" ||
64-
observedError.providerRuntimeFailureKind === "auth_scope" ||
65-
observedError.providerRuntimeFailureKind === "auth_refresh";
6662
const rawErrorConsoleSuffix =
67-
safeRawErrorPreview && !shouldSuppressRawErrorConsoleSuffix
63+
safeRawErrorPreview &&
64+
!shouldSuppressRawErrorConsoleSuffix(observedError.providerRuntimeFailureKind)
6865
? ` rawError=${safeRawErrorPreview}`
6966
: "";
7067
log.warn("embedded run failover decision", {

src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -206,53 +206,44 @@ describe("handleAgentEnd", () => {
206206
expect(meta.httpCode).toBe("401");
207207
});
208208

209-
it("omits raw HTML auth bodies from consoleMessage for HTML 403 auth failures", async () => {
210-
const ctx = createContext({
211-
role: "assistant",
212-
stopReason: "error",
213-
provider: "openai-codex",
214-
model: "gpt-5.4",
209+
it.each([
210+
{
215211
errorMessage: "403 <!DOCTYPE html><html><body>Access denied</body></html>",
216-
content: [{ type: "text", text: "" }],
217-
});
218-
219-
await handleAgentEnd(ctx);
220-
221-
const meta = firstWarnMeta(ctx);
222-
expect(meta.providerRuntimeFailureKind).toBe("auth_html_403");
223-
expect(meta.rawErrorPreview).toBe("403 <!DOCTYPE html><html><body>Access denied</body></html>");
224-
expect(meta.error).toBe(
225-
"Authentication failed with an HTML 403 response from the provider. Re-authenticate and verify your provider account access.",
226-
);
227-
const consoleMsg = typeof meta.consoleMessage === "string" ? meta.consoleMessage : "";
228-
expect(consoleMsg).not.toContain("rawError=");
229-
expect(consoleMsg).not.toContain("<html>");
230-
});
231-
232-
it("omits raw HTML auth bodies from consoleMessage for HTML 401 auth failures", async () => {
233-
const ctx = createContext({
234-
role: "assistant",
235-
stopReason: "error",
236-
provider: "openai-codex",
237-
model: "gpt-5.4",
212+
expectedError:
213+
"Authentication failed at the provider. Re-authenticate and verify your provider credentials and account access.",
214+
expectedKind: "auth_html",
215+
expectedPreview: "403 <!DOCTYPE html><html><body>Access denied</body></html>",
216+
},
217+
{
238218
errorMessage: "401 <!DOCTYPE html><html><body>Unauthorized</body></html>",
239-
content: [{ type: "text", text: "" }],
240-
});
219+
expectedError:
220+
"Authentication failed at the provider. Re-authenticate and verify your provider credentials and account access.",
221+
expectedKind: "auth_html",
222+
expectedPreview: "401 <!DOCTYPE html><html><body>Unauthorized</body></html>",
223+
},
224+
])(
225+
"omits raw HTML auth bodies from consoleMessage for $expectedKind failures",
226+
async ({ errorMessage, expectedError, expectedKind, expectedPreview }) => {
227+
const ctx = createContext({
228+
role: "assistant",
229+
stopReason: "error",
230+
provider: "openai-codex",
231+
model: "gpt-5.4",
232+
errorMessage,
233+
content: [{ type: "text", text: "" }],
234+
});
241235

242-
await handleAgentEnd(ctx);
236+
await handleAgentEnd(ctx);
243237

244-
const meta = firstWarnMeta(ctx);
245-
expect(meta.providerRuntimeFailureKind).toBe("auth_html_401");
246-
expect(meta.rawErrorPreview).toBe(
247-
"401 <!DOCTYPE html><html><body>Unauthorized</body></html>",
248-
);
249-
expect(meta.error).toBe(
250-
"Authentication failed with an HTML 401 response from the provider. Re-authenticate and verify your provider credentials.",
251-
);
252-
const consoleMsg = typeof meta.consoleMessage === "string" ? meta.consoleMessage : "";
253-
expect(consoleMsg).not.toContain("rawError=");
254-
expect(consoleMsg).not.toContain("<html>");
255-
});
238+
const meta = firstWarnMeta(ctx);
239+
expect(meta.providerRuntimeFailureKind).toBe(expectedKind);
240+
expect(meta.rawErrorPreview).toBe(expectedPreview);
241+
expect(meta.error).toBe(expectedError);
242+
const consoleMsg = typeof meta.consoleMessage === "string" ? meta.consoleMessage : "";
243+
expect(consoleMsg).not.toContain("rawError=");
244+
expect(consoleMsg).not.toContain("<html>");
245+
},
246+
);
256247

257248
it("keeps non-error run-end logging on debug only", async () => {
258249
const ctx = createContext(undefined);

0 commit comments

Comments
 (0)