mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
[Fix] Warm provider auth off main thread (#86281)
* fix(agents): warm provider auth off main thread Signed-off-by: samzong <samzong.lu@gmail.com> * fix(agents): keep provider auth warm read-only * fix(ci): unblock provider auth landing * ci: serialize gateway watch artifact check * fix(ci): stabilize diffs viewer asset generation * fix(agents): avoid stale plugin auth warm results * fix(agents): keep partial auth warm cache --------- Signed-off-by: samzong <samzong.lu@gmail.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ const rootEntries = [
|
||||
"src/entry.ts!",
|
||||
"src/cli/daemon-cli.ts!",
|
||||
"src/agents/code-mode.worker.ts!",
|
||||
"src/agents/model-provider-auth.worker.ts!",
|
||||
"src/infra/kysely-node-sqlite.ts!",
|
||||
"src/infra/warning-filter.ts!",
|
||||
"src/infra/command-explainer/index.ts!",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,9 @@ export async function buildDiffsViewerRuntime(targetName) {
|
||||
target: "es2020",
|
||||
format: "esm",
|
||||
minify: true,
|
||||
define: {
|
||||
NaN: "Number.NaN",
|
||||
},
|
||||
legalComments: "none",
|
||||
outfile: outputPath,
|
||||
write: false,
|
||||
|
||||
@@ -80,6 +80,7 @@ const requiredPathGroups = [
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/compat.js",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/agents/model-provider-auth.worker.js",
|
||||
"dist/task-registry-control.runtime.js",
|
||||
"dist/telegram-ingress-worker.runtime.js",
|
||||
"dist/build-info.json",
|
||||
|
||||
@@ -52,6 +52,7 @@ export {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
getRuntimeAuthProfileStoreSnapshot,
|
||||
hasAnyAuthProfileStoreSource,
|
||||
loadAuthProfileStoreForSecretsRuntime,
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from "./persisted.js";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots as clearRuntimeAuthProfileStoreSnapshotsImpl,
|
||||
getRuntimeAuthProfileStoreSnapshot,
|
||||
getRuntimeAuthProfileStoreSnapshot as getRuntimeAuthProfileStoreSnapshotImpl,
|
||||
hasRuntimeAuthProfileStoreSnapshot,
|
||||
replaceRuntimeAuthProfileStoreSnapshots as replaceRuntimeAuthProfileStoreSnapshotsImpl,
|
||||
setRuntimeAuthProfileStoreSnapshot,
|
||||
@@ -150,8 +150,8 @@ function resolveRuntimeAuthProfileStore(
|
||||
): AuthProfileStore | null {
|
||||
const mainKey = resolveAuthStorePath(undefined);
|
||||
const requestedKey = resolveAuthStorePath(agentDir);
|
||||
const mainStore = getRuntimeAuthProfileStoreSnapshot(undefined);
|
||||
const requestedStore = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
const mainStore = getRuntimeAuthProfileStoreSnapshotImpl(undefined);
|
||||
const requestedStore = getRuntimeAuthProfileStoreSnapshotImpl(agentDir);
|
||||
|
||||
if (!agentDir || requestedKey === mainKey) {
|
||||
if (!mainStore) {
|
||||
@@ -899,6 +899,8 @@ export function ensureAuthProfileStore(
|
||||
externalCli?: ExternalCliAuthDiscovery;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
externalCliProfileIds?: Iterable<string>;
|
||||
readOnly?: boolean;
|
||||
syncExternalCli?: boolean;
|
||||
},
|
||||
): AuthProfileStore {
|
||||
const externalCli = resolveExternalCliOverlayOptions(options);
|
||||
@@ -921,7 +923,12 @@ export function ensureAuthProfileStore(
|
||||
|
||||
export function ensureAuthProfileStoreWithoutExternalProfiles(
|
||||
agentDir?: string,
|
||||
options?: { allowKeychainPrompt?: boolean; resolveLegacyOAuthSidecars?: boolean },
|
||||
options?: {
|
||||
allowKeychainPrompt?: boolean;
|
||||
readOnly?: boolean;
|
||||
resolveLegacyOAuthSidecars?: boolean;
|
||||
syncExternalCli?: boolean;
|
||||
},
|
||||
): AuthProfileStore {
|
||||
const effectiveOptions: LoadAuthProfileStoreOptions = {
|
||||
...options,
|
||||
@@ -1015,6 +1022,12 @@ export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthPro
|
||||
|
||||
export { hasAnyAuthProfileStoreSource } from "./source-check.js";
|
||||
|
||||
export function getRuntimeAuthProfileStoreSnapshot(
|
||||
agentDir?: string,
|
||||
): AuthProfileStore | undefined {
|
||||
return getRuntimeAuthProfileStoreSnapshotImpl(agentDir);
|
||||
}
|
||||
|
||||
export function replaceRuntimeAuthProfileStoreSnapshots(
|
||||
entries: Array<{ agentDir?: string; store: AuthProfileStore }>,
|
||||
): void {
|
||||
|
||||
@@ -65,6 +65,7 @@ export type ProviderCredentialPrecedence = "profile-first" | "env-first";
|
||||
export type RuntimeProviderAuthLookup = {
|
||||
envApiKey: Pick<EnvApiKeyLookupOptions, "aliasMap" | "candidateMap" | "authEvidenceMap">;
|
||||
syntheticAuthProviderRefs?: readonly string[];
|
||||
syntheticAuthProviderRefsComplete?: boolean;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("model-auth");
|
||||
@@ -125,6 +126,7 @@ export function createRuntimeProviderAuthLookup(params: {
|
||||
syntheticAuthProviderRefs: syntheticAuthProviderRefs?.complete
|
||||
? syntheticAuthProviderRefs.refs
|
||||
: undefined,
|
||||
syntheticAuthProviderRefsComplete: syntheticAuthProviderRefs?.complete,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles.js";
|
||||
import type { ModelCatalogEntry } from "./model-catalog.types.js";
|
||||
|
||||
const modelCatalogMocks = vi.hoisted(() => ({
|
||||
@@ -14,6 +19,7 @@ const modelAuthMocks = vi.hoisted(() => ({
|
||||
authEvidenceMap: {},
|
||||
},
|
||||
syntheticAuthProviderRefs: [],
|
||||
syntheticAuthProviderRefsComplete: true,
|
||||
})),
|
||||
hasRuntimeAvailableProviderAuth:
|
||||
vi.fn<
|
||||
@@ -31,6 +37,9 @@ const authProfilesMocks = vi.hoisted(() => ({
|
||||
ensureAuthProfileStoreWithoutExternalProfiles: vi.fn(() => ({ profiles: {} })),
|
||||
externalCliDiscoveryForProviders: vi.fn(() => ({}) as never),
|
||||
externalCliDiscoveryForProviderAuth: vi.fn(() => ({}) as never),
|
||||
getRuntimeAuthProfileStoreSnapshot: vi.fn<(agentDir?: string) => AuthProfileStore | undefined>(
|
||||
() => undefined,
|
||||
),
|
||||
listProfilesForProvider: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
@@ -49,6 +58,7 @@ vi.mock("./auth-profiles.js", () => ({
|
||||
authProfilesMocks.ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
externalCliDiscoveryForProviders: authProfilesMocks.externalCliDiscoveryForProviders,
|
||||
externalCliDiscoveryForProviderAuth: authProfilesMocks.externalCliDiscoveryForProviderAuth,
|
||||
getRuntimeAuthProfileStoreSnapshot: authProfilesMocks.getRuntimeAuthProfileStoreSnapshot,
|
||||
listProfilesForProvider: authProfilesMocks.listProfilesForProvider,
|
||||
}));
|
||||
|
||||
@@ -59,15 +69,18 @@ vi.mock("./workspace.js", () => ({
|
||||
vi.mock("./agent-scope-config.js", () => ({
|
||||
listAgentIds: () => ["default"],
|
||||
resolveAgentDir: () => "/warm/default-agent",
|
||||
resolveDefaultAgentDir: () => "/warm/default-agent",
|
||||
resolveAgentWorkspaceDir: () => "/warm/default-workspace",
|
||||
resolveDefaultAgentId: () => "default",
|
||||
}));
|
||||
|
||||
const {
|
||||
clearCurrentProviderAuthState,
|
||||
buildCurrentProviderAuthStateSnapshot,
|
||||
createProviderAuthChecker,
|
||||
hasAuthForModelProvider,
|
||||
warmCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread,
|
||||
} = await import("./model-provider-auth.js");
|
||||
|
||||
describe("prepared provider auth state", () => {
|
||||
@@ -109,6 +122,62 @@ describe("prepared provider auth state", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("disables persisted auth-store sync for read-only warm snapshots", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const externalCli = { mode: "scoped" };
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
{ id: "gpt", name: "gpt", provider: "openai" },
|
||||
]);
|
||||
authProfilesMocks.externalCliDiscoveryForProviders.mockReturnValue(externalCli as never);
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(false);
|
||||
|
||||
await buildCurrentProviderAuthStateSnapshot(cfg, { readOnlyAuthStore: true });
|
||||
|
||||
expect(authProfilesMocks.ensureAuthProfileStore).toHaveBeenCalledWith("/warm/default-agent", {
|
||||
config: cfg,
|
||||
externalCli,
|
||||
readOnly: true,
|
||||
syncExternalCli: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not cache false worker answers for process-local plugin synthetic auth", async () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
"plugin-provider": {
|
||||
api: "plugin-api",
|
||||
baseUrl: "https://example.com/v1",
|
||||
models: [{ id: "plugin-model", name: "Plugin Model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
{ id: "plugin-model", name: "Plugin Model", provider: "plugin-provider" },
|
||||
]);
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(false);
|
||||
|
||||
const snapshot = await buildCurrentProviderAuthStateSnapshot(cfg, {
|
||||
runtimeAuthLookups: new Map([
|
||||
[
|
||||
"default",
|
||||
{
|
||||
envApiKey: {
|
||||
aliasMap: {},
|
||||
candidateMap: {},
|
||||
authEvidenceMap: {},
|
||||
},
|
||||
syntheticAuthProviderRefs: ["plugin-api"],
|
||||
syntheticAuthProviderRefsComplete: true,
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(snapshot.agents[0]?.providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("hasAuthForModelProvider returns the prepared answer after warm and falls through to compute after clear", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
@@ -326,4 +395,198 @@ describe("prepared provider auth state", () => {
|
||||
await expect(hasAuthForModelProvider({ provider: "openai", cfg })).resolves.toBe(true);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("publishes provider auth state produced by the off-main-thread warm runner", async () => {
|
||||
const cfg = { gateway: { port: 18789 } } as OpenClawConfig;
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
{ id: "gpt", name: "gpt", provider: "openai" },
|
||||
]);
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(true);
|
||||
const snapshot = await buildCurrentProviderAuthStateSnapshot(cfg);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
|
||||
clearCurrentProviderAuthState();
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockClear();
|
||||
const runWorker = vi.fn(async () => snapshot);
|
||||
await warmCurrentProviderAuthStateOffMainThread(cfg, { runWorker });
|
||||
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(false);
|
||||
await expect(hasAuthForModelProvider({ provider: "openai", cfg })).resolves.toBe(true);
|
||||
const runtimeAuthLookup =
|
||||
modelAuthMocks.createRuntimeProviderAuthLookup.mock.results.at(-1)?.value;
|
||||
expect(runWorker).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
runtimeAuthLookups: [{ agentId: "default", lookup: runtimeAuthLookup }],
|
||||
timeoutMs: 120_000,
|
||||
isCancelled: expect.any(Function),
|
||||
workerUrl: undefined,
|
||||
});
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes runtime auth profile snapshots to the off-main-thread warm runner", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const store = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
runtime: {
|
||||
type: "api_key" as const,
|
||||
provider: "openai",
|
||||
key: "test-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
authProfilesMocks.getRuntimeAuthProfileStoreSnapshot.mockImplementation((agentDir) =>
|
||||
agentDir === "/warm/default-agent" ? store : undefined,
|
||||
);
|
||||
const snapshot = {
|
||||
agents: [
|
||||
{
|
||||
agentId: "default",
|
||||
configFingerprint: "fingerprint",
|
||||
providers: [["openai", true] as [string, boolean]],
|
||||
},
|
||||
],
|
||||
};
|
||||
const runWorker = vi.fn(async () => snapshot);
|
||||
|
||||
await warmCurrentProviderAuthStateOffMainThread(cfg, { runWorker });
|
||||
|
||||
const runtimeAuthLookup =
|
||||
modelAuthMocks.createRuntimeProviderAuthLookup.mock.results.at(-1)?.value;
|
||||
expect(runWorker).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
runtimeAuthStores: [
|
||||
{
|
||||
agentDir: "/warm/default-agent",
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
runtime: {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
runtimeAuthLookups: [{ agentId: "default", lookup: runtimeAuthLookup }],
|
||||
timeoutMs: 120_000,
|
||||
isCancelled: expect.any(Function),
|
||||
workerUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps off-main-thread warm partial when plugin synthetic auth lookup is incomplete", async () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
authProfilesMocks.getRuntimeAuthProfileStoreSnapshot.mockReturnValue(undefined);
|
||||
modelAuthMocks.createRuntimeProviderAuthLookup.mockReturnValueOnce({
|
||||
envApiKey: {
|
||||
aliasMap: {},
|
||||
candidateMap: {},
|
||||
authEvidenceMap: {},
|
||||
},
|
||||
syntheticAuthProviderRefs: [],
|
||||
syntheticAuthProviderRefsComplete: false,
|
||||
});
|
||||
const runWorker = vi.fn(async () => ({ agents: [] }));
|
||||
|
||||
await warmCurrentProviderAuthStateOffMainThread(cfg, { runWorker });
|
||||
|
||||
expect(runWorker).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
runtimeAuthLookups: [
|
||||
{
|
||||
agentId: "default",
|
||||
lookup: {
|
||||
envApiKey: {
|
||||
aliasMap: {},
|
||||
candidateMap: {},
|
||||
authEvidenceMap: {},
|
||||
},
|
||||
syntheticAuthProviderRefs: [],
|
||||
syntheticAuthProviderRefsComplete: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
omitFalseProviderAuth: true,
|
||||
timeoutMs: 120_000,
|
||||
isCancelled: expect.any(Function),
|
||||
workerUrl: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("terminates the off-main-thread warm worker when cancellation fires", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-provider-auth-worker-"));
|
||||
const workerPath = path.join(tempDir, "slow-worker.mjs");
|
||||
const markerPath = path.join(tempDir, "worker-finished");
|
||||
await fs.writeFile(
|
||||
workerPath,
|
||||
`
|
||||
import fs from "node:fs";
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
setTimeout(() => {
|
||||
fs.writeFileSync(workerData.cfg.markerPath, "finished");
|
||||
parentPort.postMessage({
|
||||
status: "ok",
|
||||
snapshot: {
|
||||
agents: [{
|
||||
agentId: "default",
|
||||
configFingerprint: "fingerprint",
|
||||
providers: [["openai", true]]
|
||||
}]
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
`,
|
||||
);
|
||||
let cancelled = false;
|
||||
|
||||
try {
|
||||
const warmPromise = warmCurrentProviderAuthStateOffMainThread(
|
||||
{ markerPath } as unknown as OpenClawConfig,
|
||||
{
|
||||
isCancelled: () => cancelled,
|
||||
timeoutMs: 5_000,
|
||||
workerUrl: pathToFileURL(workerPath),
|
||||
},
|
||||
);
|
||||
await Promise.resolve();
|
||||
cancelled = true;
|
||||
await warmPromise;
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
||||
await expect(fs.access(markerPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not publish an off-main-thread warm after the prepared auth state is cleared", async () => {
|
||||
const cfg = { gateway: { port: 18789 } } as OpenClawConfig;
|
||||
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
|
||||
{ id: "gpt", name: "gpt", provider: "openai" },
|
||||
]);
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(true);
|
||||
const snapshot = await buildCurrentProviderAuthStateSnapshot(cfg);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
|
||||
clearCurrentProviderAuthState();
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockClear();
|
||||
let resolveWorker: ((value: typeof snapshot) => void) | undefined;
|
||||
const warmPromise = warmCurrentProviderAuthStateOffMainThread(cfg, {
|
||||
runWorker: () =>
|
||||
new Promise((resolve) => {
|
||||
resolveWorker = resolve;
|
||||
}),
|
||||
});
|
||||
await Promise.resolve();
|
||||
clearCurrentProviderAuthState();
|
||||
resolveWorker?.(snapshot);
|
||||
await warmPromise;
|
||||
|
||||
modelAuthMocks.hasRuntimeAvailableProviderAuth.mockReturnValue(false);
|
||||
await expect(hasAuthForModelProvider({ provider: "openai", cfg })).resolves.toBe(false);
|
||||
expect(modelAuthMocks.hasRuntimeAvailableProviderAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
@@ -11,6 +14,7 @@ import {
|
||||
externalCliDiscoveryForProviders,
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
getRuntimeAuthProfileStoreSnapshot,
|
||||
listProfilesForProvider,
|
||||
type AuthProfileStore,
|
||||
} from "./auth-profiles.js";
|
||||
@@ -35,18 +39,75 @@ type PreparedProviderAuthState = {
|
||||
providers: ReadonlyMap<string, boolean>;
|
||||
};
|
||||
|
||||
// One entry per configured agent, keyed by agentId. Populated by
|
||||
// warmCurrentProviderAuthState at gateway startup / on reload; consulted by
|
||||
// hasAuthForModelProvider on every model-listing call.
|
||||
export type ProviderAuthWarmSnapshot = {
|
||||
agents: Array<{
|
||||
agentId: string;
|
||||
configFingerprint: string;
|
||||
providers: Array<[string, boolean]>;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ProviderAuthWarmWorkerResult =
|
||||
| {
|
||||
status: "ok";
|
||||
snapshot: ProviderAuthWarmSnapshot;
|
||||
}
|
||||
| {
|
||||
status: "failed";
|
||||
error: string;
|
||||
};
|
||||
|
||||
type ProviderAuthWarmRuntimeAuthStore = {
|
||||
agentDir?: string;
|
||||
store: AuthProfileStore;
|
||||
};
|
||||
|
||||
type ProviderAuthWarmRuntimeAuthLookup = {
|
||||
agentId: string;
|
||||
lookup: RuntimeProviderAuthLookup;
|
||||
};
|
||||
|
||||
type ProviderAuthWarmWorkerRunner = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
runtimeAuthStores?: ProviderAuthWarmRuntimeAuthStore[];
|
||||
runtimeAuthLookups?: ProviderAuthWarmRuntimeAuthLookup[];
|
||||
omitFalseProviderAuth?: boolean;
|
||||
timeoutMs: number;
|
||||
isCancelled: () => boolean;
|
||||
workerUrl?: URL;
|
||||
}) => Promise<ProviderAuthWarmSnapshot>;
|
||||
|
||||
const PROVIDER_AUTH_WARM_WORKER_TIMEOUT_MS = 120_000;
|
||||
const PROVIDER_AUTH_WARM_CANCEL_POLL_MS = 25;
|
||||
|
||||
// One entry per configured agent, keyed by agentId. Populated by the provider
|
||||
// auth warm path; consulted by hasAuthForModelProvider on every model-listing call.
|
||||
let currentProviderAuthStates: ReadonlyMap<string, PreparedProviderAuthState> | null = null;
|
||||
const configFingerprintCache = new WeakMap<OpenClawConfig, string>();
|
||||
// Generation counter guards against an in-flight warm publishing stale
|
||||
// state after a subsequent warm or clear has invalidated it.
|
||||
let currentProviderAuthStateGeneration = 0;
|
||||
let currentProviderAuthWarmWorker:
|
||||
| {
|
||||
worker: Worker;
|
||||
cancelled: boolean;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
function cancelCurrentProviderAuthWarmWorker(): void {
|
||||
const current = currentProviderAuthWarmWorker;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
current.cancelled = true;
|
||||
currentProviderAuthWarmWorker = undefined;
|
||||
void current.worker.terminate();
|
||||
}
|
||||
|
||||
export function clearCurrentProviderAuthState(): void {
|
||||
currentProviderAuthStates = null;
|
||||
currentProviderAuthStateGeneration += 1;
|
||||
cancelCurrentProviderAuthWarmWorker();
|
||||
}
|
||||
|
||||
function resolvePreparedStateForCaller(params: {
|
||||
@@ -93,7 +154,7 @@ export async function hasAuthForModelProvider(params: {
|
||||
resolveRuntimeAuthLookup?: () => RuntimeProviderAuthLookup;
|
||||
}): Promise<boolean> {
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
// The prepared map is built by warmCurrentProviderAuthState — one entry per
|
||||
// The prepared map is built by the provider auth warm path — one entry per
|
||||
// configured agent, keyed by agentId. Only consult it when the caller's
|
||||
// full auth context matches the warmed scope; otherwise fall through to
|
||||
// compute so callers that narrow the scope — e.g. gateway `models.list`
|
||||
@@ -196,19 +257,76 @@ export function createProviderAuthChecker(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function warmCurrentProviderAuthState(
|
||||
function serializeProviderAuthStates(
|
||||
states: ReadonlyMap<string, PreparedProviderAuthState>,
|
||||
): ProviderAuthWarmSnapshot {
|
||||
return {
|
||||
agents: [...states.values()].map((state) => ({
|
||||
agentId: state.agentId,
|
||||
configFingerprint: state.configFingerprint,
|
||||
providers: [...state.providers.entries()],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function publishProviderAuthWarmSnapshot(snapshot: ProviderAuthWarmSnapshot): void {
|
||||
currentProviderAuthStates = new Map(
|
||||
snapshot.agents.map((state) => [
|
||||
state.agentId,
|
||||
{
|
||||
agentId: state.agentId,
|
||||
configFingerprint: state.configFingerprint,
|
||||
providers: new Map(state.providers),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveProviderConfigApi(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
provider: string,
|
||||
): string | undefined {
|
||||
const providers = cfg?.models?.providers ?? {};
|
||||
const direct = providers[provider];
|
||||
if (direct?.api) {
|
||||
return direct.api;
|
||||
}
|
||||
const normalized = normalizeProviderId(provider);
|
||||
const matched = Object.entries(providers).find(
|
||||
([key]) => normalizeProviderId(key) === normalized,
|
||||
)?.[1];
|
||||
return matched?.api;
|
||||
}
|
||||
|
||||
function shouldOmitFalsePreparedAuthForProcessSyntheticProvider(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
runtimeAuthLookup: RuntimeProviderAuthLookup;
|
||||
}): boolean {
|
||||
const syntheticRefs = params.runtimeAuthLookup.syntheticAuthProviderRefs;
|
||||
if (!syntheticRefs?.length) {
|
||||
return false;
|
||||
}
|
||||
const eligibleRefs = new Set(syntheticRefs.map((ref) => normalizeProviderId(ref)));
|
||||
const providerApi = resolveProviderConfigApi(params.cfg, params.provider);
|
||||
return [params.provider, providerApi]
|
||||
.filter((ref): ref is string => typeof ref === "string" && ref.trim().length > 0)
|
||||
.some((ref) => eligibleRefs.has(normalizeProviderId(ref)));
|
||||
}
|
||||
|
||||
export async function buildCurrentProviderAuthStateSnapshot(
|
||||
cfg: OpenClawConfig,
|
||||
options: { isCancelled?: () => boolean } = {},
|
||||
): Promise<void> {
|
||||
// Claim a fresh generation; any concurrent warm or clear bumps this and
|
||||
// turns our published state stale.
|
||||
currentProviderAuthStateGeneration += 1;
|
||||
const ownGeneration = currentProviderAuthStateGeneration;
|
||||
const isWarmStale = () =>
|
||||
options.isCancelled?.() === true || ownGeneration !== currentProviderAuthStateGeneration;
|
||||
options: {
|
||||
isCancelled?: () => boolean;
|
||||
readOnlyAuthStore?: boolean;
|
||||
runtimeAuthLookups?: ReadonlyMap<string, RuntimeProviderAuthLookup>;
|
||||
omitFalseProviderAuth?: boolean;
|
||||
} = {},
|
||||
): Promise<ProviderAuthWarmSnapshot> {
|
||||
const isWarmStale = () => options.isCancelled?.() === true;
|
||||
const catalog = await loadModelCatalog({ config: cfg, readOnly: true });
|
||||
if (isWarmStale()) {
|
||||
return;
|
||||
return { agents: [] };
|
||||
}
|
||||
const providers = new Set<string>();
|
||||
for (const entry of catalog) {
|
||||
@@ -222,27 +340,37 @@ export async function warmCurrentProviderAuthState(
|
||||
// work is the auth-discovery sweep against that agent's store.
|
||||
for (const agentId of listAgentIds(cfg)) {
|
||||
if (isWarmStale()) {
|
||||
return;
|
||||
return { agents: [] };
|
||||
}
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const agentDir = resolveAgentDir(cfg, agentId);
|
||||
const runtimeAuthLookup = createRuntimeProviderAuthLookup({
|
||||
cfg,
|
||||
workspaceDir,
|
||||
});
|
||||
const runtimeAuthLookup =
|
||||
options.runtimeAuthLookups?.get(agentId) ??
|
||||
createRuntimeProviderAuthLookup({
|
||||
cfg,
|
||||
workspaceDir,
|
||||
});
|
||||
// One AuthProfileStore scoped to every candidate provider; without this
|
||||
// the per-provider externalCli discovery rebuilds the store ~N times.
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
config: cfg,
|
||||
externalCli: externalCliDiscoveryForProviders({
|
||||
cfg,
|
||||
providers: providerList,
|
||||
}),
|
||||
const externalCli = externalCliDiscoveryForProviders({
|
||||
cfg,
|
||||
providers: providerList,
|
||||
});
|
||||
const store = options.readOnlyAuthStore
|
||||
? ensureAuthProfileStore(agentDir, {
|
||||
config: cfg,
|
||||
externalCli,
|
||||
readOnly: true,
|
||||
syncExternalCli: false,
|
||||
})
|
||||
: ensureAuthProfileStore(agentDir, {
|
||||
config: cfg,
|
||||
externalCli,
|
||||
});
|
||||
const state = new Map<string, boolean>();
|
||||
for (const provider of providers) {
|
||||
if (isWarmStale()) {
|
||||
return;
|
||||
return { agents: [] };
|
||||
}
|
||||
const value = await hasAuthForModelProvider({
|
||||
provider,
|
||||
@@ -252,6 +380,17 @@ export async function warmCurrentProviderAuthState(
|
||||
store,
|
||||
runtimeAuthLookup,
|
||||
});
|
||||
if (
|
||||
!value &&
|
||||
(options.omitFalseProviderAuth ||
|
||||
shouldOmitFalsePreparedAuthForProcessSyntheticProvider({
|
||||
cfg,
|
||||
provider,
|
||||
runtimeAuthLookup,
|
||||
}))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
state.set(provider, value);
|
||||
}
|
||||
states.set(agentId, {
|
||||
@@ -260,10 +399,278 @@ export async function warmCurrentProviderAuthState(
|
||||
providers: state,
|
||||
});
|
||||
}
|
||||
return serializeProviderAuthStates(states);
|
||||
}
|
||||
|
||||
export async function warmCurrentProviderAuthState(
|
||||
cfg: OpenClawConfig,
|
||||
options: { isCancelled?: () => boolean } = {},
|
||||
): Promise<void> {
|
||||
// Claim a fresh generation; any concurrent warm or clear bumps this and
|
||||
// turns our published state stale.
|
||||
currentProviderAuthStateGeneration += 1;
|
||||
const ownGeneration = currentProviderAuthStateGeneration;
|
||||
const isWarmStale = () =>
|
||||
options.isCancelled?.() === true || ownGeneration !== currentProviderAuthStateGeneration;
|
||||
const snapshot = await buildCurrentProviderAuthStateSnapshot(cfg, {
|
||||
isCancelled: isWarmStale,
|
||||
});
|
||||
if (isWarmStale()) {
|
||||
return;
|
||||
}
|
||||
if (options.isCancelled?.() || ownGeneration !== currentProviderAuthStateGeneration) {
|
||||
// A newer warm or clear ran while we were building; skip publication so
|
||||
// the newer answer wins.
|
||||
return;
|
||||
}
|
||||
currentProviderAuthStates = states;
|
||||
publishProviderAuthWarmSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function resolveProviderAuthWarmWorkerUrl(currentModuleUrl: string): URL {
|
||||
const currentPath = fileURLToPath(currentModuleUrl);
|
||||
const distMarker = `${path.sep}dist${path.sep}`;
|
||||
const distIndex = currentPath.lastIndexOf(distMarker);
|
||||
if (distIndex >= 0) {
|
||||
const distRoot = currentPath.slice(0, distIndex + distMarker.length - 1);
|
||||
return pathToFileURL(path.join(distRoot, "agents", "model-provider-auth.worker.js"));
|
||||
}
|
||||
const extension = path.extname(currentPath) || ".js";
|
||||
return new URL(`./model-provider-auth.worker${extension}`, currentModuleUrl);
|
||||
}
|
||||
|
||||
function isProviderAuthWarmSnapshot(value: unknown): value is ProviderAuthWarmSnapshot {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== "object" ||
|
||||
!Array.isArray((value as { agents?: unknown }).agents)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (value as ProviderAuthWarmSnapshot).agents.every(
|
||||
(agent) =>
|
||||
typeof agent.agentId === "string" &&
|
||||
typeof agent.configFingerprint === "string" &&
|
||||
Array.isArray(agent.providers) &&
|
||||
agent.providers.every(
|
||||
(entry) =>
|
||||
Array.isArray(entry) &&
|
||||
entry.length === 2 &&
|
||||
typeof entry[0] === "string" &&
|
||||
typeof entry[1] === "boolean",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function isProviderAuthWarmWorkerResult(value: unknown): value is ProviderAuthWarmWorkerResult {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const result = value as ProviderAuthWarmWorkerResult;
|
||||
if (result.status === "failed") {
|
||||
return typeof result.error === "string";
|
||||
}
|
||||
return result.status === "ok" && isProviderAuthWarmSnapshot(result.snapshot);
|
||||
}
|
||||
|
||||
function createProviderAuthWarmPresenceStore(store: AuthProfileStore): AuthProfileStore {
|
||||
const profiles: AuthProfileStore["profiles"] = {};
|
||||
for (const [profileId, credential] of Object.entries(store.profiles)) {
|
||||
profiles[profileId] = {
|
||||
type: "api_key",
|
||||
provider: credential.provider,
|
||||
};
|
||||
}
|
||||
return {
|
||||
version: store.version,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
function collectProviderAuthWarmRuntimeAuthStores(
|
||||
cfg: OpenClawConfig,
|
||||
): ProviderAuthWarmRuntimeAuthStore[] {
|
||||
const entries: ProviderAuthWarmRuntimeAuthStore[] = [];
|
||||
const seen = new Set<string | undefined>();
|
||||
const addStore = (agentDir?: string) => {
|
||||
if (seen.has(agentDir)) {
|
||||
return;
|
||||
}
|
||||
seen.add(agentDir);
|
||||
const store = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
if (!store) {
|
||||
return;
|
||||
}
|
||||
entries.push({
|
||||
...(agentDir === undefined ? {} : { agentDir }),
|
||||
store: createProviderAuthWarmPresenceStore(store),
|
||||
});
|
||||
};
|
||||
|
||||
addStore();
|
||||
for (const agentId of listAgentIds(cfg)) {
|
||||
addStore(resolveAgentDir(cfg, agentId));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectProviderAuthWarmRuntimeAuthLookups(cfg: OpenClawConfig): {
|
||||
entries: ProviderAuthWarmRuntimeAuthLookup[];
|
||||
omitFalseProviderAuth: boolean;
|
||||
} {
|
||||
const entries: ProviderAuthWarmRuntimeAuthLookup[] = [];
|
||||
let omitFalseProviderAuth = false;
|
||||
for (const agentId of listAgentIds(cfg)) {
|
||||
const lookup = createRuntimeProviderAuthLookup({
|
||||
cfg,
|
||||
workspaceDir: resolveAgentWorkspaceDir(cfg, agentId),
|
||||
});
|
||||
if (lookup.syntheticAuthProviderRefsComplete === false) {
|
||||
omitFalseProviderAuth = true;
|
||||
}
|
||||
entries.push({ agentId, lookup });
|
||||
}
|
||||
return { entries, omitFalseProviderAuth };
|
||||
}
|
||||
|
||||
function runProviderAuthWarmWorker(params: {
|
||||
cfg: OpenClawConfig;
|
||||
runtimeAuthStores?: ProviderAuthWarmRuntimeAuthStore[];
|
||||
runtimeAuthLookups?: ProviderAuthWarmRuntimeAuthLookup[];
|
||||
omitFalseProviderAuth?: boolean;
|
||||
timeoutMs: number;
|
||||
isCancelled: () => boolean;
|
||||
workerUrl?: URL;
|
||||
}): Promise<ProviderAuthWarmSnapshot> {
|
||||
const worker = new Worker(params.workerUrl ?? resolveProviderAuthWarmWorkerUrl(import.meta.url), {
|
||||
workerData: {
|
||||
cfg: params.cfg,
|
||||
...(params.runtimeAuthStores?.length ? { runtimeAuthStores: params.runtimeAuthStores } : {}),
|
||||
...(params.runtimeAuthLookups?.length
|
||||
? { runtimeAuthLookups: params.runtimeAuthLookups }
|
||||
: {}),
|
||||
...(params.omitFalseProviderAuth ? { omitFalseProviderAuth: true } : {}),
|
||||
},
|
||||
});
|
||||
worker.unref?.();
|
||||
const handle = {
|
||||
worker,
|
||||
cancelled: false,
|
||||
};
|
||||
currentProviderAuthWarmWorker = handle;
|
||||
return new Promise<ProviderAuthWarmSnapshot>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let cancelTimer: ReturnType<typeof setInterval> | undefined;
|
||||
const finish = (complete: () => void) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (currentProviderAuthWarmWorker === handle) {
|
||||
currentProviderAuthWarmWorker = undefined;
|
||||
}
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (cancelTimer) {
|
||||
clearInterval(cancelTimer);
|
||||
}
|
||||
complete();
|
||||
};
|
||||
const cancelWorker = () => {
|
||||
handle.cancelled = true;
|
||||
void worker.terminate();
|
||||
finish(() => resolve({ agents: [] }));
|
||||
};
|
||||
timer = setTimeout(() => {
|
||||
handle.cancelled = true;
|
||||
void worker.terminate();
|
||||
finish(() => reject(new Error("provider auth warm worker timed out")));
|
||||
}, params.timeoutMs);
|
||||
timer.unref?.();
|
||||
cancelTimer = setInterval(() => {
|
||||
if (params.isCancelled()) {
|
||||
cancelWorker();
|
||||
}
|
||||
}, PROVIDER_AUTH_WARM_CANCEL_POLL_MS);
|
||||
cancelTimer.unref?.();
|
||||
worker.once("message", (message: unknown) => {
|
||||
void worker.terminate();
|
||||
finish(() => {
|
||||
if (handle.cancelled) {
|
||||
resolve({ agents: [] });
|
||||
return;
|
||||
}
|
||||
if (!isProviderAuthWarmWorkerResult(message)) {
|
||||
reject(new Error("invalid provider auth warm worker response"));
|
||||
return;
|
||||
}
|
||||
if (message.status === "failed") {
|
||||
reject(new Error(message.error));
|
||||
return;
|
||||
}
|
||||
resolve(message.snapshot);
|
||||
});
|
||||
});
|
||||
worker.once("error", (error) => {
|
||||
finish(() => {
|
||||
if (handle.cancelled) {
|
||||
resolve({ agents: [] });
|
||||
return;
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
worker.once("exit", (code) => {
|
||||
if (settled || code === 0) {
|
||||
return;
|
||||
}
|
||||
finish(() => {
|
||||
if (handle.cancelled) {
|
||||
resolve({ agents: [] });
|
||||
return;
|
||||
}
|
||||
reject(new Error(`provider auth warm worker exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
if (params.isCancelled()) {
|
||||
cancelWorker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function warmCurrentProviderAuthStateOffMainThread(
|
||||
cfg: OpenClawConfig,
|
||||
options: {
|
||||
isCancelled?: () => boolean;
|
||||
timeoutMs?: number;
|
||||
workerUrl?: URL;
|
||||
runWorker?: ProviderAuthWarmWorkerRunner;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
currentProviderAuthStateGeneration += 1;
|
||||
const ownGeneration = currentProviderAuthStateGeneration;
|
||||
cancelCurrentProviderAuthWarmWorker();
|
||||
const isWarmStale = () =>
|
||||
options.isCancelled?.() === true || ownGeneration !== currentProviderAuthStateGeneration;
|
||||
if (isWarmStale()) {
|
||||
return;
|
||||
}
|
||||
const runtimeAuthStores = collectProviderAuthWarmRuntimeAuthStores(cfg);
|
||||
const runtimeAuthLookups = collectProviderAuthWarmRuntimeAuthLookups(cfg);
|
||||
const snapshot = await (options.runWorker ?? runProviderAuthWarmWorker)({
|
||||
cfg,
|
||||
...(runtimeAuthStores.length ? { runtimeAuthStores } : {}),
|
||||
...(runtimeAuthLookups.entries.length
|
||||
? { runtimeAuthLookups: runtimeAuthLookups.entries }
|
||||
: {}),
|
||||
...(runtimeAuthLookups.omitFalseProviderAuth ? { omitFalseProviderAuth: true } : {}),
|
||||
timeoutMs: options.timeoutMs ?? PROVIDER_AUTH_WARM_WORKER_TIMEOUT_MS,
|
||||
isCancelled: isWarmStale,
|
||||
workerUrl: options.workerUrl,
|
||||
});
|
||||
if (isWarmStale()) {
|
||||
return;
|
||||
}
|
||||
publishProviderAuthWarmSnapshot(snapshot);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "./auth-profiles.js";
|
||||
import { clearCurrentProviderAuthState } from "./model-provider-auth.js";
|
||||
import { runProviderAuthWarmWorkerInput } from "./model-provider-auth.worker.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const envKeys = ["OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY", "OPENCLAW_STATE_DIR"] as const;
|
||||
|
||||
function restoreEnv(previous: Record<(typeof envKeys)[number], string | undefined>): void {
|
||||
for (const key of envKeys) {
|
||||
if (previous[key] === undefined) {
|
||||
delete process.env[key];
|
||||
continue;
|
||||
}
|
||||
process.env[key] = previous[key];
|
||||
}
|
||||
}
|
||||
|
||||
describe("provider auth warm worker", () => {
|
||||
afterEach(() => {
|
||||
clearCurrentProviderAuthState();
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves runtime-only auth profile snapshots in the worker warm input", async () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-provider-auth-worker-"));
|
||||
tempDirs.push(root);
|
||||
const previousEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])) as Record<
|
||||
(typeof envKeys)[number],
|
||||
string | undefined
|
||||
>;
|
||||
process.env.OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY = "1";
|
||||
process.env.OPENCLAW_STATE_DIR = path.join(root, "state");
|
||||
|
||||
try {
|
||||
const agentDir = path.join(root, "agent");
|
||||
const cfg = {
|
||||
agents: { list: [{ id: "main", agentDir }] },
|
||||
models: {
|
||||
providers: {
|
||||
"runtime-only": {
|
||||
baseUrl: "https://example.com/v1",
|
||||
api: "openai",
|
||||
models: [{ id: "runtime-model", name: "Runtime Model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const result = await runProviderAuthWarmWorkerInput({
|
||||
cfg,
|
||||
runtimeAuthStores: [
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"runtime-only:default": {
|
||||
type: "api_key",
|
||||
provider: "runtime-only",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
if (result.status !== "ok") {
|
||||
return;
|
||||
}
|
||||
expect(result.snapshot.agents[0]?.providers).toContainEqual(["runtime-only", true]);
|
||||
} finally {
|
||||
restoreEnv(previousEnv);
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { replaceRuntimeAuthProfileStoreSnapshots, type AuthProfileStore } from "./auth-profiles.js";
|
||||
import type { RuntimeProviderAuthLookup } from "./model-auth.js";
|
||||
import { buildCurrentProviderAuthStateSnapshot } from "./model-provider-auth.js";
|
||||
|
||||
type ProviderAuthWarmRuntimeAuthStore = {
|
||||
agentDir?: string;
|
||||
store: AuthProfileStore;
|
||||
};
|
||||
|
||||
type ProviderAuthWarmWorkerInput = {
|
||||
cfg: OpenClawConfig;
|
||||
runtimeAuthStores?: ProviderAuthWarmRuntimeAuthStore[];
|
||||
runtimeAuthLookups?: Array<{
|
||||
agentId: string;
|
||||
lookup: RuntimeProviderAuthLookup;
|
||||
}>;
|
||||
omitFalseProviderAuth?: boolean;
|
||||
};
|
||||
|
||||
type ProviderAuthWarmWorkerResult =
|
||||
| {
|
||||
status: "ok";
|
||||
snapshot: Awaited<ReturnType<typeof buildCurrentProviderAuthStateSnapshot>>;
|
||||
}
|
||||
| {
|
||||
status: "failed";
|
||||
error: string;
|
||||
};
|
||||
|
||||
function isWorkerInput(value: unknown): value is ProviderAuthWarmWorkerInput {
|
||||
return (
|
||||
!!value &&
|
||||
typeof value === "object" &&
|
||||
"cfg" in value &&
|
||||
(!("runtimeAuthStores" in value) ||
|
||||
Array.isArray((value as { runtimeAuthStores?: unknown }).runtimeAuthStores)) &&
|
||||
(!("runtimeAuthLookups" in value) ||
|
||||
Array.isArray((value as { runtimeAuthLookups?: unknown }).runtimeAuthLookups)) &&
|
||||
(!("omitFalseProviderAuth" in value) ||
|
||||
typeof (value as { omitFalseProviderAuth?: unknown }).omitFalseProviderAuth === "boolean")
|
||||
);
|
||||
}
|
||||
|
||||
export async function runProviderAuthWarmWorkerInput(
|
||||
input: unknown,
|
||||
): Promise<ProviderAuthWarmWorkerResult> {
|
||||
if (!isWorkerInput(input)) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: "invalid provider auth warm worker input",
|
||||
};
|
||||
}
|
||||
try {
|
||||
if (input.runtimeAuthStores?.length) {
|
||||
replaceRuntimeAuthProfileStoreSnapshots(input.runtimeAuthStores);
|
||||
}
|
||||
const snapshot = await buildCurrentProviderAuthStateSnapshot(input.cfg, {
|
||||
readOnlyAuthStore: true,
|
||||
runtimeAuthLookups: new Map(
|
||||
input.runtimeAuthLookups?.map(({ agentId, lookup }) => [agentId, lookup]),
|
||||
),
|
||||
omitFalseProviderAuth: input.omitFalseProviderAuth,
|
||||
});
|
||||
return {
|
||||
status: "ok",
|
||||
snapshot,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (parentPort) {
|
||||
const sendToParent: (message: ProviderAuthWarmWorkerResult) => void =
|
||||
parentPort.postMessage.bind(parentPort);
|
||||
sendToParent(await runProviderAuthWarmWorkerInput(workerData));
|
||||
}
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
classifyMediaReferenceSource,
|
||||
normalizeMediaReferenceSource,
|
||||
} from "../../media/media-reference.js";
|
||||
import type { ImageCompressionModelPolicy, ImageCompressionPolicy } from "../../media/web-media.js";
|
||||
import type {
|
||||
ImageCompressionModelPolicy,
|
||||
ImageCompressionPolicy,
|
||||
WebMediaResult,
|
||||
} from "../../media/web-media.js";
|
||||
import {
|
||||
describeImageWithModel,
|
||||
describeImagesWithModel,
|
||||
@@ -76,10 +80,23 @@ import {
|
||||
const DEFAULT_PROMPT = "Describe the image.";
|
||||
const DEFAULT_MAX_IMAGES = 20;
|
||||
|
||||
type ImageWebMediaRuntime = Pick<
|
||||
typeof import("../../media/web-media.js"),
|
||||
"loadWebMedia" | "optimizeImageBufferForWebMedia"
|
||||
>;
|
||||
type ImageToolLoadWebMediaOptions = {
|
||||
maxBytes?: number;
|
||||
sandboxValidated?: boolean;
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
imageCompression?: ImageCompressionPolicy;
|
||||
localRoots?: readonly string[] | "any";
|
||||
inboundRoots?: readonly string[];
|
||||
ssrfPolicy?: ReturnType<typeof resolveRemoteMediaSsrfPolicy>;
|
||||
};
|
||||
|
||||
type ImageWebMediaRuntime = {
|
||||
loadWebMedia: (
|
||||
mediaUrl: string,
|
||||
options?: ImageToolLoadWebMediaOptions,
|
||||
) => Promise<WebMediaResult>;
|
||||
optimizeImageBufferForWebMedia: (typeof import("../../media/web-media.js"))["optimizeImageBufferForWebMedia"];
|
||||
};
|
||||
|
||||
async function loadImageWebMediaRuntime(): Promise<ImageWebMediaRuntime> {
|
||||
return await import("../../media/web-media.js");
|
||||
|
||||
@@ -24,7 +24,7 @@ const mocks = vi.hoisted(() => ({
|
||||
),
|
||||
refreshActiveSecretsRuntimeSnapshot: vi.fn(async () => false),
|
||||
clearCurrentProviderAuthState: vi.fn(),
|
||||
warmCurrentProviderAuthState: vi.fn(async (_cfg: unknown) => {}),
|
||||
warmCurrentProviderAuthStateOffMainThread: vi.fn(async (_cfg: unknown) => {}),
|
||||
buildAuthHealthSummary: vi.fn(
|
||||
(): AuthHealthSummary => ({ now: 0, warnAfterMs: 0, profiles: [], providers: [] }),
|
||||
),
|
||||
@@ -74,7 +74,7 @@ vi.mock("../../secrets/runtime.js", () => ({
|
||||
|
||||
vi.mock("../../agents/model-provider-auth.js", () => ({
|
||||
clearCurrentProviderAuthState: mocks.clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthState: mocks.warmCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread: mocks.warmCurrentProviderAuthStateOffMainThread,
|
||||
}));
|
||||
|
||||
import {
|
||||
@@ -618,7 +618,7 @@ describe("models.authLogout", () => {
|
||||
});
|
||||
expect(mocks.refreshActiveSecretsRuntimeSnapshot).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.clearCurrentProviderAuthState).toHaveBeenCalled();
|
||||
expect(mocks.warmCurrentProviderAuthState).toHaveBeenCalledWith({});
|
||||
expect(mocks.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledWith({});
|
||||
const [ok, payload] = firstRespondCall(opts) ?? [];
|
||||
expect(ok).toBe(true);
|
||||
expect((payload as ModelAuthLogoutResult).removedProfiles).toEqual(["openrouter:default"]);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread,
|
||||
} from "../../agents/model-provider-auth.js";
|
||||
import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js";
|
||||
import { normalizeProviderId } from "../../agents/provider-id.js";
|
||||
@@ -391,7 +391,7 @@ export const modelsAuthStatusHandlers: GatewayRequestHandlers = {
|
||||
await refreshActiveSecretsRuntimeSnapshot();
|
||||
invalidateModelAuthStatusCache();
|
||||
clearCurrentProviderAuthState();
|
||||
void warmCurrentProviderAuthState(context.getRuntimeConfig()).catch((err) => {
|
||||
void warmCurrentProviderAuthStateOffMainThread(context.getRuntimeConfig()).catch((err) => {
|
||||
log.warn(`provider auth state rewarm after logout failed: ${formatForLog(err)}`);
|
||||
});
|
||||
const { runIds: abortedRunIds } = abortChatRunsForProvider(
|
||||
|
||||
@@ -44,7 +44,7 @@ const hoisted = vi.hoisted(() => ({
|
||||
reloadEvents: [] as string[],
|
||||
resetModelCatalogCache: vi.fn(() => {}),
|
||||
clearCurrentProviderAuthState: vi.fn(() => {}),
|
||||
warmCurrentProviderAuthState: vi.fn(async (_cfg: OpenClawConfig) => {}),
|
||||
warmCurrentProviderAuthStateOffMainThread: vi.fn(async (_cfg: OpenClawConfig) => {}),
|
||||
disposeAllSessionMcpRuntimes: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
@@ -113,9 +113,9 @@ vi.mock("../agents/model-provider-auth.js", () => ({
|
||||
hoisted.reloadEvents.push("clear-provider-auth");
|
||||
hoisted.clearCurrentProviderAuthState();
|
||||
},
|
||||
warmCurrentProviderAuthState: async (cfg: OpenClawConfig) => {
|
||||
warmCurrentProviderAuthStateOffMainThread: async (cfg: OpenClawConfig) => {
|
||||
hoisted.reloadEvents.push("warm-provider-auth");
|
||||
await hoisted.warmCurrentProviderAuthState(cfg);
|
||||
await hoisted.warmCurrentProviderAuthStateOffMainThread(cfg);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -171,7 +171,7 @@ afterEach(() => {
|
||||
hoisted.reloadEvents.length = 0;
|
||||
hoisted.resetModelCatalogCache.mockClear();
|
||||
hoisted.clearCurrentProviderAuthState.mockClear();
|
||||
hoisted.warmCurrentProviderAuthState.mockClear();
|
||||
hoisted.warmCurrentProviderAuthStateOffMainThread.mockClear();
|
||||
hoisted.disposeAllSessionMcpRuntimes.mockClear();
|
||||
hoisted.disposeAllSessionMcpRuntimes.mockResolvedValue(undefined);
|
||||
});
|
||||
@@ -240,7 +240,7 @@ describe("gateway hot reload model state", () => {
|
||||
"clear-provider-auth",
|
||||
"warm-provider-auth",
|
||||
]);
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledWith(nextConfig);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledWith(nextConfig);
|
||||
});
|
||||
|
||||
it("disposes cached MCP runtimes on MCP config hot reloads", async () => {
|
||||
@@ -267,7 +267,7 @@ describe("gateway hot reload model state", () => {
|
||||
);
|
||||
|
||||
expect(hoisted.disposeAllSessionMcpRuntimes).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledWith(nextConfig);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledWith(nextConfig);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { resetModelCatalogCache } from "../agents/model-catalog.js";
|
||||
import {
|
||||
clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread,
|
||||
} from "../agents/model-provider-auth.js";
|
||||
import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js";
|
||||
import type { CliDeps } from "../cli/deps.types.js";
|
||||
@@ -500,7 +500,7 @@ export function createGatewayReloadHandlers(params: GatewayReloadHandlerParams)
|
||||
|
||||
applyGatewayLaneConcurrency(nextConfig);
|
||||
|
||||
void warmCurrentProviderAuthState(nextConfig).catch((err) => {
|
||||
void warmCurrentProviderAuthStateOffMainThread(nextConfig).catch((err) => {
|
||||
params.logReload.warn(`provider auth state rewarm failed: ${String(err)}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ const hoisted = vi.hoisted(() => {
|
||||
}));
|
||||
const ensureOpenClawModelsJson = vi.fn(async () => {});
|
||||
const clearCurrentProviderAuthState = vi.fn();
|
||||
const warmCurrentProviderAuthState = vi.fn(async (_cfg?: unknown, _options?: unknown) => {});
|
||||
const warmCurrentProviderAuthStateOffMainThread = vi.fn(
|
||||
async (_cfg?: unknown, _options?: unknown) => {},
|
||||
);
|
||||
const setAuthProfileFailureHook = vi.fn();
|
||||
const transcriptsAutoStartService = {
|
||||
start: vi.fn(),
|
||||
@@ -81,7 +83,7 @@ const hoisted = vi.hoisted(() => {
|
||||
getModelRefStatus,
|
||||
ensureOpenClawModelsJson,
|
||||
clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread,
|
||||
setAuthProfileFailureHook,
|
||||
transcriptsAutoStartService,
|
||||
createTranscriptsAutoStartService,
|
||||
@@ -179,7 +181,7 @@ vi.mock("../agents/models-config.js", () => ({
|
||||
|
||||
vi.mock("../agents/model-provider-auth.js", () => ({
|
||||
clearCurrentProviderAuthState: hoisted.clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthState: hoisted.warmCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread: hoisted.warmCurrentProviderAuthStateOffMainThread,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", async () => {
|
||||
@@ -293,8 +295,8 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
hoisted.ensureOpenClawModelsJson.mockReset();
|
||||
hoisted.ensureOpenClawModelsJson.mockResolvedValue(undefined);
|
||||
hoisted.clearCurrentProviderAuthState.mockClear();
|
||||
hoisted.warmCurrentProviderAuthState.mockReset();
|
||||
hoisted.warmCurrentProviderAuthState.mockResolvedValue(undefined);
|
||||
hoisted.warmCurrentProviderAuthStateOffMainThread.mockReset();
|
||||
hoisted.warmCurrentProviderAuthStateOffMainThread.mockResolvedValue(undefined);
|
||||
hoisted.setAuthProfileFailureHook.mockClear();
|
||||
hoisted.transcriptsAutoStartService.start.mockClear();
|
||||
hoisted.transcriptsAutoStartService.stop.mockClear();
|
||||
@@ -832,11 +834,11 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(hoisted.setAuthProfileFailureHook).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(hoisted.warmCurrentProviderAuthState).not.toHaveBeenCalled();
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await vi.waitFor(() => {
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
@@ -898,17 +900,17 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await vi.waitFor(() => {
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const hook = hoisted.setAuthProfileFailureHook.mock.calls[0]?.[0] as (() => void) | undefined;
|
||||
hook?.();
|
||||
expect(hoisted.clearCurrentProviderAuthState).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await vi.waitFor(() => {
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(2);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
@@ -978,12 +980,12 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
|
||||
await sidecar.stop();
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(hoisted.warmCurrentProviderAuthState).not.toHaveBeenCalled();
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).not.toHaveBeenCalled();
|
||||
|
||||
const hook = hoisted.setAuthProfileFailureHook.mock.calls[0]?.[0] as (() => void) | undefined;
|
||||
hook?.();
|
||||
expect(hoisted.clearCurrentProviderAuthState).not.toHaveBeenCalled();
|
||||
expect(hoisted.warmCurrentProviderAuthState).not.toHaveBeenCalled();
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
@@ -1010,7 +1012,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await vi.waitFor(() => {
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const hook = hoisted.setAuthProfileFailureHook.mock.calls[0]?.[0] as (() => void) | undefined;
|
||||
@@ -1021,15 +1023,19 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
hook();
|
||||
currentCfg = afterFailureCfg;
|
||||
hook();
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.clearCurrentProviderAuthState).toHaveBeenCalledTimes(2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await vi.waitFor(() => {
|
||||
expect(hoisted.warmCurrentProviderAuthState).toHaveBeenCalledTimes(2);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(hoisted.warmCurrentProviderAuthState.mock.calls[0]?.[0]).toBe(reloadedCfg);
|
||||
expect(hoisted.warmCurrentProviderAuthState.mock.calls[1]?.[0]).toBe(afterFailureCfg);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread.mock.calls[0]?.[0]).toBe(
|
||||
reloadedCfg,
|
||||
);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread.mock.calls[1]?.[0]).toBe(
|
||||
afterFailureCfg,
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ function scheduleProviderAuthStatePrewarm(params: {
|
||||
const isStopped = () => stopped;
|
||||
const delayMs = params.delayMs ?? PROVIDER_AUTH_PREWARM_START_DELAY_MS;
|
||||
void (async () => {
|
||||
const { clearCurrentProviderAuthState, warmCurrentProviderAuthState } =
|
||||
const { clearCurrentProviderAuthState, warmCurrentProviderAuthStateOffMainThread } =
|
||||
await import("../agents/model-provider-auth.js");
|
||||
const { setAuthProfileFailureHook } = await import("../agents/auth-profiles.js");
|
||||
const runRewarm = async (reason: string) => {
|
||||
@@ -202,7 +202,7 @@ function scheduleProviderAuthStatePrewarm(params: {
|
||||
rewarmInFlight = true;
|
||||
try {
|
||||
const metrics = await measureProviderAuthWarm(() =>
|
||||
warmCurrentProviderAuthState(cfg, { isCancelled: isStopped }),
|
||||
warmCurrentProviderAuthStateOffMainThread(cfg, { isCancelled: isStopped }),
|
||||
);
|
||||
if (isStopped()) {
|
||||
return;
|
||||
@@ -255,7 +255,7 @@ function scheduleProviderAuthStatePrewarm(params: {
|
||||
}
|
||||
const cfg = params.getConfig();
|
||||
const metrics = await measureProviderAuthWarm(() =>
|
||||
warmCurrentProviderAuthState(cfg, { isCancelled: isStopped }),
|
||||
warmCurrentProviderAuthStateOffMainThread(cfg, { isCancelled: isStopped }),
|
||||
);
|
||||
if (isStopped()) {
|
||||
return;
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("tsdown config", () => {
|
||||
"agents/model-catalog.runtime",
|
||||
"agents/models-config.runtime",
|
||||
"cli/gateway-lifecycle.runtime",
|
||||
"agents/model-provider-auth.worker",
|
||||
"plugins/memory-state",
|
||||
"subagent-registry.runtime",
|
||||
"task-registry-control.runtime",
|
||||
|
||||
@@ -565,6 +565,7 @@ describe("collectMissingPackPaths", () => {
|
||||
"scripts/lib/official-external-provider-catalog.json",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/agents/model-provider-auth.worker.js",
|
||||
"dist/task-registry-control.runtime.js",
|
||||
"dist/telegram-ingress-worker.runtime.js",
|
||||
bundledDistPluginFile("telegram", "runtime-api.js"),
|
||||
@@ -597,6 +598,7 @@ describe("collectMissingPackPaths", () => {
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"dist/plugin-sdk/root-alias.cjs",
|
||||
"dist/agents/model-provider-auth.worker.js",
|
||||
"dist/task-registry-control.runtime.js",
|
||||
"dist/telegram-ingress-worker.runtime.js",
|
||||
"dist/build-info.json",
|
||||
|
||||
@@ -233,6 +233,7 @@ function buildCoreDistEntries(): Record<string, string> {
|
||||
"agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts",
|
||||
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
|
||||
"agents/code-mode.worker": "src/agents/code-mode.worker.ts",
|
||||
"agents/model-provider-auth.worker": "src/agents/model-provider-auth.worker.ts",
|
||||
"acp/control-plane/manager": "src/acp/control-plane/manager.ts",
|
||||
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
|
||||
"provider-dispatcher.runtime": "src/auto-reply/reply/provider-dispatcher.runtime.ts",
|
||||
|
||||
Reference in New Issue
Block a user