From 025da9bbe1b9479705a453736b0333131bc9357f Mon Sep 17 00:00:00 2001 From: jatmn Date: Wed, 4 Nov 2026 20:06:30 -0800 Subject: [PATCH] fix(requesty): keep a saved Requesty key from passing as shell input A keyed canonical Requesty profile writes its saved key into REQUESTY_API_KEY, both in the startup env and, since 1ce28be5, on in-process apply. A keyless canonical Requesty profile adopts REQUESTY_API_KEY as shell input. Nothing recorded where that value came from, so selecting a keyless profile after a keyed one, in process or after a relaunch from the keyed startup file, sent requests on the keyed profile's account. Requesty now uses the origin marker that Runcrate already uses for the same keyless-adoption rule. CLAUDE_CODE_PROVIDER_PROFILE_REQUESTY_KEY_SHA256 is written where the saved key is installed (profile apply, startup bootstrap, profile update, launch env), kept across env reapply, forwarded to teammate panes, and cleared with the rest of the profile env. Keyless adoption and the alignment check refuse a key that carries the marker. Genuine shell input has no marker and still authenticates a keyless profile. Co-Authored-By: Claude Opus 5.5 --- src/utils/providerProfile.ts | 26 +++++++++++++++++++++++ src/utils/providerProfiles.test.ts | 33 ++++++++++++++++++++++++++++++ src/utils/providerProfiles.ts | 33 +++++++++++++++++++++++++++--- src/utils/swarm/spawnUtils.ts | 1 + 4 files changed, 90 insertions(+), 3 deletions(-) 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',