fix(websearch): honor disabled steering per launch

This commit is contained in:
Tam Nhu Tran committed 2026-07-29 13:22:21 -04:00
1 parent 09757b418e
commit abc3002e89
18 files changed
+165 -76

No files matched your search

+10 -2
View File
@@ -15,6 +15,7 @@ import {
appendThirdPartyWebSearchToolArgs, appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext, createWebSearchTraceContext,
} from '../../utils/websearch-manager'; } from '../../utils/websearch-manager';
import type { WebSearchLaunchState } from '../../utils/websearch-manager';
import { appendThirdPartyImageAnalysisToolArgs } from '../../utils/image-analysis'; import { appendThirdPartyImageAnalysisToolArgs } from '../../utils/image-analysis';
import { appendBrowserToolArgs } from '../../utils/browser'; import { appendBrowserToolArgs } from '../../utils/browser';
import { getDefaultAccount } from '../accounts/account-manager'; import { getDefaultAccount } from '../accounts/account-manager';
@@ -51,6 +52,8 @@ export interface ClaudeLaunchContext {
sessionId: string | undefined; sessionId: string | undefined;
/** Whether image analysis MCP is ready */ /** Whether image analysis MCP is ready */
imageAnalysisMcpReady: boolean; imageAnalysisMcpReady: boolean;
/** WebSearch state captured when launch provisioning ran */
webSearchLaunch: WebSearchLaunchState;
/** Browser runtime environment variables (undefined if browser not active) */ /** Browser runtime environment variables (undefined if browser not active) */
browserRuntimeEnv: NodeJS.ProcessEnv | undefined; browserRuntimeEnv: NodeJS.ProcessEnv | undefined;
/** Inherited Claude config dir for continuity */ /** Inherited Claude config dir for continuity */
@@ -82,6 +85,7 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
skipLocalAuth, skipLocalAuth,
sessionId, sessionId,
imageAnalysisMcpReady, imageAnalysisMcpReady,
webSearchLaunch,
browserRuntimeEnv, browserRuntimeEnv,
inheritedClaudeConfigDir, inheritedClaudeConfigDir,
codexReasoningProxy, codexReasoningProxy,
@@ -124,8 +128,12 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
? appendBrowserToolArgs(imageAnalysisArgs) ? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs; : imageAnalysisArgs;
const launchArgs = isSubcommand const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(browserArgs) ? appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled)
: ['--settings', settingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)]; : [
'--settings',
settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
// Inject web search trace context into env // Inject web search trace context into env
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
+3 -2
View File
@@ -19,7 +19,6 @@ import {
import { applyExtendedContextConfig } from '../config/extended-context-config'; import { applyExtendedContextConfig } from '../config/extended-context-config';
import { CLIProxyProvider } from '../types'; import { CLIProxyProvider } from '../types';
import { CompositeTierConfig } from '../../config/unified-config-types'; import { CompositeTierConfig } from '../../config/unified-config-types';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import { import {
applyImageAnalysisRuntimeOverrides, applyImageAnalysisRuntimeOverrides,
getImageAnalysisHookEnv, getImageAnalysisHookEnv,
@@ -78,6 +77,8 @@ export interface ProxyChainConfig {
claudeConfigDir?: string; claudeConfigDir?: string;
/** Execution-aware image analysis env prepared by the caller */ /** Execution-aware image analysis env prepared by the caller */
imageAnalysisEnv?: Record<string, string>; imageAnalysisEnv?: Record<string, string>;
/** WebSearch hook env captured from the launch configuration snapshot. */
webSearchEnv?: Record<string, string>;
/** Optional browser runtime env for Claude browser MCP reuse. */ /** Optional browser runtime env for Claude browser MCP reuse. */
browserRuntimeEnv?: Record<string, string>; browserRuntimeEnv?: Record<string, string>;
} }
@@ -246,6 +247,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
compositeDefaultTier, compositeDefaultTier,
claudeConfigDir, claudeConfigDir,
imageAnalysisEnv: resolvedImageAnalysisEnv, imageAnalysisEnv: resolvedImageAnalysisEnv,
webSearchEnv = {},
browserRuntimeEnv, browserRuntimeEnv,
} = config; } = config;
@@ -386,7 +388,6 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
}; };
// Add hook environment variables // Add hook environment variables
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisEnv = resolvedImageAnalysisEnv ?? getImageAnalysisHookEnv(provider); const imageAnalysisEnv = resolvedImageAnalysisEnv ?? getImageAnalysisHookEnv(provider);
// Merge all environment variables (filter undefined values) // Merge all environment variables (filter undefined values)
+8 -6
View File
@@ -52,7 +52,7 @@ import {
resolveCliproxyImageAnalysisEnv, resolveCliproxyImageAnalysisEnv,
} from './env-resolver'; } from './env-resolver';
import { checkOrJoinProxy, registerProxySession } from './session-bridge'; import { checkOrJoinProxy, registerProxySession } from './session-bridge';
import { getWebSearchHookEnv } from '../../utils/websearch-manager'; import { resolveWebSearchLaunchState } from '../../utils/websearch-manager';
import { import {
handleLogout, handleLogout,
handleImport, handleImport,
@@ -276,10 +276,9 @@ export async function execClaudeWithCLIProxy(
await handleImport(authCtx); await handleImport(authCtx);
// Setup first-class CCS WebSearch runtime for non-strict user launches. // Setup first-class CCS WebSearch runtime for non-strict user launches.
const shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch(); const webSearchLaunch = resolveWebSearchLaunchState();
if (shouldDisplayWebSearchStatus) { ensureWebSearchMcpForLaunch(webSearchLaunch.config);
displayWebSearchStatus(); displayWebSearchStatus(webSearchLaunch.config);
}
// 3. Ensure OAuth completed (if provider requires it) // 3. Ensure OAuth completed (if provider requires it)
const remoteAuthToken = proxyConfig.authToken?.trim(); const remoteAuthToken = proxyConfig.authToken?.trim();
@@ -483,6 +482,7 @@ export async function execClaudeWithCLIProxy(
compositeDefaultTier: cfg.compositeDefaultTier, compositeDefaultTier: cfg.compositeDefaultTier,
claudeConfigDir: inheritedClaudeConfigDir, claudeConfigDir: inheritedClaudeConfigDir,
imageAnalysisEnv, imageAnalysisEnv,
webSearchEnv: webSearchLaunch.hookEnv,
}); });
// 9b. Build env-dependent proxy chain (tool-sanitization + codex-reasoning) // 9b. Build env-dependent proxy chain (tool-sanitization + codex-reasoning)
@@ -527,6 +527,7 @@ export async function execClaudeWithCLIProxy(
compositeDefaultTier: cfg.compositeDefaultTier, compositeDefaultTier: cfg.compositeDefaultTier,
claudeConfigDir: inheritedClaudeConfigDir, claudeConfigDir: inheritedClaudeConfigDir,
imageAnalysisEnv, imageAnalysisEnv,
webSearchEnv: webSearchLaunch.hookEnv,
browserRuntimeEnv, browserRuntimeEnv,
}); });
@@ -542,7 +543,7 @@ export async function execClaudeWithCLIProxy(
); );
} }
const webSearchEnv = getWebSearchHookEnv(); const webSearchEnv = webSearchLaunch.hookEnv;
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
logger.info('browser-env-keys', 'CCS_BROWSER_* keys in environment', { logger.info('browser-env-keys', 'CCS_BROWSER_* keys in environment', {
keys: Object.keys(env) keys: Object.keys(env)
@@ -580,6 +581,7 @@ export async function execClaudeWithCLIProxy(
skipLocalAuth, skipLocalAuth,
sessionId, sessionId,
imageAnalysisMcpReady, imageAnalysisMcpReady,
webSearchLaunch,
browserRuntimeEnv, browserRuntimeEnv,
inheritedClaudeConfigDir, inheritedClaudeConfigDir,
codexReasoningProxy, codexReasoningProxy,
+20 -7
View File
@@ -18,11 +18,11 @@ import { normalizeCopilotConfigWithWarnings } from './copilot-model-normalizer';
import { CopilotStatus } from './types'; import { CopilotStatus } from './types';
import { fail, info, ok, warn } from '../utils/ui'; import { fail, info, ok, warn } from '../utils/ui';
import { import {
getWebSearchHookEnv,
appendThirdPartyWebSearchToolArgs, appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext, createWebSearchTraceContext,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager'; } from '../utils/websearch-manager';
import type { WebSearchLaunchState } from '../utils/websearch-manager';
import { import {
appendThirdPartyImageAnalysisToolArgs, appendThirdPartyImageAnalysisToolArgs,
ensureImageAnalysisMcpOrThrow, ensureImageAnalysisMcpOrThrow,
@@ -71,6 +71,17 @@ export async function getCopilotStatus(config: CopilotConfig): Promise<CopilotSt
}; };
} }
export function buildCopilotClaudeLaunchArgs(
claudeArgs: string[],
imageAnalysisMcpReady: boolean,
webSearchEnabled: boolean
): string[] {
const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(claudeArgs)
: claudeArgs;
return appendThirdPartyWebSearchToolArgs(imageAnalysisArgs, webSearchEnabled);
}
/** /**
* Generate environment variables for Claude Code to use copilot-api. * Generate environment variables for Claude Code to use copilot-api.
* Uses model mapping for opus/sonnet/haiku tiers if configured. * Uses model mapping for opus/sonnet/haiku tiers if configured.
@@ -185,7 +196,8 @@ export async function executeCopilotProfile(
config: CopilotConfig, config: CopilotConfig,
claudeArgs: string[], claudeArgs: string[],
claudeConfigDir?: string, claudeConfigDir?: string,
claudeCliPath: string = 'claude' claudeCliPath: string = 'claude',
webSearchLaunch?: WebSearchLaunchState
): Promise<number> { ): Promise<number> {
const { config: normalizedConfig, warnings } = normalizeCopilotConfigWithWarnings(config); const { config: normalizedConfig, warnings } = normalizeCopilotConfigWithWarnings(config);
@@ -276,7 +288,7 @@ export async function executeCopilotProfile(
syncImageAnalysisMcpToConfigDir(claudeConfigDir); syncImageAnalysisMcpToConfigDir(claudeConfigDir);
// Merge with current environment (global env first, copilot overrides, then hook env vars) // Merge with current environment (global env first, copilot overrides, then hook env vars)
const webSearchEnv = getWebSearchHookEnv(); const webSearchEnv = webSearchLaunch?.hookEnv ?? {};
const imageAnalysisResolution = await resolveCopilotImageAnalysisEnv(); const imageAnalysisResolution = await resolveCopilotImageAnalysisEnv();
const imageAnalysisProvisioningFailed = const imageAnalysisProvisioningFailed =
!imageAnalysisMcpReady && imageAnalysisResolution.env.CCS_IMAGE_ANALYSIS_ENABLED === '1'; !imageAnalysisMcpReady && imageAnalysisResolution.env.CCS_IMAGE_ANALYSIS_ENABLED === '1';
@@ -305,10 +317,11 @@ export async function executeCopilotProfile(
// Spawn Claude CLI // Spawn Claude CLI
const spawnStartedAt = Date.now(); const spawnStartedAt = Date.now();
return new Promise((resolve) => { return new Promise((resolve) => {
const imageAnalysisArgs = imageAnalysisMcpReady const launchArgs = buildCopilotClaudeLaunchArgs(
? appendThirdPartyImageAnalysisToolArgs(claudeArgs) claudeArgs,
: claudeArgs; imageAnalysisMcpReady,
const launchArgs = appendThirdPartyWebSearchToolArgs(imageAnalysisArgs); webSearchLaunch?.enabled ?? true
);
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
launcher: 'copilot.executor', launcher: 'copilot.executor',
args: launchArgs, args: launchArgs,
+12 -4
View File
@@ -9,9 +9,9 @@ import { fail, info, ok } from '../utils/ui';
import { import {
appendThirdPartyWebSearchToolArgs, appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext, createWebSearchTraceContext,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager'; } from '../utils/websearch-manager';
import type { WebSearchLaunchState } from '../utils/websearch-manager';
import { getImageAnalysisHookEnv, resolveImageAnalysisRuntimeStatus } from '../utils/hooks'; import { getImageAnalysisHookEnv, resolveImageAnalysisRuntimeStatus } from '../utils/hooks';
import { stripClaudeCodeEnv } from '../utils/shell-executor'; import { stripClaudeCodeEnv } from '../utils/shell-executor';
import { checkAuthStatus } from './cursor-auth'; import { checkAuthStatus } from './cursor-auth';
@@ -106,11 +106,19 @@ export async function resolveCursorImageAnalysisEnv(
return { env, warning: null }; return { env, warning: null };
} }
export function buildCursorClaudeLaunchArgs(
claudeArgs: string[],
webSearchEnabled: boolean
): string[] {
return appendThirdPartyWebSearchToolArgs(claudeArgs, webSearchEnabled);
}
export async function executeCursorProfile( export async function executeCursorProfile(
config: CursorConfig, config: CursorConfig,
claudeArgs: string[], claudeArgs: string[],
claudeConfigDir?: string, claudeConfigDir?: string,
claudeCliPath = 'claude' claudeCliPath = 'claude',
webSearchLaunch?: WebSearchLaunchState
): Promise<number> { ): Promise<number> {
if (!config.enabled) { if (!config.enabled) {
process.stderr.write(fail('Cursor integration is not enabled.') + '\n'); process.stderr.write(fail('Cursor integration is not enabled.') + '\n');
@@ -163,7 +171,7 @@ export async function executeCursorProfile(
const cursorEnv = generateCursorEnv(config, daemonToken, claudeConfigDir); const cursorEnv = generateCursorEnv(config, daemonToken, claudeConfigDir);
const globalEnvConfig = getGlobalEnvConfig(); const globalEnvConfig = getGlobalEnvConfig();
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
const webSearchEnv = getWebSearchHookEnv(); const webSearchEnv = webSearchLaunch?.hookEnv ?? {};
const { env: imageAnalysisEnv, warning: imageAnalysisWarning } = const { env: imageAnalysisEnv, warning: imageAnalysisWarning } =
await resolveCursorImageAnalysisEnv(); await resolveCursorImageAnalysisEnv();
const env = stripClaudeCodeEnv({ const env = stripClaudeCodeEnv({
@@ -184,7 +192,7 @@ export async function executeCursorProfile(
syncWebSearchMcpToConfigDir(claudeConfigDir); syncWebSearchMcpToConfigDir(claudeConfigDir);
return new Promise((resolve) => { return new Promise((resolve) => {
const launchArgs = appendThirdPartyWebSearchToolArgs(claudeArgs); const launchArgs = buildCursorClaudeLaunchArgs(claudeArgs, webSearchLaunch?.enabled ?? true);
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
launcher: 'cursor.executor', launcher: 'cursor.executor',
args: launchArgs, args: launchArgs,
+8 -4
View File
@@ -55,8 +55,8 @@ import {
appendWebSearchTrace, appendWebSearchTrace,
createWebSearchTraceContext, createWebSearchTraceContext,
ensureWebSearchMcpForLaunch, ensureWebSearchMcpForLaunch,
getWebSearchHookEnv,
readWebSearchTraceRecords, readWebSearchTraceRecords,
resolveWebSearchLaunchState,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager'; } from '../utils/websearch-manager';
import { getCcsDir, getGlobalEnvConfig, loadSettings } from '../config/config-loader-facade'; import { getCcsDir, getGlobalEnvConfig, loadSettings } from '../config/config-loader-facade';
@@ -159,7 +159,8 @@ export class HeadlessExecutor {
); );
} }
ensureWebSearchMcpForLaunch(); const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir); syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir); syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
@@ -257,6 +258,7 @@ export class HeadlessExecutor {
let runtimeEnvVars: NodeJS.ProcessEnv = { let runtimeEnvVars: NodeJS.ProcessEnv = {
...stripAnthropicRoutingEnv({ ...globalEnv, ...settingsEnv }, settingsEnv), ...stripAnthropicRoutingEnv({ ...globalEnv, ...settingsEnv }, settingsEnv),
...webSearchLaunch.hookEnv,
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}), ...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
CCS_PROFILE_TYPE: 'settings', CCS_PROFILE_TYPE: 'settings',
CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1', CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1',
@@ -380,7 +382,10 @@ export class HeadlessExecutor {
const imageAnalysisArgs = imageAnalysisMcpReady const imageAnalysisArgs = imageAnalysisMcpReady
? appendThirdPartyImageAnalysisToolArgs(args) ? appendThirdPartyImageAnalysisToolArgs(args)
: args; : args;
const launchArgs = appendThirdPartyWebSearchToolArgs(imageAnalysisArgs); const launchArgs = appendThirdPartyWebSearchToolArgs(
imageAnalysisArgs,
webSearchLaunch.enabled
);
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
launcher: 'delegation.headless-executor', launcher: 'delegation.headless-executor',
args: launchArgs, args: launchArgs,
@@ -467,7 +472,6 @@ export class HeadlessExecutor {
const cleanEnv = stripClaudeCodeEnv({ const cleanEnv = stripClaudeCodeEnv({
...stripAnthropicRoutingEnv(process.env), ...stripAnthropicRoutingEnv(process.env),
...getClaudeLaunchEnvOverrides(), ...getClaudeLaunchEnvOverrides(),
...getWebSearchHookEnv(),
...runtimeEnvVars, ...runtimeEnvVars,
...imageAnalysisEnv, ...imageAnalysisEnv,
...traceEnv, ...traceEnv,
+8 -3
View File
@@ -5,7 +5,10 @@
*/ */
import { fail, info } from '../../utils/ui'; import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpForLaunch } from '../../utils/websearch-manager'; import {
ensureWebSearchMcpForLaunch,
resolveWebSearchLaunchState,
} from '../../utils/websearch-manager';
import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis'; import { ensureImageAnalysisMcpOrThrow } from '../../utils/image-analysis';
import { import {
ensureProfileHooks as ensureImageAnalyzerHooks, ensureProfileHooks as ensureImageAnalyzerHooks,
@@ -23,7 +26,8 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise<void>
resolveProfileContinuityInheritance, resolveProfileContinuityInheritance,
} = ctx; } = ctx;
ensureWebSearchMcpForLaunch(); const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow();
if (resolvedTarget === 'claude') { if (resolvedTarget === 'claude') {
if (imageAnalysisMcpReady) { if (imageAnalysisMcpReady) {
@@ -60,7 +64,8 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise<void>
copilotConfig, copilotConfig,
remainingArgs, remainingArgs,
continuityInheritance.claudeConfigDir, continuityInheritance.claudeConfigDir,
claudeCli claudeCli,
webSearchLaunch
); );
process.exit(exitCode); process.exit(exitCode);
} }
+8 -3
View File
@@ -5,7 +5,10 @@
*/ */
import { fail, info } from '../../utils/ui'; import { fail, info } from '../../utils/ui';
import { ensureWebSearchMcpForLaunch } from '../../utils/websearch-manager'; import {
ensureWebSearchMcpForLaunch,
resolveWebSearchLaunchState,
} from '../../utils/websearch-manager';
import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector'; import { ensureProfileHooks as ensureImageAnalyzerHooks } from '../../utils/hooks/image-analyzer-profile-hook-injector';
import { installImageAnalyzerHook } from '../../utils/hooks'; import { installImageAnalyzerHook } from '../../utils/hooks';
import type { ProfileDispatchContext } from '../dispatcher-context'; import type { ProfileDispatchContext } from '../dispatcher-context';
@@ -19,7 +22,8 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise<void>
resolveProfileContinuityInheritance, resolveProfileContinuityInheritance,
} = ctx; } = ctx;
ensureWebSearchMcpForLaunch(); const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
installImageAnalyzerHook(); installImageAnalyzerHook();
ensureImageAnalyzerHooks({ ensureImageAnalyzerHooks({
profileName: profileInfo.name, profileName: profileInfo.name,
@@ -48,7 +52,8 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise<void>
cursorConfig, cursorConfig,
remainingArgs, remainingArgs,
continuityInheritance.claudeConfigDir, continuityInheritance.claudeConfigDir,
claudeCli claudeCli,
webSearchLaunch
); );
process.exit(exitCode); process.exit(exitCode);
} }
+13 -11
View File
@@ -16,10 +16,10 @@ import {
import { import {
ensureWebSearchMcpForLaunch, ensureWebSearchMcpForLaunch,
displayWebSearchStatus, displayWebSearchStatus,
getWebSearchHookEnv,
syncWebSearchMcpToConfigDir, syncWebSearchMcpToConfigDir,
appendThirdPartyWebSearchToolArgs, appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext, createWebSearchTraceContext,
resolveWebSearchLaunchState,
} from '../../utils/websearch-manager'; } from '../../utils/websearch-manager';
import { import {
ensureImageAnalysisMcpOrThrow, ensureImageAnalysisMcpOrThrow,
@@ -94,18 +94,16 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
if (browserAttachRuntime?.warning) { if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`); process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
} }
let shouldDisplayWebSearchStatus = true; const webSearchLaunch = resolveWebSearchLaunchState();
if (resolvedTarget === 'claude') { if (resolvedTarget === 'claude') {
shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch(); ensureWebSearchMcpForLaunch(webSearchLaunch.config);
if (browserRuntimeEnv) { if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow(); ensureBrowserMcpOrThrow();
} }
} }
// Display WebSearch status (single line, equilibrium UX) // Display WebSearch status (single line, equilibrium UX)
if (shouldDisplayWebSearchStatus) { displayWebSearchStatus(webSearchLaunch.config);
displayWebSearchStatus();
}
const continuityInheritance = const continuityInheritance =
resolvedTarget === 'claude' resolvedTarget === 'claude'
@@ -275,7 +273,7 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
targetRemainingArgs, targetRemainingArgs,
}); });
const webSearchEnv = getWebSearchHookEnv(); const webSearchEnv = webSearchLaunch.hookEnv;
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig(); const globalEnvConfig = getGlobalEnvConfig();
@@ -395,11 +393,11 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
? stripClaudeSubcommandSessionArgs(browserArgs) ? stripClaudeSubcommandSessionArgs(browserArgs)
: browserArgs; : browserArgs;
const launchArgs = isSubcommand const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(subcommandArgs) ? appendThirdPartyWebSearchToolArgs(subcommandArgs, webSearchLaunch.enabled)
: [ : [
'--settings', '--settings',
launchSettings.settingsPath, launchSettings.settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs), ...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
]; ];
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile.proxy', launcher: 'ccs.settings-profile.proxy',
@@ -418,8 +416,12 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
const isSubcommand = isClaudeSubcommandInvocation(browserArgs); const isSubcommand = isClaudeSubcommandInvocation(browserArgs);
const subcommandArgs = isSubcommand ? stripClaudeSubcommandSessionArgs(browserArgs) : browserArgs; const subcommandArgs = isSubcommand ? stripClaudeSubcommandSessionArgs(browserArgs) : browserArgs;
const launchArgs = isSubcommand const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(subcommandArgs) ? appendThirdPartyWebSearchToolArgs(subcommandArgs, webSearchLaunch.enabled)
: ['--settings', expandedSettingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)]; : [
'--settings',
expandedSettingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile', launcher: 'ccs.settings-profile',
args: launchArgs, args: launchArgs,
+2
View File
@@ -73,6 +73,8 @@ export {
// Re-export Claude launch arg helpers // Re-export Claude launch arg helpers
export { appendThirdPartyWebSearchToolArgs } from './websearch/claude-tool-args'; export { appendThirdPartyWebSearchToolArgs } from './websearch/claude-tool-args';
export { resolveWebSearchLaunchState } from './websearch/launch-state';
export type { WebSearchConfigSnapshot, WebSearchLaunchState } from './websearch/launch-state';
// Re-export trace helpers // Re-export trace helpers
export { export {
+7 -2
View File
@@ -135,9 +135,14 @@ function ensureWebSearchSteeringPrompt(args: string[]): string[] {
return [...optionArgs, ...steeringArgs, ...trailingArgs]; return [...optionArgs, ...steeringArgs, ...trailingArgs];
} }
export function appendThirdPartyWebSearchToolArgs(args: string[]): string[] { export function appendThirdPartyWebSearchToolArgs(
args: string[],
webSearchEnabled = true
): string[] {
// Claude subcommands (agents, doctor, mcp, ...) reject top-level session flags // Claude subcommands (agents, doctor, mcp, ...) reject top-level session flags
// like `--append-system-prompt` and `--disallowedTools`. Issue #1218. // like `--append-system-prompt` and `--disallowedTools`. Issue #1218.
if (isClaudeSubcommandInvocation(args)) return args; if (isClaudeSubcommandInvocation(args)) return args;
return ensureWebSearchSteeringPrompt(ensureDisallowedNativeWebSearchTool(args));
const disallowedArgs = ensureDisallowedNativeWebSearchTool(args);
return webSearchEnabled ? ensureWebSearchSteeringPrompt(disallowedArgs) : disallowedArgs;
} }
+4 -2
View File
@@ -14,6 +14,7 @@ import { getCcsHooksDir } from '../config-manager';
import { getClaudeSettingsPath } from '../claude-config-path'; import { getClaudeSettingsPath } from '../claude-config-path';
import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils'; import { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
import { getWebSearchConfig } from '../../config/config-loader-facade'; import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
// Hook file name // Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs'; const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
@@ -35,9 +36,10 @@ export function getHookPath(): string {
* Get WebSearch hook configuration for settings.json * Get WebSearch hook configuration for settings.json
* Timeout is computed from max provider timeout in config.yaml + buffer * Timeout is computed from max provider timeout in config.yaml + buffer
*/ */
export function getWebSearchHookConfig(): Record<string, unknown> { export function getWebSearchHookConfig(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): Record<string, unknown> {
const hookPath = getHookPath(); const hookPath = getHookPath();
const wsConfig = getWebSearchConfig();
// Compute max timeout from enabled providers // Compute max timeout from enabled providers
const timeouts: number[] = []; const timeouts: number[] = [];
+4 -2
View File
@@ -9,6 +9,7 @@
import { normalizeSearxngBaseUrl } from './types'; import { normalizeSearxngBaseUrl } from './types';
import { resolveAllowedWebSearchTraceFile } from './trace'; import { resolveAllowedWebSearchTraceFile } from './trace';
import { getWebSearchConfig } from '../../config/config-loader-facade'; import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
/** /**
* Get environment variables for WebSearch hook configuration. * Get environment variables for WebSearch hook configuration.
@@ -17,8 +18,9 @@ import { getWebSearchConfig } from '../../config/config-loader-facade';
* *
* @returns Record of environment variables to set before spawning Claude * @returns Record of environment variables to set before spawning Claude
*/ */
export function getWebSearchHookEnv(): Record<string, string> { export function getWebSearchHookEnv(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): Record<string, string> {
const env: Record<string, string> = { const env: Record<string, string> = {
CCS_WEBSEARCH_ENABLED: '0', CCS_WEBSEARCH_ENABLED: '0',
CCS_WEBSEARCH_SKIP: '0', CCS_WEBSEARCH_SKIP: '0',
+4 -3
View File
@@ -13,6 +13,7 @@ import { info, warn } from '../ui';
import { getCcsDir, getCcsHooksDir } from '../config-manager'; import { getCcsDir, getCcsHooksDir } from '../config-manager';
import { getHookPath } from './hook-config'; import { getHookPath } from './hook-config';
import { getWebSearchConfig } from '../../config/config-loader-facade'; import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
// Re-export from hook-config for backward compatibility // Re-export from hook-config for backward compatibility
export { getHookPath, getWebSearchHookConfig } from './hook-config'; export { getHookPath, getWebSearchHookConfig } from './hook-config';
@@ -76,10 +77,10 @@ export function hasWebSearchHook(): boolean {
* *
* @returns true if hook installed successfully * @returns true if hook installed successfully
*/ */
export function installWebSearchHook(): boolean { export function installWebSearchHook(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
try { try {
const wsConfig = getWebSearchConfig();
// Skip if disabled // Skip if disabled
if (!wsConfig.enabled) { if (!wsConfig.enabled) {
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
+2
View File
@@ -71,6 +71,8 @@ export {
// Claude launch args // Claude launch args
export { appendThirdPartyWebSearchToolArgs } from './claude-tool-args'; export { appendThirdPartyWebSearchToolArgs } from './claude-tool-args';
export { resolveWebSearchLaunchState } from './launch-state';
export type { WebSearchConfigSnapshot, WebSearchLaunchState } from './launch-state';
// Trace helpers // Trace helpers
export { export {
+19
View File
@@ -0,0 +1,19 @@
import { getWebSearchConfig } from '../../config/config-loader-facade';
import { getWebSearchHookEnv } from './hook-env';
export type WebSearchConfigSnapshot = ReturnType<typeof getWebSearchConfig>;
export interface WebSearchLaunchState {
config: WebSearchConfigSnapshot;
enabled: boolean;
hookEnv: Record<string, string>;
}
export function resolveWebSearchLaunchState(): WebSearchLaunchState {
const config = getWebSearchConfig();
return {
config,
enabled: config.enabled,
hookEnv: getWebSearchHookEnv(config),
};
}
+19 -15
View File
@@ -16,6 +16,7 @@ import {
isClaudeUserConfigLockUnavailableError as isLockUnavailableError, isClaudeUserConfigLockUnavailableError as isLockUnavailableError,
withClaudeUserConfigLock, withClaudeUserConfigLock,
} from '../claude-user-config-lock'; } from '../claude-user-config-lock';
import type { WebSearchConfigSnapshot } from './launch-state';
const WEBSEARCH_MCP_SERVER = 'ccs-websearch-server.cjs'; const WEBSEARCH_MCP_SERVER = 'ccs-websearch-server.cjs';
const WEBSEARCH_MCP_SERVER_NAME = 'ccs-websearch'; const WEBSEARCH_MCP_SERVER_NAME = 'ccs-websearch';
@@ -189,14 +190,15 @@ function removeManagedServerConfig(configPath: string): boolean {
} }
} }
export function installWebSearchMcpServer(): boolean { export function installWebSearchMcpServer(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
if (!wsConfig.enabled) { if (!wsConfig.enabled) {
appendWebSearchTrace('websearch_mcp_install_skipped', { reason: 'disabled' }); appendWebSearchTrace('websearch_mcp_install_skipped', { reason: 'disabled' });
return false; return false;
} }
if (!installWebSearchHook()) { if (!installWebSearchHook(wsConfig)) {
appendWebSearchTrace('websearch_mcp_install_failed', { reason: 'hook_unavailable' }); appendWebSearchTrace('websearch_mcp_install_failed', { reason: 'hook_unavailable' });
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
process.stderr.write( process.stderr.write(
@@ -267,8 +269,9 @@ export function installWebSearchMcpServer(): boolean {
} }
} }
export function ensureWebSearchMcpConfig(): boolean { export function ensureWebSearchMcpConfig(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
if (!wsConfig.enabled) { if (!wsConfig.enabled) {
appendWebSearchTrace('websearch_mcp_config_skipped', { reason: 'disabled' }); appendWebSearchTrace('websearch_mcp_config_skipped', { reason: 'disabled' });
return false; return false;
@@ -366,15 +369,16 @@ export function ensureWebSearchMcpConfig(): boolean {
} }
} }
export function ensureWebSearchMcp(): boolean { export function ensureWebSearchMcp(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): boolean {
if (!wsConfig.enabled) { if (!wsConfig.enabled) {
appendWebSearchTrace('websearch_mcp_ensure_skipped', { reason: 'disabled' }); appendWebSearchTrace('websearch_mcp_ensure_skipped', { reason: 'disabled' });
return false; return false;
} }
const installed = installWebSearchMcpServer(); const installed = installWebSearchMcpServer(wsConfig);
const configured = installed && ensureWebSearchMcpConfig(); const configured = installed && ensureWebSearchMcpConfig(wsConfig);
appendWebSearchTrace('websearch_mcp_ensure_result', { installed, configured }); appendWebSearchTrace('websearch_mcp_ensure_result', { installed, configured });
return installed && configured; return installed && configured;
} }
@@ -428,13 +432,14 @@ export function uninstallWebSearchMcp(): boolean {
return removedConfig || removedServer; return removedConfig || removedServer;
} }
export function ensureWebSearchMcpOrThrow(): void { export function ensureWebSearchMcpOrThrow(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): void {
if (!wsConfig.enabled) { if (!wsConfig.enabled) {
return; return;
} }
if (!ensureWebSearchMcp()) { if (!ensureWebSearchMcp(wsConfig)) {
throw new Error('WebSearch is enabled, but CCS could not prepare the local WebSearch tool.'); throw new Error('WebSearch is enabled, but CCS could not prepare the local WebSearch tool.');
} }
} }
@@ -447,7 +452,6 @@ export function ensureWebSearchMcpOrThrow(): void {
* native WebSearch and inject fallback steering while the constrained MCP * native WebSearch and inject fallback steering while the constrained MCP
* search path is unavailable. * search path is unavailable.
*/ */
export function ensureWebSearchMcpForLaunch(): boolean { export function ensureWebSearchMcpForLaunch(wsConfig: WebSearchConfigSnapshot): void {
ensureWebSearchMcpOrThrow(); ensureWebSearchMcpOrThrow(wsConfig);
return true;
} }
+14 -10
View File
@@ -18,6 +18,7 @@ import { getOpenCodeCliStatus } from './opencode-cli';
import { getWebSearchApiKeyStates } from './provider-secrets'; import { getWebSearchApiKeyStates } from './provider-secrets';
import { normalizeSearxngBaseUrl, type WebSearchCliInfo, type WebSearchStatus } from './types'; import { normalizeSearxngBaseUrl, type WebSearchCliInfo, type WebSearchStatus } from './types';
import { getWebSearchConfig } from '../../config/config-loader-facade'; import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
const PROVIDER_STATE_FILE = 'websearch-provider-state.json'; const PROVIDER_STATE_FILE = 'websearch-provider-state.json';
@@ -112,8 +113,7 @@ function applyCooldownStatus(
}; };
} }
function getLegacyProviderStatuses(): WebSearchCliInfo[] { function getLegacyProviderStatuses(wsConfig: WebSearchConfigSnapshot): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
const agyStatus = getAgyCliStatus(); const agyStatus = getAgyCliStatus();
const geminiStatus = getGeminiCliStatus(); const geminiStatus = getGeminiCliStatus();
const grokStatus = getGrokCliStatus(); const grokStatus = getGrokCliStatus();
@@ -197,8 +197,9 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] {
/** /**
* Get all WebSearch providers with their current status. * Get all WebSearch providers with their current status.
*/ */
export function getWebSearchCliProviders(): WebSearchCliInfo[] { export function getWebSearchCliProviders(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): WebSearchCliInfo[] {
const apiKeyStates = getWebSearchApiKeyStates(); const apiKeyStates = getWebSearchApiKeyStates();
const cooldowns = readProviderCooldowns(); const cooldowns = readProviderCooldowns();
const providers: WebSearchCliInfo[] = [ const providers: WebSearchCliInfo[] = [
@@ -283,7 +284,7 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] {
}, },
]; ];
return [...providers, ...getLegacyProviderStatuses()].map((provider) => return [...providers, ...getLegacyProviderStatuses(wsConfig)].map((provider) =>
applyCooldownStatus(provider, cooldowns) applyCooldownStatus(provider, cooldowns)
); );
} }
@@ -355,17 +356,20 @@ export function buildWebSearchReadiness(
/** /**
* Get WebSearch readiness status for display. * Get WebSearch readiness status for display.
*/ */
export function getWebSearchReadiness(): WebSearchStatus { export function getWebSearchReadiness(
const wsConfig = getWebSearchConfig(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
const providers = getWebSearchCliProviders(); ): WebSearchStatus {
const providers = getWebSearchCliProviders(wsConfig);
return buildWebSearchReadiness(wsConfig.enabled, providers); return buildWebSearchReadiness(wsConfig.enabled, providers);
} }
/** /**
* Display WebSearch status (single line, equilibrium UX). * Display WebSearch status (single line, equilibrium UX).
*/ */
export function displayWebSearchStatus(): void { export function displayWebSearchStatus(
const status = getWebSearchReadiness(); wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): void {
const status = getWebSearchReadiness(wsConfig);
switch (status.readiness) { switch (status.readiness) {
case 'ready': case 'ready':