Skip to content

Commit 816038e

Browse files
authored
doctor: add memory search lint findings (#97137)
* doctor: add memory search lint findings * fix(doctor): quiet memory lint for auth-profile sources * fix(doctor): require provider-specific auth source for memory lint * fix(doctor): preserve qmd memory lint warnings * fix(doctor): validate auth source credentials
1 parent 92a2681 commit 816038e

9 files changed

Lines changed: 714 additions & 45 deletions

src/agents/auth-profiles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export {
5959
ensureAuthProfileStore,
6060
ensureAuthProfileStoreWithoutExternalProfiles,
6161
getRuntimeAuthProfileStoreSnapshot,
62+
hasAuthProfileStoreSourceForProvider,
6263
hasAnyAuthProfileStoreSource,
6364
hasLocalAuthProfileStoreSource,
6465
loadAuthProfileStoreForSecretsRuntime,

src/agents/auth-profiles/persisted.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ function parseCredentialEntry(
212212
if (!AUTH_PROFILE_TYPES.has(typed.type as AuthProfileCredential["type"])) {
213213
return { ok: false, reason: "invalid_type" };
214214
}
215-
const provider = typed.provider ?? fallbackProvider;
215+
const provider = typed.provider || fallbackProvider;
216216
const normalizedProvider = typeof provider === "string" ? normalizeProviderId(provider) : "";
217217
if (!normalizedProvider) {
218218
return { ok: false, reason: "missing_provider" };
@@ -246,7 +246,7 @@ function warnRejectedCredentialEntries(source: string, rejected: RejectedCredent
246246
});
247247
}
248248

249-
function coerceLegacyAuthStore(raw: unknown): LegacyAuthStore | null {
249+
export function coerceLegacyAuthStore(raw: unknown): LegacyAuthStore | null {
250250
if (!isRecord(raw)) {
251251
return null;
252252
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import fs from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import { hasAuthProfileStoreSourceForProvider } from "./source-check.js";
6+
7+
describe("hasAuthProfileStoreSourceForProvider", () => {
8+
afterEach(() => {
9+
vi.unstubAllEnvs();
10+
});
11+
12+
async function withAgentStore(profiles: Record<string, unknown>) {
13+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-source-"));
14+
const stateDir = path.join(root, "state");
15+
const agentDir = path.join(root, "agent");
16+
await fs.mkdir(agentDir, { recursive: true });
17+
await fs.mkdir(stateDir, { recursive: true });
18+
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
19+
await fs.writeFile(
20+
path.join(agentDir, "auth-profiles.json"),
21+
JSON.stringify({ version: 1, profiles }),
22+
);
23+
return { agentDir };
24+
}
25+
26+
async function withLegacyAuthStore(profiles: Record<string, unknown>) {
27+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-source-"));
28+
const stateDir = path.join(root, "state");
29+
const agentDir = path.join(root, "agent");
30+
await fs.mkdir(agentDir, { recursive: true });
31+
await fs.mkdir(stateDir, { recursive: true });
32+
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
33+
await fs.writeFile(path.join(agentDir, "auth.json"), JSON.stringify(profiles));
34+
return { agentDir };
35+
}
36+
37+
it("counts provider-specific usable credentials", async () => {
38+
const { agentDir } = await withAgentStore({
39+
"openai:default": { type: "api_key", provider: "openai", key: "sk-test" },
40+
});
41+
42+
expect(hasAuthProfileStoreSourceForProvider("openai", agentDir)).toBe(true);
43+
});
44+
45+
it("counts legacy auth stores with alias fields and fallback providers", async () => {
46+
const { agentDir } = await withLegacyAuthStore({
47+
openai: { mode: "apiKey", apiKey: "sk-test" },
48+
});
49+
50+
expect(hasAuthProfileStoreSourceForProvider("openai", agentDir)).toBe(true);
51+
});
52+
53+
it("ignores malformed provider fields instead of throwing", async () => {
54+
const { agentDir } = await withAgentStore({
55+
"openai:default": { type: "api_key", key: "sk-test" },
56+
"openai:other": { type: "api_key", provider: 123, key: "sk-test" },
57+
});
58+
59+
expect(hasAuthProfileStoreSourceForProvider("openai", agentDir)).toBe(false);
60+
});
61+
62+
it("does not count profile ids that are bound to a different credential provider", async () => {
63+
const { agentDir } = await withAgentStore({
64+
"openai:default": { type: "api_key", provider: "anthropic", key: "sk-test" },
65+
});
66+
67+
expect(hasAuthProfileStoreSourceForProvider("openai", agentDir)).toBe(false);
68+
});
69+
70+
it("honors configured profile order constraints", async () => {
71+
const { agentDir } = await withAgentStore({
72+
"openai:default": { type: "api_key", provider: "openai", key: "sk-test" },
73+
"openai:expired": {
74+
type: "token",
75+
provider: "openai",
76+
token: "expired-token",
77+
expires: Date.now() - 1000,
78+
},
79+
});
80+
81+
expect(
82+
hasAuthProfileStoreSourceForProvider("openai", agentDir, {
83+
profileIds: ["openai:expired"],
84+
}),
85+
).toBe(false);
86+
expect(
87+
hasAuthProfileStoreSourceForProvider("openai", agentDir, {
88+
profileIds: ["openai:default"],
89+
}),
90+
).toBe(true);
91+
});
92+
93+
it("treats explicit empty profile order as no usable profile", async () => {
94+
const { agentDir } = await withAgentStore({
95+
"openai:default": { type: "api_key", provider: "openai", key: "sk-test" },
96+
});
97+
98+
expect(
99+
hasAuthProfileStoreSourceForProvider("openai", agentDir, {
100+
profileIds: [],
101+
}),
102+
).toBe(false);
103+
});
104+
105+
it("does not count empty provider profiles as credential evidence", async () => {
106+
const { agentDir } = await withAgentStore({
107+
"openai:default": { type: "api_key", provider: "openai" },
108+
});
109+
110+
expect(hasAuthProfileStoreSourceForProvider("openai", agentDir)).toBe(false);
111+
});
112+
113+
it("does not count expired token profiles as credential evidence", async () => {
114+
const { agentDir } = await withAgentStore({
115+
"openai:token": {
116+
type: "token",
117+
provider: "openai",
118+
token: "expired-token",
119+
expires: Date.now() - 1000,
120+
},
121+
});
122+
123+
expect(hasAuthProfileStoreSourceForProvider("openai", agentDir)).toBe(false);
124+
});
125+
});

src/agents/auth-profiles/source-check.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
* These checks intentionally avoid loading secret-bearing credential payloads.
44
*/
55
import fs from "node:fs";
6+
import { evaluateStoredCredentialEligibility } from "./credential-state.js";
67
import {
78
resolveAuthStatePath,
89
resolveAuthStorePath,
910
resolveLegacyAuthStorePath,
1011
} from "./path-resolve.js";
12+
import { coerceLegacyAuthStore, coercePersistedAuthProfileStore } from "./persisted.js";
1113
import {
1214
getRuntimeAuthProfileStoreSnapshot,
1315
hasAnyRuntimeAuthProfileStoreSource,
1416
} from "./runtime-snapshots.js";
1517
import { readPersistedAuthProfileStateRaw, readPersistedAuthProfileStoreRaw } from "./sqlite.js";
18+
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
1619

1720
// Auth-profile source checks look at runtime snapshots, JSON compatibility
1821
// files, legacy files, and SQLite stores without materializing secret values.
@@ -24,6 +27,72 @@ function hasStoredAuthProfileFiles(agentDir?: string): boolean {
2427
);
2528
}
2629

30+
function readJsonFile(pathname: string): unknown {
31+
try {
32+
return JSON.parse(fs.readFileSync(pathname, "utf8")) as unknown;
33+
} catch {
34+
return null;
35+
}
36+
}
37+
38+
function normalizeProvider(provider: string): string {
39+
return provider.trim().toLowerCase();
40+
}
41+
42+
function isAuthProfileCredential(value: unknown): value is AuthProfileCredential {
43+
if (!value || typeof value !== "object") {
44+
return false;
45+
}
46+
const credential = value as { provider?: unknown; type?: unknown };
47+
const type = credential.type;
48+
return (
49+
typeof credential.provider === "string" &&
50+
(type === "api_key" || type === "token" || type === "oauth")
51+
);
52+
}
53+
54+
function isEligibleProviderCredential(rawCredential: unknown, expectedProvider: string): boolean {
55+
if (!isAuthProfileCredential(rawCredential)) {
56+
return false;
57+
}
58+
return (
59+
normalizeProvider(rawCredential.provider) === expectedProvider &&
60+
evaluateStoredCredentialEligibility({ credential: rawCredential }).eligible
61+
);
62+
}
63+
64+
function coerceRawStoreProfiles(raw: unknown): Record<string, AuthProfileCredential> | null {
65+
return coercePersistedAuthProfileStore(raw)?.profiles ?? coerceLegacyAuthStore(raw);
66+
}
67+
68+
function rawStoreHasProviderProfile(
69+
raw: unknown,
70+
provider: string,
71+
profileIds?: readonly string[],
72+
): boolean {
73+
const profiles = coerceRawStoreProfiles(raw);
74+
if (!profiles) {
75+
return false;
76+
}
77+
const expected = normalizeProvider(provider);
78+
const credentials =
79+
profileIds?.map((profileId) => profiles[profileId]) ?? Object.values(profiles);
80+
for (const rawCredential of credentials) {
81+
if (isEligibleProviderCredential(rawCredential, expected)) {
82+
return true;
83+
}
84+
}
85+
return false;
86+
}
87+
88+
function runtimeStoreHasProviderProfile(
89+
store: AuthProfileStore | undefined,
90+
provider: string,
91+
profileIds?: readonly string[],
92+
): boolean {
93+
return rawStoreHasProviderProfile(store, provider, profileIds);
94+
}
95+
2796
/** Returns true when any local/runtime/main auth profile source exists. */
2897
export function hasAnyAuthProfileStoreSource(agentDir?: string): boolean {
2998
if (hasLocalAuthProfileStoreSource(agentDir)) {
@@ -60,3 +129,63 @@ export function hasLocalAuthProfileStoreSource(agentDir?: string): boolean {
60129
readPersistedAuthProfileStoreRaw(agentDir) || readPersistedAuthProfileStateRaw(agentDir),
61130
);
62131
}
132+
133+
type AuthProfileSourceForProviderOptions = {
134+
/** Optional hard order/profile constraint from config auth.order. */
135+
profileIds?: readonly string[];
136+
};
137+
138+
/** Returns true when a read-only auth-profile source contains a profile for a provider. */
139+
export function hasAuthProfileStoreSourceForProvider(
140+
provider: string,
141+
agentDir?: string,
142+
options?: AuthProfileSourceForProviderOptions,
143+
): boolean {
144+
if (!normalizeProvider(provider)) {
145+
return false;
146+
}
147+
const profileIds = options?.profileIds;
148+
if (profileIds?.length === 0) {
149+
return false;
150+
}
151+
const localRuntimeStore = getRuntimeAuthProfileStoreSnapshot(agentDir);
152+
if (runtimeStoreHasProviderProfile(localRuntimeStore, provider, profileIds)) {
153+
return true;
154+
}
155+
if (
156+
rawStoreHasProviderProfile(readJsonFile(resolveAuthStorePath(agentDir)), provider, profileIds)
157+
) {
158+
return true;
159+
}
160+
if (
161+
rawStoreHasProviderProfile(
162+
readJsonFile(resolveLegacyAuthStorePath(agentDir)),
163+
provider,
164+
profileIds,
165+
)
166+
) {
167+
return true;
168+
}
169+
if (
170+
rawStoreHasProviderProfile(readPersistedAuthProfileStoreRaw(agentDir), provider, profileIds)
171+
) {
172+
return true;
173+
}
174+
175+
if (!agentDir) {
176+
return false;
177+
}
178+
const mainRuntimeStore = getRuntimeAuthProfileStoreSnapshot();
179+
if (runtimeStoreHasProviderProfile(mainRuntimeStore, provider, profileIds)) {
180+
return true;
181+
}
182+
if (rawStoreHasProviderProfile(readJsonFile(resolveAuthStorePath()), provider, profileIds)) {
183+
return true;
184+
}
185+
if (
186+
rawStoreHasProviderProfile(readJsonFile(resolveLegacyAuthStorePath()), provider, profileIds)
187+
) {
188+
return true;
189+
}
190+
return rawStoreHasProviderProfile(readPersistedAuthProfileStoreRaw(), provider, profileIds);
191+
}

src/agents/auth-profiles/store.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,11 @@ export function ensureAuthProfileStoreForLocalUpdate(agentDir?: string): AuthPro
10101010
});
10111011
}
10121012

1013-
export { hasAnyAuthProfileStoreSource, hasLocalAuthProfileStoreSource } from "./source-check.js";
1013+
export {
1014+
hasAnyAuthProfileStoreSource,
1015+
hasAuthProfileStoreSourceForProvider,
1016+
hasLocalAuthProfileStoreSource,
1017+
} from "./source-check.js";
10141018

10151019
/** Return the current runtime auth-profile snapshot for an agent dir. */
10161020
export function getRuntimeAuthProfileStoreSnapshot(

0 commit comments

Comments
 (0)