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 <noreply@anthropic.com>
This commit is contained in:
jatmn
2026-11-04 20:06:30 -08:00
co-authored by Claude Opus 5.5
parent 1ce28be535
commit 025da9bbe1
4 changed files with 90 additions and 3 deletions
+26
View File
@@ -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<Parameters<typeof buildStartupEnvFromProfile>[0]>
+33
View File
@@ -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 } =
+30 -3
View File
@@ -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
+1
View File
@@ -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',