fix: align model status auth evidence paths

This commit is contained in:
Shakker
2026-04-29 21:51:49 +01:00
parent 98e4c18e39
commit 4109446c2f
5 changed files with 172 additions and 16 deletions

View File

@@ -1,7 +1,8 @@
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
import {
resolveProviderEnvApiKeyCandidates,
resolveProviderEnvAuthEvidence,
resolveProviderEnvApiKeyCandidates,
resolveProviderEnvAuthLookupKeys,
} from "../../agents/model-auth-env-vars.js";
import { resolveEnvApiKey } from "../../agents/model-auth-env.js";
import { resolveAwsSdkEnvVarName } from "../../agents/model-auth-runtime-shared.js";
@@ -89,10 +90,7 @@ export function createModelListAuthIndex(
addProvider(credential.provider);
}
for (const provider of new Set([
...Object.keys(envCandidateMap),
...Object.keys(authEvidenceMap),
])) {
for (const provider of resolveProviderEnvAuthLookupKeys(lookupParams)) {
if (
resolveEnvApiKey(provider, env, {
aliasMap,

View File

@@ -22,7 +22,19 @@ vi.mock("../../agents/model-auth.js", () => ({
const raw = cfg.models?.providers?.[provider]?.apiKey;
return typeof raw === "string" && raw.trim().length > 0 && raw !== "ollama-local";
},
resolveEnvApiKey: (provider: string) => {
resolveEnvApiKey: (
provider: string,
_env?: NodeJS.ProcessEnv,
options?: { workspaceDir?: string },
) => {
if (provider === "workspace-cloud") {
return options?.workspaceDir === "/tmp/workspace"
? {
source: "workspace cloud credentials",
apiKey: "workspace-cloud-local-credentials",
}
: null;
}
const keys =
provider === "anthropic"
? ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]
@@ -322,4 +334,48 @@ describe("buildProbeTargets reason codes", () => {
);
});
});
it("uses workspace-scoped auth evidence when building env probe targets", async () => {
mockStore = {
version: 1,
profiles: {},
order: {},
};
loadModelCatalogMock.mockResolvedValue([
{ provider: "workspace-cloud", id: "workspace-model", name: "Workspace Model" },
]);
const withoutWorkspace = await buildProbeTargets({
cfg: {} as OpenClawConfig,
providers: ["workspace-cloud"],
modelCandidates: [],
options: {
timeoutMs: 5_000,
concurrency: 1,
maxTokens: 16,
},
});
const withWorkspace = await buildProbeTargets({
cfg: {} as OpenClawConfig,
workspaceDir: "/tmp/workspace",
providers: ["workspace-cloud"],
modelCandidates: [],
options: {
timeoutMs: 5_000,
concurrency: 1,
maxTokens: 16,
},
});
expect(withoutWorkspace.targets).toEqual([]);
expect(withWorkspace.targets).toHaveLength(1);
expect(withWorkspace.targets[0]).toEqual(
expect.objectContaining({
provider: "workspace-cloud",
source: "env",
label: "env",
model: { provider: "workspace-cloud", model: "workspace-model" },
}),
);
});
});

View File

@@ -249,11 +249,12 @@ async function maybeResolveUnresolvedRefIssue(params: {
export async function buildProbeTargets(params: {
cfg: OpenClawConfig;
workspaceDir?: string;
providers: string[];
modelCandidates: string[];
options: AuthProbeOptions;
}): Promise<{ targets: AuthProbeTarget[]; results: AuthProbeResult[] }> {
const { cfg, providers, modelCandidates, options } = params;
const { cfg, providers, modelCandidates, options, workspaceDir } = params;
const store = ensureAuthProfileStore();
const providerFilter = options.provider?.trim();
const providerFilterKey = providerFilter ? normalizeProviderId(providerFilter) : null;
@@ -380,7 +381,10 @@ export async function buildProbeTargets(params: {
continue;
}
const envKey = resolveEnvApiKey(providerKey);
const envKey = resolveEnvApiKey(providerKey, process.env, {
config: cfg,
workspaceDir,
});
const hasUsableModelsJsonKey = hasUsableCustomProviderApiKey(cfg, providerKey);
if (!envKey && !hasUsableModelsJsonKey) {
continue;
@@ -494,6 +498,9 @@ async function probeTarget(params: {
async function runTargetsWithConcurrency(params: {
cfg: OpenClawConfig;
agentId?: string;
agentDir?: string;
workspaceDir?: string;
targets: AuthProbeTarget[];
timeoutMs: number;
maxTokens: number;
@@ -503,9 +510,12 @@ async function runTargetsWithConcurrency(params: {
const { cfg, targets, timeoutMs, maxTokens, onProgress } = params;
const concurrency = Math.max(1, Math.min(targets.length || 1, params.concurrency));
const agentId = resolveDefaultAgentId(cfg);
const agentDir = resolveOpenClawAgentDir();
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId) ?? resolveDefaultAgentWorkspaceDir();
const agentId = params.agentId ?? resolveDefaultAgentId(cfg);
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
const workspaceDir =
params.workspaceDir ??
resolveAgentWorkspaceDir(cfg, agentId) ??
resolveDefaultAgentWorkspaceDir();
const sessionDir = resolveSessionTranscriptsDirForAgent(agentId);
await fs.mkdir(workspaceDir, { recursive: true });
@@ -550,6 +560,9 @@ async function runTargetsWithConcurrency(params: {
export async function runAuthProbes(params: {
cfg: OpenClawConfig;
agentId?: string;
agentDir?: string;
workspaceDir?: string;
providers: string[];
modelCandidates: string[];
options: AuthProbeOptions;
@@ -558,6 +571,7 @@ export async function runAuthProbes(params: {
const startedAt = Date.now();
const plan = await buildProbeTargets({
cfg: params.cfg,
workspaceDir: params.workspaceDir,
providers: params.providers,
modelCandidates: params.modelCandidates,
options: params.options,
@@ -569,6 +583,9 @@ export async function runAuthProbes(params: {
const results = totalTargets
? await runTargetsWithConcurrency({
cfg: params.cfg,
agentId: params.agentId,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
targets: plan.targets,
timeoutMs: params.options.timeoutMs,
maxTokens: params.options.maxTokens,

View File

@@ -16,7 +16,11 @@ import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles/paths
import { ensureAuthProfileStoreWithoutExternalProfiles as ensureAuthProfileStore } from "../../agents/auth-profiles/store.js";
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
import { resolveProfileUnusableUntilForDisplay } from "../../agents/auth-profiles/usage.js";
import { resolveProviderEnvApiKeyCandidates } from "../../agents/model-auth-env-vars.js";
import {
resolveProviderEnvApiKeyCandidates,
resolveProviderEnvAuthEvidence,
resolveProviderEnvAuthLookupKeys,
} from "../../agents/model-auth-env-vars.js";
import { resolveEnvApiKey } from "../../agents/model-auth.js";
import {
buildModelAliasIndex,
@@ -247,12 +251,21 @@ export async function modelsStatusCommand(
const providersFromEnv = new Set<string>();
// Use the shared provider-env registry so `models status` stays aligned with
// env-backed providers beyond the text-model defaults (for example image-gen).
const envCandidateMap = resolveProviderEnvApiKeyCandidates({
const envLookupParams = {
config: cfg,
workspaceDir,
});
for (const provider of Object.keys(envCandidateMap).toSorted()) {
if (resolveEnvApiKey(provider, process.env, { config: cfg, workspaceDir })) {
};
const envCandidateMap = resolveProviderEnvApiKeyCandidates(envLookupParams);
const authEvidenceMap = resolveProviderEnvAuthEvidence(envLookupParams);
for (const provider of resolveProviderEnvAuthLookupKeys(envLookupParams)) {
if (
resolveEnvApiKey(provider, process.env, {
config: cfg,
workspaceDir,
candidateMap: envCandidateMap,
authEvidenceMap,
})
) {
providersFromEnv.add(provider);
}
}
@@ -384,6 +397,9 @@ export async function modelsStatusCommand(
async (update) => {
return await runAuthProbes({
cfg,
agentId: workspaceAgentId,
agentDir,
workspaceDir,
providers,
modelCandidates,
options: {

View File

@@ -91,6 +91,18 @@ const mocks = vi.hoisted(() => {
"openai-codex": ["OPENAI_OAUTH_TOKEN"],
fal: ["FAL_KEY"],
}),
resolveProviderEnvAuthEvidence: vi.fn().mockReturnValue({}),
resolveProviderEnvAuthLookupKeys: vi
.fn()
.mockImplementation(() => [
"anthropic",
"google",
"minimax",
"minimax-portal",
"openai",
"openai-codex",
"fal",
]),
listKnownProviderEnvApiKeyNames: vi
.fn()
.mockReturnValue([
@@ -195,6 +207,8 @@ vi.mock("../../agents/model-auth.js", () => ({
}));
vi.mock("../../agents/model-auth-env-vars.js", () => ({
resolveProviderEnvApiKeyCandidates: mocks.resolveProviderEnvApiKeyCandidates,
resolveProviderEnvAuthEvidence: mocks.resolveProviderEnvAuthEvidence,
resolveProviderEnvAuthLookupKeys: mocks.resolveProviderEnvAuthLookupKeys,
listKnownProviderEnvApiKeyNames: mocks.listKnownProviderEnvApiKeyNames,
}));
vi.mock("../../agents/model-selection-cli.js", () => ({
@@ -527,6 +541,61 @@ describe("modelsStatusCommand auth overview", () => {
}
});
it("includes auth-evidence-only providers in the auth overview", async () => {
const localRuntime = createRuntime();
const originalKeysImpl = mocks.resolveProviderEnvAuthLookupKeys.getMockImplementation();
const originalEvidenceImpl = mocks.resolveProviderEnvAuthEvidence.getMockImplementation();
const originalEnvImpl = mocks.resolveEnvApiKey.getMockImplementation();
mocks.resolveProviderEnvAuthLookupKeys.mockReturnValue(["workspace-cloud"]);
mocks.resolveProviderEnvAuthEvidence.mockReturnValue({
"workspace-cloud": [
{
type: "local-file-with-env",
credentialMarker: "workspace-cloud-local-credentials",
source: "workspace cloud credentials",
},
],
});
mocks.resolveEnvApiKey.mockImplementation(
(provider: string, _env?: NodeJS.ProcessEnv, options?: { workspaceDir?: string }) =>
provider === "workspace-cloud" && options?.workspaceDir === "/tmp/openclaw-agent/workspace"
? {
apiKey: "workspace-cloud-local-credentials",
source: "workspace cloud credentials",
}
: null,
);
try {
await modelsStatusCommand({ json: true }, localRuntime as never);
const payload = JSON.parse(String((localRuntime.log as Mock).mock.calls[0]?.[0]));
expect(payload.auth.providers).toEqual(
expect.arrayContaining([
expect.objectContaining({
provider: "workspace-cloud",
effective: expect.objectContaining({ kind: "env" }),
env: expect.objectContaining({ source: "workspace cloud credentials" }),
}),
]),
);
} finally {
if (originalKeysImpl) {
mocks.resolveProviderEnvAuthLookupKeys.mockImplementation(originalKeysImpl);
}
if (originalEvidenceImpl) {
mocks.resolveProviderEnvAuthEvidence.mockImplementation(originalEvidenceImpl);
}
if (originalEnvImpl) {
mocks.resolveEnvApiKey.mockImplementation(originalEnvImpl);
} else if (defaultResolveEnvApiKeyImpl) {
mocks.resolveEnvApiKey.mockImplementation(defaultResolveEnvApiKeyImpl);
} else {
mocks.resolveEnvApiKey.mockImplementation(() => null);
}
}
});
it("reports defaults source when --agent has no overrides", async () => {
await withAgentScopeOverrides(
{