source dump of claude code
at main 439 lines 15 kB view raw
1/** 2 * Auto-install logic for the official Anthropic marketplace. 3 * 4 * This module handles automatically installing the official marketplace 5 * on startup for new users, with appropriate checks for: 6 * - Enterprise policy restrictions 7 * - Git availability 8 * - Previous installation attempts 9 */ 10 11import { join } from 'path' 12import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' 13import { logEvent } from '../../services/analytics/index.js' 14import { getGlobalConfig, saveGlobalConfig } from '../config.js' 15import { logForDebugging } from '../debug.js' 16import { isEnvTruthy } from '../envUtils.js' 17import { toError } from '../errors.js' 18import { logError } from '../log.js' 19import { checkGitAvailable, markGitUnavailable } from './gitAvailability.js' 20import { isSourceAllowedByPolicy } from './marketplaceHelpers.js' 21import { 22 addMarketplaceSource, 23 getMarketplacesCacheDir, 24 loadKnownMarketplacesConfig, 25 saveKnownMarketplacesConfig, 26} from './marketplaceManager.js' 27import { 28 OFFICIAL_MARKETPLACE_NAME, 29 OFFICIAL_MARKETPLACE_SOURCE, 30} from './officialMarketplace.js' 31import { fetchOfficialMarketplaceFromGcs } from './officialMarketplaceGcs.js' 32 33/** 34 * Reason why the official marketplace was not installed 35 */ 36export type OfficialMarketplaceSkipReason = 37 | 'already_attempted' 38 | 'already_installed' 39 | 'policy_blocked' 40 | 'git_unavailable' 41 | 'gcs_unavailable' 42 | 'unknown' 43 44/** 45 * Check if official marketplace auto-install is disabled via environment variable. 46 */ 47export function isOfficialMarketplaceAutoInstallDisabled(): boolean { 48 return isEnvTruthy( 49 process.env.CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL, 50 ) 51} 52 53/** 54 * Configuration for retry logic 55 */ 56export const RETRY_CONFIG = { 57 MAX_ATTEMPTS: 10, 58 INITIAL_DELAY_MS: 60 * 60 * 1000, // 1 hour 59 BACKOFF_MULTIPLIER: 2, 60 MAX_DELAY_MS: 7 * 24 * 60 * 60 * 1000, // 1 week 61} 62 63/** 64 * Calculate next retry delay using exponential backoff 65 */ 66function calculateNextRetryDelay(retryCount: number): number { 67 const delay = 68 RETRY_CONFIG.INITIAL_DELAY_MS * 69 Math.pow(RETRY_CONFIG.BACKOFF_MULTIPLIER, retryCount) 70 return Math.min(delay, RETRY_CONFIG.MAX_DELAY_MS) 71} 72 73/** 74 * Determine if installation should be retried based on failure reason and retry state 75 */ 76function shouldRetryInstallation( 77 config: ReturnType<typeof getGlobalConfig>, 78): boolean { 79 // If never attempted, should try 80 if (!config.officialMarketplaceAutoInstallAttempted) { 81 return true 82 } 83 84 // If already installed successfully, don't retry 85 if (config.officialMarketplaceAutoInstalled) { 86 return false 87 } 88 89 const failReason = config.officialMarketplaceAutoInstallFailReason 90 const retryCount = config.officialMarketplaceAutoInstallRetryCount || 0 91 const nextRetryTime = config.officialMarketplaceAutoInstallNextRetryTime 92 const now = Date.now() 93 94 // Check if we've exceeded max attempts 95 if (retryCount >= RETRY_CONFIG.MAX_ATTEMPTS) { 96 return false 97 } 98 99 // Permanent failures - don't retry 100 if (failReason === 'policy_blocked') { 101 return false 102 } 103 104 // Check if enough time has passed for next retry 105 if (nextRetryTime && now < nextRetryTime) { 106 return false 107 } 108 109 // Retry for temporary failures (unknown), semi-permanent (git_unavailable), 110 // and legacy state (undefined failReason from before retry logic existed) 111 return ( 112 failReason === 'unknown' || 113 failReason === 'git_unavailable' || 114 failReason === 'gcs_unavailable' || 115 failReason === undefined 116 ) 117} 118 119/** 120 * Result of the auto-install check 121 */ 122export type OfficialMarketplaceCheckResult = { 123 /** Whether the marketplace was successfully installed */ 124 installed: boolean 125 /** Whether the installation was skipped (and why) */ 126 skipped: boolean 127 /** Reason for skipping, if applicable */ 128 reason?: OfficialMarketplaceSkipReason 129 /** Whether saving retry metadata to config failed */ 130 configSaveFailed?: boolean 131} 132 133/** 134 * Check and install the official marketplace on startup. 135 * 136 * This function is designed to be called as a fire-and-forget operation 137 * during startup. It will: 138 * 1. Check if installation was already attempted 139 * 2. Check if marketplace is already installed 140 * 3. Check enterprise policy restrictions 141 * 4. Check git availability 142 * 5. Attempt installation 143 * 6. Record the result in GlobalConfig 144 * 145 * @returns Result indicating whether installation succeeded or was skipped 146 */ 147export async function checkAndInstallOfficialMarketplace(): Promise<OfficialMarketplaceCheckResult> { 148 const config = getGlobalConfig() 149 150 // Check if we should retry installation 151 if (!shouldRetryInstallation(config)) { 152 const reason: OfficialMarketplaceSkipReason = 153 config.officialMarketplaceAutoInstallFailReason ?? 'already_attempted' 154 logForDebugging(`Official marketplace auto-install skipped: ${reason}`) 155 return { 156 installed: false, 157 skipped: true, 158 reason, 159 } 160 } 161 162 try { 163 // Check if auto-install is disabled via env var 164 if (isOfficialMarketplaceAutoInstallDisabled()) { 165 logForDebugging( 166 'Official marketplace auto-install disabled via env var, skipping', 167 ) 168 saveGlobalConfig(current => ({ 169 ...current, 170 officialMarketplaceAutoInstallAttempted: true, 171 officialMarketplaceAutoInstalled: false, 172 officialMarketplaceAutoInstallFailReason: 'policy_blocked', 173 })) 174 logEvent('tengu_official_marketplace_auto_install', { 175 installed: false, 176 skipped: true, 177 policy_blocked: true, 178 }) 179 return { installed: false, skipped: true, reason: 'policy_blocked' } 180 } 181 182 // Check if marketplace is already installed 183 const knownMarketplaces = await loadKnownMarketplacesConfig() 184 if (knownMarketplaces[OFFICIAL_MARKETPLACE_NAME]) { 185 logForDebugging( 186 `Official marketplace '${OFFICIAL_MARKETPLACE_NAME}' already installed, skipping`, 187 ) 188 // Mark as attempted so we don't check again 189 saveGlobalConfig(current => ({ 190 ...current, 191 officialMarketplaceAutoInstallAttempted: true, 192 officialMarketplaceAutoInstalled: true, 193 })) 194 return { installed: false, skipped: true, reason: 'already_installed' } 195 } 196 197 // Check enterprise policy restrictions 198 if (!isSourceAllowedByPolicy(OFFICIAL_MARKETPLACE_SOURCE)) { 199 logForDebugging( 200 'Official marketplace blocked by enterprise policy, skipping', 201 ) 202 saveGlobalConfig(current => ({ 203 ...current, 204 officialMarketplaceAutoInstallAttempted: true, 205 officialMarketplaceAutoInstalled: false, 206 officialMarketplaceAutoInstallFailReason: 'policy_blocked', 207 })) 208 logEvent('tengu_official_marketplace_auto_install', { 209 installed: false, 210 skipped: true, 211 policy_blocked: true, 212 }) 213 return { installed: false, skipped: true, reason: 'policy_blocked' } 214 } 215 216 // inc-5046: try GCS mirror first — doesn't need git, doesn't hit GitHub. 217 // Backend (anthropic#317037) publishes a marketplace zip to the same 218 // bucket as the native binary. If GCS succeeds, register the marketplace 219 // with source:'github' (still true — GCS is a mirror) and skip git 220 // entirely. 221 const cacheDir = getMarketplacesCacheDir() 222 const installLocation = join(cacheDir, OFFICIAL_MARKETPLACE_NAME) 223 const gcsSha = await fetchOfficialMarketplaceFromGcs( 224 installLocation, 225 cacheDir, 226 ) 227 if (gcsSha !== null) { 228 const known = await loadKnownMarketplacesConfig() 229 known[OFFICIAL_MARKETPLACE_NAME] = { 230 source: OFFICIAL_MARKETPLACE_SOURCE, 231 installLocation, 232 lastUpdated: new Date().toISOString(), 233 } 234 await saveKnownMarketplacesConfig(known) 235 236 saveGlobalConfig(current => ({ 237 ...current, 238 officialMarketplaceAutoInstallAttempted: true, 239 officialMarketplaceAutoInstalled: true, 240 officialMarketplaceAutoInstallFailReason: undefined, 241 officialMarketplaceAutoInstallRetryCount: undefined, 242 officialMarketplaceAutoInstallLastAttemptTime: undefined, 243 officialMarketplaceAutoInstallNextRetryTime: undefined, 244 })) 245 logEvent('tengu_official_marketplace_auto_install', { 246 installed: true, 247 skipped: false, 248 via_gcs: true, 249 }) 250 return { installed: true, skipped: false } 251 } 252 // GCS failed (404 until backend writes, or network). Fall through to git 253 // ONLY if the kill-switch allows — same gate as refreshMarketplace(). 254 if ( 255 !getFeatureValue_CACHED_MAY_BE_STALE( 256 'tengu_plugin_official_mkt_git_fallback', 257 true, 258 ) 259 ) { 260 logForDebugging( 261 'Official marketplace GCS failed; git fallback disabled by flag — skipping install', 262 ) 263 // Same retry-with-backoff metadata as git_unavailable below — transient 264 // GCS failures should retry with exponential backoff, not give up. 265 const retryCount = 266 (config.officialMarketplaceAutoInstallRetryCount || 0) + 1 267 const now = Date.now() 268 const nextRetryTime = now + calculateNextRetryDelay(retryCount) 269 saveGlobalConfig(current => ({ 270 ...current, 271 officialMarketplaceAutoInstallAttempted: true, 272 officialMarketplaceAutoInstalled: false, 273 officialMarketplaceAutoInstallFailReason: 'gcs_unavailable', 274 officialMarketplaceAutoInstallRetryCount: retryCount, 275 officialMarketplaceAutoInstallLastAttemptTime: now, 276 officialMarketplaceAutoInstallNextRetryTime: nextRetryTime, 277 })) 278 logEvent('tengu_official_marketplace_auto_install', { 279 installed: false, 280 skipped: true, 281 gcs_unavailable: true, 282 retry_count: retryCount, 283 }) 284 return { installed: false, skipped: true, reason: 'gcs_unavailable' } 285 } 286 287 // Check git availability 288 const gitAvailable = await checkGitAvailable() 289 if (!gitAvailable) { 290 logForDebugging( 291 'Git not available, skipping official marketplace auto-install', 292 ) 293 const retryCount = 294 (config.officialMarketplaceAutoInstallRetryCount || 0) + 1 295 const now = Date.now() 296 const nextRetryDelay = calculateNextRetryDelay(retryCount) 297 const nextRetryTime = now + nextRetryDelay 298 299 let configSaveFailed = false 300 try { 301 saveGlobalConfig(current => ({ 302 ...current, 303 officialMarketplaceAutoInstallAttempted: true, 304 officialMarketplaceAutoInstalled: false, 305 officialMarketplaceAutoInstallFailReason: 'git_unavailable', 306 officialMarketplaceAutoInstallRetryCount: retryCount, 307 officialMarketplaceAutoInstallLastAttemptTime: now, 308 officialMarketplaceAutoInstallNextRetryTime: nextRetryTime, 309 })) 310 } catch (saveError) { 311 configSaveFailed = true 312 // Log the error properly so it gets tracked 313 const configError = toError(saveError) 314 logError(configError) 315 316 logForDebugging( 317 `Failed to save marketplace auto-install git_unavailable state: ${saveError}`, 318 { level: 'error' }, 319 ) 320 } 321 logEvent('tengu_official_marketplace_auto_install', { 322 installed: false, 323 skipped: true, 324 git_unavailable: true, 325 retry_count: retryCount, 326 }) 327 return { 328 installed: false, 329 skipped: true, 330 reason: 'git_unavailable', 331 configSaveFailed, 332 } 333 } 334 335 // Attempt installation 336 logForDebugging('Attempting to auto-install official marketplace') 337 await addMarketplaceSource(OFFICIAL_MARKETPLACE_SOURCE) 338 339 // Success 340 logForDebugging('Successfully auto-installed official marketplace') 341 const previousRetryCount = 342 config.officialMarketplaceAutoInstallRetryCount || 0 343 saveGlobalConfig(current => ({ 344 ...current, 345 officialMarketplaceAutoInstallAttempted: true, 346 officialMarketplaceAutoInstalled: true, 347 // Clear retry metadata on success 348 officialMarketplaceAutoInstallFailReason: undefined, 349 officialMarketplaceAutoInstallRetryCount: undefined, 350 officialMarketplaceAutoInstallLastAttemptTime: undefined, 351 officialMarketplaceAutoInstallNextRetryTime: undefined, 352 })) 353 logEvent('tengu_official_marketplace_auto_install', { 354 installed: true, 355 skipped: false, 356 retry_count: previousRetryCount, 357 }) 358 return { installed: true, skipped: false } 359 } catch (error) { 360 // Handle installation failure 361 const errorMessage = error instanceof Error ? error.message : String(error) 362 363 // On macOS, /usr/bin/git is an xcrun shim that always exists on PATH, so 364 // checkGitAvailable() (which only does `which git`) passes even without 365 // Xcode CLT installed. The shim then fails at clone time with 366 // "xcrun: error: invalid active developer path (...)". Poison the memoized 367 // availability check so other git callers in this session skip cleanly, 368 // then return silently without recording any attempt state — next startup 369 // tries fresh (no backoff machinery for what is effectively "git absent"). 370 if (errorMessage.includes('xcrun: error:')) { 371 markGitUnavailable() 372 logForDebugging( 373 'Official marketplace auto-install: git is a non-functional macOS xcrun shim, treating as git_unavailable', 374 ) 375 logEvent('tengu_official_marketplace_auto_install', { 376 installed: false, 377 skipped: true, 378 git_unavailable: true, 379 macos_xcrun_shim: true, 380 }) 381 return { 382 installed: false, 383 skipped: true, 384 reason: 'git_unavailable', 385 } 386 } 387 388 logForDebugging( 389 `Failed to auto-install official marketplace: ${errorMessage}`, 390 { level: 'error' }, 391 ) 392 logError(toError(error)) 393 394 const retryCount = 395 (config.officialMarketplaceAutoInstallRetryCount || 0) + 1 396 const now = Date.now() 397 const nextRetryDelay = calculateNextRetryDelay(retryCount) 398 const nextRetryTime = now + nextRetryDelay 399 400 let configSaveFailed = false 401 try { 402 saveGlobalConfig(current => ({ 403 ...current, 404 officialMarketplaceAutoInstallAttempted: true, 405 officialMarketplaceAutoInstalled: false, 406 officialMarketplaceAutoInstallFailReason: 'unknown', 407 officialMarketplaceAutoInstallRetryCount: retryCount, 408 officialMarketplaceAutoInstallLastAttemptTime: now, 409 officialMarketplaceAutoInstallNextRetryTime: nextRetryTime, 410 })) 411 } catch (saveError) { 412 configSaveFailed = true 413 // Log the error properly so it gets tracked 414 const configError = toError(saveError) 415 logError(configError) 416 417 logForDebugging( 418 `Failed to save marketplace auto-install failure state: ${saveError}`, 419 { level: 'error' }, 420 ) 421 422 // Still return the failure result even if config save failed 423 // This ensures we report the installation failure correctly 424 } 425 logEvent('tengu_official_marketplace_auto_install', { 426 installed: false, 427 skipped: true, 428 failed: true, 429 retry_count: retryCount, 430 }) 431 432 return { 433 installed: false, 434 skipped: true, 435 reason: 'unknown', 436 configSaveFailed, 437 } 438 } 439}