mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Direct TUI launches read cc-haha managed settings without the desktop host's session-env injection, so a stale proxy URL could survive after switching to an Anthropic-compatible provider. The provider runtime env calculation now has one shared source of truth and CLI config loading recomputes managed provider env from the active provider index before applying it. Constraint: Direct source-based TUI usage must keep working with .env when no desktop provider is configured Rejected: Inject desktop runtime only from the embedded terminal shell | native terminals and source TUI launches would still read stale managed settings Confidence: high Scope-risk: moderate Directive: Keep provider env derivation shared between ProviderService and CLI managedEnv loading Tested: bun test src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/conversation-service.test.ts Tested: bun run check:server Not-tested: Live provider request from embedded terminal TUI
232 lines
9.4 KiB
TypeScript
232 lines
9.4 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { isRemoteManagedSettingsEligible } from '../services/remoteManagedSettings/syncCache.js'
|
|
import { mergeActiveProviderManagedEnv } from '../server/services/providerRuntimeEnv.js'
|
|
import { clearCACertsCache } from './caCerts.js'
|
|
import { getGlobalConfig } from './config.js'
|
|
import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'
|
|
import {
|
|
isProviderManagedEnvVar,
|
|
SAFE_ENV_VARS,
|
|
} from './managedEnvConstants.js'
|
|
import { normalizeLegacyDeepSeekManagedEnv } from './providerManagedEnvCompat.js'
|
|
import { clearMTLSCache } from './mtls.js'
|
|
import { clearProxyCache, configureGlobalAgents } from './proxy.js'
|
|
import { isSettingSourceEnabled } from './settings/constants.js'
|
|
import {
|
|
getSettings_DEPRECATED,
|
|
getSettingsForSource,
|
|
} from './settings/settings.js'
|
|
|
|
/**
|
|
* `claude ssh` remote: ANTHROPIC_UNIX_SOCKET routes auth through a -R forwarded
|
|
* socket to a local proxy, and the launcher sets a handful of placeholder auth
|
|
* env vars that the remote's ~/.claude settings.env MUST NOT clobber (see
|
|
* isAnthropicAuthEnabled). Strip them from any settings-sourced env object.
|
|
*/
|
|
function withoutSSHTunnelVars(
|
|
env: Record<string, string> | undefined,
|
|
): Record<string, string> {
|
|
if (!env || !process.env.ANTHROPIC_UNIX_SOCKET) return env || {}
|
|
const {
|
|
ANTHROPIC_UNIX_SOCKET: _1,
|
|
ANTHROPIC_BASE_URL: _2,
|
|
ANTHROPIC_API_KEY: _3,
|
|
ANTHROPIC_AUTH_TOKEN: _4,
|
|
CLAUDE_CODE_OAUTH_TOKEN: _5,
|
|
...rest
|
|
} = env
|
|
return rest
|
|
}
|
|
|
|
/**
|
|
* When the host owns inference routing (sets
|
|
* CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST in spawn env), strip
|
|
* provider-selection / model-default vars from settings-sourced env so a
|
|
* user's ~/.claude/settings.json can't redirect requests away from the
|
|
* host-configured provider.
|
|
*/
|
|
function withoutHostManagedProviderVars(
|
|
env: Record<string, string> | undefined,
|
|
): Record<string, string> {
|
|
if (!env) return {}
|
|
if (!isEnvTruthy(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST)) {
|
|
return env
|
|
}
|
|
const out: Record<string, string> = {}
|
|
for (const [key, value] of Object.entries(env)) {
|
|
if (!isProviderManagedEnvVar(key)) {
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Snapshot of env keys present before any settings.env is applied — for CCD,
|
|
* these are the keys the desktop host set to orchestrate the subprocess.
|
|
* Settings must not override them (OTEL_LOGS_EXPORTER=console would corrupt
|
|
* the stdio JSON-RPC transport). Keys added LATER by user/project settings
|
|
* are not in this set, so mid-session settings.json changes still apply.
|
|
* Lazy-captured on first applySafeConfigEnvironmentVariables() call.
|
|
*/
|
|
let ccdSpawnEnvKeys: Set<string> | null | undefined
|
|
|
|
function withoutCcdSpawnEnvKeys(
|
|
env: Record<string, string> | undefined,
|
|
): Record<string, string> {
|
|
if (!env || !ccdSpawnEnvKeys) return env || {}
|
|
const out: Record<string, string> = {}
|
|
for (const [key, value] of Object.entries(env)) {
|
|
if (!ccdSpawnEnvKeys.has(key)) out[key] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Compose the strip filters applied to every settings-sourced env object.
|
|
*/
|
|
function filterSettingsEnv(
|
|
env: Record<string, string> | undefined,
|
|
): Record<string, string> {
|
|
return withoutCcdSpawnEnvKeys(
|
|
withoutHostManagedProviderVars(withoutSSHTunnelVars(env)),
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Read env vars from ~/.claude/cc-haha/settings.json (Haha-specific provider
|
|
* config). This file is written by ProviderService.syncToSettings() and
|
|
* contains ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, model defaults, etc.
|
|
* Returns an empty object if the file doesn't exist or is invalid.
|
|
*/
|
|
function getCcHahaSettingsEnv(): Record<string, string> {
|
|
try {
|
|
const ccHahaSettings = join(getClaudeConfigHomeDir(), 'cc-haha', 'settings.json')
|
|
const raw = readFileSync(ccHahaSettings, 'utf-8')
|
|
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|
|
const settingsEnv = normalizeLegacyDeepSeekManagedEnv(parsed.env ?? {}).env
|
|
return mergeActiveProviderManagedEnv(settingsEnv, getClaudeConfigHomeDir())
|
|
} catch {
|
|
return mergeActiveProviderManagedEnv({}, getClaudeConfigHomeDir())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trusted setting sources whose env vars can be applied before the trust dialog.
|
|
*
|
|
* - userSettings (~/.claude/settings.json): controlled by the user, not project-specific
|
|
* - flagSettings (--settings CLI flag or SDK inline settings): explicitly passed by the user
|
|
* - policySettings (managed settings from enterprise API or local managed-settings.json):
|
|
* controlled by IT/admin (highest priority, cannot be overridden)
|
|
*
|
|
* Project-scoped sources (projectSettings, localSettings) are excluded because they live
|
|
* inside the project directory and could be committed by a malicious actor to redirect
|
|
* traffic (e.g., ANTHROPIC_BASE_URL) to an attacker-controlled server.
|
|
*/
|
|
const TRUSTED_SETTING_SOURCES = [
|
|
'userSettings',
|
|
'flagSettings',
|
|
'policySettings',
|
|
] as const
|
|
|
|
/**
|
|
* Apply environment variables from trusted sources to process.env.
|
|
* Called before the trust dialog so that user/enterprise env vars like
|
|
* ANTHROPIC_BASE_URL take effect during first-run/onboarding.
|
|
*
|
|
* For trusted sources (user settings, managed settings, CLI flags), ALL env vars
|
|
* are applied — including ones like ANTHROPIC_BASE_URL that would be dangerous
|
|
* from project-scoped settings.
|
|
*
|
|
* For project-scoped sources (projectSettings, localSettings), only safe env vars
|
|
* from the SAFE_ENV_VARS allowlist are applied. These are applied after trust is
|
|
* fully established via applyConfigEnvironmentVariables().
|
|
*/
|
|
export function applySafeConfigEnvironmentVariables(): void {
|
|
// Capture CCD spawn-env keys before any settings.env is applied (once).
|
|
if (ccdSpawnEnvKeys === undefined) {
|
|
ccdSpawnEnvKeys =
|
|
process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-desktop'
|
|
? new Set(Object.keys(process.env))
|
|
: null
|
|
}
|
|
|
|
// Global config (~/.claude.json) is user-controlled. In CCD mode,
|
|
// filterSettingsEnv strips keys that were in the spawn env snapshot so
|
|
// the desktop host's operational vars (OTEL, etc.) are not overridden.
|
|
Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env))
|
|
|
|
// Apply ALL env vars from trusted setting sources, policySettings last.
|
|
// Gate on isSettingSourceEnabled so SDK settingSources: [] (isolation mode)
|
|
// doesn't get clobbered by ~/.claude/settings.json env (gh#217). policy/flag
|
|
// sources are always enabled, so this only ever filters userSettings.
|
|
for (const source of TRUSTED_SETTING_SOURCES) {
|
|
if (source === 'policySettings') continue
|
|
if (!isSettingSourceEnabled(source)) continue
|
|
Object.assign(
|
|
process.env,
|
|
filterSettingsEnv(getSettingsForSource(source)?.env),
|
|
)
|
|
}
|
|
|
|
// cc-haha provider isolation: apply env from ~/.claude/cc-haha/settings.json
|
|
// AFTER userSettings so Haha-specific provider config takes priority over
|
|
// the original Claude Code's settings. This prevents Haha from polluting
|
|
// ~/.claude/settings.json while still allowing it to override provider vars.
|
|
Object.assign(process.env, filterSettingsEnv(getCcHahaSettingsEnv()))
|
|
|
|
// Compute remote-managed-settings eligibility now, with userSettings and
|
|
// flagSettings env applied. Eligibility reads CLAUDE_CODE_USE_BEDROCK,
|
|
// ANTHROPIC_BASE_URL — both settable via settings.env.
|
|
// getSettingsForSource('policySettings') below consults the remote cache,
|
|
// which guards on this. The two-phase structure makes the ordering
|
|
// dependency visible: non-policy env → eligibility → policy env.
|
|
isRemoteManagedSettingsEligible()
|
|
|
|
Object.assign(
|
|
process.env,
|
|
filterSettingsEnv(getSettingsForSource('policySettings')?.env),
|
|
)
|
|
|
|
// Apply only safe env vars from the fully-merged settings (which includes
|
|
// project-scoped sources). For safe vars that also exist in trusted sources,
|
|
// the merged value (which may come from a higher-priority project source)
|
|
// will overwrite the trusted value — this is acceptable since these vars are
|
|
// in the safe allowlist. Only policySettings values are guaranteed to survive
|
|
// unchanged (it has the highest merge priority in both loops) — except
|
|
// provider-routing vars, which filterSettingsEnv strips from every source
|
|
// when CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is set.
|
|
const settingsEnv = filterSettingsEnv(getSettings_DEPRECATED()?.env)
|
|
for (const [key, value] of Object.entries(settingsEnv)) {
|
|
if (SAFE_ENV_VARS.has(key.toUpperCase())) {
|
|
process.env[key] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply environment variables from settings to process.env.
|
|
* This applies ALL environment variables (except provider-routing vars when
|
|
* CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is set — see filterSettingsEnv) and
|
|
* should only be called after trust is established. This applies potentially
|
|
* dangerous environment variables such as LD_PRELOAD, PATH, etc.
|
|
*/
|
|
export function applyConfigEnvironmentVariables(): void {
|
|
Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env))
|
|
|
|
Object.assign(process.env, filterSettingsEnv(getSettings_DEPRECATED()?.env))
|
|
|
|
// cc-haha provider isolation: same as in applySafeConfigEnvironmentVariables,
|
|
// apply Haha-specific env last so it overrides the original settings.
|
|
Object.assign(process.env, filterSettingsEnv(getCcHahaSettingsEnv()))
|
|
|
|
// Clear caches so agents are rebuilt with the new env vars
|
|
clearCACertsCache()
|
|
clearMTLSCache()
|
|
clearProxyCache()
|
|
|
|
// Reconfigure proxy/mTLS agents to pick up any proxy env vars from settings
|
|
configureGlobalAgents()
|
|
}
|