From abc3002e89cb6f7fd7659b41084b6c835008c6cd Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 29 Jul 2026 13:05:16 -0400 Subject: [PATCH] fix(websearch): honor disabled steering per launch --- src/cliproxy/executor/claude-launcher.ts | 12 +++++++-- src/cliproxy/executor/env-resolver.ts | 5 ++-- src/cliproxy/executor/index.ts | 14 +++++----- src/copilot/copilot-executor.ts | 27 ++++++++++++++----- src/cursor/cursor-profile-executor.ts | 16 ++++++++--- src/delegation/headless-executor.ts | 12 ++++++--- src/dispatcher/flows/copilot-flow.ts | 11 +++++--- src/dispatcher/flows/cursor-flow.ts | 11 +++++--- src/dispatcher/flows/settings-flow.ts | 24 +++++++++-------- src/utils/websearch-manager.ts | 2 ++ src/utils/websearch/claude-tool-args.ts | 9 +++++-- src/utils/websearch/hook-config.ts | 6 +++-- src/utils/websearch/hook-env.ts | 6 +++-- src/utils/websearch/hook-installer.ts | 7 ++--- src/utils/websearch/index.ts | 2 ++ src/utils/websearch/launch-state.ts | 19 +++++++++++++ src/utils/websearch/mcp-installer.ts | 34 +++++++++++++----------- src/utils/websearch/status.ts | 24 ++++++++++------- 18 files changed, 165 insertions(+), 76 deletions(-) create mode 100644 src/utils/websearch/launch-state.ts diff --git a/src/cliproxy/executor/claude-launcher.ts b/src/cliproxy/executor/claude-launcher.ts index ff189bbf..4f03938a 100644 --- a/src/cliproxy/executor/claude-launcher.ts +++ b/src/cliproxy/executor/claude-launcher.ts @@ -15,6 +15,7 @@ import { appendThirdPartyWebSearchToolArgs, createWebSearchTraceContext, } from '../../utils/websearch-manager'; +import type { WebSearchLaunchState } from '../../utils/websearch-manager'; import { appendThirdPartyImageAnalysisToolArgs } from '../../utils/image-analysis'; import { appendBrowserToolArgs } from '../../utils/browser'; import { getDefaultAccount } from '../accounts/account-manager'; @@ -51,6 +52,8 @@ export interface ClaudeLaunchContext { sessionId: string | undefined; /** Whether image analysis MCP is ready */ imageAnalysisMcpReady: boolean; + /** WebSearch state captured when launch provisioning ran */ + webSearchLaunch: WebSearchLaunchState; /** Browser runtime environment variables (undefined if browser not active) */ browserRuntimeEnv: NodeJS.ProcessEnv | undefined; /** Inherited Claude config dir for continuity */ @@ -82,6 +85,7 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise; + /** WebSearch hook env captured from the launch configuration snapshot. */ + webSearchEnv?: Record; /** Optional browser runtime env for Claude browser MCP reuse. */ browserRuntimeEnv?: Record; } @@ -246,6 +247,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record { const { config: normalizedConfig, warnings } = normalizeCopilotConfigWithWarnings(config); @@ -276,7 +288,7 @@ export async function executeCopilotProfile( syncImageAnalysisMcpToConfigDir(claudeConfigDir); // 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 imageAnalysisProvisioningFailed = !imageAnalysisMcpReady && imageAnalysisResolution.env.CCS_IMAGE_ANALYSIS_ENABLED === '1'; @@ -305,10 +317,11 @@ export async function executeCopilotProfile( // Spawn Claude CLI const spawnStartedAt = Date.now(); return new Promise((resolve) => { - const imageAnalysisArgs = imageAnalysisMcpReady - ? appendThirdPartyImageAnalysisToolArgs(claudeArgs) - : claudeArgs; - const launchArgs = appendThirdPartyWebSearchToolArgs(imageAnalysisArgs); + const launchArgs = buildCopilotClaudeLaunchArgs( + claudeArgs, + imageAnalysisMcpReady, + webSearchLaunch?.enabled ?? true + ); const traceEnv = createWebSearchTraceContext({ launcher: 'copilot.executor', args: launchArgs, diff --git a/src/cursor/cursor-profile-executor.ts b/src/cursor/cursor-profile-executor.ts index 5716ace2..e88a59e0 100644 --- a/src/cursor/cursor-profile-executor.ts +++ b/src/cursor/cursor-profile-executor.ts @@ -9,9 +9,9 @@ import { fail, info, ok } from '../utils/ui'; import { appendThirdPartyWebSearchToolArgs, createWebSearchTraceContext, - getWebSearchHookEnv, syncWebSearchMcpToConfigDir, } from '../utils/websearch-manager'; +import type { WebSearchLaunchState } from '../utils/websearch-manager'; import { getImageAnalysisHookEnv, resolveImageAnalysisRuntimeStatus } from '../utils/hooks'; import { stripClaudeCodeEnv } from '../utils/shell-executor'; import { checkAuthStatus } from './cursor-auth'; @@ -106,11 +106,19 @@ export async function resolveCursorImageAnalysisEnv( return { env, warning: null }; } +export function buildCursorClaudeLaunchArgs( + claudeArgs: string[], + webSearchEnabled: boolean +): string[] { + return appendThirdPartyWebSearchToolArgs(claudeArgs, webSearchEnabled); +} + export async function executeCursorProfile( config: CursorConfig, claudeArgs: string[], claudeConfigDir?: string, - claudeCliPath = 'claude' + claudeCliPath = 'claude', + webSearchLaunch?: WebSearchLaunchState ): Promise { if (!config.enabled) { 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 globalEnvConfig = getGlobalEnvConfig(); const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; - const webSearchEnv = getWebSearchHookEnv(); + const webSearchEnv = webSearchLaunch?.hookEnv ?? {}; const { env: imageAnalysisEnv, warning: imageAnalysisWarning } = await resolveCursorImageAnalysisEnv(); const env = stripClaudeCodeEnv({ @@ -184,7 +192,7 @@ export async function executeCursorProfile( syncWebSearchMcpToConfigDir(claudeConfigDir); return new Promise((resolve) => { - const launchArgs = appendThirdPartyWebSearchToolArgs(claudeArgs); + const launchArgs = buildCursorClaudeLaunchArgs(claudeArgs, webSearchLaunch?.enabled ?? true); const traceEnv = createWebSearchTraceContext({ launcher: 'cursor.executor', args: launchArgs, diff --git a/src/delegation/headless-executor.ts b/src/delegation/headless-executor.ts index 3bb0d813..58d43b01 100644 --- a/src/delegation/headless-executor.ts +++ b/src/delegation/headless-executor.ts @@ -55,8 +55,8 @@ import { appendWebSearchTrace, createWebSearchTraceContext, ensureWebSearchMcpForLaunch, - getWebSearchHookEnv, readWebSearchTraceRecords, + resolveWebSearchLaunchState, syncWebSearchMcpToConfigDir, } from '../utils/websearch-manager'; 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(); syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir); syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir); @@ -257,6 +258,7 @@ export class HeadlessExecutor { let runtimeEnvVars: NodeJS.ProcessEnv = { ...stripAnthropicRoutingEnv({ ...globalEnv, ...settingsEnv }, settingsEnv), + ...webSearchLaunch.hookEnv, ...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}), CCS_PROFILE_TYPE: 'settings', CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1', @@ -380,7 +382,10 @@ export class HeadlessExecutor { const imageAnalysisArgs = imageAnalysisMcpReady ? appendThirdPartyImageAnalysisToolArgs(args) : args; - const launchArgs = appendThirdPartyWebSearchToolArgs(imageAnalysisArgs); + const launchArgs = appendThirdPartyWebSearchToolArgs( + imageAnalysisArgs, + webSearchLaunch.enabled + ); const traceEnv = createWebSearchTraceContext({ launcher: 'delegation.headless-executor', args: launchArgs, @@ -467,7 +472,6 @@ export class HeadlessExecutor { const cleanEnv = stripClaudeCodeEnv({ ...stripAnthropicRoutingEnv(process.env), ...getClaudeLaunchEnvOverrides(), - ...getWebSearchHookEnv(), ...runtimeEnvVars, ...imageAnalysisEnv, ...traceEnv, diff --git a/src/dispatcher/flows/copilot-flow.ts b/src/dispatcher/flows/copilot-flow.ts index d92d31e8..7afb9874 100644 --- a/src/dispatcher/flows/copilot-flow.ts +++ b/src/dispatcher/flows/copilot-flow.ts @@ -5,7 +5,10 @@ */ 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 { ensureProfileHooks as ensureImageAnalyzerHooks, @@ -23,7 +26,8 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise resolveProfileContinuityInheritance, } = ctx; - ensureWebSearchMcpForLaunch(); + const webSearchLaunch = resolveWebSearchLaunchState(); + ensureWebSearchMcpForLaunch(webSearchLaunch.config); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); if (resolvedTarget === 'claude') { if (imageAnalysisMcpReady) { @@ -60,7 +64,8 @@ export async function runCopilotFlow(ctx: ProfileDispatchContext): Promise copilotConfig, remainingArgs, continuityInheritance.claudeConfigDir, - claudeCli + claudeCli, + webSearchLaunch ); process.exit(exitCode); } diff --git a/src/dispatcher/flows/cursor-flow.ts b/src/dispatcher/flows/cursor-flow.ts index 0d703e52..cc5af5ae 100644 --- a/src/dispatcher/flows/cursor-flow.ts +++ b/src/dispatcher/flows/cursor-flow.ts @@ -5,7 +5,10 @@ */ 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 { installImageAnalyzerHook } from '../../utils/hooks'; import type { ProfileDispatchContext } from '../dispatcher-context'; @@ -19,7 +22,8 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise resolveProfileContinuityInheritance, } = ctx; - ensureWebSearchMcpForLaunch(); + const webSearchLaunch = resolveWebSearchLaunchState(); + ensureWebSearchMcpForLaunch(webSearchLaunch.config); installImageAnalyzerHook(); ensureImageAnalyzerHooks({ profileName: profileInfo.name, @@ -48,7 +52,8 @@ export async function runCursorFlow(ctx: ProfileDispatchContext): Promise cursorConfig, remainingArgs, continuityInheritance.claudeConfigDir, - claudeCli + claudeCli, + webSearchLaunch ); process.exit(exitCode); } diff --git a/src/dispatcher/flows/settings-flow.ts b/src/dispatcher/flows/settings-flow.ts index 83b8dc26..ef08ca51 100644 --- a/src/dispatcher/flows/settings-flow.ts +++ b/src/dispatcher/flows/settings-flow.ts @@ -16,10 +16,10 @@ import { import { ensureWebSearchMcpForLaunch, displayWebSearchStatus, - getWebSearchHookEnv, syncWebSearchMcpToConfigDir, appendThirdPartyWebSearchToolArgs, createWebSearchTraceContext, + resolveWebSearchLaunchState, } from '../../utils/websearch-manager'; import { ensureImageAnalysisMcpOrThrow, @@ -94,18 +94,16 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise { +export function getWebSearchHookConfig( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): Record { const hookPath = getHookPath(); - const wsConfig = getWebSearchConfig(); // Compute max timeout from enabled providers const timeouts: number[] = []; diff --git a/src/utils/websearch/hook-env.ts b/src/utils/websearch/hook-env.ts index 8a95bcbe..8eb5a83e 100644 --- a/src/utils/websearch/hook-env.ts +++ b/src/utils/websearch/hook-env.ts @@ -9,6 +9,7 @@ import { normalizeSearxngBaseUrl } from './types'; import { resolveAllowedWebSearchTraceFile } from './trace'; import { getWebSearchConfig } from '../../config/config-loader-facade'; +import type { WebSearchConfigSnapshot } from './launch-state'; /** * 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 */ -export function getWebSearchHookEnv(): Record { - const wsConfig = getWebSearchConfig(); +export function getWebSearchHookEnv( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): Record { const env: Record = { CCS_WEBSEARCH_ENABLED: '0', CCS_WEBSEARCH_SKIP: '0', diff --git a/src/utils/websearch/hook-installer.ts b/src/utils/websearch/hook-installer.ts index 64a18797..02d1429c 100644 --- a/src/utils/websearch/hook-installer.ts +++ b/src/utils/websearch/hook-installer.ts @@ -13,6 +13,7 @@ import { info, warn } from '../ui'; import { getCcsDir, getCcsHooksDir } from '../config-manager'; import { getHookPath } from './hook-config'; import { getWebSearchConfig } from '../../config/config-loader-facade'; +import type { WebSearchConfigSnapshot } from './launch-state'; // Re-export from hook-config for backward compatibility export { getHookPath, getWebSearchHookConfig } from './hook-config'; @@ -76,10 +77,10 @@ export function hasWebSearchHook(): boolean { * * @returns true if hook installed successfully */ -export function installWebSearchHook(): boolean { +export function installWebSearchHook( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): boolean { try { - const wsConfig = getWebSearchConfig(); - // Skip if disabled if (!wsConfig.enabled) { if (process.env.CCS_DEBUG) { diff --git a/src/utils/websearch/index.ts b/src/utils/websearch/index.ts index 8d746801..c69b9b7f 100644 --- a/src/utils/websearch/index.ts +++ b/src/utils/websearch/index.ts @@ -71,6 +71,8 @@ export { // Claude launch args export { appendThirdPartyWebSearchToolArgs } from './claude-tool-args'; +export { resolveWebSearchLaunchState } from './launch-state'; +export type { WebSearchConfigSnapshot, WebSearchLaunchState } from './launch-state'; // Trace helpers export { diff --git a/src/utils/websearch/launch-state.ts b/src/utils/websearch/launch-state.ts new file mode 100644 index 00000000..1d1c606b --- /dev/null +++ b/src/utils/websearch/launch-state.ts @@ -0,0 +1,19 @@ +import { getWebSearchConfig } from '../../config/config-loader-facade'; +import { getWebSearchHookEnv } from './hook-env'; + +export type WebSearchConfigSnapshot = ReturnType; + +export interface WebSearchLaunchState { + config: WebSearchConfigSnapshot; + enabled: boolean; + hookEnv: Record; +} + +export function resolveWebSearchLaunchState(): WebSearchLaunchState { + const config = getWebSearchConfig(); + return { + config, + enabled: config.enabled, + hookEnv: getWebSearchHookEnv(config), + }; +} diff --git a/src/utils/websearch/mcp-installer.ts b/src/utils/websearch/mcp-installer.ts index 5944e500..e8965756 100644 --- a/src/utils/websearch/mcp-installer.ts +++ b/src/utils/websearch/mcp-installer.ts @@ -16,6 +16,7 @@ import { isClaudeUserConfigLockUnavailableError as isLockUnavailableError, withClaudeUserConfigLock, } from '../claude-user-config-lock'; +import type { WebSearchConfigSnapshot } from './launch-state'; const WEBSEARCH_MCP_SERVER = 'ccs-websearch-server.cjs'; const WEBSEARCH_MCP_SERVER_NAME = 'ccs-websearch'; @@ -189,14 +190,15 @@ function removeManagedServerConfig(configPath: string): boolean { } } -export function installWebSearchMcpServer(): boolean { - const wsConfig = getWebSearchConfig(); +export function installWebSearchMcpServer( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): boolean { if (!wsConfig.enabled) { appendWebSearchTrace('websearch_mcp_install_skipped', { reason: 'disabled' }); return false; } - if (!installWebSearchHook()) { + if (!installWebSearchHook(wsConfig)) { appendWebSearchTrace('websearch_mcp_install_failed', { reason: 'hook_unavailable' }); if (process.env.CCS_DEBUG) { process.stderr.write( @@ -267,8 +269,9 @@ export function installWebSearchMcpServer(): boolean { } } -export function ensureWebSearchMcpConfig(): boolean { - const wsConfig = getWebSearchConfig(); +export function ensureWebSearchMcpConfig( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): boolean { if (!wsConfig.enabled) { appendWebSearchTrace('websearch_mcp_config_skipped', { reason: 'disabled' }); return false; @@ -366,15 +369,16 @@ export function ensureWebSearchMcpConfig(): boolean { } } -export function ensureWebSearchMcp(): boolean { - const wsConfig = getWebSearchConfig(); +export function ensureWebSearchMcp( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): boolean { if (!wsConfig.enabled) { appendWebSearchTrace('websearch_mcp_ensure_skipped', { reason: 'disabled' }); return false; } - const installed = installWebSearchMcpServer(); - const configured = installed && ensureWebSearchMcpConfig(); + const installed = installWebSearchMcpServer(wsConfig); + const configured = installed && ensureWebSearchMcpConfig(wsConfig); appendWebSearchTrace('websearch_mcp_ensure_result', { installed, configured }); return installed && configured; } @@ -428,13 +432,14 @@ export function uninstallWebSearchMcp(): boolean { return removedConfig || removedServer; } -export function ensureWebSearchMcpOrThrow(): void { - const wsConfig = getWebSearchConfig(); +export function ensureWebSearchMcpOrThrow( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): void { if (!wsConfig.enabled) { return; } - if (!ensureWebSearchMcp()) { + if (!ensureWebSearchMcp(wsConfig)) { 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 * search path is unavailable. */ -export function ensureWebSearchMcpForLaunch(): boolean { - ensureWebSearchMcpOrThrow(); - return true; +export function ensureWebSearchMcpForLaunch(wsConfig: WebSearchConfigSnapshot): void { + ensureWebSearchMcpOrThrow(wsConfig); } diff --git a/src/utils/websearch/status.ts b/src/utils/websearch/status.ts index af16138c..946fd28f 100644 --- a/src/utils/websearch/status.ts +++ b/src/utils/websearch/status.ts @@ -18,6 +18,7 @@ import { getOpenCodeCliStatus } from './opencode-cli'; import { getWebSearchApiKeyStates } from './provider-secrets'; import { normalizeSearxngBaseUrl, type WebSearchCliInfo, type WebSearchStatus } from './types'; import { getWebSearchConfig } from '../../config/config-loader-facade'; +import type { WebSearchConfigSnapshot } from './launch-state'; const PROVIDER_STATE_FILE = 'websearch-provider-state.json'; @@ -112,8 +113,7 @@ function applyCooldownStatus( }; } -function getLegacyProviderStatuses(): WebSearchCliInfo[] { - const wsConfig = getWebSearchConfig(); +function getLegacyProviderStatuses(wsConfig: WebSearchConfigSnapshot): WebSearchCliInfo[] { const agyStatus = getAgyCliStatus(); const geminiStatus = getGeminiCliStatus(); const grokStatus = getGrokCliStatus(); @@ -197,8 +197,9 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] { /** * Get all WebSearch providers with their current status. */ -export function getWebSearchCliProviders(): WebSearchCliInfo[] { - const wsConfig = getWebSearchConfig(); +export function getWebSearchCliProviders( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): WebSearchCliInfo[] { const apiKeyStates = getWebSearchApiKeyStates(); const cooldowns = readProviderCooldowns(); 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) ); } @@ -355,17 +356,20 @@ export function buildWebSearchReadiness( /** * Get WebSearch readiness status for display. */ -export function getWebSearchReadiness(): WebSearchStatus { - const wsConfig = getWebSearchConfig(); - const providers = getWebSearchCliProviders(); +export function getWebSearchReadiness( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): WebSearchStatus { + const providers = getWebSearchCliProviders(wsConfig); return buildWebSearchReadiness(wsConfig.enabled, providers); } /** * Display WebSearch status (single line, equilibrium UX). */ -export function displayWebSearchStatus(): void { - const status = getWebSearchReadiness(); +export function displayWebSearchStatus( + wsConfig: WebSearchConfigSnapshot = getWebSearchConfig() +): void { + const status = getWebSearchReadiness(wsConfig); switch (status.readiness) { case 'ready':