mirror of https://github.com/openclaw/openclaw.git
fix: clamp copilot auth refresh overflow (#55360) (thanks @michael-abdo)
This commit is contained in:
parent
f0c1057f68
commit
85b169c453
|
|
@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Talk/macOS: stop direct system-voice failures from replaying system speech, use app-locale fallback for shared watchdog timing, and add regression coverage for the macOS fallback route and language-aware timeout policy. (#53511) thanks @hongsw.
|
||||
- Discord/gateway cleanup: keep late Carbon reconnect-exhausted errors suppressed through startup/dispose cleanup so Discord monitor shutdown no longer crashes on late gateway close events. (#55373) Thanks @Takhoffman.
|
||||
- Discord/gateway shutdown: treat expected reconnect-exhausted events during intentional lifecycle stop as clean shutdowns so startup-abort cleanup no longer surfaces false gateway failures. (#55324) Thanks @joelnishanth.
|
||||
- GitHub Copilot/auth refresh: treat large `expires_at` values as seconds epochs and clamp far-future runtime auth refresh timers so Copilot token refresh cannot fall into a `setTimeout` overflow hot loop. (#55360) Thanks @michael-abdo.
|
||||
|
||||
## 2026.3.24
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveCopilotApiToken } from "./github-copilot-token.js";
|
||||
|
||||
describe("resolveCopilotApiToken", () => {
|
||||
it("treats 11-digit expires_at values as seconds epochs", async () => {
|
||||
const fetchImpl = vi.fn(async () => ({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
token: "copilot-token",
|
||||
expires_at: 12_345_678_901,
|
||||
}),
|
||||
}));
|
||||
|
||||
const result = await resolveCopilotApiToken({
|
||||
githubToken: "github-token",
|
||||
cachePath: "/tmp/github-copilot-token-test.json",
|
||||
loadJsonFileImpl: () => undefined,
|
||||
saveJsonFileImpl: () => undefined,
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(result.expiresAt).toBe(12_345_678_901_000);
|
||||
});
|
||||
});
|
||||
|
|
@ -36,15 +36,16 @@ function parseCopilotTokenResponse(value: unknown): {
|
|||
}
|
||||
|
||||
// GitHub returns a unix timestamp (seconds), but we defensively accept ms too.
|
||||
// Use a 1e11 threshold so large seconds-epoch values are not misread as ms.
|
||||
let expiresAtMs: number;
|
||||
if (typeof expiresAt === "number" && Number.isFinite(expiresAt)) {
|
||||
expiresAtMs = expiresAt > 10_000_000_000 ? expiresAt : expiresAt * 1000;
|
||||
expiresAtMs = expiresAt < 100_000_000_000 ? expiresAt * 1000 : expiresAt;
|
||||
} else if (typeof expiresAt === "string" && expiresAt.trim().length > 0) {
|
||||
const parsed = Number.parseInt(expiresAt, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
throw new Error("Copilot token response has invalid expires_at");
|
||||
}
|
||||
expiresAtMs = parsed > 10_000_000_000 ? parsed : parsed * 1000;
|
||||
expiresAtMs = parsed < 100_000_000_000 ? parsed * 1000 : parsed;
|
||||
} else {
|
||||
throw new Error("Copilot token response missing expires_at");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import {
|
|||
parseImageSizeError,
|
||||
pickFallbackThinkingLevel,
|
||||
} from "../pi-embedded-helpers.js";
|
||||
import { clampRuntimeAuthRefreshDelayMs } from "../runtime-auth-refresh.js";
|
||||
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
|
||||
import { isLikelyMutatingToolName } from "../tool-mutation.js";
|
||||
import { derivePromptTokens, normalizeUsage, type UsageLike } from "../usage.js";
|
||||
|
|
@ -507,7 +508,11 @@ export async function runEmbeddedPiAgent(
|
|||
clearRuntimeAuthRefreshTimer();
|
||||
const now = Date.now();
|
||||
const refreshAt = runtimeAuthState.expiresAt - RUNTIME_AUTH_REFRESH_MARGIN_MS;
|
||||
const delayMs = Math.max(RUNTIME_AUTH_REFRESH_MIN_DELAY_MS, refreshAt - now);
|
||||
const delayMs = clampRuntimeAuthRefreshDelayMs({
|
||||
refreshAt,
|
||||
now,
|
||||
minDelayMs: RUNTIME_AUTH_REFRESH_MIN_DELAY_MS,
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
if (runtimeAuthRefreshCancelled) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { clampRuntimeAuthRefreshDelayMs } from "./runtime-auth-refresh.js";
|
||||
|
||||
describe("clampRuntimeAuthRefreshDelayMs", () => {
|
||||
it("clamps far-future refresh delays to a timer-safe ceiling", () => {
|
||||
expect(
|
||||
clampRuntimeAuthRefreshDelayMs({
|
||||
refreshAt: 12_345_678_901_000,
|
||||
now: 0,
|
||||
minDelayMs: 60_000,
|
||||
}),
|
||||
).toBe(2_147_483_647);
|
||||
});
|
||||
|
||||
it("still respects the configured minimum delay", () => {
|
||||
expect(
|
||||
clampRuntimeAuthRefreshDelayMs({
|
||||
refreshAt: 1_000,
|
||||
now: 900,
|
||||
minDelayMs: 60_000,
|
||||
}),
|
||||
).toBe(60_000);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
const MAX_SAFE_TIMEOUT_MS = 2_147_483_647;
|
||||
|
||||
export function clampRuntimeAuthRefreshDelayMs(params: {
|
||||
refreshAt: number;
|
||||
now: number;
|
||||
minDelayMs: number;
|
||||
}): number {
|
||||
return Math.min(MAX_SAFE_TIMEOUT_MS, Math.max(params.minDelayMs, params.refreshAt - params.now));
|
||||
}
|
||||
Loading…
Reference in New Issue