diff --git a/CHANGELOG.md b/CHANGELOG.md index 49848cbfeca..768d53de133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index ec514b03a3a..c003eaab53d 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -48,7 +48,7 @@ export { suggestOAuthProfileIdForLegacyDefault, } from "./auth-profiles/repair.js"; export { - buildPortableAuthProfileSecretsStoreForAgentCopy, + buildPortableAuthProfileStoreForAgentCopy, isAuthProfileCredentialPortableForAgentCopy, resolveAuthProfilePortability, type AuthProfilePortability, diff --git a/src/agents/auth-profiles/portability.test.ts b/src/agents/auth-profiles/portability.test.ts index c2e4f9c7ee4..85623170073 100644 --- a/src/agents/auth-profiles/portability.test.ts +++ b/src/agents/auth-profiles/portability.test.ts @@ -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"]); diff --git a/src/agents/auth-profiles/portability.ts b/src/agents/auth-profiles/portability.ts index b4fea4958f2..82d206c49c4 100644 --- a/src/agents/auth-profiles/portability.ts +++ b/src/agents/auth-profiles/portability.ts @@ -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, }; diff --git a/src/commands/agents.add.test.ts b/src/commands/agents.add.test.ts index d8d3d2e1b92..865c8e45f28 100644 --- a/src/commands/agents.add.test.ts +++ b/src/commands/agents.add.test.ts @@ -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", ]); }); diff --git a/src/commands/agents.commands.add.ts b/src/commands/agents.commands.add.ts index 36953559438..c7e152c86a3 100644 --- a/src/commands/agents.commands.add.ts +++ b/src/commands/agents.commands.add.ts @@ -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 &&