File size: 13,780 Bytes
39e315a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | import { profileCheckpoint } from '../utils/startupProfiler.js'
import '../bootstrap/state.js'
import '../utils/config.js'
import type { Attributes, MetricOptions } from '@opentelemetry/api'
import memoize from 'lodash-es/memoize.js'
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
import type { AttributedCounter } from '../bootstrap/state.js'
import { getSessionCounter, setMeter } from '../bootstrap/state.js'
import { shutdownLspServerManager } from '../services/lsp/manager.js'
import { populateOAuthAccountInfoIfNeeded } from '../services/oauth/client.js'
import {
initializePolicyLimitsLoadingPromise,
isPolicyLimitsEligible,
} from '../services/policyLimits/index.js'
import {
initializeRemoteManagedSettingsLoadingPromise,
isEligibleForRemoteManagedSettings,
waitForRemoteManagedSettingsToLoad,
} from '../services/remoteManagedSettings/index.js'
import { preconnectAnthropicApi } from '../utils/apiPreconnect.js'
import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js'
import { registerCleanup } from '../utils/cleanupRegistry.js'
import { enableConfigs, recordFirstStartTime } from '../utils/config.js'
import { logForDebugging } from '../utils/debug.js'
import { detectCurrentRepository } from '../utils/detectRepository.js'
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
import { initJetBrainsDetection } from '../utils/envDynamic.js'
import { isEnvTruthy } from '../utils/envUtils.js'
import { ConfigParseError, errorMessage } from '../utils/errors.js'
// showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init
import {
gracefulShutdownSync,
setupGracefulShutdown,
} from '../utils/gracefulShutdown.js'
import {
applyConfigEnvironmentVariables,
applySafeConfigEnvironmentVariables,
} from '../utils/managedEnv.js'
import { configureGlobalMTLS } from '../utils/mtls.js'
import {
ensureScratchpadDir,
isScratchpadEnabled,
} from '../utils/permissions/filesystem.js'
// initializeTelemetry is loaded lazily via import() in setMeterState() to defer
// ~400KB of OpenTelemetry + protobuf modules until telemetry is actually initialized.
// gRPC exporters (~700KB via @grpc/grpc-js) are further lazy-loaded within instrumentation.ts.
import { configureGlobalAgents } from '../utils/proxy.js'
import { isBetaTracingEnabled } from '../utils/telemetry/betaSessionTracing.js'
import { getTelemetryAttributes } from '../utils/telemetryAttributes.js'
import { setShellIfWindows } from '../utils/windowsPaths.js'
// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
// Track if telemetry has been initialized to prevent double initialization
let telemetryInitialized = false
export const init = memoize(async (): Promise<void> => {
const initStartTime = Date.now()
logForDiagnosticsNoPII('info', 'init_started')
profileCheckpoint('init_function_start')
// Validate configs are valid and enable configuration system
try {
const configsStart = Date.now()
enableConfigs()
logForDiagnosticsNoPII('info', 'init_configs_enabled', {
duration_ms: Date.now() - configsStart,
})
profileCheckpoint('init_configs_enabled')
// Apply only safe environment variables before trust dialog
// Full environment variables are applied after trust is established
const envVarsStart = Date.now()
applySafeConfigEnvironmentVariables()
// Apply NODE_EXTRA_CA_CERTS from settings.json to process.env early,
// before any TLS connections. Bun caches the TLS cert store at boot
// via BoringSSL, so this must happen before the first TLS handshake.
applyExtraCACertsFromConfig()
logForDiagnosticsNoPII('info', 'init_safe_env_vars_applied', {
duration_ms: Date.now() - envVarsStart,
})
profileCheckpoint('init_safe_env_vars_applied')
// Make sure things get flushed on exit
setupGracefulShutdown()
profileCheckpoint('init_after_graceful_shutdown')
// Initialize 1P event logging (no security concerns, but deferred to avoid
// loading OpenTelemetry sdk-logs at startup). growthbook.js is already in
// the module cache by this point (firstPartyEventLogger imports it), so the
// second dynamic import adds no load cost.
void Promise.all([
import('../services/analytics/firstPartyEventLogger.js'),
import('../services/analytics/growthbook.js'),
]).then(([fp, gb]) => {
fp.initialize1PEventLogging()
// Rebuild the logger provider if tengu_1p_event_batch_config changes
// mid-session. Change detection (isEqual) is inside the handler so
// unchanged refreshes are no-ops.
gb.onGrowthBookRefresh(() => {
void fp.reinitialize1PEventLoggingIfConfigChanged()
})
})
profileCheckpoint('init_after_1p_event_logging')
// Populate OAuth account info if it is not already cached in config. This is needed since the
// OAuth account info may not be populated when logging in through the VSCode extension.
void populateOAuthAccountInfoIfNeeded()
profileCheckpoint('init_after_oauth_populate')
// Initialize JetBrains IDE detection asynchronously (populates cache for later sync access)
void initJetBrainsDetection()
profileCheckpoint('init_after_jetbrains_detection')
// Detect GitHub repository asynchronously (populates cache for gitDiff PR linking)
void detectCurrentRepository()
// Initialize the loading promise early so that other systems (like plugin hooks)
// can await remote settings loading. The promise includes a timeout to prevent
// deadlocks if loadRemoteManagedSettings() is never called (e.g., Agent SDK tests).
if (isEligibleForRemoteManagedSettings()) {
initializeRemoteManagedSettingsLoadingPromise()
}
if (isPolicyLimitsEligible()) {
initializePolicyLimitsLoadingPromise()
}
profileCheckpoint('init_after_remote_settings_check')
// Record the first start time
recordFirstStartTime()
// Configure global mTLS settings
const mtlsStart = Date.now()
logForDebugging('[init] configureGlobalMTLS starting')
configureGlobalMTLS()
logForDiagnosticsNoPII('info', 'init_mtls_configured', {
duration_ms: Date.now() - mtlsStart,
})
logForDebugging('[init] configureGlobalMTLS complete')
// Configure global HTTP agents (proxy and/or mTLS)
const proxyStart = Date.now()
logForDebugging('[init] configureGlobalAgents starting')
configureGlobalAgents()
logForDiagnosticsNoPII('info', 'init_proxy_configured', {
duration_ms: Date.now() - proxyStart,
})
logForDebugging('[init] configureGlobalAgents complete')
profileCheckpoint('init_network_configured')
// Preconnect to the Anthropic API — overlap TCP+TLS handshake
// (~100-200ms) with the ~100ms of action-handler work before the API
// request. After CA certs + proxy agents are configured so the warmed
// connection uses the right transport. Fire-and-forget; skipped for
// proxy/mTLS/unix/cloud-provider where the SDK's dispatcher wouldn't
// reuse the global pool.
preconnectAnthropicApi()
// CCR upstreamproxy: start the local CONNECT relay so agent subprocesses
// can reach org-configured upstreams with credential injection. Gated on
// CLAUDE_CODE_REMOTE + GrowthBook; fail-open on any error. Lazy import so
// non-CCR startups don't pay the module load. The getUpstreamProxyEnv
// function is registered with subprocessEnv.ts so subprocess spawning can
// inject proxy vars without a static import of the upstreamproxy module.
if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) {
try {
const { initUpstreamProxy, getUpstreamProxyEnv } = await import(
'../upstreamproxy/upstreamproxy.js'
)
const { registerUpstreamProxyEnvFn } = await import(
'../utils/subprocessEnv.js'
)
registerUpstreamProxyEnvFn(getUpstreamProxyEnv)
await initUpstreamProxy()
} catch (err) {
logForDebugging(
`[init] upstreamproxy init failed: ${err instanceof Error ? err.message : String(err)}; continuing without proxy`,
{ level: 'warn' },
)
}
}
// Set up git-bash if relevant
setShellIfWindows()
// Register LSP manager cleanup (initialization happens in main.tsx after --plugin-dir is processed)
registerCleanup(shutdownLspServerManager)
// gh-32730: teams created by subagents (or main agent without
// explicit TeamDelete) were left on disk forever. Register cleanup
// for all teams created this session. Lazy import: swarm code is
// behind feature gate and most sessions never create teams.
registerCleanup(async () => {
const { cleanupSessionTeams } = await import(
'../utils/swarm/teamHelpers.js'
)
await cleanupSessionTeams()
})
// Initialize scratchpad directory if enabled
if (isScratchpadEnabled()) {
const scratchpadStart = Date.now()
await ensureScratchpadDir()
logForDiagnosticsNoPII('info', 'init_scratchpad_created', {
duration_ms: Date.now() - scratchpadStart,
})
}
logForDiagnosticsNoPII('info', 'init_completed', {
duration_ms: Date.now() - initStartTime,
})
profileCheckpoint('init_function_end')
} catch (error) {
if (error instanceof ConfigParseError) {
// Skip the interactive Ink dialog when we can't safely render it.
// The dialog breaks JSON consumers (e.g. desktop marketplace plugin
// manager running `plugin marketplace list --json` in a VM sandbox).
if (getIsNonInteractiveSession()) {
process.stderr.write(
`Configuration error in ${error.filePath}: ${error.message}\n`,
)
gracefulShutdownSync(1)
return
}
// Show the invalid config dialog with the error object and wait for it to complete
return import('../components/InvalidConfigDialog.js').then(m =>
m.showInvalidConfigDialog({ error }),
)
// Dialog itself handles process.exit, so we don't need additional cleanup here
} else {
// For non-config errors, rethrow them
throw error
}
}
})
/**
* Initialize telemetry after trust has been granted.
* For remote-settings-eligible users, waits for settings to load (non-blocking),
* then re-applies env vars (to include remote settings) before initializing telemetry.
* For non-eligible users, initializes telemetry immediately.
* This should only be called once, after the trust dialog has been accepted.
*/
export function initializeTelemetryAfterTrust(): void {
if (isEligibleForRemoteManagedSettings()) {
// For SDK/headless mode with beta tracing, initialize eagerly first
// to ensure the tracer is ready before the first query runs.
// The async path below will still run but doInitializeTelemetry() guards against double init.
if (getIsNonInteractiveSession() && isBetaTracingEnabled()) {
void doInitializeTelemetry().catch(error => {
logForDebugging(
`[3P telemetry] Eager telemetry init failed (beta tracing): ${errorMessage(error)}`,
{ level: 'error' },
)
})
}
logForDebugging(
'[3P telemetry] Waiting for remote managed settings before telemetry init',
)
void waitForRemoteManagedSettingsToLoad()
.then(async () => {
logForDebugging(
'[3P telemetry] Remote managed settings loaded, initializing telemetry',
)
// Re-apply env vars to pick up remote settings before initializing telemetry.
applyConfigEnvironmentVariables()
await doInitializeTelemetry()
})
.catch(error => {
logForDebugging(
`[3P telemetry] Telemetry init failed (remote settings path): ${errorMessage(error)}`,
{ level: 'error' },
)
})
} else {
void doInitializeTelemetry().catch(error => {
logForDebugging(
`[3P telemetry] Telemetry init failed: ${errorMessage(error)}`,
{ level: 'error' },
)
})
}
}
async function doInitializeTelemetry(): Promise<void> {
if (telemetryInitialized) {
// Already initialized, nothing to do
return
}
// Set flag before init to prevent double initialization
telemetryInitialized = true
try {
await setMeterState()
} catch (error) {
// Reset flag on failure so subsequent calls can retry
telemetryInitialized = false
throw error
}
}
async function setMeterState(): Promise<void> {
// Lazy-load instrumentation to defer ~400KB of OpenTelemetry + protobuf
const { initializeTelemetry } = await import(
'../utils/telemetry/instrumentation.js'
)
// Initialize customer OTLP telemetry (metrics, logs, traces)
const meter = await initializeTelemetry()
if (meter) {
// Create factory function for attributed counters
const createAttributedCounter = (
name: string,
options: MetricOptions,
): AttributedCounter => {
const counter = meter?.createCounter(name, options)
return {
add(value: number, additionalAttributes: Attributes = {}) {
// Always fetch fresh telemetry attributes to ensure they're up to date
const currentAttributes = getTelemetryAttributes()
const mergedAttributes = {
...currentAttributes,
...additionalAttributes,
}
counter?.add(value, mergedAttributes)
},
}
}
setMeter(meter, createAttributedCounter)
// Increment session counter here because the startup telemetry path
// runs before this async initialization completes, so the counter
// would be null there.
getSessionCounter()?.add(1)
}
}
|