follow-up: align ingress, atomic paths, and channel tests with credential semantics (#33733)

Merged via squash.

Prepared head SHA: c290c2ab6a
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Reviewed-by: @joshavant
This commit is contained in:
Josh Avant
2026-03-03 20:29:46 -06:00
committed by GitHub
parent 6842877b2e
commit 1c200ca7ae
36 changed files with 1130 additions and 219 deletions

View File

@@ -4,6 +4,7 @@ import {
formatRemainingShort,
} from "../agents/auth-health.js";
import {
type AuthCredentialReasonCode,
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
ensureAuthProfileStore,
@@ -203,6 +204,7 @@ type AuthIssue = {
profileId: string;
provider: string;
status: string;
reasonCode?: AuthCredentialReasonCode;
remainingMs?: number;
};
@@ -222,6 +224,9 @@ export function resolveUnusableProfileHint(params: {
}
function formatAuthIssueHint(issue: AuthIssue): string | null {
if (issue.reasonCode === "invalid_expires") {
return "Invalid token expires metadata. Set a future Unix ms timestamp or remove expires.";
}
if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) {
return `Deprecated profile. Use ${formatCliCommand("openclaw models auth setup-token")} or ${formatCliCommand(
"openclaw configure",
@@ -239,7 +244,8 @@ function formatAuthIssueLine(issue: AuthIssue): string {
const remaining =
issue.remainingMs !== undefined ? ` (${formatRemainingShort(issue.remainingMs)})` : "";
const hint = formatAuthIssueHint(issue);
return `- ${issue.profileId}: ${issue.status}${remaining}${hint ? `${hint}` : ""}`;
const reason = issue.reasonCode ? ` [${issue.reasonCode}]` : "";
return `- ${issue.profileId}: ${issue.status}${reason}${remaining}${hint ? `${hint}` : ""}`;
}
export async function noteAuthProfileHealth(params: {
@@ -340,6 +346,7 @@ export async function noteAuthProfileHealth(params: {
profileId: issue.profileId,
provider: issue.provider,
status: issue.status,
reasonCode: issue.reasonCode,
remainingMs: issue.remainingMs,
}),
)

View File

@@ -0,0 +1,166 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "../../agents/auth-profiles.js";
import type { OpenClawConfig } from "../../config/config.js";
let mockStore: AuthProfileStore;
let mockAllowedProfiles: string[];
const resolveAuthProfileOrderMock = vi.fn(() => mockAllowedProfiles);
const resolveAuthProfileEligibilityMock = vi.fn(() => ({
eligible: false,
reasonCode: "invalid_expires" as const,
}));
const resolveSecretRefStringMock = vi.fn(async () => "resolved-secret");
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn(async () => []),
}));
vi.mock("../../secrets/resolve.js", () => ({
resolveSecretRefString: resolveSecretRefStringMock,
}));
vi.mock("../../agents/auth-profiles.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/auth-profiles.js")>();
return {
...actual,
ensureAuthProfileStore: () => mockStore,
listProfilesForProvider: (_store: AuthProfileStore, provider: string) =>
Object.entries(mockStore.profiles)
.filter(
([, profile]) =>
typeof profile.provider === "string" && profile.provider.toLowerCase() === provider,
)
.map(([profileId]) => profileId),
resolveAuthProfileDisplayLabel: ({ profileId }: { profileId: string }) => profileId,
resolveAuthProfileOrder: resolveAuthProfileOrderMock,
resolveAuthProfileEligibility: resolveAuthProfileEligibilityMock,
};
});
const { buildProbeTargets } = await import("./list.probe.js");
describe("buildProbeTargets reason codes", () => {
beforeEach(() => {
mockStore = {
version: 1,
profiles: {
"anthropic:default": {
type: "token",
provider: "anthropic",
tokenRef: { source: "env", provider: "default", id: "ANTHROPIC_TOKEN" },
expires: 0,
},
},
order: {
anthropic: ["anthropic:default"],
},
};
mockAllowedProfiles = [];
resolveAuthProfileOrderMock.mockClear();
resolveAuthProfileEligibilityMock.mockClear();
resolveSecretRefStringMock.mockReset();
resolveSecretRefStringMock.mockResolvedValue("resolved-secret");
resolveAuthProfileEligibilityMock.mockReturnValue({
eligible: false,
reasonCode: "invalid_expires",
});
});
it("reports invalid_expires with a legacy-compatible first error line", async () => {
const plan = await buildProbeTargets({
cfg: {
auth: {
order: {
anthropic: ["anthropic:default"],
},
},
} as OpenClawConfig,
providers: ["anthropic"],
modelCandidates: ["anthropic/claude-sonnet-4-6"],
options: {
timeoutMs: 5_000,
concurrency: 1,
maxTokens: 16,
},
});
expect(plan.targets).toHaveLength(0);
expect(plan.results).toHaveLength(1);
expect(plan.results[0]?.reasonCode).toBe("invalid_expires");
expect(plan.results[0]?.error?.split("\n")[0]).toBe(
"Auth profile credentials are missing or expired.",
);
expect(plan.results[0]?.error).toContain("[invalid_expires]");
});
it("reports excluded_by_auth_order when profile id is not present in explicit order", async () => {
mockStore.order = {
anthropic: ["anthropic:work"],
};
const plan = await buildProbeTargets({
cfg: {
auth: {
order: {
anthropic: ["anthropic:work"],
},
},
} as OpenClawConfig,
providers: ["anthropic"],
modelCandidates: ["anthropic/claude-sonnet-4-6"],
options: {
timeoutMs: 5_000,
concurrency: 1,
maxTokens: 16,
},
});
expect(plan.targets).toHaveLength(0);
expect(plan.results).toHaveLength(1);
expect(plan.results[0]?.reasonCode).toBe("excluded_by_auth_order");
expect(plan.results[0]?.error).toBe("Excluded by auth.order for this provider.");
});
it("reports unresolved_ref when a ref-only profile cannot resolve its SecretRef", async () => {
mockStore = {
version: 1,
profiles: {
"anthropic:default": {
type: "token",
provider: "anthropic",
tokenRef: { source: "env", provider: "default", id: "MISSING_ANTHROPIC_TOKEN" },
},
},
order: {
anthropic: ["anthropic:default"],
},
};
mockAllowedProfiles = ["anthropic:default"];
resolveSecretRefStringMock.mockRejectedValueOnce(new Error("missing secret"));
const plan = await buildProbeTargets({
cfg: {
auth: {
order: {
anthropic: ["anthropic:default"],
},
},
} as OpenClawConfig,
providers: ["anthropic"],
modelCandidates: ["anthropic/claude-sonnet-4-6"],
options: {
timeoutMs: 5_000,
concurrency: 1,
maxTokens: 16,
},
});
expect(plan.targets).toHaveLength(0);
expect(plan.results).toHaveLength(1);
expect(plan.results[0]?.reasonCode).toBe("unresolved_ref");
expect(plan.results[0]?.error?.split("\n")[0]).toBe(
"Auth profile credentials are missing or expired.",
);
expect(plan.results[0]?.error).toContain("[unresolved_ref]");
expect(plan.results[0]?.error).toContain("env:default:MISSING_ANTHROPIC_TOKEN");
});
});

View File

@@ -3,9 +3,12 @@ import fs from "node:fs/promises";
import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import {
type AuthProfileCredential,
type AuthProfileEligibilityReasonCode,
ensureAuthProfileStore,
listProfilesForProvider,
resolveAuthProfileDisplayLabel,
resolveAuthProfileEligibility,
resolveAuthProfileOrder,
} from "../../agents/auth-profiles.js";
import { describeFailoverError } from "../../agents/failover-error.js";
@@ -23,6 +26,8 @@ import {
resolveSessionTranscriptPath,
resolveSessionTranscriptsDirForAgent,
} from "../../config/sessions/paths.js";
import { coerceSecretRef, normalizeSecretInputString } from "../../config/types.secrets.js";
import { type SecretRefResolveCache, resolveSecretRefString } from "../../secrets/resolve.js";
import { redactSecrets } from "../status-all/format.js";
import { DEFAULT_PROVIDER, formatMs } from "./shared.js";
@@ -38,6 +43,15 @@ export type AuthProbeStatus =
| "unknown"
| "no_model";
export type AuthProbeReasonCode =
| "excluded_by_auth_order"
| "missing_credential"
| "expired"
| "invalid_expires"
| "unresolved_ref"
| "ineligible_profile"
| "no_model";
export type AuthProbeResult = {
provider: string;
model?: string;
@@ -46,6 +60,7 @@ export type AuthProbeResult = {
source: "profile" | "env" | "models.json";
mode?: string;
status: AuthProbeStatus;
reasonCode?: AuthProbeReasonCode;
error?: string;
latencyMs?: number;
};
@@ -139,7 +154,91 @@ function selectProbeModel(params: {
return null;
}
function buildProbeTargets(params: {
function mapEligibilityReasonToProbeReasonCode(
reasonCode: AuthProfileEligibilityReasonCode,
): AuthProbeReasonCode {
if (reasonCode === "missing_credential") {
return "missing_credential";
}
if (reasonCode === "expired") {
return "expired";
}
if (reasonCode === "invalid_expires") {
return "invalid_expires";
}
if (reasonCode === "unresolved_ref") {
return "unresolved_ref";
}
return "ineligible_profile";
}
function formatMissingCredentialProbeError(reasonCode: AuthProbeReasonCode): string {
const legacyLine = "Auth profile credentials are missing or expired.";
if (reasonCode === "expired") {
return `${legacyLine}\n↳ Auth reason [expired]: token credentials are expired.`;
}
if (reasonCode === "invalid_expires") {
return `${legacyLine}\n↳ Auth reason [invalid_expires]: token expires must be a positive Unix ms timestamp.`;
}
if (reasonCode === "missing_credential") {
return `${legacyLine}\n↳ Auth reason [missing_credential]: no inline credential or SecretRef is configured.`;
}
if (reasonCode === "unresolved_ref") {
return `${legacyLine}\n↳ Auth reason [unresolved_ref]: configured SecretRef could not be resolved.`;
}
return `${legacyLine}\n↳ Auth reason [ineligible_profile]: profile is incompatible with provider config.`;
}
function resolveProbeSecretRef(profile: AuthProfileCredential, cfg: OpenClawConfig) {
const defaults = cfg.secrets?.defaults;
if (profile.type === "api_key") {
if (normalizeSecretInputString(profile.key) !== undefined) {
return null;
}
return coerceSecretRef(profile.keyRef, defaults);
}
if (profile.type === "token") {
if (normalizeSecretInputString(profile.token) !== undefined) {
return null;
}
return coerceSecretRef(profile.tokenRef, defaults);
}
return null;
}
function formatUnresolvedRefProbeError(refLabel: string): string {
const legacyLine = "Auth profile credentials are missing or expired.";
return `${legacyLine}\n↳ Auth reason [unresolved_ref]: could not resolve SecretRef "${refLabel}".`;
}
async function maybeResolveUnresolvedRefIssue(params: {
cfg: OpenClawConfig;
profile?: AuthProfileCredential;
cache: SecretRefResolveCache;
}): Promise<{ reasonCode: "unresolved_ref"; error: string } | null> {
if (!params.profile) {
return null;
}
const ref = resolveProbeSecretRef(params.profile, params.cfg);
if (!ref) {
return null;
}
try {
await resolveSecretRefString(ref, {
config: params.cfg,
env: process.env,
cache: params.cache,
});
return null;
} catch {
return {
reasonCode: "unresolved_ref",
error: formatUnresolvedRefProbeError(`${ref.source}:${ref.provider}:${ref.id}`),
};
}
}
export async function buildProbeTargets(params: {
cfg: OpenClawConfig;
providers: string[];
modelCandidates: string[];
@@ -150,133 +249,162 @@ function buildProbeTargets(params: {
const providerFilter = options.provider?.trim();
const providerFilterKey = providerFilter ? normalizeProviderId(providerFilter) : null;
const profileFilter = new Set((options.profileIds ?? []).map((id) => id.trim()).filter(Boolean));
const refResolveCache: SecretRefResolveCache = {};
const catalog = await loadModelCatalog({ config: cfg });
const candidates = buildCandidateMap(modelCandidates);
const targets: AuthProbeTarget[] = [];
const results: AuthProbeResult[] = [];
return loadModelCatalog({ config: cfg }).then((catalog) => {
const candidates = buildCandidateMap(modelCandidates);
const targets: AuthProbeTarget[] = [];
const results: AuthProbeResult[] = [];
for (const provider of providers) {
const providerKey = normalizeProviderId(provider);
if (providerFilterKey && providerKey !== providerFilterKey) {
continue;
}
for (const provider of providers) {
const providerKey = normalizeProviderId(provider);
if (providerFilterKey && providerKey !== providerFilterKey) {
continue;
}
const model = selectProbeModel({
provider: providerKey,
candidates,
catalog,
});
const model = selectProbeModel({
provider: providerKey,
candidates,
catalog,
});
const profileIds = listProfilesForProvider(store, providerKey);
const explicitOrder = (() => {
return (
findNormalizedProviderValue(store.order, providerKey) ??
findNormalizedProviderValue(cfg?.auth?.order, providerKey)
);
})();
const allowedProfiles =
explicitOrder && explicitOrder.length > 0
? new Set(resolveAuthProfileOrder({ cfg, store, provider: providerKey }))
: null;
const filteredProfiles = profileFilter.size
? profileIds.filter((id) => profileFilter.has(id))
: profileIds;
const profileIds = listProfilesForProvider(store, providerKey);
const explicitOrder = (() => {
return (
findNormalizedProviderValue(store.order, providerKey) ??
findNormalizedProviderValue(cfg?.auth?.order, providerKey)
);
})();
const allowedProfiles =
explicitOrder && explicitOrder.length > 0
? new Set(resolveAuthProfileOrder({ cfg, store, provider: providerKey }))
: null;
const filteredProfiles = profileFilter.size
? profileIds.filter((id) => profileFilter.has(id))
: profileIds;
if (filteredProfiles.length > 0) {
for (const profileId of filteredProfiles) {
const profile = store.profiles[profileId];
const mode = profile?.type;
const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
if (explicitOrder && !explicitOrder.includes(profileId)) {
results.push({
provider: providerKey,
model: model ? `${model.provider}/${model.model}` : undefined,
profileId,
label,
source: "profile",
mode,
status: "unknown",
error: "Excluded by auth.order for this provider.",
});
continue;
}
if (allowedProfiles && !allowedProfiles.has(profileId)) {
results.push({
provider: providerKey,
model: model ? `${model.provider}/${model.model}` : undefined,
profileId,
label,
source: "profile",
mode,
status: "unknown",
error: "Auth profile credentials are missing or expired.",
});
continue;
}
if (!model) {
results.push({
provider: providerKey,
model: undefined,
profileId,
label,
source: "profile",
mode,
status: "no_model",
error: "No model available for probe",
});
continue;
}
targets.push({
if (filteredProfiles.length > 0) {
for (const profileId of filteredProfiles) {
const profile = store.profiles[profileId];
const mode = profile?.type;
const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
if (explicitOrder && !explicitOrder.includes(profileId)) {
results.push({
provider: providerKey,
model,
profileId,
model: model ? `${model.provider}/${model.model}` : undefined,
label,
source: "profile",
mode,
status: "unknown",
reasonCode: "excluded_by_auth_order",
error: "Excluded by auth.order for this provider.",
});
continue;
}
if (allowedProfiles && !allowedProfiles.has(profileId)) {
const eligibility = resolveAuthProfileEligibility({
cfg,
store,
provider: providerKey,
profileId,
});
const reasonCode = mapEligibilityReasonToProbeReasonCode(eligibility.reasonCode);
results.push({
provider: providerKey,
model: model ? `${model.provider}/${model.model}` : undefined,
profileId,
label,
source: "profile",
mode,
status: "unknown",
reasonCode,
error: formatMissingCredentialProbeError(reasonCode),
});
continue;
}
continue;
}
if (profileFilter.size > 0) {
continue;
}
const envKey = resolveEnvApiKey(providerKey);
const customKey = getCustomProviderApiKey(cfg, providerKey);
if (!envKey && !customKey) {
continue;
}
const label = envKey ? "env" : "models.json";
const source = envKey ? "env" : "models.json";
const mode = envKey?.source.includes("OAUTH_TOKEN") ? "oauth" : "api_key";
if (!model) {
results.push({
provider: providerKey,
model: undefined,
label,
source,
mode,
status: "no_model",
error: "No model available for probe",
const unresolvedRefIssue = await maybeResolveUnresolvedRefIssue({
cfg,
profile,
cache: refResolveCache,
});
if (unresolvedRefIssue) {
results.push({
provider: providerKey,
model: model ? `${model.provider}/${model.model}` : undefined,
profileId,
label,
source: "profile",
mode,
status: "unknown",
reasonCode: unresolvedRefIssue.reasonCode,
error: unresolvedRefIssue.error,
});
continue;
}
if (!model) {
results.push({
provider: providerKey,
model: undefined,
profileId,
label,
source: "profile",
mode,
status: "no_model",
reasonCode: "no_model",
error: "No model available for probe",
});
continue;
}
targets.push({
provider: providerKey,
model,
profileId,
label,
source: "profile",
mode,
});
continue;
}
continue;
}
targets.push({
if (profileFilter.size > 0) {
continue;
}
const envKey = resolveEnvApiKey(providerKey);
const customKey = getCustomProviderApiKey(cfg, providerKey);
if (!envKey && !customKey) {
continue;
}
const label = envKey ? "env" : "models.json";
const source = envKey ? "env" : "models.json";
const mode = envKey?.source.includes("OAUTH_TOKEN") ? "oauth" : "api_key";
if (!model) {
results.push({
provider: providerKey,
model,
model: undefined,
label,
source,
mode,
status: "no_model",
reasonCode: "no_model",
error: "No model available for probe",
});
continue;
}
return { targets, results };
});
targets.push({
provider: providerKey,
model,
label,
source,
mode,
});
}
return { targets, results };
}
async function probeTarget(params: {
@@ -299,6 +427,7 @@ async function probeTarget(params: {
source: target.source,
mode: target.mode,
status: "no_model",
reasonCode: "no_model",
error: "No model available for probe",
};
}