fix(auth): preserve copied auth profile order (#100833)

* fix(auth): preserve auth profile order when copying store to spawned agent

* fix(auth): preserve copied profile order

Co-authored-by: Vivek Behani <vivekbehani@me.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
machine3at 2026-07-06 23:22:55 +05:30 committed by GitHub
parent 3dd5339a53
commit c432c8c014
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 46 additions and 11 deletions

View file

@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **Agent auth copy order:** preserve the source agent's portable auth-profile precedence when copying credentials to a new agent while excluding skipped profiles and transient auth state. (#100833) Thanks @machine3at.
- **Cron edit delivery:** preserve each job's implicit delivery mode when applying partial delivery updates, so disabling best-effort delivery no longer turns detached job announcements off. (#100846) Thanks @machine3at.
- **Control UI session creation:** keep newly created sessions at the front of the stable sidebar order after selecting another session. Thanks @shakkernerd.
- **FTS-only memory startup:** skip plugin capability discovery when `memorySearch.provider` is explicitly `none`, avoiding an unnecessary cold-start scan.

View file

@ -48,7 +48,7 @@ export {
suggestOAuthProfileIdForLegacyDefault,
} from "./auth-profiles/repair.js";
export {
buildPortableAuthProfileSecretsStoreForAgentCopy,
buildPortableAuthProfileStoreForAgentCopy,
isAuthProfileCredentialPortableForAgentCopy,
resolveAuthProfilePortability,
type AuthProfilePortability,

View file

@ -5,7 +5,7 @@
*/
import { describe, expect, it } from "vitest";
import {
buildPortableAuthProfileSecretsStoreForAgentCopy,
buildPortableAuthProfileStoreForAgentCopy,
resolveAuthProfilePortability,
} from "./portability.js";
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
@ -35,7 +35,7 @@ describe("auth profile portability", () => {
},
};
const portable = buildPortableAuthProfileSecretsStoreForAgentCopy(store);
const portable = buildPortableAuthProfileStoreForAgentCopy(store);
expect(portable.copiedProfileIds).toEqual(["openai:api-key", "github-copilot:default"]);
expect(portable.skippedProfileIds).toEqual(["openai:default"]);

View file

@ -60,9 +60,9 @@ export function isAuthProfileCredentialPortableForAgentCopy(
return resolveAuthProfilePortability(credential).portable;
}
/** Builds an agent-copy store containing only portable credentials. */
export function buildPortableAuthProfileSecretsStoreForAgentCopy(store: AuthProfileStore): {
store: AuthProfileSecretsStore;
/** Builds an agent-copy store containing only portable credentials and their order. */
export function buildPortableAuthProfileStoreForAgentCopy(store: AuthProfileStore): {
store: AuthProfileStore;
copiedProfileIds: string[];
skippedProfileIds: string[];
} {
@ -79,8 +79,19 @@ export function buildPortableAuthProfileSecretsStoreForAgentCopy(store: AuthProf
}),
) as AuthProfileSecretsStore["profiles"];
const copiedSet = new Set(copiedProfileIds);
const order = Object.fromEntries(
Object.entries(store.order ?? {})
.map(([provider, ids]) => [provider, ids.filter((id) => copiedSet.has(id))] as const)
.filter(([, ids]) => ids.length > 0),
);
return {
store: { version: AUTH_STORE_VERSION, profiles },
store: {
version: AUTH_STORE_VERSION,
profiles,
...(Object.keys(order).length > 0 ? { order } : {}),
},
copiedProfileIds,
skippedProfileIds,
};

View file

@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { AUTH_STORE_VERSION } from "../agents/auth-profiles/constants.js";
import { resolveAuthProfileOrder } from "../agents/auth-profiles/order.js";
import { loadPersistedAuthProfileStore } from "../agents/auth-profiles/persisted.js";
import { saveAuthProfileStore } from "../agents/auth-profiles/store.js";
import { formatCliCommand } from "../cli/command-format.js";
@ -237,6 +238,11 @@ describe("agents add command", () => {
provider: "openai",
key: "sk-test",
},
"openai:backup": {
type: "api_key",
provider: "openai",
key: "sk-backup",
},
"github-copilot:default": {
type: "token",
provider: "github-copilot",
@ -250,6 +256,12 @@ describe("agents add command", () => {
expires: Date.now() + 60_000,
},
},
order: {
openai: ["openai:oauth", "openai:backup", "openai:default"],
"github-copilot": ["github-copilot:default"],
},
lastGood: { openai: "openai:default" },
usageStats: { "openai:default": { lastUsed: 1_000 } },
},
sourceAgentDir,
);
@ -259,10 +271,21 @@ describe("agents add command", () => {
destAgentDir,
});
expect(result).toEqual({ copied: 2, skipped: 1 });
expect(result).toEqual({ copied: 3, skipped: 1 });
const copied = loadPersistedAuthProfileStore(destAgentDir);
expect(Object.keys(copied?.profiles ?? {}).toSorted()).toEqual([
"github-copilot:default",
"openai:backup",
"openai:default",
]);
expect(copied?.order).toEqual({
openai: ["openai:backup", "openai:default"],
"github-copilot": ["github-copilot:default"],
});
expect(copied?.lastGood).toBeUndefined();
expect(copied?.usageStats).toBeUndefined();
expect(resolveAuthProfileOrder({ store: copied!, provider: "openai" })).toEqual([
"openai:backup",
"openai:default",
]);
});

View file

@ -11,7 +11,7 @@ import {
resolveDefaultAgentId,
} from "../agents/agent-scope.js";
import {
buildPortableAuthProfileSecretsStoreForAgentCopy,
buildPortableAuthProfileStoreForAgentCopy,
ensureAuthProfileStore,
} from "../agents/auth-profiles.js";
import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js";
@ -79,7 +79,7 @@ async function copyPortableAuthProfiles(params: {
if (!sourceStore || Object.keys(sourceStore.profiles).length === 0) {
return { copied: 0, skipped: 0 };
}
const portable = buildPortableAuthProfileSecretsStoreForAgentCopy(sourceStore);
const portable = buildPortableAuthProfileStoreForAgentCopy(sourceStore);
if (portable.copiedProfileIds.length === 0) {
return { copied: 0, skipped: portable.skippedProfileIds.length };
}
@ -337,7 +337,7 @@ export async function agentsAddCommand(
const sourceStore = loadPersistedAuthProfileStore(sourceAgentDir);
const destStore = loadPersistedAuthProfileStore(agentDir);
const portable = sourceStore
? buildPortableAuthProfileSecretsStoreForAgentCopy(sourceStore)
? buildPortableAuthProfileStoreForAgentCopy(sourceStore)
: undefined;
if (
portable &&