mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 06:44:56 +00:00
Preserve runtime external auth snapshots (#85558)
Summary: - The PR adds runtime-only external OAuth provenance to auth-profile stores, updates save/merge/read paths to ... e profiles in active snapshots while filtering disk persistence, and expands auth-profile regression tests. - PR surface: Source +381, Tests +974. Total +1355 across 8 files. - Reproducibility: yes. from source: current main writes the disk-filtered localStore into an existing runtime ... tches the reported credential drop path. I did not run a failing current-main repro in this read-only pass. Automerge notes: - PR branch already contained follow-up commit before automerge: Preserve runtime external auth snapshots Validation: - ClawSweeper review passed for heada73074ed45. - Required merge gates passed before the squash merge. Prepared head SHA:a73074ed45Review: https://github.com/openclaw/openclaw/pull/85558#issuecomment-4523577269 Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -1,27 +1,42 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveOAuthDir } from "../config/paths.js";
|
||||
import { legacyOAuthSidecarTestUtils } from "./auth-profiles/legacy-oauth-sidecar.js";
|
||||
import { resolveAuthStatePath, resolveAuthStorePath } from "./auth-profiles/paths.js";
|
||||
import { getRuntimeAuthProfileStoreSnapshot } from "./auth-profiles/runtime-snapshots.js";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStoreForLocalUpdate,
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
saveAuthProfileStore,
|
||||
} from "./auth-profiles/store.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
|
||||
const listRuntimeExternalAuthProfilesMock = vi.hoisted(() =>
|
||||
vi.fn<(_params: unknown) => unknown[]>(() => []),
|
||||
);
|
||||
const externalAuthMocks = vi.hoisted(() => ({
|
||||
listRuntimeExternalAuthProfiles: vi.fn((params?: { store?: unknown }) => {
|
||||
const store = params?.store as { profiles?: Record<string, unknown> } | undefined;
|
||||
return Object.entries(store?.profiles ?? {})
|
||||
.filter(([, credential]) => (credential as { type?: string }).type === "oauth")
|
||||
.map(([profileId, credential]) => ({
|
||||
profileId,
|
||||
credential,
|
||||
persistence: externalAuthMocks.shouldPersistExternalAuthProfile({ profileId })
|
||||
? "persisted"
|
||||
: "runtime-only",
|
||||
}));
|
||||
}),
|
||||
overlayExternalAuthProfiles: vi.fn((store: unknown) => store),
|
||||
shouldPersistExternalAuthProfile: vi.fn((_params?: { profileId?: string }) => true),
|
||||
}));
|
||||
|
||||
vi.mock("./auth-profiles/external-auth.js", () => ({
|
||||
listRuntimeExternalAuthProfiles: listRuntimeExternalAuthProfilesMock,
|
||||
overlayExternalAuthProfiles: <T>(store: T) => store,
|
||||
shouldPersistExternalAuthProfile: () => true,
|
||||
listRuntimeExternalAuthProfiles: externalAuthMocks.listRuntimeExternalAuthProfiles,
|
||||
overlayExternalAuthProfiles: externalAuthMocks.overlayExternalAuthProfiles,
|
||||
shouldPersistExternalAuthProfile: externalAuthMocks.shouldPersistExternalAuthProfile,
|
||||
syncPersistedExternalCliAuthProfiles: <T>(store: T) => store,
|
||||
}));
|
||||
|
||||
@@ -68,12 +83,12 @@ describe("saveAuthProfileStore", () => {
|
||||
};
|
||||
|
||||
try {
|
||||
listRuntimeExternalAuthProfilesMock.mockClear();
|
||||
externalAuthMocks.listRuntimeExternalAuthProfiles.mockClear();
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
expect(listRuntimeExternalAuthProfilesMock).toHaveBeenCalledTimes(1);
|
||||
expect(listRuntimeExternalAuthProfilesMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
expect(externalAuthMocks.listRuntimeExternalAuthProfiles).toHaveBeenCalledTimes(1);
|
||||
expect(externalAuthMocks.listRuntimeExternalAuthProfiles.mock.calls[0]?.[0]).toMatchObject({
|
||||
store,
|
||||
agentDir,
|
||||
});
|
||||
@@ -82,6 +97,12 @@ describe("saveAuthProfileStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
externalAuthMocks.listRuntimeExternalAuthProfiles.mockClear();
|
||||
externalAuthMocks.overlayExternalAuthProfiles.mockImplementation((store) => store);
|
||||
externalAuthMocks.shouldPersistExternalAuthProfile.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("strips plaintext when keyRef/tokenRef are present", async () => {
|
||||
const structuredCloneSpy = vi.spyOn(globalThis, "structuredClone");
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-"));
|
||||
@@ -445,6 +466,643 @@ describe("saveAuthProfileStore", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps runtime-only external cli oauth profiles in active runtime snapshots", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-external-"));
|
||||
const externalProfileId = "anthropic:claude-cli";
|
||||
const localAnthropicProfileId = "anthropic:local";
|
||||
const localProfileId = "openai:default";
|
||||
externalAuthMocks.shouldPersistExternalAuthProfile.mockImplementation(
|
||||
(params?: { profileId?: string }) => params?.profileId !== externalProfileId,
|
||||
);
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[externalProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "stale-external-access",
|
||||
refresh: "stale-external-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const runtimeStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [externalProfileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[externalProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: 2,
|
||||
},
|
||||
[localProfileId]: {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-local",
|
||||
},
|
||||
[localAnthropicProfileId]: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-anthropic-local",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [externalProfileId],
|
||||
openai: [localProfileId],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: externalProfileId,
|
||||
openai: localProfileId,
|
||||
},
|
||||
usageStats: {
|
||||
[externalProfileId]: {
|
||||
lastUsed: 123,
|
||||
},
|
||||
[localProfileId]: {
|
||||
lastUsed: 456,
|
||||
},
|
||||
},
|
||||
};
|
||||
externalAuthMocks.overlayExternalAuthProfiles.mockImplementation((store) => {
|
||||
const base = store as AuthProfileStore;
|
||||
const externalUsage = base.usageStats?.[externalProfileId] ?? { lastUsed: 123 };
|
||||
return {
|
||||
...base,
|
||||
profiles: {
|
||||
...base.profiles,
|
||||
[externalProfileId]: runtimeStore.profiles[externalProfileId],
|
||||
},
|
||||
order: {
|
||||
...base.order,
|
||||
anthropic: [externalProfileId],
|
||||
},
|
||||
lastGood: {
|
||||
...base.lastGood,
|
||||
anthropic: externalProfileId,
|
||||
},
|
||||
usageStats: {
|
||||
...base.usageStats,
|
||||
[externalProfileId]: externalUsage,
|
||||
},
|
||||
runtimeExternalProfileIds: [externalProfileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
};
|
||||
});
|
||||
|
||||
saveAuthProfileStore(runtimeStore, agentDir);
|
||||
|
||||
const persisted = JSON.parse(await fs.readFile(resolveAuthStorePath(agentDir), "utf8")) as {
|
||||
profiles: Record<string, unknown>;
|
||||
};
|
||||
expect(persisted.profiles[externalProfileId]).toBeUndefined();
|
||||
expectProfileFields(persisted.profiles[localProfileId], {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-local",
|
||||
});
|
||||
|
||||
const persistedState = JSON.parse(
|
||||
await fs.readFile(resolveAuthStatePath(agentDir), "utf8"),
|
||||
) as {
|
||||
order?: Record<string, string[]>;
|
||||
lastGood?: Record<string, string>;
|
||||
usageStats?: Record<string, unknown>;
|
||||
};
|
||||
expect(persistedState.order?.anthropic).toBeUndefined();
|
||||
expect(persistedState.lastGood?.anthropic).toBeUndefined();
|
||||
expect(persistedState.usageStats?.[externalProfileId]).toBeUndefined();
|
||||
expect(persistedState.order?.openai).toEqual([localProfileId]);
|
||||
|
||||
const runtime = ensureAuthProfileStore(agentDir);
|
||||
expectProfileFields(runtime.profiles[externalProfileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
});
|
||||
expect(runtime.order?.anthropic).toEqual([externalProfileId]);
|
||||
expect(runtime.lastGood?.anthropic).toBe(externalProfileId);
|
||||
expect(runtime.usageStats?.[externalProfileId]?.lastUsed).toBe(123);
|
||||
|
||||
const runtimeWithoutExternal = ensureAuthProfileStoreWithoutExternalProfiles(agentDir);
|
||||
expect(runtimeWithoutExternal.profiles[externalProfileId]).toBeUndefined();
|
||||
expect(runtimeWithoutExternal.order?.anthropic).toBeUndefined();
|
||||
expect(runtimeWithoutExternal.lastGood?.anthropic).toBeUndefined();
|
||||
expect(runtimeWithoutExternal.usageStats?.[externalProfileId]).toBeUndefined();
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
...runtimeStore,
|
||||
profiles: {
|
||||
...runtimeStore.profiles,
|
||||
[externalProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-external-access",
|
||||
refresh: "refreshed-external-refresh",
|
||||
expires: 3,
|
||||
},
|
||||
},
|
||||
usageStats: {
|
||||
...runtimeStore.usageStats,
|
||||
[externalProfileId]: {
|
||||
lastUsed: 789,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
const snapshotAfterRuntimeBackedSave = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expectProfileFields(snapshotAfterRuntimeBackedSave?.profiles[externalProfileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-external-access",
|
||||
refresh: "refreshed-external-refresh",
|
||||
});
|
||||
expect(snapshotAfterRuntimeBackedSave?.usageStats?.[externalProfileId]?.lastUsed).toBe(789);
|
||||
|
||||
saveAuthProfileStore(runtimeWithoutExternal, agentDir);
|
||||
const persistedAfterDiskBackedSave = JSON.parse(
|
||||
await fs.readFile(resolveAuthStorePath(agentDir), "utf8"),
|
||||
) as {
|
||||
profiles: Record<string, unknown>;
|
||||
};
|
||||
expect(persistedAfterDiskBackedSave.profiles[externalProfileId]).toBeUndefined();
|
||||
const snapshotAfterDiskBackedSave = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshotAfterDiskBackedSave?.runtimeExternalProfileIds).toEqual([externalProfileId]);
|
||||
expect(snapshotAfterDiskBackedSave?.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expectProfileFields(snapshotAfterDiskBackedSave?.profiles[externalProfileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-external-access",
|
||||
refresh: "refreshed-external-refresh",
|
||||
});
|
||||
expectProfileFields(snapshotAfterDiskBackedSave?.profiles[localProfileId], {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-local",
|
||||
});
|
||||
expect(snapshotAfterDiskBackedSave?.order?.anthropic).toEqual([externalProfileId]);
|
||||
expect(snapshotAfterDiskBackedSave?.lastGood?.anthropic).toBe(externalProfileId);
|
||||
expect(snapshotAfterDiskBackedSave?.usageStats?.[externalProfileId]?.lastUsed).toBe(789);
|
||||
const ensuredRuntime = ensureAuthProfileStore(agentDir);
|
||||
expectProfileFields(ensuredRuntime.profiles[localProfileId], {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
key: "sk-local",
|
||||
});
|
||||
expect(ensuredRuntime.order?.anthropic).toEqual([externalProfileId]);
|
||||
expect(ensuredRuntime.lastGood?.anthropic).toBe(externalProfileId);
|
||||
expect(ensuredRuntime.usageStats?.[externalProfileId]?.lastUsed).toBe(789);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
...runtimeWithoutExternal,
|
||||
order: {
|
||||
...runtimeWithoutExternal.order,
|
||||
anthropic: [localAnthropicProfileId],
|
||||
},
|
||||
lastGood: {
|
||||
...runtimeWithoutExternal.lastGood,
|
||||
anthropic: localAnthropicProfileId,
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
const snapshotAfterExplicitOrderSave = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expectProfileFields(snapshotAfterExplicitOrderSave?.profiles[externalProfileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-external-access",
|
||||
refresh: "refreshed-external-refresh",
|
||||
});
|
||||
expect(snapshotAfterExplicitOrderSave?.order?.anthropic).toEqual([localAnthropicProfileId]);
|
||||
expect(snapshotAfterExplicitOrderSave?.lastGood?.anthropic).toBe(localAnthropicProfileId);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
...runtimeWithoutExternal,
|
||||
runtimeExternalProfileIds: [],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
const snapshotAfterAuthoritativeRemoval = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshotAfterAuthoritativeRemoval?.runtimeExternalProfileIds).toEqual([]);
|
||||
expect(snapshotAfterAuthoritativeRemoval?.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expect(snapshotAfterAuthoritativeRemoval?.profiles[externalProfileId]).toBeUndefined();
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves unrelated runtime-only external profiles after scoped runtime saves", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-scoped-"));
|
||||
const scopedProfileId = "anthropic:claude-cli";
|
||||
const unrelatedProfileId = "minimax:minimax-cli";
|
||||
externalAuthMocks.shouldPersistExternalAuthProfile.mockReturnValue(false);
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [scopedProfileId, unrelatedProfileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[scopedProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "old-scoped-access",
|
||||
refresh: "old-scoped-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
[unrelatedProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "minimax-portal",
|
||||
access: "unrelated-access",
|
||||
refresh: "unrelated-refresh",
|
||||
expires: 2,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [scopedProfileId],
|
||||
"minimax-portal": [unrelatedProfileId],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: scopedProfileId,
|
||||
"minimax-portal": unrelatedProfileId,
|
||||
},
|
||||
usageStats: {
|
||||
[scopedProfileId]: { lastUsed: 10 },
|
||||
[unrelatedProfileId]: { lastUsed: 20 },
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [scopedProfileId],
|
||||
profiles: {
|
||||
[scopedProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "new-scoped-access",
|
||||
refresh: "new-scoped-refresh",
|
||||
expires: 3,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [scopedProfileId],
|
||||
},
|
||||
usageStats: {
|
||||
[scopedProfileId]: { lastUsed: 30 },
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const snapshot = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshot?.runtimeExternalProfileIds).toEqual([scopedProfileId, unrelatedProfileId]);
|
||||
expect(snapshot?.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expectProfileFields(snapshot?.profiles[scopedProfileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "new-scoped-access",
|
||||
refresh: "new-scoped-refresh",
|
||||
});
|
||||
expectProfileFields(snapshot?.profiles[unrelatedProfileId], {
|
||||
type: "oauth",
|
||||
provider: "minimax-portal",
|
||||
access: "unrelated-access",
|
||||
refresh: "unrelated-refresh",
|
||||
});
|
||||
expect(snapshot?.usageStats?.[scopedProfileId]?.lastUsed).toBe(30);
|
||||
expect(snapshot?.usageStats?.[unrelatedProfileId]?.lastUsed).toBe(20);
|
||||
expect(snapshot?.order?.anthropic).toEqual([scopedProfileId]);
|
||||
expect(snapshot?.order?.["minimax-portal"]).toEqual([unrelatedProfileId]);
|
||||
const scopedRead = ensureAuthProfileStore(agentDir, {
|
||||
externalCliProviderIds: ["anthropic"],
|
||||
});
|
||||
expect(scopedRead.profiles[unrelatedProfileId]).toBeUndefined();
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [scopedProfileId],
|
||||
profiles: {
|
||||
[scopedProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "newer-scoped-access",
|
||||
refresh: "newer-scoped-refresh",
|
||||
expires: 4,
|
||||
},
|
||||
[unrelatedProfileId]: {
|
||||
type: "oauth",
|
||||
provider: "minimax-portal",
|
||||
access: "unrelated-access",
|
||||
refresh: "unrelated-refresh",
|
||||
expires: 2,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [scopedProfileId],
|
||||
"minimax-portal": [unrelatedProfileId],
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const snapshotAfterProfileCarryingScopedSave = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshotAfterProfileCarryingScopedSave?.runtimeExternalProfileIds).toEqual([
|
||||
scopedProfileId,
|
||||
unrelatedProfileId,
|
||||
]);
|
||||
expect(snapshotAfterProfileCarryingScopedSave?.runtimeExternalProfileIdsAuthoritative).toBe(
|
||||
true,
|
||||
);
|
||||
const runtimeWithoutExternal = ensureAuthProfileStoreWithoutExternalProfiles(agentDir);
|
||||
expect(runtimeWithoutExternal.profiles[unrelatedProfileId]).toBeUndefined();
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not persist profiles already marked runtime-only external", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-runtime-only-"));
|
||||
const profileId = "anthropic:claude-cli";
|
||||
|
||||
try {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [profileId],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: profileId,
|
||||
},
|
||||
usageStats: {
|
||||
[profileId]: { lastUsed: 10 },
|
||||
},
|
||||
};
|
||||
replaceRuntimeAuthProfileStoreSnapshots([{ agentDir, store }]);
|
||||
|
||||
saveAuthProfileStore(store, agentDir);
|
||||
|
||||
const authProfiles = JSON.parse(
|
||||
await fs.readFile(resolveAuthStorePath(agentDir), "utf8"),
|
||||
) as {
|
||||
profiles: Record<string, unknown>;
|
||||
};
|
||||
expect(authProfiles.profiles[profileId]).toBeUndefined();
|
||||
|
||||
const snapshot = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshot?.runtimeExternalProfileIds).toEqual([profileId]);
|
||||
expect(snapshot?.profiles[profileId]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
});
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not persist runtime-only external profiles without an installed snapshot", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-unsnapshotted-"));
|
||||
const profileId = "openai-codex:default";
|
||||
|
||||
try {
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "runtime-access",
|
||||
refresh: "runtime-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const authProfiles = JSON.parse(
|
||||
await fs.readFile(resolveAuthStorePath(agentDir), "utf8"),
|
||||
) as {
|
||||
profiles: Record<string, unknown>;
|
||||
};
|
||||
expect(authProfiles.profiles[profileId]).toBeUndefined();
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns active runtime-only external profiles on unscoped reads", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-read-runtime-only-"));
|
||||
const profileId = "openai-codex:default";
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "runtime-access",
|
||||
refresh: "runtime-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
usageStats: {
|
||||
[profileId]: { lastUsed: 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.runtimeExternalProfileIds).toEqual([profileId]);
|
||||
expectProfileFields(store.profiles[profileId], {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "runtime-access",
|
||||
refresh: "runtime-refresh",
|
||||
});
|
||||
expect(store.usageStats?.[profileId]?.lastUsed).toBe(10);
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not resurrect runtime-only profiles after authoritative empty overlays", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-read-removed-"));
|
||||
const profileId = "anthropic:claude-cli";
|
||||
externalAuthMocks.overlayExternalAuthProfiles.mockImplementation((store) => ({
|
||||
...(store as AuthProfileStore),
|
||||
runtimeExternalProfileIds: [],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
}));
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "runtime-access",
|
||||
refresh: "runtime-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(store.runtimeExternalProfileIds).toEqual([]);
|
||||
expect(store.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expect(store.profiles[profileId]).toBeUndefined();
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("persists refreshed runtime-only external OAuth credentials", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-refreshed-"));
|
||||
const profileId = "anthropic:claude-cli";
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-access",
|
||||
refresh: "refreshed-refresh",
|
||||
expires: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const authProfiles = JSON.parse(
|
||||
await fs.readFile(resolveAuthStorePath(agentDir), "utf8"),
|
||||
) as {
|
||||
profiles: Record<string, unknown>;
|
||||
};
|
||||
expectProfileFields(authProfiles.profiles[profileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-access",
|
||||
refresh: "refreshed-refresh",
|
||||
});
|
||||
|
||||
const activeRuntime = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
if (!activeRuntime) {
|
||||
throw new Error("expected active runtime auth snapshot");
|
||||
}
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
...activeRuntime,
|
||||
usageStats: {
|
||||
[profileId]: { lastUsed: 20 },
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const authProfilesAfterUsageSave = JSON.parse(
|
||||
await fs.readFile(resolveAuthStorePath(agentDir), "utf8"),
|
||||
) as {
|
||||
profiles: Record<string, unknown>;
|
||||
};
|
||||
expectProfileFields(authProfilesAfterUsageSave.profiles[profileId], {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "refreshed-access",
|
||||
refresh: "refreshed-refresh",
|
||||
});
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("writes runtime scheduling state to auth-state.json only", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-state-"));
|
||||
try {
|
||||
@@ -723,4 +1381,116 @@ describe("saveAuthProfileStore", () => {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps local replacements for old runtime-only profile ids visible", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-replace-"));
|
||||
const profileId = "anthropic:claude-cli";
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const snapshot = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshot?.runtimeExternalProfileIds).toEqual([]);
|
||||
expect(snapshot?.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expect(snapshot?.profiles[profileId]).toMatchObject({
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
});
|
||||
|
||||
const runtimeWithoutExternal = ensureAuthProfileStoreWithoutExternalProfiles(agentDir);
|
||||
expect(runtimeWithoutExternal.profiles[profileId]).toMatchObject({
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
});
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("clears non-authoritative runtime-only metadata after local replacements", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-save-replace-scoped-"));
|
||||
const profileId = "anthropic:claude-cli";
|
||||
|
||||
try {
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
agentDir,
|
||||
store: {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "external-access",
|
||||
refresh: "external-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const snapshot = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
expect(snapshot?.runtimeExternalProfileIds).toBeUndefined();
|
||||
expect(snapshot?.runtimeExternalProfileIdsAuthoritative).toBeUndefined();
|
||||
expect(snapshot?.profiles[profileId]).toMatchObject({
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
});
|
||||
} finally {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,6 +121,10 @@ function hasPersistableExternalCliSyncCandidate(
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasScopedExternalCliOverlay(params?: ExternalCliOverlayOptions): boolean {
|
||||
return Boolean(params?.externalCliProviderIds || params?.externalCliProfileIds);
|
||||
}
|
||||
|
||||
export function overlayExternalAuthProfiles(
|
||||
store: AuthProfileStore,
|
||||
params?: { agentDir?: string; env?: NodeJS.ProcessEnv } & ExternalCliOverlayOptions,
|
||||
@@ -131,7 +135,9 @@ export function overlayExternalAuthProfiles(
|
||||
env: params?.env,
|
||||
externalCli: params,
|
||||
});
|
||||
return overlayRuntimeExternalOAuthProfiles(store, profiles);
|
||||
return overlayRuntimeExternalOAuthProfiles(store, profiles, {
|
||||
runtimeExternalProfileIdsAuthoritative: !hasScopedExternalCliOverlay(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldPersistExternalAuthProfile(params: {
|
||||
|
||||
@@ -51,4 +51,69 @@ describe("overlayRuntimeExternalOAuthProfiles", () => {
|
||||
structuredCloneSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves existing runtime-only provenance for non-authoritative overlays", () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: ["minimax:minimax-cli"],
|
||||
profiles: {
|
||||
"anthropic:claude-cli": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "old-access",
|
||||
refresh: "old-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
"minimax:minimax-cli": {
|
||||
type: "oauth",
|
||||
provider: "minimax-portal",
|
||||
access: "minimax-access",
|
||||
refresh: "minimax-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const overlaid = overlayRuntimeExternalOAuthProfiles(store, [
|
||||
{
|
||||
profileId: "anthropic:claude-cli",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "new-access",
|
||||
refresh: "new-refresh",
|
||||
expires: 2,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(overlaid.runtimeExternalProfileIds).toEqual([
|
||||
"anthropic:claude-cli",
|
||||
"minimax:minimax-cli",
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves existing runtime-only provenance for authoritative overlays", () => {
|
||||
const store: AuthProfileStore = {
|
||||
version: 1,
|
||||
runtimeExternalProfileIds: ["minimax:minimax-cli"],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
"minimax:minimax-cli": {
|
||||
type: "oauth",
|
||||
provider: "minimax-portal",
|
||||
access: "minimax-access",
|
||||
refresh: "minimax-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const overlaid = overlayRuntimeExternalOAuthProfiles(store, [], {
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
});
|
||||
|
||||
expect(overlaid.runtimeExternalProfileIds).toEqual(["minimax:minimax-cli"]);
|
||||
expect(overlaid.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,15 +169,29 @@ export function shouldBootstrapFromExternalCliCredential(params: {
|
||||
export function overlayRuntimeExternalOAuthProfiles(
|
||||
store: AuthProfileStore,
|
||||
profiles: Iterable<RuntimeExternalOAuthProfile>,
|
||||
options?: { runtimeExternalProfileIdsAuthoritative?: boolean },
|
||||
): AuthProfileStore {
|
||||
const externalProfiles = Array.from(profiles);
|
||||
if (externalProfiles.length === 0) {
|
||||
return store;
|
||||
}
|
||||
const next = cloneAuthProfileStore(store);
|
||||
for (const profile of externalProfiles) {
|
||||
next.profiles[profile.profileId] = profile.credential;
|
||||
}
|
||||
const runtimeOnlyProfileIds = new Set(
|
||||
externalProfiles
|
||||
.filter((profile) => profile.persistence !== "persisted")
|
||||
.map((profile) => profile.profileId),
|
||||
);
|
||||
for (const profileId of store.runtimeExternalProfileIds ?? []) {
|
||||
if (next.profiles[profileId]) {
|
||||
runtimeOnlyProfileIds.add(profileId);
|
||||
}
|
||||
}
|
||||
next.runtimeExternalProfileIds =
|
||||
runtimeOnlyProfileIds.size > 0 || options?.runtimeExternalProfileIdsAuthoritative === true
|
||||
? [...runtimeOnlyProfileIds].toSorted()
|
||||
: undefined;
|
||||
next.runtimeExternalProfileIdsAuthoritative =
|
||||
options?.runtimeExternalProfileIdsAuthoritative === true ? true : undefined;
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import { resolveOAuthDir } from "../../config/paths.js";
|
||||
import { AUTH_STORE_VERSION } from "./constants.js";
|
||||
import { legacyOAuthSidecarTestUtils } from "./legacy-oauth-sidecar.js";
|
||||
import { resolveAuthStorePath } from "./paths.js";
|
||||
import { coercePersistedAuthProfileStore, loadPersistedAuthProfileStore } from "./persisted.js";
|
||||
import {
|
||||
coercePersistedAuthProfileStore,
|
||||
loadPersistedAuthProfileStore,
|
||||
mergeAuthProfileStores,
|
||||
} from "./persisted.js";
|
||||
|
||||
function withEnvValue(key: string, value: string | undefined): () => void {
|
||||
const previous = process.env[key];
|
||||
@@ -212,4 +216,139 @@ describe("persisted auth profile boundary", () => {
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("lets authoritative runtime external metadata remove stale base profiles", () => {
|
||||
const merged = mergeAuthProfileStores(
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
runtimeExternalProfileIds: ["anthropic:claude-cli"],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
"anthropic:claude-cli": {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "stale-access",
|
||||
refresh: "stale-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: ["anthropic:claude-cli"],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: "anthropic:claude-cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
runtimeExternalProfileIds: [],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(merged.runtimeExternalProfileIds).toEqual([]);
|
||||
expect(merged.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expect(merged.profiles["anthropic:claude-cli"]).toBeUndefined();
|
||||
expect(merged.order?.anthropic).toBeUndefined();
|
||||
expect(merged.lastGood?.anthropic).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps override profiles when authoritative metadata removes base runtime external state", () => {
|
||||
const profileId = "anthropic:claude-cli";
|
||||
const merged = mergeAuthProfileStores(
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "stale-access",
|
||||
refresh: "stale-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [profileId],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: profileId,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
runtimeExternalProfileIds: [],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [profileId],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: profileId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(merged.runtimeExternalProfileIds).toEqual([]);
|
||||
expect(merged.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expect(merged.profiles[profileId]).toMatchObject({
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-local",
|
||||
});
|
||||
expect(merged.order?.anthropic).toEqual([profileId]);
|
||||
expect(merged.lastGood?.anthropic).toBe(profileId);
|
||||
});
|
||||
|
||||
it("preserves inherited base runtime external profiles during agent-store merges", () => {
|
||||
const profileId = "anthropic:claude-cli";
|
||||
const merged = mergeAuthProfileStores(
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
runtimeExternalProfileIds: [profileId],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "main-access",
|
||||
refresh: "main-refresh",
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
anthropic: [profileId],
|
||||
},
|
||||
lastGood: {
|
||||
anthropic: profileId,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
runtimeExternalProfileIds: [],
|
||||
runtimeExternalProfileIdsAuthoritative: true,
|
||||
profiles: {},
|
||||
},
|
||||
{ preserveBaseRuntimeExternalProfiles: true },
|
||||
);
|
||||
|
||||
expect(merged.runtimeExternalProfileIds).toEqual([profileId]);
|
||||
expect(merged.runtimeExternalProfileIdsAuthoritative).toBe(true);
|
||||
expect(merged.profiles[profileId]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "main-access",
|
||||
refresh: "main-refresh",
|
||||
});
|
||||
expect(merged.order?.anthropic).toEqual([profileId]);
|
||||
expect(merged.lastGood?.anthropic).toBe(profileId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -715,23 +715,96 @@ function reconcileMainStoreOAuthProfileDrift(params: {
|
||||
export function mergeAuthProfileStores(
|
||||
base: AuthProfileStore,
|
||||
override: AuthProfileStore,
|
||||
options?: { preserveBaseRuntimeExternalProfiles?: boolean },
|
||||
): AuthProfileStore {
|
||||
if (
|
||||
Object.keys(override.profiles).length === 0 &&
|
||||
!override.order &&
|
||||
!override.lastGood &&
|
||||
!override.usageStats
|
||||
!override.usageStats &&
|
||||
override.runtimeExternalProfileIds === undefined &&
|
||||
override.runtimeExternalProfileIdsAuthoritative !== true
|
||||
) {
|
||||
return base;
|
||||
}
|
||||
const overrideProfileIds = new Set(Object.keys(override.profiles));
|
||||
const overrideRuntimeExternalProfileIds = new Set(override.runtimeExternalProfileIds ?? []);
|
||||
const removedRuntimeExternalProfileIds = new Set(
|
||||
override.runtimeExternalProfileIdsAuthoritative === true &&
|
||||
options?.preserveBaseRuntimeExternalProfiles !== true
|
||||
? (base.runtimeExternalProfileIds ?? []).filter(
|
||||
(profileId) =>
|
||||
!overrideRuntimeExternalProfileIds.has(profileId) && !overrideProfileIds.has(profileId),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
const profiles = { ...base.profiles, ...override.profiles };
|
||||
for (const profileId of removedRuntimeExternalProfileIds) {
|
||||
delete profiles[profileId];
|
||||
}
|
||||
const mergedOrder = mergeRecord(base.order, override.order);
|
||||
const order = mergedOrder
|
||||
? Object.fromEntries(
|
||||
Object.entries(mergedOrder)
|
||||
.map(([provider, profileIds]) => [
|
||||
provider,
|
||||
profileIds.filter((profileId) => profiles[profileId]),
|
||||
])
|
||||
.filter(([, profileIds]) => profileIds.length > 0),
|
||||
)
|
||||
: undefined;
|
||||
const mergedLastGood = mergeRecord(base.lastGood, override.lastGood);
|
||||
const lastGood = mergedLastGood
|
||||
? Object.fromEntries(
|
||||
Object.entries(mergedLastGood).filter(([, profileId]) => profiles[profileId]),
|
||||
)
|
||||
: undefined;
|
||||
const mergedUsageStats = mergeRecord(base.usageStats, override.usageStats);
|
||||
const usageStats = mergedUsageStats
|
||||
? Object.fromEntries(
|
||||
Object.entries(mergedUsageStats).filter(([profileId]) => profiles[profileId]),
|
||||
)
|
||||
: undefined;
|
||||
const merged = {
|
||||
version: Math.max(base.version, override.version ?? base.version),
|
||||
profiles: { ...base.profiles, ...override.profiles },
|
||||
order: mergeRecord(base.order, override.order),
|
||||
lastGood: mergeRecord(base.lastGood, override.lastGood),
|
||||
usageStats: mergeRecord(base.usageStats, override.usageStats),
|
||||
profiles,
|
||||
order,
|
||||
lastGood,
|
||||
usageStats,
|
||||
};
|
||||
return reconcileMainStoreOAuthProfileDrift({ base, override, merged });
|
||||
const baseRuntimeExternalProfileIds =
|
||||
override.runtimeExternalProfileIdsAuthoritative === true &&
|
||||
options?.preserveBaseRuntimeExternalProfiles !== true
|
||||
? []
|
||||
: (base.runtimeExternalProfileIds ?? []).filter(
|
||||
(profileId) => !overrideProfileIds.has(profileId),
|
||||
);
|
||||
const runtimeExternalProfileIds = [
|
||||
...baseRuntimeExternalProfileIds,
|
||||
...(override.runtimeExternalProfileIds ?? []),
|
||||
]
|
||||
.filter((profileId) => merged.profiles[profileId])
|
||||
.toSorted();
|
||||
const runtimeExternalProfileIdsAuthoritative =
|
||||
base.runtimeExternalProfileIdsAuthoritative === true ||
|
||||
override.runtimeExternalProfileIdsAuthoritative === true;
|
||||
const runtimeExternalProfileMetadata =
|
||||
runtimeExternalProfileIds.length > 0 || runtimeExternalProfileIdsAuthoritative
|
||||
? {
|
||||
runtimeExternalProfileIds: [...new Set(runtimeExternalProfileIds)],
|
||||
...(runtimeExternalProfileIdsAuthoritative
|
||||
? { runtimeExternalProfileIdsAuthoritative: true }
|
||||
: {}),
|
||||
}
|
||||
: {};
|
||||
return reconcileMainStoreOAuthProfileDrift({
|
||||
base,
|
||||
override,
|
||||
merged: {
|
||||
...merged,
|
||||
...runtimeExternalProfileMetadata,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function buildPersistedAuthProfileSecretsStore(
|
||||
|
||||
@@ -161,7 +161,9 @@ function resolveRuntimeAuthProfileStore(
|
||||
}
|
||||
|
||||
if (mainStore && requestedStore) {
|
||||
return mergeAuthProfileStores(mainStore, requestedStore);
|
||||
return mergeAuthProfileStores(mainStore, requestedStore, {
|
||||
preserveBaseRuntimeExternalProfiles: true,
|
||||
});
|
||||
}
|
||||
if (requestedStore) {
|
||||
const persistedMainStore = loadAuthProfileStoreForAgent(undefined, {
|
||||
@@ -169,7 +171,9 @@ function resolveRuntimeAuthProfileStore(
|
||||
syncExternalCli: false,
|
||||
...resolvePersistedLoadOptions(options),
|
||||
});
|
||||
return mergeAuthProfileStores(persistedMainStore, requestedStore);
|
||||
return mergeAuthProfileStores(persistedMainStore, requestedStore, {
|
||||
preserveBaseRuntimeExternalProfiles: true,
|
||||
});
|
||||
}
|
||||
if (mainStore) {
|
||||
return mainStore;
|
||||
@@ -294,6 +298,12 @@ function resolveExternalCliOverlayOptions(
|
||||
};
|
||||
}
|
||||
|
||||
function hasScopedExternalCliOverlay(options: ResolvedExternalCliOverlayOptions): boolean {
|
||||
return (
|
||||
options.externalCliProviderIds !== undefined || options.externalCliProfileIds !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function maybeSyncPersistedExternalCliAuthProfiles(params: {
|
||||
store: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
@@ -385,6 +395,24 @@ function shouldKeepProfileInLocalStore(params: {
|
||||
if (params.options?.filterExternalAuthProfiles === false) {
|
||||
return true;
|
||||
}
|
||||
if (params.store.runtimeExternalProfileIds?.includes(params.profileId)) {
|
||||
const persistedCredential = loadPersistedAuthProfileStore(params.agentDir)?.profiles[
|
||||
params.profileId
|
||||
];
|
||||
if (persistedCredential) {
|
||||
return shouldPersistRuntimeExternalOAuthProfile({
|
||||
profileId: params.profileId,
|
||||
credential: params.credential,
|
||||
profiles: params.externalProfiles(),
|
||||
});
|
||||
}
|
||||
const runtimeCredential = getRuntimeAuthProfileStoreSnapshot(params.agentDir)?.profiles[
|
||||
params.profileId
|
||||
];
|
||||
if (!runtimeCredential || isDeepStrictEqual(runtimeCredential, params.credential)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return shouldPersistRuntimeExternalOAuthProfile({
|
||||
profileId: params.profileId,
|
||||
credential: params.credential,
|
||||
@@ -392,6 +420,44 @@ function shouldKeepProfileInLocalStore(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function pruneAuthProfileStoreReferences(
|
||||
store: AuthProfileStore,
|
||||
keptProfileIds: Set<string>,
|
||||
): void {
|
||||
store.order = store.order
|
||||
? Object.fromEntries(
|
||||
Object.entries(store.order)
|
||||
.map(([provider, profileIds]) => [
|
||||
provider,
|
||||
profileIds.filter((profileId) => keptProfileIds.has(profileId)),
|
||||
])
|
||||
.filter(([, profileIds]) => profileIds.length > 0),
|
||||
)
|
||||
: undefined;
|
||||
store.lastGood = store.lastGood
|
||||
? Object.fromEntries(
|
||||
Object.entries(store.lastGood).filter(([, profileId]) => keptProfileIds.has(profileId)),
|
||||
)
|
||||
: undefined;
|
||||
store.usageStats = store.usageStats
|
||||
? Object.fromEntries(
|
||||
Object.entries(store.usageStats).filter(([profileId]) => keptProfileIds.has(profileId)),
|
||||
)
|
||||
: undefined;
|
||||
store.runtimeExternalProfileIds = store.runtimeExternalProfileIds
|
||||
?.filter((profileId) => keptProfileIds.has(profileId))
|
||||
.toSorted();
|
||||
if (
|
||||
store.runtimeExternalProfileIds?.length === 0 &&
|
||||
store.runtimeExternalProfileIdsAuthoritative !== true
|
||||
) {
|
||||
store.runtimeExternalProfileIds = undefined;
|
||||
}
|
||||
if (store.runtimeExternalProfileIdsAuthoritative === true) {
|
||||
store.runtimeExternalProfileIds ??= [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildLocalAuthProfileStoreForSave(params: {
|
||||
store: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
@@ -417,33 +483,216 @@ function buildLocalAuthProfileStoreForSave(params: {
|
||||
),
|
||||
);
|
||||
const keptProfileIds = new Set(Object.keys(localStore.profiles));
|
||||
localStore.order = localStore.order
|
||||
? Object.fromEntries(
|
||||
Object.entries(localStore.order)
|
||||
.map(([provider, profileIds]) => [
|
||||
provider,
|
||||
profileIds.filter((profileId) => keptProfileIds.has(profileId)),
|
||||
])
|
||||
.filter(([, profileIds]) => profileIds.length > 0),
|
||||
)
|
||||
: undefined;
|
||||
localStore.lastGood = localStore.lastGood
|
||||
? Object.fromEntries(
|
||||
Object.entries(localStore.lastGood).filter(([, profileId]) =>
|
||||
keptProfileIds.has(profileId),
|
||||
),
|
||||
)
|
||||
: undefined;
|
||||
localStore.usageStats = localStore.usageStats
|
||||
? Object.fromEntries(
|
||||
Object.entries(localStore.usageStats).filter(([profileId]) =>
|
||||
keptProfileIds.has(profileId),
|
||||
),
|
||||
)
|
||||
: undefined;
|
||||
pruneAuthProfileStoreReferences(localStore, keptProfileIds);
|
||||
if (params.options?.filterExternalAuthProfiles !== false) {
|
||||
localStore.runtimeExternalProfileIds = undefined;
|
||||
localStore.runtimeExternalProfileIdsAuthoritative = undefined;
|
||||
}
|
||||
return localStore;
|
||||
}
|
||||
|
||||
function buildAuthProfileStoreWithoutExternalProfiles(params: {
|
||||
store: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
options?: Pick<LoadAuthProfileStoreOptions, "allowKeychainPrompt" | "resolveLegacyOAuthSidecars">;
|
||||
}): AuthProfileStore {
|
||||
const runtimeExternalProfileIds = new Set(params.store.runtimeExternalProfileIds ?? []);
|
||||
const localStore = cloneAuthProfileStore(params.store);
|
||||
if (runtimeExternalProfileIds.size === 0) {
|
||||
localStore.runtimeExternalProfileIds = undefined;
|
||||
localStore.runtimeExternalProfileIdsAuthoritative = undefined;
|
||||
return localStore;
|
||||
}
|
||||
for (const profileId of runtimeExternalProfileIds) {
|
||||
delete localStore.profiles[profileId];
|
||||
}
|
||||
const keptProfileIds = new Set(Object.keys(localStore.profiles));
|
||||
pruneAuthProfileStoreReferences(localStore, keptProfileIds);
|
||||
localStore.runtimeExternalProfileIds = undefined;
|
||||
localStore.runtimeExternalProfileIdsAuthoritative = undefined;
|
||||
const persistedStore = loadAuthProfileStoreWithoutExternalProfiles(
|
||||
params.agentDir,
|
||||
params.options,
|
||||
);
|
||||
return mergeAuthProfileStores(persistedStore, localStore);
|
||||
}
|
||||
|
||||
function buildRuntimeAuthProfileStoreForSave(params: {
|
||||
store: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
options?: SaveAuthProfileStoreOptions;
|
||||
}): AuthProfileStore {
|
||||
return buildLocalAuthProfileStoreForSave({
|
||||
...params,
|
||||
options: {
|
||||
...params.options,
|
||||
filterExternalAuthProfiles: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setRuntimeExternalProfileMetadata(params: {
|
||||
store: AuthProfileStore;
|
||||
profileIds: ReadonlySet<string>;
|
||||
authoritative: boolean;
|
||||
}): void {
|
||||
const profileIds = [...params.profileIds].toSorted();
|
||||
params.store.runtimeExternalProfileIds =
|
||||
profileIds.length > 0 || params.authoritative ? profileIds : undefined;
|
||||
params.store.runtimeExternalProfileIdsAuthoritative = params.authoritative ? true : undefined;
|
||||
}
|
||||
|
||||
function mergeRuntimeExternalProfileReferences(params: {
|
||||
next: AuthProfileStore;
|
||||
existing: AuthProfileStore;
|
||||
}): AuthProfileStore {
|
||||
const runtimeExternalProfileIds = new Set(params.existing.runtimeExternalProfileIds ?? []);
|
||||
if (params.next.runtimeExternalProfileIdsAuthoritative === true) {
|
||||
return params.next;
|
||||
}
|
||||
if (runtimeExternalProfileIds.size === 0) {
|
||||
return params.next;
|
||||
}
|
||||
const merged = cloneAuthProfileStore(params.next);
|
||||
const mergedRuntimeExternalProfileIds = new Set(merged.runtimeExternalProfileIds ?? []);
|
||||
const backfilledRuntimeExternalProfileIds = new Set<string>();
|
||||
for (const profileId of runtimeExternalProfileIds) {
|
||||
const existingCredential = params.existing.profiles[profileId];
|
||||
const nextCredential = merged.profiles[profileId];
|
||||
if (nextCredential) {
|
||||
if (
|
||||
mergedRuntimeExternalProfileIds.has(profileId) ||
|
||||
(existingCredential && isDeepStrictEqual(nextCredential, existingCredential))
|
||||
) {
|
||||
mergedRuntimeExternalProfileIds.add(profileId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!existingCredential) {
|
||||
continue;
|
||||
}
|
||||
merged.profiles[profileId] = existingCredential;
|
||||
mergedRuntimeExternalProfileIds.add(profileId);
|
||||
backfilledRuntimeExternalProfileIds.add(profileId);
|
||||
if (params.existing.usageStats?.[profileId]) {
|
||||
merged.usageStats = {
|
||||
...merged.usageStats,
|
||||
[profileId]: params.existing.usageStats[profileId],
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const [provider, profileIds] of Object.entries(params.existing.order ?? {})) {
|
||||
const externalProfileIds = profileIds.filter((profileId) =>
|
||||
backfilledRuntimeExternalProfileIds.has(profileId),
|
||||
);
|
||||
if (externalProfileIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (merged.order?.[provider]) {
|
||||
continue;
|
||||
}
|
||||
const existingOrder = merged.order?.[provider] ?? [];
|
||||
merged.order = {
|
||||
...merged.order,
|
||||
[provider]: [
|
||||
...externalProfileIds,
|
||||
...existingOrder.filter((profileId) => !externalProfileIds.includes(profileId)),
|
||||
],
|
||||
};
|
||||
}
|
||||
for (const [provider, profileId] of Object.entries(params.existing.lastGood ?? {})) {
|
||||
if (!backfilledRuntimeExternalProfileIds.has(profileId) || merged.lastGood?.[provider]) {
|
||||
continue;
|
||||
}
|
||||
merged.lastGood = {
|
||||
...merged.lastGood,
|
||||
[provider]: profileId,
|
||||
};
|
||||
}
|
||||
setRuntimeExternalProfileMetadata({
|
||||
store: merged,
|
||||
profileIds: mergedRuntimeExternalProfileIds,
|
||||
authoritative: params.existing.runtimeExternalProfileIdsAuthoritative === true,
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
function mergeRuntimeExternalProfileState(params: {
|
||||
next: AuthProfileStore;
|
||||
existing: AuthProfileStore;
|
||||
}): AuthProfileStore {
|
||||
const existingRuntimeProfileIds = new Set(params.existing.runtimeExternalProfileIds ?? []);
|
||||
if (existingRuntimeProfileIds.size === 0) {
|
||||
return params.next;
|
||||
}
|
||||
const merged = cloneAuthProfileStore(params.next);
|
||||
const mergedRuntimeProfileIds = new Set(merged.runtimeExternalProfileIds ?? []);
|
||||
const activeRuntimeProfileIds = new Set<string>();
|
||||
const nextRuntimeProfileIdsAuthoritative =
|
||||
params.next.runtimeExternalProfileIdsAuthoritative === true;
|
||||
for (const profileId of existingRuntimeProfileIds) {
|
||||
if (nextRuntimeProfileIdsAuthoritative && !mergedRuntimeProfileIds.has(profileId)) {
|
||||
continue;
|
||||
}
|
||||
const existingCredential = params.existing.profiles[profileId];
|
||||
if (!existingCredential) {
|
||||
continue;
|
||||
}
|
||||
const nextCredential = merged.profiles[profileId];
|
||||
if (nextCredential) {
|
||||
if (
|
||||
mergedRuntimeProfileIds.has(profileId) ||
|
||||
isDeepStrictEqual(nextCredential, existingCredential)
|
||||
) {
|
||||
mergedRuntimeProfileIds.add(profileId);
|
||||
activeRuntimeProfileIds.add(profileId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
merged.profiles[profileId] = existingCredential;
|
||||
mergedRuntimeProfileIds.add(profileId);
|
||||
activeRuntimeProfileIds.add(profileId);
|
||||
}
|
||||
if (activeRuntimeProfileIds.size === 0) {
|
||||
return params.next;
|
||||
}
|
||||
for (const profileId of activeRuntimeProfileIds) {
|
||||
if (params.existing.usageStats?.[profileId]) {
|
||||
merged.usageStats = {
|
||||
...merged.usageStats,
|
||||
[profileId]: params.existing.usageStats[profileId],
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const [provider, profileIds] of Object.entries(params.existing.order ?? {})) {
|
||||
const externalProfileIds = profileIds.filter((profileId) =>
|
||||
activeRuntimeProfileIds.has(profileId),
|
||||
);
|
||||
if (externalProfileIds.length === 0 || merged.order?.[provider]) {
|
||||
continue;
|
||||
}
|
||||
merged.order = {
|
||||
...merged.order,
|
||||
[provider]: externalProfileIds,
|
||||
};
|
||||
}
|
||||
for (const [provider, profileId] of Object.entries(params.existing.lastGood ?? {})) {
|
||||
if (!activeRuntimeProfileIds.has(profileId) || merged.lastGood?.[provider]) {
|
||||
continue;
|
||||
}
|
||||
merged.lastGood = {
|
||||
...merged.lastGood,
|
||||
[provider]: profileId,
|
||||
};
|
||||
}
|
||||
setRuntimeExternalProfileMetadata({
|
||||
store: merged,
|
||||
profileIds: mergedRuntimeProfileIds,
|
||||
authoritative: params.existing.runtimeExternalProfileIdsAuthoritative === true,
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function updateAuthProfileStoreWithLock(params: {
|
||||
agentDir?: string;
|
||||
saveOptions?: SaveAuthProfileStoreOptions;
|
||||
@@ -591,10 +840,15 @@ export function loadAuthProfileStoreForRuntime(
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
return overlayExternalAuthProfiles(mergeAuthProfileStores(mainStore, store), {
|
||||
agentDir,
|
||||
...externalCli,
|
||||
});
|
||||
return overlayExternalAuthProfiles(
|
||||
mergeAuthProfileStores(mainStore, store, {
|
||||
preserveBaseRuntimeExternalProfiles: true,
|
||||
}),
|
||||
{
|
||||
agentDir,
|
||||
...externalCli,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function loadAuthProfileStoreForSecretsRuntime(
|
||||
@@ -632,7 +886,9 @@ export function loadAuthProfileStoreWithoutExternalProfiles(
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
|
||||
return mergeAuthProfileStores(mainStore, store);
|
||||
return mergeAuthProfileStores(mainStore, store, {
|
||||
preserveBaseRuntimeExternalProfiles: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureAuthProfileStore(
|
||||
@@ -646,13 +902,21 @@ export function ensureAuthProfileStore(
|
||||
},
|
||||
): AuthProfileStore {
|
||||
const externalCli = resolveExternalCliOverlayOptions(options);
|
||||
return overlayExternalAuthProfiles(
|
||||
const runtimeStore = resolveRuntimeAuthProfileStore(agentDir, options);
|
||||
const store = overlayExternalAuthProfiles(
|
||||
ensureAuthProfileStoreWithoutExternalProfiles(agentDir, options),
|
||||
{
|
||||
agentDir,
|
||||
...externalCli,
|
||||
},
|
||||
);
|
||||
if (!runtimeStore || hasScopedExternalCliOverlay(externalCli)) {
|
||||
return store;
|
||||
}
|
||||
return mergeRuntimeExternalProfileState({
|
||||
next: store,
|
||||
existing: runtimeStore,
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureAuthProfileStoreWithoutExternalProfiles(
|
||||
@@ -665,7 +929,11 @@ export function ensureAuthProfileStoreWithoutExternalProfiles(
|
||||
};
|
||||
const runtimeStore = resolveRuntimeAuthProfileStore(agentDir, effectiveOptions);
|
||||
if (runtimeStore) {
|
||||
return runtimeStore;
|
||||
return buildAuthProfileStoreWithoutExternalProfiles({
|
||||
store: runtimeStore,
|
||||
agentDir,
|
||||
options: effectiveOptions,
|
||||
});
|
||||
}
|
||||
const store = loadAuthProfileStoreForAgent(agentDir, effectiveOptions);
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
@@ -675,7 +943,9 @@ export function ensureAuthProfileStoreWithoutExternalProfiles(
|
||||
}
|
||||
|
||||
const mainStore = loadAuthProfileStoreForAgent(undefined, effectiveOptions);
|
||||
return mergeAuthProfileStores(mainStore, store);
|
||||
return mergeAuthProfileStores(mainStore, store, {
|
||||
preserveBaseRuntimeExternalProfiles: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function findPersistedAuthProfileCredential(params: {
|
||||
@@ -738,7 +1008,9 @@ export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthPro
|
||||
readOnly: true,
|
||||
syncExternalCli: false,
|
||||
});
|
||||
return mergeAuthProfileStores(mainStore, store);
|
||||
return mergeAuthProfileStores(mainStore, store, {
|
||||
preserveBaseRuntimeExternalProfiles: true,
|
||||
});
|
||||
}
|
||||
|
||||
export { hasAnyAuthProfileStoreSource } from "./source-check.js";
|
||||
@@ -784,6 +1056,16 @@ export function saveAuthProfileStore(
|
||||
store: localStore,
|
||||
});
|
||||
if (hasRuntimeAuthProfileStoreSnapshot(agentDir)) {
|
||||
setRuntimeAuthProfileStoreSnapshot(localStore, agentDir);
|
||||
const existingRuntimeStore = getRuntimeAuthProfileStoreSnapshot(agentDir);
|
||||
const nextRuntimeStore = buildRuntimeAuthProfileStoreForSave({ store, agentDir, options });
|
||||
setRuntimeAuthProfileStoreSnapshot(
|
||||
existingRuntimeStore
|
||||
? mergeRuntimeExternalProfileReferences({
|
||||
next: nextRuntimeStore,
|
||||
existing: existingRuntimeStore,
|
||||
})
|
||||
: nextRuntimeStore,
|
||||
agentDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,13 @@ export type AuthProfileStateStore = {
|
||||
version: number;
|
||||
} & AuthProfileState;
|
||||
|
||||
export type AuthProfileStore = AuthProfileSecretsStore & AuthProfileState;
|
||||
export type AuthProfileStore = AuthProfileSecretsStore &
|
||||
AuthProfileState & {
|
||||
/** Runtime-only provenance for external OAuth profiles overlaid onto this store. */
|
||||
runtimeExternalProfileIds?: string[];
|
||||
/** True when the runtime external profile set was freshly resolved, even if empty. */
|
||||
runtimeExternalProfileIdsAuthoritative?: boolean;
|
||||
};
|
||||
|
||||
export type AuthProfileIdRepairResult = {
|
||||
config: OpenClawConfig;
|
||||
|
||||
Reference in New Issue
Block a user