source dump of claude code
at main 202 lines 8.4 kB view raw
1import { feature } from 'bun:bundle' 2import { 3 checkGate_CACHED_OR_BLOCKING, 4 getDynamicConfig_CACHED_MAY_BE_STALE, 5 getFeatureValue_CACHED_MAY_BE_STALE, 6} from '../services/analytics/growthbook.js' 7// Namespace import breaks the bridgeEnabled → auth → config → bridgeEnabled 8// cycle — authModule.foo is a live binding, so by the time the helpers below 9// call it, auth.js is fully loaded. Previously used require() for the same 10// deferral, but require() hits a CJS cache that diverges from the ESM 11// namespace after mock.module() (daemon/auth.test.ts), breaking spyOn. 12import * as authModule from '../utils/auth.js' 13import { isEnvTruthy } from '../utils/envUtils.js' 14import { lt } from '../utils/semver.js' 15 16/** 17 * Runtime check for bridge mode entitlement. 18 * 19 * Remote Control requires a claude.ai subscription (the bridge auths to CCR 20 * with the claude.ai OAuth token). isClaudeAISubscriber() excludes 21 * Bedrock/Vertex/Foundry, apiKeyHelper/gateway deployments, env-var API keys, 22 * and Console API logins — none of which have the OAuth token CCR needs. 23 * See github.com/deshaw/anthropic-issues/issues/24. 24 * 25 * The `feature('BRIDGE_MODE')` guard ensures the GrowthBook string literal 26 * is only referenced when bridge mode is enabled at build time. 27 */ 28export function isBridgeEnabled(): boolean { 29 // Positive ternary pattern — see docs/feature-gating.md. 30 // Negative pattern (if (!feature(...)) return) does not eliminate 31 // inline string literals from external builds. 32 return feature('BRIDGE_MODE') 33 ? isClaudeAISubscriber() && 34 getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_bridge', false) 35 : false 36} 37 38/** 39 * Blocking entitlement check for Remote Control. 40 * 41 * Returns cached `true` immediately (fast path). If the disk cache says 42 * `false` or is missing, awaits GrowthBook init and fetches the fresh 43 * server value (slow path, max ~5s), then writes it to disk. 44 * 45 * Use at entitlement gates where a stale `false` would unfairly block access. 46 * For user-facing error paths, prefer `getBridgeDisabledReason()` which gives 47 * a specific diagnostic. For render-body UI visibility checks, use 48 * `isBridgeEnabled()` instead. 49 */ 50export async function isBridgeEnabledBlocking(): Promise<boolean> { 51 return feature('BRIDGE_MODE') 52 ? isClaudeAISubscriber() && 53 (await checkGate_CACHED_OR_BLOCKING('tengu_ccr_bridge')) 54 : false 55} 56 57/** 58 * Diagnostic message for why Remote Control is unavailable, or null if 59 * it's enabled. Call this instead of a bare `isBridgeEnabledBlocking()` 60 * check when you need to show the user an actionable error. 61 * 62 * The GrowthBook gate targets on organizationUUID, which comes from 63 * config.oauthAccount — populated by /api/oauth/profile during login. 64 * That endpoint requires the user:profile scope. Tokens without it 65 * (setup-token, CLAUDE_CODE_OAUTH_TOKEN env var, or pre-scope-expansion 66 * logins) leave oauthAccount unpopulated, so the gate falls back to 67 * false and users see a dead-end "not enabled" message with no hint 68 * that re-login would fix it. See CC-1165 / gh-33105. 69 */ 70export async function getBridgeDisabledReason(): Promise<string | null> { 71 if (feature('BRIDGE_MODE')) { 72 if (!isClaudeAISubscriber()) { 73 return 'Remote Control requires a claude.ai subscription. Run `claude auth login` to sign in with your claude.ai account.' 74 } 75 if (!hasProfileScope()) { 76 return 'Remote Control requires a full-scope login token. Long-lived tokens (from `claude setup-token` or CLAUDE_CODE_OAUTH_TOKEN) are limited to inference-only for security reasons. Run `claude auth login` to use Remote Control.' 77 } 78 if (!getOauthAccountInfo()?.organizationUuid) { 79 return 'Unable to determine your organization for Remote Control eligibility. Run `claude auth login` to refresh your account information.' 80 } 81 if (!(await checkGate_CACHED_OR_BLOCKING('tengu_ccr_bridge'))) { 82 return 'Remote Control is not yet enabled for your account.' 83 } 84 return null 85 } 86 return 'Remote Control is not available in this build.' 87} 88 89// try/catch: main.tsx:5698 calls isBridgeEnabled() while defining the Commander 90// program, before enableConfigs() runs. isClaudeAISubscriber() → getGlobalConfig() 91// throws "Config accessed before allowed" there. Pre-config, no OAuth token can 92// exist anyway — false is correct. Same swallow getFeatureValue_CACHED_MAY_BE_STALE 93// already does at growthbook.ts:775-780. 94function isClaudeAISubscriber(): boolean { 95 try { 96 return authModule.isClaudeAISubscriber() 97 } catch { 98 return false 99 } 100} 101function hasProfileScope(): boolean { 102 try { 103 return authModule.hasProfileScope() 104 } catch { 105 return false 106 } 107} 108function getOauthAccountInfo(): ReturnType< 109 typeof authModule.getOauthAccountInfo 110> { 111 try { 112 return authModule.getOauthAccountInfo() 113 } catch { 114 return undefined 115 } 116} 117 118/** 119 * Runtime check for the env-less (v2) REPL bridge path. 120 * Returns true when the GrowthBook flag `tengu_bridge_repl_v2` is enabled. 121 * 122 * This gates which implementation initReplBridge uses — NOT whether bridge 123 * is available at all (see isBridgeEnabled above). Daemon/print paths stay 124 * on the env-based implementation regardless of this gate. 125 */ 126export function isEnvLessBridgeEnabled(): boolean { 127 return feature('BRIDGE_MODE') 128 ? getFeatureValue_CACHED_MAY_BE_STALE('tengu_bridge_repl_v2', false) 129 : false 130} 131 132/** 133 * Kill-switch for the `cse_*` → `session_*` client-side retag shim. 134 * 135 * The shim exists because compat/convert.go:27 validates TagSession and the 136 * claude.ai frontend routes on `session_*`, while v2 worker endpoints hand out 137 * `cse_*`. Once the server tags by environment_kind and the frontend accepts 138 * `cse_*` directly, flip this to false to make toCompatSessionId a no-op. 139 * Defaults to true — the shim stays active until explicitly disabled. 140 */ 141export function isCseShimEnabled(): boolean { 142 return feature('BRIDGE_MODE') 143 ? getFeatureValue_CACHED_MAY_BE_STALE( 144 'tengu_bridge_repl_v2_cse_shim_enabled', 145 true, 146 ) 147 : true 148} 149 150/** 151 * Returns an error message if the current CLI version is below the 152 * minimum required for the v1 (env-based) Remote Control path, or null if the 153 * version is fine. The v2 (env-less) path uses checkEnvLessBridgeMinVersion() 154 * in envLessBridgeConfig.ts instead — the two implementations have independent 155 * version floors. 156 * 157 * Uses cached (non-blocking) GrowthBook config. If GrowthBook hasn't 158 * loaded yet, the default '0.0.0' means the check passes — a safe fallback. 159 */ 160export function checkBridgeMinVersion(): string | null { 161 // Positive pattern — see docs/feature-gating.md. 162 // Negative pattern (if (!feature(...)) return) does not eliminate 163 // inline string literals from external builds. 164 if (feature('BRIDGE_MODE')) { 165 const config = getDynamicConfig_CACHED_MAY_BE_STALE<{ 166 minVersion: string 167 }>('tengu_bridge_min_version', { minVersion: '0.0.0' }) 168 if (config.minVersion && lt(MACRO.VERSION, config.minVersion)) { 169 return `Your version of Claude Code (${MACRO.VERSION}) is too old for Remote Control.\nVersion ${config.minVersion} or higher is required. Run \`claude update\` to update.` 170 } 171 } 172 return null 173} 174 175/** 176 * Default for remoteControlAtStartup when the user hasn't explicitly set it. 177 * When the CCR_AUTO_CONNECT build flag is present (ant-only) and the 178 * tengu_cobalt_harbor GrowthBook gate is on, all sessions connect to CCR by 179 * default — the user can still opt out by setting remoteControlAtStartup=false 180 * in config (explicit settings always win over this default). 181 * 182 * Defined here rather than in config.ts to avoid a direct 183 * config.ts → growthbook.ts import cycle (growthbook.ts → user.ts → config.ts). 184 */ 185export function getCcrAutoConnectDefault(): boolean { 186 return feature('CCR_AUTO_CONNECT') 187 ? getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_harbor', false) 188 : false 189} 190 191/** 192 * Opt-in CCR mirror mode — every local session spawns an outbound-only 193 * Remote Control session that receives forwarded events. Separate from 194 * getCcrAutoConnectDefault (bidirectional Remote Control). Env var wins for 195 * local opt-in; GrowthBook controls rollout. 196 */ 197export function isCcrMirrorEnabled(): boolean { 198 return feature('CCR_MIRROR') 199 ? isEnvTruthy(process.env.CLAUDE_CODE_CCR_MIRROR) || 200 getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_mirror', false) 201 : false 202}