From 41bf5075e0ece90401dfa22fe9ffbed797825387 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 16 Apr 2026 22:59:43 -0400 Subject: [PATCH] hotfix(browser): avoid aborting launch on missing managed profile - skip managed Claude browser attach when the default CCS browser profile directory has not been created yet - keep explicit env override flows failing fast so real misconfiguration still surfaces clearly - add regression coverage for default and settings-profile launches --- src/ccs.ts | 47 +++++++++---------- src/cliproxy/executor/index.ts | 24 ++++------ src/utils/browser/browser-settings.ts | 36 ++++++++++++++ src/utils/browser/chrome-reuse.ts | 1 + src/utils/browser/index.ts | 5 ++ .../default-profile-browser-launch.test.ts | 44 +++++++++++++++++ .../settings-profile-browser-launch.test.ts | 44 +++++++++++++++++ 7 files changed, 161 insertions(+), 40 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 3cedddbb..d66ded4a 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -40,7 +40,7 @@ import { appendBrowserToolArgs, ensureBrowserMcpOrThrow, getEffectiveClaudeBrowserAttachConfig, - resolveBrowserRuntimeEnv, + resolveOptionalBrowserAttachRuntime, syncBrowserMcpToConfigDir, } from './utils/browser'; import { @@ -1057,14 +1057,21 @@ async function main(): Promise { // Settings-based profiles (glm, glmt) are third-party providers const imageAnalysisMcpReady = resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true; - let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv']; const browserAttachConfig = resolvedTarget === 'claude' ? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig()) : undefined; + const browserAttachRuntime = + resolvedTarget === 'claude' && browserAttachConfig?.enabled + ? await resolveOptionalBrowserAttachRuntime(browserAttachConfig) + : undefined; + const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv; + if (browserAttachRuntime?.warning) { + console.error(warn(browserAttachRuntime.warning)); + } if (resolvedTarget === 'claude') { ensureWebSearchMcpOrThrow(); - if (browserAttachConfig?.enabled) { + if (browserRuntimeEnv) { ensureBrowserMcpOrThrow(); } } @@ -1091,7 +1098,7 @@ async function main(): Promise { syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir); syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir); if ( - browserAttachConfig?.enabled && + browserRuntimeEnv && inheritedClaudeConfigDir && !syncBrowserMcpToConfigDir(inheritedClaudeConfigDir) ) { @@ -1301,17 +1308,6 @@ async function main(): Promise { // Explicitly inject effective settings env vars so stale ANTHROPIC_* // values from prior sessions cannot leak into the active profile. - if (browserAttachConfig?.enabled) { - browserRuntimeEnv = { - ...(await resolveBrowserRuntimeEnv({ - profileDir: browserAttachConfig.userDataDir, - devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort - ? String(browserAttachConfig.devtoolsPort) - : undefined, - })), - }; - } - const envVars: NodeJS.ProcessEnv = { ...globalEnv, ...settingsEnv, @@ -1471,23 +1467,22 @@ async function main(): Promise { CCS_WEBSEARCH_SKIP: '1', CCS_IMAGE_ANALYSIS_SKIP: '1', }; - let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv']; const browserAttachConfig = resolvedTarget === 'claude' ? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig()) : undefined; + const browserAttachRuntime = + resolvedTarget === 'claude' && browserAttachConfig?.enabled + ? await resolveOptionalBrowserAttachRuntime(browserAttachConfig) + : undefined; + const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv; + if (browserAttachRuntime?.warning) { + console.error(warn(browserAttachRuntime.warning)); + } if (resolvedTarget === 'claude') { - if (browserAttachConfig?.enabled) { + if (browserRuntimeEnv) { ensureBrowserMcpOrThrow(); - browserRuntimeEnv = { - ...(await resolveBrowserRuntimeEnv({ - profileDir: browserAttachConfig.userDataDir, - devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort - ? String(browserAttachConfig.devtoolsPort) - : undefined, - })), - }; Object.assign(envVars, browserRuntimeEnv); } const defaultContinuityInheritance = await resolveProfileContinuityInheritance({ @@ -1505,7 +1500,7 @@ async function main(): Promise { if (defaultContinuityInheritance.claudeConfigDir) { envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir; if ( - browserAttachConfig?.enabled && + browserRuntimeEnv && !syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir) ) { throw new Error( diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index f3d9f1a9..0ec24898 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -67,7 +67,7 @@ import { appendBrowserToolArgs, ensureBrowserMcpOrThrow, getEffectiveClaudeBrowserAttachConfig, - resolveBrowserRuntimeEnv, + resolveOptionalBrowserAttachRuntime, syncBrowserMcpToConfigDir, } from '../../utils/browser'; import { @@ -265,7 +265,14 @@ export async function execClaudeWithCLIProxy( ensureWebSearchMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); const browserAttachConfig = getEffectiveClaudeBrowserAttachConfig(getBrowserConfig()); - if (browserAttachConfig.enabled) { + const browserAttachRuntime = browserAttachConfig.enabled + ? await resolveOptionalBrowserAttachRuntime(browserAttachConfig) + : undefined; + const browserRuntimeEnv = browserAttachRuntime?.runtimeEnv; + if (browserAttachRuntime?.warning) { + console.error(warn(browserAttachRuntime.warning)); + } + if (browserRuntimeEnv) { ensureBrowserMcpOrThrow(); } displayWebSearchStatus(); @@ -1054,7 +1061,7 @@ export async function execClaudeWithCLIProxy( syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir); if ( - browserAttachConfig.enabled && + browserRuntimeEnv && inheritedClaudeConfigDir && !syncBrowserMcpToConfigDir(inheritedClaudeConfigDir) ) { @@ -1157,17 +1164,6 @@ export async function execClaudeWithCLIProxy( } // 11. Build final environment with all proxy chains - const browserRuntimeEnv = browserAttachConfig.enabled - ? { - ...(await resolveBrowserRuntimeEnv({ - profileDir: browserAttachConfig.userDataDir, - devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort - ? String(browserAttachConfig.devtoolsPort) - : undefined, - })), - } - : undefined; - const env = buildClaudeEnvironment({ provider, useRemoteProxy, diff --git a/src/utils/browser/browser-settings.ts b/src/utils/browser/browser-settings.ts index e1a9f6fe..7dfd99e1 100644 --- a/src/utils/browser/browser-settings.ts +++ b/src/utils/browser/browser-settings.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import type { BrowserConfig } from '../../config/unified-config-types'; import { getCcsDir } from '../config-manager'; import { expandPath } from '../helpers'; +import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse'; export type BrowserOverrideSource = 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR'; @@ -18,6 +19,11 @@ export function getRecommendedBrowserUserDataDir(): string { return path.join(getCcsDir(), 'browser', 'chrome-user-data'); } +export interface BrowserAttachRuntimeResolution { + runtimeEnv?: BrowserRuntimeEnv; + warning?: string; +} + export function resolveBrowserUserDataDir(value?: string): string | undefined { return value?.trim() ? expandPath(value) : undefined; } @@ -81,6 +87,36 @@ export function getEffectiveClaudeBrowserAttachConfig( }; } +export async function resolveOptionalBrowserAttachRuntime( + config: EffectiveClaudeBrowserAttachConfig +): Promise { + if (!config.enabled) { + return {}; + } + + try { + return { + runtimeEnv: await resolveBrowserRuntimeEnv({ + profileDir: config.userDataDir, + devtoolsPort: config.hasExplicitDevtoolsPort ? String(config.devtoolsPort) : undefined, + }), + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const usesManagedDefaultDir = + config.source === 'config' && + path.resolve(config.userDataDir) === path.resolve(getRecommendedBrowserUserDataDir()); + + if (usesManagedDefaultDir && message.includes('Chrome profile directory is invalid')) { + return { + warning: `Claude Browser Attach is enabled, but the managed browser profile does not exist yet (${config.userDataDir}). Launching without browser tools. Run \`ccs browser doctor\` to finish setup.`, + }; + } + + throw error; + } +} + function parseDevtoolsPort(value?: string): number | undefined { if (!value?.trim() || !/^\d+$/.test(value.trim())) { return undefined; diff --git a/src/utils/browser/chrome-reuse.ts b/src/utils/browser/chrome-reuse.ts index 976c39ef..14a20ba7 100644 --- a/src/utils/browser/chrome-reuse.ts +++ b/src/utils/browser/chrome-reuse.ts @@ -9,6 +9,7 @@ export interface BrowserReuseOptions { } export interface BrowserRuntimeEnv { + [key: string]: string; CCS_BROWSER_USER_DATA_DIR: string; CCS_BROWSER_DEVTOOLS_HOST: string; CCS_BROWSER_DEVTOOLS_PORT: string; diff --git a/src/utils/browser/index.ts b/src/utils/browser/index.ts index 22f92251..502534a0 100644 --- a/src/utils/browser/index.ts +++ b/src/utils/browser/index.ts @@ -21,6 +21,11 @@ export { getRecommendedBrowserUserDataDir, getBrowserAttachOverride, getEffectiveClaudeBrowserAttachConfig, + resolveOptionalBrowserAttachRuntime, +} from './browser-settings'; +export type { + BrowserAttachRuntimeResolution, + EffectiveClaudeBrowserAttachConfig, } from './browser-settings'; export { diff --git a/tests/unit/targets/default-profile-browser-launch.test.ts b/tests/unit/targets/default-profile-browser-launch.test.ts index 93dd4552..5545f366 100644 --- a/tests/unit/targets/default-profile-browser-launch.test.ts +++ b/tests/unit/targets/default-profile-browser-launch.test.ts @@ -247,6 +247,50 @@ server.listen(0, '127.0.0.1', () => { expect(launchedEnv).toContain('wsUrl=ws://127.0.0.1/devtools/browser/default-target'); }); + it('skips managed browser attach when the default CCS browser profile directory is missing', () => { + if (process.platform === 'win32') return; + + const originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tmpHome; + + try { + mutateUnifiedConfig((config) => { + config.browser = { + claude: { + enabled: true, + user_data_dir: '', + devtools_port: 9222, + }, + codex: { + enabled: true, + }, + }; + }); + + const result = runCcs(['default', 'smoke'], { + ...baseEnv, + }); + + expect(result.status).toBe(0); + expect(result.stderr).toContain('Launching without browser tools'); + expect(result.stderr).toContain('ccs browser doctor'); + + const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8'); + expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET); + + const launchedEnv = fs.readFileSync(claudeEnvLogPath, 'utf8'); + expect(launchedEnv).toContain('userDataDir='); + expect(launchedEnv).not.toContain('.ccs/browser/chrome-user-data'); + expect(launchedEnv).not.toContain('ws://127.0.0.1/devtools/browser/'); + } finally { + if (originalCcsHome !== undefined) { + process.env.CCS_HOME = originalCcsHome; + } else { + delete process.env.CCS_HOME; + } + } + }); + it('uses config-backed browser attach settings when env overrides are absent', async () => { if (process.platform === 'win32') return; diff --git a/tests/unit/targets/settings-profile-browser-launch.test.ts b/tests/unit/targets/settings-profile-browser-launch.test.ts index b94846fc..653271b1 100644 --- a/tests/unit/targets/settings-profile-browser-launch.test.ts +++ b/tests/unit/targets/settings-profile-browser-launch.test.ts @@ -198,6 +198,50 @@ server.listen(0, '127.0.0.1', () => { expect(launchedEnv).toContain('wsUrl=ws://127.0.0.1/devtools/browser/browser-target'); }); + it('skips managed browser attach for settings-profile launches when the default CCS browser profile directory is missing', () => { + if (process.platform === 'win32') return; + + const originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tmpHome; + + try { + mutateUnifiedConfig((config) => { + config.browser = { + claude: { + enabled: true, + user_data_dir: '', + devtools_port: 9222, + }, + codex: { + enabled: true, + }, + }; + }); + + const result = runCcs(['glm', 'smoke'], { + ...baseEnv, + }); + + expect(result.status).toBe(0); + expect(result.stderr).toContain('Launching without browser tools'); + expect(result.stderr).toContain('ccs browser doctor'); + + const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8'); + expect(launchedArgs).not.toContain(BROWSER_PROMPT_SNIPPET); + + const launchedEnv = fs.readFileSync(claudeEnvLogPath, 'utf8'); + expect(launchedEnv).toContain('userDataDir='); + expect(launchedEnv).not.toContain('.ccs/browser/chrome-user-data'); + expect(launchedEnv).not.toContain('ws://127.0.0.1/devtools/browser/'); + } finally { + if (originalCcsHome !== undefined) { + process.env.CCS_HOME = originalCcsHome; + } else { + delete process.env.CCS_HOME; + } + } + }); + it('uses config-backed browser attach settings for settings-profile launches', async () => { if (process.platform === 'win32') return;