diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 520e2c725..c718fd0f2 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -68,6 +68,8 @@ export const PROFILE_ORCAROUTER_KEY_SHA256 = 'CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256' export const PROFILE_RUNCRATE_KEY_SHA256 = 'CLAUDE_CODE_PROVIDER_PROFILE_RUNCRATE_KEY_SHA256' +export const PROFILE_REQUESTY_KEY_SHA256 = + 'CLAUDE_CODE_PROVIDER_PROFILE_REQUESTY_KEY_SHA256' // Provenance marker for the OPENAI_AUTH_* trio. A child inherits this with the // trio, so a retarget can tell profile-managed auth from an explicit shell // credential without comparing values that may have changed since the profile @@ -94,6 +96,10 @@ export function runcrateCredentialFingerprint(value: string): string { return managedCredentialFingerprint(value) } +export function requestyCredentialFingerprint(value: string): string { + return managedCredentialFingerprint(value) +} + export function isManagedOrcarouterCredential(env: NodeJS.ProcessEnv): boolean { const key = sanitizeApiKey(env.ORCAROUTER_API_KEY) // Credential origin is independent of selection markers: legacy profile @@ -106,10 +112,16 @@ export function isManagedRuncrateCredential(env: NodeJS.ProcessEnv): boolean { return !!key && env[PROFILE_RUNCRATE_KEY_SHA256] === runcrateCredentialFingerprint(key) } +export function isManagedRequestyCredential(env: NodeJS.ProcessEnv): boolean { + const key = sanitizeApiKey(env.REQUESTY_API_KEY) + return !!key && env[PROFILE_REQUESTY_KEY_SHA256] === requestyCredentialFingerprint(key) +} + const PROFILE_ENV_KEYS = [ 'CLAUDE_CODE_USE_GEMINI_VERTEX', PROFILE_ORCAROUTER_KEY_SHA256, PROFILE_RUNCRATE_KEY_SHA256, + PROFILE_REQUESTY_KEY_SHA256, PROFILE_OPENAI_AUTH_MANAGED, 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GITHUB', @@ -231,6 +243,7 @@ export type ProviderProfile = export type ProfileEnv = { CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256?: string CLAUDE_CODE_PROVIDER_PROFILE_RUNCRATE_KEY_SHA256?: string + CLAUDE_CODE_PROVIDER_PROFILE_REQUESTY_KEY_SHA256?: string ANTHROPIC_BASE_URL?: string ANTHROPIC_MODEL?: string ANTHROPIC_API_KEY?: string @@ -2777,6 +2790,11 @@ export async function buildLaunchEnv(options: { (!sanitizeApiKey(processEnv.RUNCRATE_API_KEY) && persistedEnv.RUNCRATE_API_KEY))) { env[PROFILE_RUNCRATE_KEY_SHA256] = runcrateCredentialFingerprint(dedicatedValue) } + if (dedicatedKey === 'REQUESTY_API_KEY' && + (persistedRequestyProfileKey || + (!sanitizeApiKey(processEnv.REQUESTY_API_KEY) && persistedEnv.REQUESTY_API_KEY))) { + env[PROFILE_REQUESTY_KEY_SHA256] = requestyCredentialFingerprint(dedicatedValue) + } } } if (effectiveOpenAIRouteId === 'nvidia-nim') { @@ -2986,6 +3004,11 @@ export function applyProfileEnvToProcessEnv( sanitizeApiKey(targetEnv.RUNCRATE_API_KEY) === sanitizeApiKey(nextEnv.RUNCRATE_API_KEY) ? targetEnv[PROFILE_RUNCRATE_KEY_SHA256] : undefined + const retainedRequestyOrigin = + isManagedRequestyCredential(targetEnv) && + sanitizeApiKey(targetEnv.REQUESTY_API_KEY) === sanitizeApiKey(nextEnv.REQUESTY_API_KEY) + ? targetEnv[PROFILE_REQUESTY_KEY_SHA256] + : undefined clearManagedProfileEnv(targetEnv) Object.assign(targetEnv, nextEnv) if (retainedOrcarouterOrigin) { @@ -2994,6 +3017,9 @@ export function applyProfileEnvToProcessEnv( if (retainedRuncrateOrigin) { targetEnv[PROFILE_RUNCRATE_KEY_SHA256] = retainedRuncrateOrigin } + if (retainedRequestyOrigin) { + targetEnv[PROFILE_REQUESTY_KEY_SHA256] = retainedRequestyOrigin + } } type StartupEnvOptions = NonNullable[0]> diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index c6c4e8df7..1e6d2cc1d 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -28,6 +28,7 @@ const RESTORED_KEYS = [ 'CLAUDE_CODE_PROVIDER_PROFILE_OPENAI_AUTH_MANAGED', 'CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256', 'CLAUDE_CODE_PROVIDER_PROFILE_RUNCRATE_KEY_SHA256', + 'CLAUDE_CODE_PROVIDER_PROFILE_REQUESTY_KEY_SHA256', 'CLAUDE_CODE_PROVIDER_ROUTE_ID', 'CLAUDE_CONFIG_DIR', 'OPENCLAUDE_CONFIG_DIR', @@ -6879,6 +6880,38 @@ test.each([false, true])('a keyless Runcrate selection rejects another profile k expect(persisted.env.RUNCRATE_API_KEY).toBeUndefined() }) +// A keyed Requesty profile mirrors its saved key into REQUESTY_API_KEY. A +// keyless Requesty selection that follows must not adopt that key as shell +// input, in process or after a relaunch from the keyed startup file. +test.each([false, true])('a keyless Requesty selection rejects another profile key (relaunched: %s)', async relaunched => { + const { setActiveProviderProfile, applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + const { applyProfileEnvToProcessEnv, buildStartupEnvFromProfile } = + await import('./providerProfile.js') + const keyed = buildRequestyProfile({ id: 'keyed-requesty', apiKey: 'account-a-key' }) + const keyless = { ...keyed, id: 'keyless-requesty', apiKey: undefined } + saveMockGlobalConfig(current => ({ ...current, providerProfiles: [keyed, keyless] })) + setActiveProviderProfile(keyed.id, { configDir: testConfigDir! }) + expect(process.env.REQUESTY_API_KEY).toBe('account-a-key') + if (relaunched) { + const persisted = JSON.parse(readFileSync( + join(testConfigDir!, '.openclaude-profile.json'), 'utf8', + )) + const env = await buildStartupEnvFromProfile({ persisted, processEnv: {} }) + expect(env.REQUESTY_API_KEY).toBe('account-a-key') + applyProfileEnvToProcessEnv(process.env, env) + } + applyProviderProfileToProcessEnv(keyless) + expect(process.env.REQUESTY_API_KEY).toBeUndefined() + expect(resolveRouteCredentialValue({ routeId: 'requesty', baseUrl: keyless.baseUrl })) + .toBeUndefined() + + // Genuine shell input still authenticates the keyless selection. + process.env.REQUESTY_API_KEY = 'shell-requesty-key' + applyProviderProfileToProcessEnv(keyless) + expect(process.env.REQUESTY_API_KEY).toBe('shell-requesty-key') +}) + for (const provider of ['runcrate', 'openai']) { test(`${provider} Runcrate profile owns its credential through apply, persist and relaunch`, async () => { const { setActiveProviderProfile, applyActiveProviderProfileFromConfig } = diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index f0bccd7ca..4be33cdff 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -40,11 +40,14 @@ import { clearManagedProfileEnv, isManagedOrcarouterCredential, isManagedRuncrateCredential, + isManagedRequestyCredential, orcarouterCredentialFingerprint, PROFILE_ORCAROUTER_KEY_SHA256, PROFILE_RUNCRATE_KEY_SHA256, + PROFILE_REQUESTY_KEY_SHA256, PROFILE_OPENAI_AUTH_MANAGED, runcrateCredentialFingerprint, + requestyCredentialFingerprint, deleteProfileFile, type ProfileFileLocation, type ProfileEnv, @@ -153,6 +156,21 @@ function rememberManagedRuncrateCredential( } } +// Requesty: same origin marker as Runcrate, so a later keyless canonical +// Requesty selection never mistakes this profile's saved key for shell input. +function rememberManagedRequestyCredential( + profile: ProviderProfile, + processEnv: NodeJS.ProcessEnv = process.env, +): void { + const value = sanitizeApiKey(profile.apiKey)?.trim() + if (value && isRequestyProfile(profile) && + processEnv[PROFILE_ENV_APPLIED_FLAG] === '1' && + trimOrUndefined(processEnv[PROFILE_ENV_APPLIED_ID]) === profile.id && + sanitizeApiKey(processEnv.REQUESTY_API_KEY)?.trim() === value) { + processEnv[PROFILE_REQUESTY_KEY_SHA256] = requestyCredentialFingerprint(value) + } +} + type ProfileCompatibilityMode = | 'anthropic' | 'gemini' @@ -967,6 +985,11 @@ function hostDedicatedCredentialsAligned( isManagedRuncrateCredential(processEnv)) { return false } + if (!profile.apiKey && + isRequestyProfile(profile) && + isManagedRequestyCredential(processEnv)) { + return false + } const dedicated = getHostDedicatedCredentialEnv( profile.baseUrl, profile.apiKey ?? '', @@ -1596,13 +1619,14 @@ export function applyProviderProfileToProcessEnv( } // REQUESTY_API_KEY is profile-managed: installing this env clears the // ambient value so a keyed profile's saved key wins. A keyless canonical - // profile adopts the ambient key here; a retargeted profile gets neither. - // Stamp the route identity the startup env also writes. + // profile adopts only genuine shell input here, never a key a keyed + // profile installed; a retargeted profile gets neither. Stamp the route + // identity the startup env also writes. if (route.routeId === 'requesty') { openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'requesty' if (isRequestyProfile(profile) && !profile.apiKey) { const ambientRequestyKey = sanitizeApiKey(process.env.REQUESTY_API_KEY) - if (ambientRequestyKey) { + if (ambientRequestyKey && !isManagedRequestyCredential(process.env)) { openAIProfileEnv.REQUESTY_API_KEY = ambientRequestyKey } } @@ -1659,6 +1683,7 @@ export function applyProviderProfileToProcessEnv( } rememberManagedOrcarouterCredential(profile) rememberManagedRuncrateCredential(profile) + rememberManagedRequestyCredential(profile) } export function applyActiveProviderProfileFromConfig( @@ -1700,6 +1725,7 @@ export function applyActiveProviderProfileFromConfig( // record is available; new environments already carry their fingerprint. rememberManagedOrcarouterCredential(activeProfile, processEnv) rememberManagedRuncrateCredential(activeProfile, processEnv) + rememberManagedRequestyCredential(activeProfile, processEnv) const isCurrentEnvProfileManaged = processEnv[PROFILE_ENV_APPLIED_FLAG] === '1' && @@ -1819,6 +1845,7 @@ export function updateProviderProfile( wasUpdated = true rememberManagedOrcarouterCredential(currentProfiles[profileIndex]!) rememberManagedRuncrateCredential(currentProfiles[profileIndex]!) + rememberManagedRequestyCredential(currentProfiles[profileIndex]!) const nextProfiles = [...currentProfiles] nextProfiles[profileIndex] = updatedProfile diff --git a/src/utils/swarm/spawnUtils.ts b/src/utils/swarm/spawnUtils.ts index af9e4d90b..76d737108 100644 --- a/src/utils/swarm/spawnUtils.ts +++ b/src/utils/swarm/spawnUtils.ts @@ -121,6 +121,7 @@ const TEAMMATE_ENV_VARS = [ // Preserve saved-key origin across panes for subsequent keyless selection. 'CLAUDE_CODE_PROVIDER_PROFILE_ORCAROUTER_KEY_SHA256', 'CLAUDE_CODE_PROVIDER_PROFILE_RUNCRATE_KEY_SHA256', + 'CLAUDE_CODE_PROVIDER_PROFILE_REQUESTY_KEY_SHA256', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_MODEL',