fix(models): refresh durable auth in worker

This commit is contained in:
joshavant
2026-08-12 12:08:49 -05:00
parent 9422628f2f
commit dc7886a2cd
3 changed files with 121 additions and 10 deletions
+1 -1
View File
@@ -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 });
+13 -9
View File
@@ -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",