mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
327432831a
commit
c5d0b7dd39
@@ -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" }));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(((
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user