mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(models): refresh durable auth in worker
This commit is contained in:
@@ -801,7 +801,7 @@ function mergeRuntimeExternalProfileReferences(params: {
|
||||
return merged;
|
||||
}
|
||||
|
||||
function preserveResolvedSecretBackedCredentials(params: {
|
||||
export function preserveResolvedSecretBackedCredentials(params: {
|
||||
next: AuthProfileStore;
|
||||
existing: AuthProfileStore;
|
||||
}): AuthProfileStore {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} from "./auth-profiles/runtime-snapshots.js";
|
||||
import { saveAuthProfileStore } from "./auth-profiles/store.js";
|
||||
import {
|
||||
encodePluginModelCatalogRelativePath,
|
||||
PLUGIN_MODEL_CATALOG_GENERATED_BY,
|
||||
@@ -35,6 +36,7 @@ const REF_ONLY_API_PROVIDER_ID = `${PROVIDER_ID}-ref-api`;
|
||||
const REF_ONLY_API_ENV = "OPENCLAW_WORKER_REF_ONLY_API_KEY";
|
||||
const REF_ONLY_TOKEN_PROVIDER_ID = `${PROVIDER_ID}-ref-token`;
|
||||
const REF_ONLY_TOKEN_ENV = "OPENCLAW_WORKER_REF_ONLY_TOKEN";
|
||||
const DURABLE_AUTH_PROVIDER_ID = `${PROVIDER_ID}-durable-auth`;
|
||||
const tempDirs = useAutoCleanupTempDirTracker((cleanup) => {
|
||||
afterEach(() => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
@@ -209,6 +211,111 @@ async function waitForMarker(marker: string): Promise<void> {
|
||||
}
|
||||
|
||||
describe("prepared model catalog worker boundary", () => {
|
||||
it("refreshes durable auth profiles added, updated, and removed after startup", async () => {
|
||||
const fixture = await createStaticSnapshot(0);
|
||||
const route = {
|
||||
provider: DURABLE_AUTH_PROVIDER_ID,
|
||||
id: "durable-model",
|
||||
name: "Durable model",
|
||||
api: "openai-completions" as const,
|
||||
baseUrl: "https://durable-auth.invalid/v1",
|
||||
};
|
||||
const config = {
|
||||
...fixture.config,
|
||||
agents: {
|
||||
...fixture.config.agents,
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
default: true,
|
||||
agentDir: fixture.agentDir,
|
||||
workspace: fixture.workspaceDir,
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const owner = Object.freeze({
|
||||
...fixture.snapshot,
|
||||
config,
|
||||
modelCatalog: { entries: [route], routeVariants: [route] },
|
||||
});
|
||||
copyPreparedModelRuntimeAuthState(fixture.snapshot, owner);
|
||||
const project = async () =>
|
||||
await loadGatewayModelCatalogSnapshot({
|
||||
getConfig: () => config,
|
||||
loadPublishedPreparedModelCatalogOwnerSnapshot: async () => owner,
|
||||
});
|
||||
const projectModels = async () => {
|
||||
const projected = await project();
|
||||
const context = {
|
||||
getRuntimeConfig: () => config,
|
||||
loadGatewayModelCatalogSnapshot: async () => projected,
|
||||
readPreparedGatewayModelCatalogSnapshot: async () => projected,
|
||||
logGateway: { debug: () => undefined },
|
||||
} as unknown as GatewayRequestContext;
|
||||
return {
|
||||
projected,
|
||||
result: await buildModelsListResult({ context, params: { view: "all" } }),
|
||||
};
|
||||
};
|
||||
const writeDurableProfile = (key?: string) =>
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: key
|
||||
? {
|
||||
[`${DURABLE_AUTH_PROVIDER_ID}:default`]: {
|
||||
type: "api_key",
|
||||
provider: DURABLE_AUTH_PROVIDER_ID,
|
||||
key,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
},
|
||||
fixture.agentDir,
|
||||
);
|
||||
|
||||
writeDurableProfile("first-key-not-real");
|
||||
const added = await projectModels();
|
||||
expect(added).toMatchObject({
|
||||
result: {
|
||||
models: [expect.objectContaining({ id: "durable-model", available: true })],
|
||||
},
|
||||
projected: {
|
||||
authStore: {
|
||||
profiles: {
|
||||
[`${DURABLE_AUTH_PROVIDER_ID}:default`]: expect.objectContaining({
|
||||
key: "first-key-not-real",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
writeDurableProfile("second-key-not-real");
|
||||
const updated = await project();
|
||||
expect(updated).toMatchObject({
|
||||
authStore: {
|
||||
profiles: {
|
||||
[`${DURABLE_AUTH_PROVIDER_ID}:default`]: expect.objectContaining({
|
||||
key: "second-key-not-real",
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
writeDurableProfile();
|
||||
const removed = await projectModels();
|
||||
expect(removed).toMatchObject({
|
||||
result: {
|
||||
models: [expect.objectContaining({ id: "durable-model", available: false })],
|
||||
},
|
||||
});
|
||||
expect(
|
||||
removed.projected.authStore.profiles[`${DURABLE_AUTH_PROVIDER_ID}:default`],
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("makes a post-startup Codex login available to direct models.list", async () => {
|
||||
const codexHome = tempDirs.make("openclaw-models-list-codex-");
|
||||
const fixture = await createStaticSnapshot(0, { CODEX_HOME: codexHome });
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
import { overlayExternalAuthProfiles } from "./auth-profiles/external-auth.js";
|
||||
import { replaceRuntimeAuthProfileStoreSnapshots } from "./auth-profiles/runtime-snapshots.js";
|
||||
import { ensureAuthProfileStoreWithoutExternalProfiles } from "./auth-profiles/store.js";
|
||||
import {
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
preserveResolvedSecretBackedCredentials,
|
||||
} from "./auth-profiles/store.js";
|
||||
import {
|
||||
fingerprintPreparedModelCatalogGeneration,
|
||||
type PreparedModelAuthRefreshWorkerInput,
|
||||
@@ -16,14 +19,15 @@ export async function runPreparedModelCatalogWorkerInput(
|
||||
): Promise<PreparedModelWorkerResult> {
|
||||
try {
|
||||
if (value.kind === "auth-refresh") {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{ agentDir: value.agentDir, store: value.authStore },
|
||||
]);
|
||||
const authStore = ensureAuthProfileStoreWithoutExternalProfiles(value.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
readOnly: true,
|
||||
syncExternalCli: false,
|
||||
...(value.inheritedAuthDir ? { inheritedAuthDir: value.inheritedAuthDir } : {}),
|
||||
// Durable profiles may be changed by another CLI process after this generation was built.
|
||||
// Reload them before adding current external overlays, while retaining only literals whose
|
||||
// unchanged SecretRefs were materialized by the owning generation.
|
||||
const authStore = preserveResolvedSecretBackedCredentials({
|
||||
next: loadAuthProfileStoreWithoutExternalProfiles(value.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
...(value.inheritedAuthDir ? { inheritedAuthDir: value.inheritedAuthDir } : {}),
|
||||
}),
|
||||
existing: value.authStore,
|
||||
});
|
||||
return {
|
||||
status: "ok",
|
||||
|
||||
Reference in New Issue
Block a user