auth: avoid external cli sync on profile upsert

This commit is contained in:
Peter Steinberger
2026-04-08 20:10:25 +01:00
parent 21ef1bf8de
commit 5b4eb267b0
3 changed files with 45 additions and 7 deletions

View File

@@ -3,7 +3,7 @@ import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
import { resolveProviderIdForAuth } from "../provider-auth-aliases.js";
import { normalizeProviderId } from "../provider-id.js";
import {
ensureAuthProfileStore,
ensureAuthProfileStoreForLocalUpdate,
saveAuthProfileStore,
updateAuthProfileStoreWithLock,
} from "./store.js";
@@ -59,9 +59,9 @@ export function upsertAuthProfile(params: {
: params.credential.type === "token"
? { ...params.credential, token: normalizeSecretInput(params.credential.token) }
: params.credential;
const store = ensureAuthProfileStore(params.agentDir);
const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir);
store.profiles[params.profileId] = credential;
saveAuthProfileStore(store, params.agentDir);
saveAuthProfileStore(store, params.agentDir, { syncExternalCli: false });
}
export async function upsertAuthProfileWithLock(params: {

View File

@@ -29,6 +29,11 @@ import type { AuthProfileStore } from "./types.js";
type LoadAuthProfileStoreOptions = {
allowKeychainPrompt?: boolean;
readOnly?: boolean;
syncExternalCli?: boolean;
};
type SaveAuthProfileStoreOptions = {
syncExternalCli?: boolean;
};
const runtimeAuthStoreSnapshots = new Map<string, AuthProfileStore>();
@@ -192,6 +197,10 @@ function syncExternalCliCredentialsTimed(
return mutated;
}
function shouldSyncExternalCliCredentials(options?: { syncExternalCli?: boolean }): boolean {
return options?.syncExternalCli !== false;
}
export function loadAuthProfileStore(): AuthProfileStore {
const asStore = loadPersistedAuthProfileStore();
if (asStore) {
@@ -238,7 +247,9 @@ function loadAuthProfileStoreForAgent(
if (asStore) {
// Runtime secret activation must remain read-only:
// sync external CLI credentials in-memory, but never persist while readOnly.
syncExternalCliCredentialsTimed(asStore, { log: !readOnly });
if (shouldSyncExternalCliCredentials(options)) {
syncExternalCliCredentialsTimed(asStore, { log: !readOnly });
}
if (!readOnly) {
writeCachedAuthProfileStore({
authPath,
@@ -279,7 +290,9 @@ function loadAuthProfileStoreForAgent(
const mergedOAuth = mergeOAuthFileIntoStore(store);
// Keep external CLI credentials visible in runtime even during read-only loads.
syncExternalCliCredentialsTimed(store, { log: !readOnly });
if (shouldSyncExternalCliCredentials(options)) {
syncExternalCliCredentialsTimed(store, { log: !readOnly });
}
const forceReadOnly = process.env.OPENCLAW_AUTH_STORE_READONLY === "1";
const shouldWrite = !readOnly && !forceReadOnly && (legacy !== null || mergedOAuth);
if (shouldWrite) {
@@ -357,6 +370,22 @@ export function ensureAuthProfileStore(
return overlayExternalAuthProfiles(merged, { agentDir });
}
export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthProfileStore {
const options: LoadAuthProfileStoreOptions = { syncExternalCli: false };
const store = loadAuthProfileStoreForAgent(agentDir, options);
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (!agentDir || authPath === mainAuthPath) {
return store;
}
const mainStore = loadAuthProfileStoreForAgent(undefined, {
readOnly: true,
syncExternalCli: false,
});
return mergeAuthProfileStores(mainStore, store);
}
export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean {
const runtimeStore = resolveRuntimeAuthProfileStore(agentDir);
if (runtimeStore && Object.keys(runtimeStore.profiles).length > 0) {
@@ -376,7 +405,11 @@ export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean {
return false;
}
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
export function saveAuthProfileStore(
store: AuthProfileStore,
agentDir?: string,
options?: SaveAuthProfileStoreOptions,
): void {
const authPath = resolveAuthStorePath(agentDir);
const statePath = resolveAuthStatePath(agentDir);
const runtimeKey = resolveRuntimeStoreKey(agentDir);
@@ -394,7 +427,9 @@ export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string)
saveJsonFile(authPath, payload);
savePersistedAuthProfileState(store, agentDir);
const runtimeStore = cloneAuthProfileStore(store);
syncExternalCliCredentialsTimed(runtimeStore, { log: false });
if (shouldSyncExternalCliCredentials(options)) {
syncExternalCliCredentialsTimed(runtimeStore, { log: false });
}
writeCachedAuthProfileStore({
authPath,
authMtimeMs: readAuthStoreMtimeMs(authPath),

View File

@@ -36,6 +36,7 @@ vi.mock("../plugins/provider-openai-codex-oauth.js", () => ({
}));
const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
const runProviderModelSelectedHook = vi.hoisted(() => vi.fn(async () => {}));
vi.mock("../plugins/provider-auth-choice.runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-auth-choice.runtime.js")>(
"../plugins/provider-auth-choice.runtime.js",
@@ -43,6 +44,7 @@ vi.mock("../plugins/provider-auth-choice.runtime.js", async () => {
return {
...actual,
resolvePluginProviders,
runProviderModelSelectedHook,
};
});
@@ -644,6 +646,7 @@ describe("applyAuthChoice", () => {
vi.unstubAllGlobals();
resolvePluginProviders.mockReset();
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
runProviderModelSelectedHook.mockClear();
detectZaiEndpoint.mockReset();
detectZaiEndpoint.mockResolvedValue(null);
loginOpenAICodexOAuth.mockReset();