mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +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,
|
appendBrowserToolArgs,
|
||||||
ensureBrowserMcpOrThrow,
|
ensureBrowserMcpOrThrow,
|
||||||
getEffectiveClaudeBrowserAttachConfig,
|
getEffectiveClaudeBrowserAttachConfig,
|
||||||
resolveBrowserRuntimeEnv,
|
resolveOptionalBrowserAttachRuntime,
|
||||||
syncBrowserMcpToConfigDir,
|
syncBrowserMcpToConfigDir,
|
||||||
} from './utils/browser';
|
} from './utils/browser';
|
||||||
import {
|
import {
|
||||||
@@ -1057,14 +1057,21 @@ async function main(): Promise<void> {
|
|||||||
// Settings-based profiles (glm, glmt) are third-party providers
|
// Settings-based profiles (glm, glmt) are third-party providers
|
||||||
const imageAnalysisMcpReady =
|
const imageAnalysisMcpReady =
|
||||||
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
|
resolvedTarget === 'claude' ? ensureImageAnalysisMcpOrThrow() : true;
|
||||||
let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv'];
|
|
||||||
const browserAttachConfig =
|
const browserAttachConfig =
|
||||||
resolvedTarget === 'claude'
|
resolvedTarget === 'claude'
|
||||||
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
||||||
: undefined;
|
: 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 (resolvedTarget === 'claude') {
|
||||||
ensureWebSearchMcpOrThrow();
|
ensureWebSearchMcpOrThrow();
|
||||||
if (browserAttachConfig?.enabled) {
|
if (browserRuntimeEnv) {
|
||||||
ensureBrowserMcpOrThrow();
|
ensureBrowserMcpOrThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1091,7 +1098,7 @@ async function main(): Promise<void> {
|
|||||||
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
|
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
|
||||||
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
||||||
if (
|
if (
|
||||||
browserAttachConfig?.enabled &&
|
browserRuntimeEnv &&
|
||||||
inheritedClaudeConfigDir &&
|
inheritedClaudeConfigDir &&
|
||||||
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
||||||
) {
|
) {
|
||||||
@@ -1301,17 +1308,6 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Explicitly inject effective settings env vars so stale ANTHROPIC_*
|
// Explicitly inject effective settings env vars so stale ANTHROPIC_*
|
||||||
// values from prior sessions cannot leak into the active profile.
|
// 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 = {
|
const envVars: NodeJS.ProcessEnv = {
|
||||||
...globalEnv,
|
...globalEnv,
|
||||||
...settingsEnv,
|
...settingsEnv,
|
||||||
@@ -1471,23 +1467,22 @@ async function main(): Promise<void> {
|
|||||||
CCS_WEBSEARCH_SKIP: '1',
|
CCS_WEBSEARCH_SKIP: '1',
|
||||||
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
||||||
};
|
};
|
||||||
let browserRuntimeEnv: TargetCredentials['browserRuntimeEnv'];
|
|
||||||
const browserAttachConfig =
|
const browserAttachConfig =
|
||||||
resolvedTarget === 'claude'
|
resolvedTarget === 'claude'
|
||||||
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
? getEffectiveClaudeBrowserAttachConfig(getBrowserConfig())
|
||||||
: undefined;
|
: 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 (resolvedTarget === 'claude') {
|
||||||
if (browserAttachConfig?.enabled) {
|
if (browserRuntimeEnv) {
|
||||||
ensureBrowserMcpOrThrow();
|
ensureBrowserMcpOrThrow();
|
||||||
browserRuntimeEnv = {
|
|
||||||
...(await resolveBrowserRuntimeEnv({
|
|
||||||
profileDir: browserAttachConfig.userDataDir,
|
|
||||||
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
|
|
||||||
? String(browserAttachConfig.devtoolsPort)
|
|
||||||
: undefined,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
Object.assign(envVars, browserRuntimeEnv);
|
Object.assign(envVars, browserRuntimeEnv);
|
||||||
}
|
}
|
||||||
const defaultContinuityInheritance = await resolveProfileContinuityInheritance({
|
const defaultContinuityInheritance = await resolveProfileContinuityInheritance({
|
||||||
@@ -1505,7 +1500,7 @@ async function main(): Promise<void> {
|
|||||||
if (defaultContinuityInheritance.claudeConfigDir) {
|
if (defaultContinuityInheritance.claudeConfigDir) {
|
||||||
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
|
envVars.CLAUDE_CONFIG_DIR = defaultContinuityInheritance.claudeConfigDir;
|
||||||
if (
|
if (
|
||||||
browserAttachConfig?.enabled &&
|
browserRuntimeEnv &&
|
||||||
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
|
!syncBrowserMcpToConfigDir(defaultContinuityInheritance.claudeConfigDir)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ import {
|
|||||||
appendBrowserToolArgs,
|
appendBrowserToolArgs,
|
||||||
ensureBrowserMcpOrThrow,
|
ensureBrowserMcpOrThrow,
|
||||||
getEffectiveClaudeBrowserAttachConfig,
|
getEffectiveClaudeBrowserAttachConfig,
|
||||||
resolveBrowserRuntimeEnv,
|
resolveOptionalBrowserAttachRuntime,
|
||||||
syncBrowserMcpToConfigDir,
|
syncBrowserMcpToConfigDir,
|
||||||
} from '../../utils/browser';
|
} from '../../utils/browser';
|
||||||
import {
|
import {
|
||||||
@@ -265,7 +265,14 @@ export async function execClaudeWithCLIProxy(
|
|||||||
ensureWebSearchMcpOrThrow();
|
ensureWebSearchMcpOrThrow();
|
||||||
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
|
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
|
||||||
const browserAttachConfig = getEffectiveClaudeBrowserAttachConfig(getBrowserConfig());
|
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();
|
ensureBrowserMcpOrThrow();
|
||||||
}
|
}
|
||||||
displayWebSearchStatus();
|
displayWebSearchStatus();
|
||||||
@@ -1054,7 +1061,7 @@ export async function execClaudeWithCLIProxy(
|
|||||||
|
|
||||||
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
||||||
if (
|
if (
|
||||||
browserAttachConfig.enabled &&
|
browserRuntimeEnv &&
|
||||||
inheritedClaudeConfigDir &&
|
inheritedClaudeConfigDir &&
|
||||||
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
!syncBrowserMcpToConfigDir(inheritedClaudeConfigDir)
|
||||||
) {
|
) {
|
||||||
@@ -1157,17 +1164,6 @@ export async function execClaudeWithCLIProxy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 11. Build final environment with all proxy chains
|
// 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({
|
const env = buildClaudeEnvironment({
|
||||||
provider,
|
provider,
|
||||||
useRemoteProxy,
|
useRemoteProxy,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as path from 'path';
|
|||||||
import type { BrowserConfig } from '../../config/unified-config-types';
|
import type { BrowserConfig } from '../../config/unified-config-types';
|
||||||
import { getCcsDir } from '../config-manager';
|
import { getCcsDir } from '../config-manager';
|
||||||
import { expandPath } from '../helpers';
|
import { expandPath } from '../helpers';
|
||||||
|
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
||||||
|
|
||||||
export type BrowserOverrideSource = 'CCS_BROWSER_USER_DATA_DIR' | 'CCS_BROWSER_PROFILE_DIR';
|
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');
|
return path.join(getCcsDir(), 'browser', 'chrome-user-data');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BrowserAttachRuntimeResolution {
|
||||||
|
runtimeEnv?: BrowserRuntimeEnv;
|
||||||
|
warning?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveBrowserUserDataDir(value?: string): string | undefined {
|
export function resolveBrowserUserDataDir(value?: string): string | undefined {
|
||||||
return value?.trim() ? expandPath(value) : 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 {
|
function parseDevtoolsPort(value?: string): number | undefined {
|
||||||
if (!value?.trim() || !/^\d+$/.test(value.trim())) {
|
if (!value?.trim() || !/^\d+$/.test(value.trim())) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface BrowserReuseOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BrowserRuntimeEnv {
|
export interface BrowserRuntimeEnv {
|
||||||
|
[key: string]: string;
|
||||||
CCS_BROWSER_USER_DATA_DIR: string;
|
CCS_BROWSER_USER_DATA_DIR: string;
|
||||||
CCS_BROWSER_DEVTOOLS_HOST: string;
|
CCS_BROWSER_DEVTOOLS_HOST: string;
|
||||||
CCS_BROWSER_DEVTOOLS_PORT: string;
|
CCS_BROWSER_DEVTOOLS_PORT: string;
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export {
|
|||||||
getRecommendedBrowserUserDataDir,
|
getRecommendedBrowserUserDataDir,
|
||||||
getBrowserAttachOverride,
|
getBrowserAttachOverride,
|
||||||
getEffectiveClaudeBrowserAttachConfig,
|
getEffectiveClaudeBrowserAttachConfig,
|
||||||
|
resolveOptionalBrowserAttachRuntime,
|
||||||
|
} from './browser-settings';
|
||||||
|
export type {
|
||||||
|
BrowserAttachRuntimeResolution,
|
||||||
|
EffectiveClaudeBrowserAttachConfig,
|
||||||
} from './browser-settings';
|
} from './browser-settings';
|
||||||
|
|
||||||
export {
|
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');
|
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 () => {
|
it('uses config-backed browser attach settings when env overrides are absent', async () => {
|
||||||
if (process.platform === 'win32') return;
|
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');
|
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 () => {
|
it('uses config-backed browser attach settings for settings-profile launches', async () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user