refactor: retire legacy provider and secret paths (#115655)

* refactor: retire legacy provider and secret paths

* fix: remove stale cache retention import

* test: remove retired secret marker fallback
This commit is contained in:
Peter Steinberger
2026-07-29 04:36:20 -04:00
committed by GitHub
parent 327432831a
commit c5d0b7dd39
22 changed files with 88 additions and 415 deletions
@@ -2518,38 +2518,6 @@ describe("bridgeCodexAppServerStartOptions", () => {
}
});
it("accepts a legacy Codex auth-provider alias for app-server login", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
try {
upsertAuthProfile({
agentDir,
profileId: "openai:work",
credential: {
type: "token",
provider: "codex-cli",
token: "legacy-access-token",
email: "legacy-codex@example.test",
},
});
await applyCodexAppServerAuthProfile({
client: { request } as never,
agentDir,
authProfileId: "openai:work",
});
expect(request).toHaveBeenCalledWith("account/login/start", {
type: "chatgptAuthTokens",
accessToken: "legacy-access-token",
chatgptAccountId: "legacy-codex@example.test",
chatgptPlanType: null,
});
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
it("answers app-server ChatGPT token refresh requests from the bound profile", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
@@ -2769,45 +2737,6 @@ describe("bridgeCodexAppServerStartOptions", () => {
}
});
it("accepts a refreshed Codex OAuth credential when the stored provider is a legacy alias", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
access: "refreshed-alias-access-token",
refresh: "refreshed-alias-refresh-token",
expires: Date.now() + 60_000,
accountId: "account-alias",
});
try {
upsertAuthProfile({
agentDir,
profileId: "openai:work",
credential: {
type: "oauth",
provider: "codex-cli",
access: "stale-alias-access-token",
refresh: "alias-refresh-token",
expires: Date.now() + 60_000,
accountId: "account-legacy",
email: "legacy-codex@example.test",
},
});
await expect(
refreshCodexAppServerAuthTokens({
agentDir,
authProfileId: "openai:work",
}),
).resolves.toEqual({
accessToken: "refreshed-alias-access-token",
chatgptAccountId: "account-alias",
chatgptPlanType: null,
});
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("alias-refresh-token");
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
it("preserves a stored ChatGPT plan type when building token login params", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
+19 -45
View File
@@ -43,13 +43,7 @@ import { isCodexAppServerNativeAuthProfile } from "./session-binding.js";
import { resolveCodexAppServerSpawnEnv } from "./transport-stdio.js";
const CODEX_APP_SERVER_AUTH_PROVIDER = "openai";
const OPENAI_CODEX_APP_SERVER_AUTH_PROVIDER = "openai-codex";
const LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER = "codex-cli";
const CODEX_APP_SERVER_EXTERNAL_CLI_PROVIDER_IDS = [
CODEX_APP_SERVER_AUTH_PROVIDER,
LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER,
];
const OPENAI_PROVIDER = "openai";
const CODEX_APP_SERVER_EXTERNAL_CLI_PROVIDER_IDS = [CODEX_APP_SERVER_AUTH_PROVIDER];
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai:default";
const CODEX_HOME_ENV_VAR = "CODEX_HOME";
const HOME_ENV_VAR = "HOME";
@@ -112,7 +106,6 @@ export async function bridgeCodexAppServerStartOptions(params: {
const shouldClearInheritedOpenAiApiKey = shouldClearOpenAiApiKeyForCodexAuthProfile({
store,
authProfileId,
config: params.config,
});
return shouldClearInheritedOpenAiApiKey
? withClearedEnvironmentVariables(scopedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS)
@@ -227,7 +220,7 @@ export async function resolveCodexAppServerPreparedAuthProfileSnapshot(params: {
return undefined;
}
const credential = store.profiles[profileId];
if (!credential || !isCodexAppServerAuthProfileCredential(credential, params.config)) {
if (!credential || !isCodexAppServerAuthProfileCredential(credential)) {
return undefined;
}
const loginParams = await resolveCodexAppServerAuthProfileLoginParamsInternal({
@@ -340,7 +333,7 @@ export async function resolveCodexAppServerAuthAccountCacheKey(params: {
return undefined;
}
const credential = store.profiles[profileId];
if (!credential || !isCodexAppServerAuthProfileCredential(credential, params.config)) {
if (!credential || !isCodexAppServerAuthProfileCredential(credential)) {
return undefined;
}
if (credential.type === "api_key") {
@@ -627,7 +620,7 @@ async function resolveCodexAppServerAuthProfileLoginParams(params: {
`Codex app-server auth profile "${profileId}" was not found.`,
);
}
if (profileId && profile && !isCodexAppServerAuthProfileCredential(profile, params.config)) {
if (profileId && profile && !isCodexAppServerAuthProfileCredential(profile)) {
throw new CodexAppServerAuthProfileUnavailableError(
`Codex app-server auth profile "${profileId}" must be OpenAI Codex auth or an OpenAI API-key backup.`,
);
@@ -683,7 +676,7 @@ async function resolveCodexAppServerAuthProfileLoginParamsInternal(params: {
if (!credential) {
throw new Error(`Codex app-server auth profile "${profileId}" was not found.`);
}
if (!isCodexAppServerAuthProfileCredential(credential, params.config)) {
if (!isCodexAppServerAuthProfileCredential(credential)) {
throw new Error(
`Codex app-server auth profile "${profileId}" must be OpenAI Codex auth or an OpenAI API-key backup.`,
);
@@ -867,13 +860,12 @@ async function resolveOAuthCredentialForCodexAppServer(
const persistedOAuthCredential =
!useScopedCredential &&
persistedCredential?.type === "oauth" &&
isCodexAppServerAuthProvider(persistedCredential.provider, params.config)
isCodexAppServerAuthProvider(persistedCredential.provider)
? persistedCredential
: undefined;
const ownerCredential = store.profiles[profileId];
const overlaidOAuthCredential =
ownerCredential?.type === "oauth" &&
isCodexAppServerAuthProvider(ownerCredential.provider, params.config)
ownerCredential?.type === "oauth" && isCodexAppServerAuthProvider(ownerCredential.provider)
? ownerCredential
: undefined;
if (useScopedCredential && overlaidOAuthCredential) {
@@ -904,7 +896,7 @@ async function resolveOAuthCredentialForCodexAppServer(
? undefined
: loadAuthProfileStoreForSecretsRuntime(ownerAgentDir).profiles[profileId];
const refreshedOAuthCredential =
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider, params.config)
refreshed?.type === "oauth" && isCodexAppServerAuthProvider(refreshed.provider)
? refreshed
: undefined;
if (refreshedOAuthCredential && isDeepStrictEqual(params.store.profiles[profileId], credential)) {
@@ -915,8 +907,7 @@ async function resolveOAuthCredentialForCodexAppServer(
const storedCredential = store.profiles[profileId];
const candidate = refreshedOAuthCredential
? refreshedOAuthCredential
: storedCredential?.type === "oauth" &&
isCodexAppServerAuthProvider(storedCredential.provider, params.config)
: storedCredential?.type === "oauth" && isCodexAppServerAuthProvider(storedCredential.provider)
? storedCredential
: credential;
return resolved?.apiKey ? { ...candidate, access: resolved.apiKey } : candidate;
@@ -1005,54 +996,37 @@ async function resolveScopedOAuthCredential(params: {
}
}
function isCodexAppServerAuthProvider(provider: string, config?: AuthProfileOrderConfig): boolean {
const resolvedProvider = resolveProviderIdForAuth(provider, { config });
return (
resolvedProvider === CODEX_APP_SERVER_AUTH_PROVIDER ||
resolvedProvider === OPENAI_CODEX_APP_SERVER_AUTH_PROVIDER ||
// Older Codex auth profiles stored the CLI runtime id here. The app-server
// login protocol still receives the same externally managed ChatGPT token.
resolvedProvider === LEGACY_CODEX_APP_SERVER_AUTH_PROVIDER
);
// Runtime consumes canonical auth state; doctor owns retired profile-id migration.
function isCodexAppServerAuthProvider(provider: string): boolean {
return provider.trim().toLowerCase() === CODEX_APP_SERVER_AUTH_PROVIDER;
}
function isOpenAIApiKeyBackupCredential(
credential: AuthProfileCredential,
config?: AuthProfileOrderConfig,
): boolean {
function isOpenAIApiKeyBackupCredential(credential: AuthProfileCredential): boolean {
return (
credential.type === "api_key" &&
resolveProviderIdForAuth(credential.provider, { config }) === OPENAI_PROVIDER
credential.provider.trim().toLowerCase() === CODEX_APP_SERVER_AUTH_PROVIDER
);
}
function isCodexAppServerAuthProfileCredential(
credential: AuthProfileCredential,
config?: AuthProfileOrderConfig,
): boolean {
function isCodexAppServerAuthProfileCredential(credential: AuthProfileCredential): boolean {
return (
isCodexAppServerAuthProvider(credential.provider, config) ||
isOpenAIApiKeyBackupCredential(credential, config)
isCodexAppServerAuthProvider(credential.provider) || isOpenAIApiKeyBackupCredential(credential)
);
}
function shouldClearOpenAiApiKeyForCodexAuthProfile(params: {
store: ReturnType<typeof ensureAuthProfileStore>;
authProfileId?: string;
config?: AuthProfileOrderConfig;
}): boolean {
const profileId = params.authProfileId?.trim();
const credential = profileId
? params.store.profiles[profileId]
: params.store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID];
return isCodexSubscriptionCredential(credential, params.config);
return isCodexSubscriptionCredential(credential);
}
function isCodexSubscriptionCredential(
credential: AuthProfileCredential | undefined,
config?: AuthProfileOrderConfig,
): boolean {
if (!credential || !isCodexAppServerAuthProvider(credential.provider, config)) {
function isCodexSubscriptionCredential(credential: AuthProfileCredential | undefined): boolean {
if (!credential || !isCodexAppServerAuthProvider(credential.provider)) {
return false;
}
return credential.type === "oauth" || credential.type === "token";
+1 -1
View File
@@ -5,7 +5,7 @@ import type { CacheRetention } from "../types.js";
* Defaults to "short" and uses OPENCLAW_CACHE_RETENTION for backward compatibility.
*/
export function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention {
if (cacheRetention) {
if (cacheRetention === "short" || cacheRetention === "long" || cacheRetention === "none") {
return cacheRetention;
}
if (typeof process !== "undefined" && process.env.OPENCLAW_CACHE_RETENTION === "long") {
@@ -1,3 +1,4 @@
import { resolveCacheRetention } from "../providers/cache-retention.js";
import {
splitSystemPromptCacheBoundary,
stripSystemPromptCacheBoundary,
@@ -64,8 +65,7 @@ export function resolveAnthropicEphemeralCacheControl(
baseUrl: string | undefined,
cacheRetention: AnthropicPayloadPolicyInput["cacheRetention"],
): AnthropicEphemeralCacheControl | undefined {
const retention =
cacheRetention ?? (process.env.OPENCLAW_CACHE_RETENTION === "long" ? "long" : "short");
const retention = resolveCacheRetention(cacheRetention);
if (retention === "none") {
return undefined;
}
@@ -8,6 +8,7 @@ import { getEnvApiKey } from "../env-api-keys.js";
import { applyProviderReportedUsageCost, calculateCost } from "../model-utils.js";
import { convertMessages } from "../openai-completions-messages.js";
import type { OpenAICompletionsOptions } from "../provider-options.js";
import { resolveCacheRetention } from "../providers/cache-retention.js";
import {
isOpenAIGpt54MiniModel,
isOpenAIGpt55Model,
@@ -70,7 +71,6 @@ import {
GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP,
createModelStreamCooperativeScheduler,
log,
resolveCacheRetention,
resolvePromptCacheKey,
sortTransportToolsByName,
throwIfModelStreamAborted,
@@ -5,6 +5,7 @@ import type {
ResponseFormatTextConfig,
ResponseInput,
} from "openai/resources/responses/responses.js";
import { resolveCacheRetention } from "../providers/cache-retention.js";
import {
normalizeOpenAIReasoningEffort,
resolveOpenAIReasoningEffortForModel,
@@ -39,7 +40,6 @@ import {
usesNativeOpenAICodexResponsesBackend,
} from "./openai-transport-params.js";
import {
resolveCacheRetention,
resolvePromptCacheKey,
sortTransportToolsByName,
type OpenAIModeModel,
@@ -98,18 +98,6 @@ export function createModelStreamCooperativeScheduler(
};
}
export function resolveCacheRetention(
cacheRetention: string | undefined,
): "short" | "long" | "none" {
if (cacheRetention === "short" || cacheRetention === "long" || cacheRetention === "none") {
return cacheRetention;
}
if (typeof process !== "undefined" && process.env.OPENCLAW_CACHE_RETENTION === "long") {
return "long";
}
return "short";
}
export function resolvePromptCacheKey(
options: Pick<BaseOpenAIStreamOptions, "promptCacheKey" | "sessionId"> | undefined,
cacheRetention: "short" | "long" | "none",
-47
View File
@@ -76,53 +76,6 @@ describe("resolveFastModeState", () => {
expect(state.source).toBe("config");
});
it("uses OpenAI model config for the Codex app-server runtime provider", () => {
const cfg = {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { params: { fastMode: "auto", fastAutoOnSeconds: 30 } },
},
},
},
} as OpenClawConfig;
const state = resolveFastModeState({
cfg,
provider: "openai-codex",
model: "gpt-5.5",
});
expect(state.mode).toBe("auto");
expect(state.enabled).toBe(true);
expect(state.source).toBe("config");
expect(state.fastAutoOnSeconds).toBe(30);
});
it("prefers exact Codex app-server model config over the OpenAI alias", () => {
const cfg = {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { params: { fastMode: true, fastAutoOnSeconds: 30 } },
"openai-codex/gpt-5.5": { params: { fastMode: false, fastAutoOnSeconds: 45 } },
},
},
},
} as OpenClawConfig;
const state = resolveFastModeState({
cfg,
provider: "openai-codex",
model: "gpt-5.5",
});
expect(state.enabled).toBe(false);
expect(state.mode).toBe(false);
expect(state.source).toBe("config");
expect(state.fastAutoOnSeconds).toBe(45);
});
it("formats auto mode with the default threshold", () => {
expect(formatFastModeAutoLabel()).toBe("auto (60 sec)");
expect(formatFastModeStatusValue({ mode: "auto" })).toBe("auto (60 sec)");
-32
View File
@@ -718,38 +718,6 @@ describe("resolveUsableCustomProviderApiKey", () => {
}
});
it("resolves legacy __env__ markers from process env for custom providers", () => {
const previous = process.env.BAILIAN_API_KEY;
process.env.BAILIAN_API_KEY = "sk-bailian-env"; // pragma: allowlist secret
try {
const resolved = resolveUsableCustomProviderApiKey({
cfg: {
models: {
providers: {
bailian: {
baseUrl: "https://coding.dashscope.aliyuncs.com/v1",
api: "openai-completions",
apiKey: "__env__:BAILIAN_API_KEY", // pragma: allowlist secret
models: [],
},
},
},
},
provider: "bailian",
secretSentinels: true,
});
expect(looksLikeSecretSentinel(resolved?.apiKey ?? "")).toBe(true);
expect(resolveSecretSentinel(resolved?.apiKey ?? "")).toBe("sk-bailian-env");
expect(resolved?.source).toContain("BAILIAN_API_KEY");
} finally {
if (previous === undefined) {
delete process.env.BAILIAN_API_KEY;
} else {
process.env.BAILIAN_API_KEY = previous;
}
}
});
it("does not resolve env SecretRefs when provider allowlist excludes the env id", () => {
const previous = process.env.MY_CUSTOM_KEY;
process.env.MY_CUSTOM_KEY = "sk-custom-secretref-env"; // pragma: allowlist secret
@@ -375,11 +375,7 @@ function supportsChannelCodexLogin(provider: string | null | undefined): boolean
return false;
}
const normalizedProvider = provider.trim().toLowerCase().replace(/_/gu, "-");
return (
normalizedProvider === "openai" ||
normalizedProvider === "codex" ||
normalizedProvider === "openai-codex"
);
return normalizedProvider === "openai" || normalizedProvider === "codex";
}
export function buildExternalRunFailureReply(
@@ -569,19 +569,6 @@ describe("handleLoginCommand", () => {
expect(runModelsAuthLoginFlowMock).not.toHaveBeenCalled();
});
it("normalizes Codex login aliases to the OpenAI provider", async () => {
mockSuccessfulLoginFlow();
await handleLoginCommand(
buildLoginParams("/login openai-codex", { opts: blockReplyOpts() }),
true,
);
expect(runModelsAuthLoginFlowMock).toHaveBeenCalledWith(
expect.objectContaining({ provider: "openai" }),
);
});
it("returns a friendly error for unsupported providers", async () => {
const result = await handleLoginCommand(buildLoginParams("/login anthropic"), true);
+3 -69
View File
@@ -170,7 +170,7 @@ function registerStatusCodexHarness(): void {
function saveStatusTestAuthProfile(params: {
dir: string;
profileId: string;
provider: "openai" | "openai-codex" | "anthropic";
provider: "openai" | "anthropic";
}): void {
saveStatusTestAuthProfiles({
dir: params.dir,
@@ -180,7 +180,7 @@ function saveStatusTestAuthProfile(params: {
function saveStatusTestAuthProfiles(params: {
dir: string;
profiles: Array<{ profileId: string; provider: "openai" | "openai-codex" | "anthropic" }>;
profiles: Array<{ profileId: string; provider: "openai" | "anthropic" }>;
}): void {
const agentDir = path.join(params.dir, ".openclaw", "agents", "main", "agent");
fs.mkdirSync(agentDir, { recursive: true });
@@ -190,7 +190,7 @@ function saveStatusTestAuthProfiles(params: {
profiles: Object.fromEntries(
params.profiles.map((profile) => [
profile.profileId,
profile.provider === "openai" || profile.provider === "openai-codex"
profile.provider === "openai"
? {
type: "oauth",
provider: profile.provider,
@@ -1229,72 +1229,6 @@ describe("buildStatusReply subagent summary", () => {
);
});
it("forwards legacy Codex profile providers to Codex synthetic usage", async () => {
registerStatusCodexHarness();
providerUsageMock.loadProviderUsageSummary.mockResolvedValue({
updatedAt: Date.now(),
providers: [
{
provider: "openai",
displayName: "OpenAI",
windows: [{ label: "5h", usedPercent: 9 }],
},
],
});
await withTempHome(
async (dir) => {
saveStatusTestAuthProfile({
dir,
profileId: "openai-codex:legacy",
provider: "openai-codex",
});
await buildStatusText({
cfg: {
...baseCfg,
agents: {
defaults: {
agentRuntime: { id: "codex" },
},
},
},
sessionEntry: {
sessionId: "sess-status-codex-legacy-profile",
updatedAt: 0,
authProfileOverride: "openai-codex:legacy",
},
sessionKey: "agent:main:main",
parentSessionKey: "agent:main:main",
sessionScope: "per-sender",
statusChannel: "mobilechat",
provider: "openai",
model: "gpt-5.5",
contextTokens: 32_000,
resolvedFastMode: false,
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false,
defaultGroupActivation: () => "mention",
modelAuthOverride: "oauth",
activeModelAuthOverride: "oauth",
});
const providerUsageCall = providerUsageMock.loadProviderUsageSummary.mock.calls.find(
([params]) => params?.providers?.includes("openai"),
);
expect(providerUsageCall?.[0]?.auth).toEqual([
{
...expectedCodexRuntimeUsageAuth[0],
authProfileId: "openai-codex:legacy",
},
]);
},
{ skipSessionCleanup: true, skipHomeCleanup: true },
);
});
it("loads Codex synthetic usage when no local OpenAI profile label exists", async () => {
registerStatusCodexHarness();
providerUsageMock.loadProviderUsageSummary.mockResolvedValue({
@@ -356,7 +356,7 @@ describe("normalizeCompatibilityConfigValues", () => {
token: "secretref-env:DISCORD_BOT_TOKEN",
accounts: {
work: {
token: "secretref-env:DISCORD_WORK_TOKEN",
token: "__env__:DISCORD_WORK_TOKEN",
},
},
},
@@ -378,7 +378,7 @@ describe("normalizeCompatibilityConfigValues", () => {
"Moved channels.discord.accounts.default.token secretref-env:DISCORD_BOT_TOKEN marker → structured env SecretRef.",
);
expect(res.changes).toContain(
"Moved channels.discord.accounts.work.token secretref-env:DISCORD_WORK_TOKEN marker → structured env SecretRef.",
"Moved channels.discord.accounts.work.token __env__:DISCORD_WORK_TOKEN marker → structured env SecretRef.",
);
});
+9
View File
@@ -1318,6 +1318,15 @@ describe("modelsAuthLoginCommand", () => {
);
});
it.each(["openai-codex", "codex-cli"])(
"rejects retired manual auth provider %s",
async (provider) => {
await expect(modelsAuthLoginCommand({ provider }, createRuntime())).rejects.toThrow(
`"${provider}" is a legacy provider ID; use --provider openai.`,
);
},
);
it("does not persist a cancelled manual token entry", async () => {
const runtime = createRuntime();
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((
+4 -3
View File
@@ -151,9 +151,10 @@ function resolveDefaultTokenProfileId(provider: string): string {
function normalizeManualAuthProvider(provider: string): string {
const normalized = normalizeProviderId(provider);
return normalized === "openai" || normalized === "codex" || normalized === "openai-codex"
? "openai"
: normalized;
if (normalized === "openai-codex" || normalized === "codex-cli") {
throw new Error(`"${normalized}" is a legacy provider ID; use --provider openai.`);
}
return normalized === "openai" || normalized === "codex" ? "openai" : normalized;
}
function isOpenAIProvider(provider: string): boolean {
+16 -7
View File
@@ -102,12 +102,24 @@ export function parseEnvTemplateSecretRef(
};
}
/** Parse legacy env SecretRef marker strings kept for config migration/read compatibility. */
/** Detect retired env SecretRef marker strings for migration and explicit rejection. */
export function isLegacySecretRefEnvMarker(value: unknown): value is string {
if (typeof value !== "string") {
return false;
}
const trimmed = value.trim();
return (
trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX) ||
trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
);
}
/** Parse legacy env SecretRef marker strings for config migration. */
export function parseLegacySecretRefEnvMarker(
value: unknown,
provider = DEFAULT_SECRET_PROVIDER_ALIAS,
): SecretRef | null {
if (typeof value !== "string") {
if (!isLegacySecretRefEnvMarker(value)) {
return null;
}
const trimmed = value.trim();
@@ -130,15 +142,12 @@ export function parseLegacySecretRefEnvMarker(
};
}
/** Coerce canonical, legacy, and env-shorthand secret inputs into a SecretRef. */
/** Coerce canonical and env-shorthand secret inputs into a SecretRef.
* Retired string markers are parsed only by doctor migration above. */
export function coerceSecretRef(value: unknown, defaults?: SecretDefaults): SecretRef | null {
if (isSecretRef(value)) {
return value;
}
const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env);
if (legacyEnvMarker) {
return legacyEnvMarker;
}
if (isLegacySecretRefWithoutProvider(value)) {
const provider =
value.source === "env"
@@ -22,7 +22,7 @@ const CODEX_LOGIN_PROVIDER = "openai";
const CODEX_LOGIN_METHOD = "device-code";
const CODEX_LOGIN_FLOW_TTL_MS = 15 * 60_000;
const CODEX_LOGIN_PROVIDER_ALIASES = new Set(["codex", "openai", "openai-codex"]);
const CODEX_LOGIN_PROVIDER_ALIASES = new Set(["codex", "openai"]);
type CodexLoginFlowRecord = {
expiresAt: number;
+2 -8
View File
@@ -1,7 +1,7 @@
/** Detects legacy SecretRef env markers in config values. */
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
LEGACY_SECRETREF_ENV_MARKER_PREFIX,
isLegacySecretRefEnvMarker,
parseLegacySecretRefEnvMarker,
type SecretRef,
} from "../config/types.secrets.js";
@@ -19,10 +19,6 @@ type LegacySecretRefEnvMarkerCandidate = {
ref: SecretRef | null;
};
function isLegacySecretRefEnvMarker(value: unknown): value is string {
return typeof value === "string" && value.trim().startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX);
}
function toCandidate(
target: DiscoveredConfigSecretTarget,
defaults: NonNullable<OpenClawConfig["secrets"]>["defaults"] | undefined,
@@ -73,9 +69,7 @@ export function migrateLegacySecretRefEnvMarkers(config: OpenClawConfig): {
}
// Only registered existing paths are rewritten; malformed markers remain for explicit repair.
if (setPathExistingStrict(next, candidate.pathSegments, ref)) {
changes.push(
`Moved ${candidate.path} ${LEGACY_SECRETREF_ENV_MARKER_PREFIX}${ref.id} marker → structured env SecretRef.`,
);
changes.push(`Moved ${candidate.path} ${candidate.value} marker → structured env SecretRef.`);
}
}
return { config: next, changes };
+1 -17
View File
@@ -40,16 +40,6 @@ function modelConfigKey(provider?: string, model?: string): string {
: `${providerId}/${modelId}`;
}
function modelConfigKeys(provider?: string, model?: string): string[] {
const key = modelConfigKey(provider, model);
const providerId = normalizeLowercaseStringOrEmpty(provider?.trim() ?? "");
if (providerId !== "openai-codex") {
return [key];
}
const openAiKey = modelConfigKey("openai", model);
return openAiKey === key ? [key] : [key, openAiKey];
}
export function resolveFastModeModelParams(params: {
cfg: FastModeConfig | undefined;
provider?: string;
@@ -59,13 +49,7 @@ export function resolveFastModeModelParams(params: {
if (!models) {
return undefined;
}
for (const key of modelConfigKeys(params.provider, params.model)) {
const modelConfig = models[key];
if (modelConfig?.params) {
return modelConfig.params;
}
}
return undefined;
return models[modelConfigKey(params.provider, params.model)]?.params;
}
function normalizeFastModeAutoOnSeconds(value: unknown): number | undefined {
+1 -6
View File
@@ -21,7 +21,6 @@ import {
} from "../agents/model-runtime-aliases.js";
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../agents/openai-routing.js";
import { resolveProviderIdForAuth } from "../agents/provider-auth-aliases.js";
import { resolveSessionRuntimeOverrideForProvider } from "../agents/session-runtime-compat.js";
import {
resolveInternalSessionKey,
@@ -184,11 +183,7 @@ function resolveCodexSyntheticUsageAuthProfileId(params: {
if (!credential) {
return undefined;
}
const credentialProvider = normalizeOptionalLowercaseString(credential.provider);
const resolvedProvider = resolveProviderIdForAuth(credential.provider, { config: params.cfg });
return resolvedProvider === "openai" ||
credentialProvider === "openai-codex" ||
credentialProvider === "codex-cli"
return normalizeOptionalLowercaseString(credential.provider) === "openai"
? normalizedProfileId
: undefined;
} catch {
+16
View File
@@ -105,6 +105,22 @@ describe("hasWebProviderEntryCredential", () => {
).toBe(false);
});
it.each([
{ raw: "secretref-env:CUSTOM_API_KEY", fallback: undefined },
{ raw: undefined, fallback: "__env__:CUSTOM_API_KEY" },
])("rejects retired secret markers instead of treating them as literals", ({ raw, fallback }) => {
expect(
hasWebProviderEntryCredential({
provider,
config: {},
toolConfig: undefined,
resolveRawValue: () => raw,
resolveFallbackRawValue: () => fallback,
resolveEnvValue: () => undefined,
}),
).toBe(false);
});
it("keeps non-reference config strings as literal credentials", () => {
expect(
hasWebProviderEntryCredential({
+8 -72
View File
@@ -1,4 +1,6 @@
// Shared web provider config, credential, and definition resolution.
import { coerceSecretRef, isLegacySecretRefEnvMarker } from "../config/types.secrets.js";
type WebProviderConfigSource = {
tools?: {
web?: {
@@ -8,21 +10,6 @@ type WebProviderConfigSource = {
};
};
type SecretRefSource = "env" | "file" | "exec";
type SecretRef = {
source: SecretRefSource;
provider: string;
id: string;
};
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
type RuntimeWebProviderMetadata = {
providerConfigured?: string;
selectedProvider?: string;
@@ -36,10 +23,6 @@ type ProviderWithCredential = {
type WebContentProcessEnv = Record<string, string | undefined>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeSecretInputString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
@@ -68,59 +51,6 @@ function normalizeSecretInput(value: unknown): string {
return latin1Only.trim();
}
function isSecretRef(value: unknown): value is SecretRef {
if (!isRecord(value)) {
return false;
}
if (Object.keys(value).length !== 3) {
return false;
}
return (
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.provider === "string" &&
value.provider.trim().length > 0 &&
typeof value.id === "string" &&
value.id.trim().length > 0
);
}
function coerceSecretRef(value: unknown): SecretRef | null {
if (isSecretRef(value)) {
return value;
}
if (typeof value === "string") {
const trimmed = value.trim();
const legacyPrefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
: undefined;
if (legacyPrefix) {
const id = trimmed.slice(legacyPrefix.length);
return ENV_SECRET_REF_ID_RE.test(id)
? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id }
: null;
}
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
const id = match?.[1];
return id ? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id } : null;
}
if (
isRecord(value) &&
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.id === "string" &&
value.id.trim().length > 0 &&
value.provider === undefined
) {
return {
source: value.source,
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: value.id,
};
}
return null;
}
export function resolveWebProviderConfig(
cfg: WebProviderConfigSource | undefined,
kind: "search" | "fetch",
@@ -187,6 +117,9 @@ export function hasWebProviderEntryCredential<
config: params.config,
toolConfig: params.toolConfig,
});
if (isLegacySecretRefEnvMarker(rawValue)) {
return false;
}
const configuredRef = coerceSecretRef(rawValue);
if (configuredRef && configuredRef.source !== "env") {
return true;
@@ -216,6 +149,9 @@ export function hasWebProviderEntryCredential<
config: params.config,
toolConfig: params.toolConfig,
});
if (isLegacySecretRefEnvMarker(fallbackRawValue)) {
return false;
}
const fallbackRef = coerceSecretRef(fallbackRawValue);
if (fallbackRef && fallbackRef.source !== "env") {
return true;