Skip to content

Commit 4b6c78d

Browse files
luoxiao6645claude
andcommitted
fix(status): honor selected usage auth profile
Thread preferredProfileIds through provider auth resolution so the status command uses the user-selected auth profile instead of always falling back to the default ordering. - Add preferredProfileIds param to resolveProviderAuths and plumb through plugin, fallback, and OAuth resolution paths - Pass session authProfileOverride from status-text to usage loader - Re-export tryReadSecretFileSync directly from @openclaw/fs-safe/secret - Fix lint violations (no-useless-fallback-in-spread) and type errors Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9e4eca0 commit 4b6c78d

6 files changed

Lines changed: 175 additions & 51 deletions

File tree

src/commands/status.summary.redaction.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function createRecentSessionRow(): SessionStatus {
1616
model: "gpt-5",
1717
configuredModel: "gpt-5",
1818
selectedModel: "gpt-5",
19-
modelSelectionReason: null,
19+
modelSelectionReason: "configured-default",
2020
contextTokens: 200_000,
2121
flags: ["id:sess-1"],
2222
};

src/infra/provider-usage.auth.normalizes-keys.test.ts

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,26 @@ vi.mock("../agents/auth-profiles.js", () => {
3535
lastGood?: Record<string, string>;
3636
usageStats?: Record<string, unknown>;
3737
};
38-
return {
38+
const store: {
39+
version: number;
40+
profiles: Record<string, unknown>;
41+
order?: Record<string, string[]>;
42+
lastGood?: Record<string, string>;
43+
usageStats?: Record<string, unknown>;
44+
} = {
3945
version: parsed.version ?? 1,
4046
profiles: parsed.profiles ?? {},
41-
...(parsed.order ? { order: parsed.order } : {}),
42-
...(parsed.lastGood ? { lastGood: parsed.lastGood } : {}),
43-
...(parsed.usageStats ? { usageStats: parsed.usageStats } : {}),
4447
};
48+
if (parsed.order) {
49+
store.order = parsed.order;
50+
}
51+
if (parsed.lastGood) {
52+
store.lastGood = parsed.lastGood;
53+
}
54+
if (parsed.usageStats) {
55+
store.usageStats = parsed.usageStats;
56+
}
57+
return store;
4558
} catch {
4659
return { version: 1, profiles: {} };
4760
}
@@ -59,7 +72,7 @@ vi.mock("../agents/auth-profiles.js", () => {
5972
const configured = Object.entries(params.cfg?.auth?.profiles ?? {})
6073
.filter(([, profile]) => normalizeProvider(profile?.provider) === provider)
6174
.map(([profileId]) => profileId);
62-
if (configured.length > 0) {
75+
if (configured.length > 0 && !params.store.order?.[params.provider] && !params.store.order?.[provider]) {
6376
return dedupeProfileIds(configured);
6477
}
6578
const ordered = params.store.order?.[params.provider] ?? params.store.order?.[provider];
@@ -187,6 +200,12 @@ const providerRuntimeMocks = vi.hoisted(() => ({
187200
}
188201

189202
if (params.provider === "minimax") {
203+
const portalAuth = await params.context.resolveOAuthToken({ provider: "minimax-portal" });
204+
if (portalAuth?.token) {
205+
return portalAuth.accountId
206+
? { token: portalAuth.token, accountId: portalAuth.accountId }
207+
: { token: portalAuth.token };
208+
}
190209
const token = resolveToken({
191210
providerIds: ["minimax"],
192211
envDirect: [
@@ -694,6 +713,84 @@ describe("resolveProviderAuths key normalization", () => {
694713
});
695714
});
696715

716+
it("prefers the requested OAuth profile when resolving provider usage auth", async () => {
717+
await withSuiteHome(async (home) => {
718+
await writeAuthProfiles(home, {
719+
"anthropic:default": {
720+
type: "token",
721+
provider: "anthropic",
722+
token: "default-token",
723+
},
724+
"anthropic:work": {
725+
type: "token",
726+
provider: "anthropic",
727+
token: "work-token",
728+
},
729+
});
730+
await writeProfileOrder(home, "anthropic", ["anthropic:default"]);
731+
732+
const auths = await resolveProviderAuths({
733+
providers: ["anthropic"],
734+
agentDir: agentDirForHome(home),
735+
config: {},
736+
env: buildSuiteEnv(home),
737+
preferredProfileIds: { anthropic: "anthropic:work" },
738+
});
739+
expect(auths).toEqual([{ provider: "anthropic", token: "work-token" }]);
740+
});
741+
});
742+
743+
it("ignores a preferred usage profile id owned by another provider", async () => {
744+
await withSuiteHome(async (home) => {
745+
await writeAuthProfiles(home, {
746+
"anthropic:default": {
747+
type: "token",
748+
provider: "anthropic",
749+
token: "anthropic-token",
750+
},
751+
"zai:work": {
752+
type: "token",
753+
provider: "zai",
754+
token: "wrong-provider-token",
755+
},
756+
});
757+
await writeProfileOrder(home, "anthropic", ["anthropic:default"]);
758+
759+
const auths = await resolveProviderAuths({
760+
providers: ["anthropic"],
761+
agentDir: agentDirForHome(home),
762+
config: {},
763+
env: buildSuiteEnv(home),
764+
preferredProfileIds: { anthropic: "zai:work" },
765+
});
766+
expect(auths).toEqual([{ provider: "anthropic", token: "anthropic-token" }]);
767+
});
768+
});
769+
770+
it("preserves preferred profile ids across provider override usage lookups", async () => {
771+
await withSuiteHome(async (home) => {
772+
await writeAuthProfiles(home, {
773+
"minimax-portal:work": {
774+
type: "oauth",
775+
provider: "minimax-portal",
776+
token: "portal-token",
777+
accountId: "portal-account",
778+
},
779+
});
780+
781+
const auths = await resolveProviderAuths({
782+
providers: ["minimax"],
783+
agentDir: agentDirForHome(home),
784+
config: {},
785+
env: buildSuiteEnv(home),
786+
preferredProfileIds: { minimax: "minimax-portal:work" },
787+
});
788+
expect(auths).toEqual([
789+
{ provider: "minimax", token: "portal-token", accountId: "portal-account" },
790+
]);
791+
});
792+
});
793+
697794
it("skips api_key entries in oauth token resolution order", async () => {
698795
await withSuiteHome(async (home) => {
699796
await writeAuthProfiles(home, {

src/infra/provider-usage.auth.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,9 @@ function hasProviderAuthEnvCredentialSource(params: {
8181
}): boolean {
8282
const candidates = resolveProviderAuthEnvVarCandidates({
8383
config: params.state.cfg,
84-
env: {
85-
...(process.env.VITEST ? process.env : {}),
86-
...params.state.env,
87-
},
84+
env: process.env.VITEST
85+
? { ...process.env, ...params.state.env }
86+
: { ...params.state.env },
8887
});
8988
for (const providerId of normalizeProviderIds(params.providerIds)) {
9089
const envVars = Object.hasOwn(candidates, providerId) ? candidates[providerId] : undefined;
@@ -207,17 +206,27 @@ function resolveUsageCredentialProviderIds(params: {
207206
async function resolveOAuthToken(params: {
208207
state: UsageAuthState;
209208
provider: string;
209+
preferredProfileId?: string;
210210
}): Promise<ProviderAuth | null> {
211211
if (!params.state.allowAuthProfileStore) {
212212
return null;
213213
}
214214
const store = resolveUsageAuthStore(params.state);
215+
const preferredProfileId = params.preferredProfileId?.trim();
216+
const preferredProfile = preferredProfileId ? store.profiles[preferredProfileId] : undefined;
217+
const normalizedProvider = normalizeProviderId(params.provider);
215218
const order = resolveAuthProfileOrder({
216219
cfg: params.state.cfg,
217220
store,
218221
provider: params.provider,
219222
});
220-
const deduped = dedupeProfileIds(order);
223+
const deduped = dedupeProfileIds(
224+
preferredProfileId &&
225+
preferredProfile &&
226+
normalizeProviderId(preferredProfile.provider) === normalizedProvider
227+
? [preferredProfileId, ...order]
228+
: order,
229+
);
221230

222231
for (const profileId of deduped) {
223232
const cred = store.profiles[profileId];
@@ -236,14 +245,19 @@ async function resolveOAuthToken(params: {
236245
if (!resolved) {
237246
continue;
238247
}
239-
return {
248+
const auth: ProviderAuth = {
240249
provider: params.provider as UsageProviderId,
241250
token: resolved.apiKey,
242-
accountId:
243-
cred.type === "oauth" && "accountId" in cred
244-
? (cred as { accountId?: string }).accountId
245-
: undefined,
246251
};
252+
if (
253+
cred.type === "oauth" &&
254+
"accountId" in cred &&
255+
typeof (cred as { accountId?: string }).accountId === "string" &&
256+
(cred as { accountId?: string }).accountId
257+
) {
258+
auth.accountId = (cred as { accountId?: string }).accountId;
259+
}
260+
return auth;
247261
} catch {
248262
// ignore
249263
}
@@ -255,6 +269,7 @@ async function resolveOAuthToken(params: {
255269
async function resolveProviderUsageAuthViaPlugin(params: {
256270
state: UsageAuthState;
257271
provider: UsageProviderId;
272+
preferredProfileId?: string;
258273
}): Promise<ProviderAuth | null> {
259274
const resolved = await resolveProviderUsageAuthWithPlugin({
260275
provider: params.provider,
@@ -275,33 +290,41 @@ async function resolveProviderUsageAuthViaPlugin(params: {
275290
const auth = await resolveOAuthToken({
276291
state: params.state,
277292
provider: options?.provider ?? params.provider,
293+
preferredProfileId: params.preferredProfileId,
278294
});
279-
return auth
280-
? {
281-
token: auth.token,
282-
...(auth.accountId ? { accountId: auth.accountId } : {}),
283-
}
284-
: null;
295+
if (!auth) {
296+
return null;
297+
}
298+
const resolvedAuth: { token: string; accountId?: string } = { token: auth.token };
299+
if (auth.accountId) {
300+
resolvedAuth.accountId = auth.accountId;
301+
}
302+
return resolvedAuth;
285303
},
286304
},
287305
});
288306
if (!resolved?.token) {
289307
return null;
290308
}
291-
return {
309+
const auth: ProviderAuth = {
292310
provider: params.provider,
293311
token: resolved.token,
294-
...(resolved.accountId ? { accountId: resolved.accountId } : {}),
295312
};
313+
if (resolved.accountId) {
314+
auth.accountId = resolved.accountId;
315+
}
316+
return auth;
296317
}
297318

298319
async function resolveProviderUsageAuthFallback(params: {
299320
state: UsageAuthState;
300321
provider: UsageProviderId;
322+
preferredProfileId?: string;
301323
}): Promise<ProviderAuth | null> {
302324
const oauthToken = await resolveOAuthToken({
303325
state: params.state,
304326
provider: params.provider,
327+
preferredProfileId: params.preferredProfileId,
305328
});
306329
if (oauthToken) {
307330
return oauthToken;
@@ -356,6 +379,7 @@ export async function resolveProviderAuths(params: {
356379
agentDir?: string;
357380
config?: OpenClawConfig;
358381
env?: NodeJS.ProcessEnv;
382+
preferredProfileIds?: Partial<Record<UsageProviderId, string | undefined>>;
359383
skipPluginAuthWithoutCredentialSource?: boolean;
360384
}): Promise<ProviderAuth[]> {
361385
if (params.auth) {
@@ -381,6 +405,7 @@ export async function resolveProviderAuths(params: {
381405
const pluginAuth = await resolveProviderUsageAuthViaPlugin({
382406
state: authProfileSourceState,
383407
provider,
408+
preferredProfileId: params.preferredProfileIds?.[provider],
384409
});
385410
if (pluginAuth) {
386411
auths.push(pluginAuth);
@@ -389,6 +414,7 @@ export async function resolveProviderAuths(params: {
389414
const fallbackAuth = await resolveProviderUsageAuthFallback({
390415
state: authProfileSourceState,
391416
provider,
417+
preferredProfileId: params.preferredProfileIds?.[provider],
392418
});
393419
if (fallbackAuth) {
394420
auths.push(fallbackAuth);
@@ -433,6 +459,7 @@ export async function resolveProviderAuths(params: {
433459
const pluginAuth = await resolveProviderUsageAuthViaPlugin({
434460
state,
435461
provider,
462+
preferredProfileId: params.preferredProfileIds?.[provider],
436463
});
437464
if (pluginAuth) {
438465
auths.push(pluginAuth);
@@ -442,6 +469,7 @@ export async function resolveProviderAuths(params: {
442469
const fallbackAuth = await resolveProviderUsageAuthFallback({
443470
state,
444471
provider,
472+
preferredProfileId: params.preferredProfileIds?.[provider],
445473
});
446474
if (fallbackAuth) {
447475
auths.push(fallbackAuth);

src/infra/provider-usage.load.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type UsageSummaryOptions = {
4040
config?: OpenClawConfig;
4141
env?: NodeJS.ProcessEnv;
4242
fetch?: typeof fetch;
43+
preferredProfileIds?: Partial<Record<UsageProviderId, string | undefined>>;
4344
skipPluginAuthWithoutCredentialSource?: boolean;
4445
};
4546

@@ -97,6 +98,7 @@ export async function loadProviderUsageSummary(
9798
agentDir: opts.agentDir,
9899
config,
99100
env,
101+
preferredProfileIds: opts.preferredProfileIds,
100102
skipPluginAuthWithoutCredentialSource: opts.skipPluginAuthWithoutCredentialSource,
101103
});
102104
if (auths.length === 0) {

src/infra/secret-file.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
11
import "./fs-safe-defaults.js";
2-
import {
3-
readSecretFileSync as readSecretFileSyncImpl,
4-
tryReadSecretFileSync as tryReadSecretFileSyncImpl,
5-
} from "@openclaw/fs-safe/secret";
2+
import { readSecretFileSync as readSecretFileSyncImpl } from "@openclaw/fs-safe/secret";
63
import { resolveUserPath } from "../utils.js";
74

85
export {
96
DEFAULT_SECRET_FILE_MAX_BYTES,
107
PRIVATE_SECRET_DIR_MODE,
118
PRIVATE_SECRET_FILE_MODE,
129
readSecretFileSync,
10+
tryReadSecretFileSync,
1311
type SecretFileReadOptions,
1412
} from "@openclaw/fs-safe/secret";
1513
export { writeSecretFileAtomic as writePrivateSecretFileAtomic } from "@openclaw/fs-safe/secret";
1614

17-
export function tryReadSecretFileSync(
18-
filePath: string | undefined,
19-
label: string,
20-
options: Parameters<typeof tryReadSecretFileSyncImpl>[2] = {},
21-
): string | undefined {
22-
try {
23-
return tryReadSecretFileSyncImpl(filePath, label, options);
24-
} catch {
25-
return undefined;
26-
}
27-
}
28-
2915
export type SecretFileReadResult =
3016
| {
3117
ok: true;

0 commit comments

Comments
 (0)