mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-12 20:52:54 +00:00
fix(agents): bypass stale auth for plugin harnesses
Explicit non-Codex plugin harness runtimes now bypass stale OpenClaw provider auth cooldowns before harness startup, while Codex/OpenClaw and missing-harness gates remain fail-closed. Fixes #85105.
This commit is contained in:
committed by
GitHub
parent
68b4dd1816
commit
b7d363cadf
@@ -21,6 +21,8 @@ import type { EmbeddedAgentRunResult } from "./embedded-agent-runner/types.js";
|
||||
import { FailoverError } from "./failover-error.js";
|
||||
import { resetFallbackSkipCacheForTest } from "./fallback-skip-cache.js";
|
||||
import { MissingAgentHarnessError } from "./harness/errors.js";
|
||||
import { clearAgentHarnesses, registerAgentHarness } from "./harness/registry.js";
|
||||
import type { AgentHarness } from "./harness/types.js";
|
||||
import { LiveSessionModelSwitchError } from "./live-model-switch-error.js";
|
||||
import {
|
||||
FallbackSummaryError,
|
||||
@@ -191,6 +193,7 @@ afterAll(() => {
|
||||
|
||||
function resetModelFallbackTestState(): void {
|
||||
resetFallbackSkipCacheForTest();
|
||||
clearAgentHarnesses();
|
||||
authRuntimeMock.clear();
|
||||
authRuntimeMock.runtime.ensureAuthProfileStore.mockClear();
|
||||
authRuntimeMock.runtime.loadAuthProfileStoreForRuntime.mockClear();
|
||||
@@ -971,6 +974,68 @@ describe("runWithModelFallback", () => {
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lets external plugin harnesses bypass stale provider auth cooldowns", async () => {
|
||||
const cfg = makeCfg({
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
fallbacks: ["openai/gpt-5.5"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/*": { agentRuntime: { id: "claude-tmux" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
registerAgentHarness(
|
||||
{
|
||||
id: "claude-tmux",
|
||||
label: "Claude tmux",
|
||||
supports: ({ provider }) =>
|
||||
provider === "anthropic" ? { supported: true } : { supported: false },
|
||||
runAttempt: vi.fn<AgentHarness["runAttempt"]>(async () => {
|
||||
throw new Error("fallback test should not invoke the harness runtime");
|
||||
}),
|
||||
},
|
||||
{ ownerPluginId: "claude-tmux-test" },
|
||||
);
|
||||
const tempDir = await makeAuthTempDir();
|
||||
setAuthRuntimeStore(tempDir, {
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
"anthropic:default": { type: "api_key", provider: "anthropic", key: "test-key" },
|
||||
"openai:default": { type: "api_key", provider: "openai", key: "test-key" },
|
||||
},
|
||||
usageStats: {
|
||||
"anthropic:default": {
|
||||
disabledUntil: Date.now() + 60_000,
|
||||
disabledReason: "billing",
|
||||
failureCounts: { rate_limit: 4 },
|
||||
},
|
||||
},
|
||||
});
|
||||
const run = vi.fn().mockImplementation(async (provider: string) => {
|
||||
if (provider === "anthropic") {
|
||||
return "external cli ok";
|
||||
}
|
||||
throw new Error(`unexpected provider: ${provider}`);
|
||||
});
|
||||
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
agentDir: tempDir,
|
||||
run,
|
||||
});
|
||||
|
||||
expect(result.result).toBe("external cli ok");
|
||||
expect(run).toHaveBeenCalledTimes(1);
|
||||
expect(run.mock.calls[0]).toEqual(["anthropic", "claude-sonnet-4-6"]);
|
||||
expect(result.attempts).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("lets configured CLI runtimes reach the run callback", async () => {
|
||||
const cfg = makeCfg({
|
||||
agents: {
|
||||
@@ -1781,9 +1846,9 @@ describe("runWithModelFallback", () => {
|
||||
{ provider: "openai", model: "gpt-4.1-mini" },
|
||||
{ provider: "tui-pty-mock", model: "gpt-5.5" },
|
||||
]);
|
||||
expect(providerModelNormalizationMock.normalizeProviderModelIdWithRuntime).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ provider: "tui-pty-mock" }),
|
||||
);
|
||||
expect(
|
||||
providerModelNormalizationMock.normalizeProviderModelIdWithRuntime,
|
||||
).not.toHaveBeenCalledWith(expect.objectContaining({ provider: "tui-pty-mock" }));
|
||||
});
|
||||
|
||||
it("keeps configured fallbacks before configured primary for duplicate provider model ids", () => {
|
||||
|
||||
@@ -439,18 +439,18 @@ function isCliAgentRuntime(runtime: string | undefined, cfg: OpenClawConfig | un
|
||||
return isCliRuntimeAlias(normalized) || isCliProvider(normalized, cfg);
|
||||
}
|
||||
|
||||
async function assertModelFallbackCandidateHarnessAvailable(
|
||||
async function resolveModelFallbackCandidateHarnessAuthPrecheck(
|
||||
params: ModelFallbackRuntimeContext & ModelCandidate,
|
||||
): Promise<void> {
|
||||
): Promise<{ skipsProviderAuthCooldown: boolean }> {
|
||||
if (!params.cfg) {
|
||||
return;
|
||||
return { skipsProviderAuthCooldown: false };
|
||||
}
|
||||
const agentHarnessRuntimeOverride = params.resolveAgentHarnessRuntimeOverride?.(
|
||||
params.provider,
|
||||
params.model,
|
||||
);
|
||||
if (isCliProvider(params.provider, params.cfg)) {
|
||||
return;
|
||||
return { skipsProviderAuthCooldown: false };
|
||||
}
|
||||
const agentRuntimeOverride = normalizeOptionalAgentRuntimeId(agentHarnessRuntimeOverride);
|
||||
const harnessPolicy = resolveAgentHarnessPolicy({
|
||||
@@ -469,28 +469,25 @@ async function assertModelFallbackCandidateHarnessAvailable(
|
||||
? "model"
|
||||
: harnessPolicy.runtimeSource;
|
||||
if (isCliAgentRuntime(agentRuntime, params.cfg)) {
|
||||
return;
|
||||
return { skipsProviderAuthCooldown: false };
|
||||
}
|
||||
if (
|
||||
agentRuntime === "auto" ||
|
||||
agentRuntime === "openclaw" ||
|
||||
(agentRuntime === "codex" && agentRuntimeSource === "implicit")
|
||||
) {
|
||||
return;
|
||||
if (agentRuntime === "openclaw") {
|
||||
return { skipsProviderAuthCooldown: false };
|
||||
}
|
||||
if (agentRuntime === "auto" || (agentRuntime === "codex" && agentRuntimeSource === "implicit")) {
|
||||
return { skipsProviderAuthCooldown: false };
|
||||
}
|
||||
await params.prepareAgentHarnessRuntime?.({
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
agentHarnessRuntimeOverride,
|
||||
});
|
||||
if (
|
||||
agentRuntime !== "auto" &&
|
||||
agentRuntime !== "openclaw" &&
|
||||
!(agentRuntime === "codex" && agentRuntimeSource === "implicit") &&
|
||||
!getRegisteredAgentHarness(agentRuntime)
|
||||
) {
|
||||
if (!getRegisteredAgentHarness(agentRuntime)) {
|
||||
throw new MissingAgentHarnessError(agentRuntime);
|
||||
}
|
||||
// Explicit non-Codex plugin harnesses own transport/auth; stale OpenClaw
|
||||
// provider cooldowns must not block the harness before it starts.
|
||||
return { skipsProviderAuthCooldown: agentRuntime !== "codex" };
|
||||
}
|
||||
|
||||
function resolveCandidateAttemptError(
|
||||
@@ -1275,7 +1272,7 @@ export async function runWithModelFallback<T>(
|
||||
|
||||
for (let i = 0; i < candidates.length; i += 1) {
|
||||
const candidate = candidates[i];
|
||||
await assertModelFallbackCandidateHarnessAvailable({
|
||||
const candidateHarnessAuth = await resolveModelFallbackCandidateHarnessAuthPrecheck({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -1342,7 +1339,7 @@ export async function runWithModelFallback<T>(
|
||||
let runOptions: ModelFallbackRunOptions | undefined;
|
||||
let attemptedDuringCooldown = false;
|
||||
let transientProbeProviderForAttempt: string | null = null;
|
||||
if (authRuntime && authStore) {
|
||||
if (authRuntime && authStore && !candidateHarnessAuth.skipsProviderAuthCooldown) {
|
||||
const profileIds = authRuntime.resolveAuthProfileOrder({
|
||||
cfg: params.cfg,
|
||||
store: authStore,
|
||||
|
||||
Reference in New Issue
Block a user