mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
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
This commit is contained in:
+21
-26
@@ -40,7 +40,7 @@ import {
|
||||
appendBrowserToolArgs,
|
||||
ensureBrowserMcpOrThrow,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
resolveBrowserRuntimeEnv,
|
||||
resolveOptionalBrowserAttachRuntime,
|
||||
syncBrowserMcpToConfigDir,
|
||||
} from './utils/browser';
|
||||
import {
|
||||
@@ -1057,14 +1057,21 @@ async function main(): Promise<void> {
|
||||
// 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<void> {
|
||||
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
|
||||
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
||||
if (
|
||||
browserAttachConfig?.enabled &&
|
||||
browserRuntimeEnv &&
|
||||
inheritedClaudeConfigDir &&
|
||||
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
||||
) {
|
||||
@@ -1301,17 +1308,6 @@ async function main(): Promise<void> {
|
||||
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
if (defaultContinuityInheritance.claudeConfigDir) {
|
||||
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
|
||||
if (
|
||||
browserAttachConfig?.enabled &&
|
||||
browserRuntimeEnv &&
|
||||
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
|
||||
) {
|
||||
throw new Error(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<BrowserAttachRuntimeResolution> {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,6 +21,11 @@ export {
|
||||
getRecommendedBrowserUserDataDir,
|
||||
getBrowserAttachOverride,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
resolveOptionalBrowserAttachRuntime,
|
||||
} from './browser-settings';
|
||||
export type {
|
||||
BrowserAttachRuntimeResolution,
|
||||
EffectiveClaudeBrowserAttachConfig,
|
||||
} from './browser-settings';
|
||||
|
||||
export {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user