fix(cron): propagate auth-profile resolution to isolated sessions (#20624) (#20689)

This commit is contained in:
大猫子 2026-02-23 01:45:03 +08:00 committed by GitHub
parent 3a19b0201c
commit 91944ede4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 8 deletions

View File

@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
- Providers/OpenRouter: preserve stored session provider when model IDs are vendor-prefixed (for example, `anthropic/...`) so follow-up turns do not incorrectly route to direct provider APIs. (#22753) Thanks @dndodson.
- Providers/OpenRouter: preserve the required `openrouter/` prefix for OpenRouter-native model IDs during model-ref normalization. (#12942) Thanks @omair445.
- Providers/OpenRouter: pass through provider routing parameters from model params.provider to OpenRouter request payloads for provider selection controls. (#17148) Thanks @carrotRakko.
- Cron/Auth: propagate auth-profile resolution to isolated cron sessions so provider API keys are resolved the same way as main sessions, fixing 401 errors when using providers configured via auth-profiles. (#20689) Thanks @lailoo.
- Telegram/Webhook: keep webhook monitors alive until gateway abort signals fire, preventing false channel exits and immediate webhook auto-restart loops.
- Telegram/Polling: retry recoverable setup-time network failures in monitor startup and await runner teardown before retry to avoid overlapping polling sessions.
- Telegram/Polling: clear Telegram webhooks (`deleteWebhook`) before starting long-poll `getUpdates`, including retry handling for transient cleanup failures.

View File

@ -0,0 +1,117 @@
import "./isolated-agent.mocks.js";
import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
import { makeCfg, makeJob, withTempCronHome } from "./isolated-agent.test-harness.js";
import { setupIsolatedAgentTurnMocks } from "./isolated-agent.test-setup.js";
describe("runCronIsolatedAgentTurn auth profile propagation (#20624)", () => {
beforeEach(() => {
setupIsolatedAgentTurnMocks({ fast: true });
});
it("passes authProfileId to runEmbeddedPiAgent when auth profiles exist", async () => {
await withTempCronHome(async (home) => {
// 1. Write session store
const sessionsDir = path.join(home, ".openclaw", "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
const storePath = path.join(sessionsDir, "sessions.json");
await fs.writeFile(
storePath,
JSON.stringify(
{
"agent:main:main": {
sessionId: "main-session",
updatedAt: Date.now(),
lastProvider: "webchat",
lastTo: "",
},
},
null,
2,
),
"utf-8",
);
// 2. Write auth-profiles.json in the agent directory
// resolveAgentDir returns <stateDir>/agents/main/agent
// stateDir = <home>/.openclaw
const agentDir = path.join(home, ".openclaw", "agents", "main", "agent");
await fs.mkdir(agentDir, { recursive: true });
await fs.writeFile(
path.join(agentDir, "auth-profiles.json"),
JSON.stringify({
version: 1,
profiles: {
"openrouter:default": {
type: "api_key",
provider: "openrouter",
key: "sk-or-test-key-12345",
},
},
order: {
openrouter: ["openrouter:default"],
},
}),
"utf-8",
);
// 3. Mock runEmbeddedPiAgent to return ok
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
payloads: [{ text: "done" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "openrouter", model: "kimi-k2.5" },
},
});
// 4. Run cron isolated agent turn with openrouter model
const cfg = makeCfg(home, storePath, {
agents: {
defaults: {
model: { primary: "openrouter/moonshotai/kimi-k2.5" },
workspace: path.join(home, "openclaw"),
},
},
});
const res = await runCronIsolatedAgentTurn({
cfg,
deps: {
sendMessageSlack: vi.fn(),
sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(),
sendMessageDiscord: vi.fn(),
sendMessageSignal: vi.fn(),
sendMessageIMessage: vi.fn(),
},
job: makeJob({ kind: "agentTurn", message: "check status", deliver: false }),
message: "check status",
sessionKey: "cron:job-1",
lane: "cron",
});
expect(res.status).toBe("ok");
expect(vi.mocked(runEmbeddedPiAgent)).toHaveBeenCalledTimes(1);
// 5. Check that authProfileId was passed
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0] as {
authProfileId?: string;
authProfileIdSource?: string;
};
console.log(`authProfileId passed to runEmbeddedPiAgent: ${callArgs?.authProfileId}`);
console.log(`authProfileIdSource passed: ${callArgs?.authProfileIdSource}`);
if (!callArgs?.authProfileId) {
console.log("❌ BUG CONFIRMED: isolated cron session does NOT pass authProfileId");
console.log(" This causes 401 errors when using providers that require auth profiles");
}
// This assertion will FAIL on main — proving the bug
expect(callArgs?.authProfileId).toBe("openrouter:default");
});
});
});

View File

@ -32,14 +32,20 @@ vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
}));
vi.mock("../../agents/model-selection.js", () => ({
getModelRefStatus: vi.fn().mockReturnValue({ allowed: false }),
isCliProvider: vi.fn().mockReturnValue(false),
resolveAllowedModelRef: vi.fn().mockReturnValue({ ref: { provider: "openai", model: "gpt-4" } }),
resolveConfiguredModelRef: vi.fn().mockReturnValue({ provider: "openai", model: "gpt-4" }),
resolveHooksGmailModel: vi.fn().mockReturnValue(null),
resolveThinkingDefault: vi.fn().mockReturnValue(undefined),
}));
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
return {
...actual,
getModelRefStatus: vi.fn().mockReturnValue({ allowed: false }),
isCliProvider: vi.fn().mockReturnValue(false),
resolveAllowedModelRef: vi
.fn()
.mockReturnValue({ ref: { provider: "openai", model: "gpt-4" } }),
resolveConfiguredModelRef: vi.fn().mockReturnValue({ provider: "openai", model: "gpt-4" }),
resolveHooksGmailModel: vi.fn().mockReturnValue(null),
resolveThinkingDefault: vi.fn().mockReturnValue(undefined),
};
});
vi.mock("../../agents/model-fallback.js", () => ({
runWithModelFallback: vi.fn().mockResolvedValue({

View File

@ -5,6 +5,7 @@ import {
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
import { runCliAgent } from "../../agents/cli-runner.js";
import { getCliSessionId, setCliSessionId } from "../../agents/cli-session.js";
import { lookupContextTokens } from "../../agents/context.js";
@ -432,6 +433,21 @@ export async function runCronIsolatedAgentTurn(params: {
cronSession.sessionEntry.systemSent = true;
await persistSessionEntry();
// Resolve auth profile for the session, mirroring the inbound auto-reply path
// (get-reply-run.ts). Without this, isolated cron sessions fall back to env-var
// auth which may not match the configured auth-profiles, causing 401 errors.
const authProfileId = await resolveSessionAuthProfileOverride({
cfg: cfgWithAgentDefaults,
provider,
agentDir,
sessionEntry: cronSession.sessionEntry,
sessionStore: cronSession.store,
sessionKey: agentSessionKey,
storePath: cronSession.storePath,
isNewSession: cronSession.isNewSession,
});
const authProfileIdSource = cronSession.sessionEntry.authProfileOverrideSource;
let runResult: Awaited<ReturnType<typeof runEmbeddedPiAgent>>;
let fallbackProvider = provider;
let fallbackModel = model;
@ -490,6 +506,8 @@ export async function runCronIsolatedAgentTurn(params: {
lane: params.lane ?? "cron",
provider: providerOverride,
model: modelOverride,
authProfileId,
authProfileIdSource,
thinkLevel,
verboseLevel: resolvedVerboseLevel,
timeoutMs,