Merge pull request #1676 from kaitranntt/kai/fix/1648-disabled-websearch-steering

fix(websearch): honor disabled steering per launch
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-29 13:44:56 -04:00
commit 236471100c
28 files changed
+335 -115

No files matched your search

+3 -3
View File
@@ -573,8 +573,8 @@
},
"loggerCoverage": {
"filesWithCreateLogger": 65,
"totalSourceFiles": 753,
"coverageRatio": 0.0863,
"totalSourceFiles": 754,
"coverageRatio": 0.0862,
"subdomainsWithZeroCreateLogger": [
"api",
"bin",
@@ -605,7 +605,7 @@
},
{
"subdomain": "utils",
"count": 89,
"count": 90,
"withLogger": 1
},
{
+1 -1
View File
@@ -60,7 +60,7 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| typed-error adoption (P4 locked subdomains) | 91.7% (22/24), target 40% |
| hotpath console.error/warn occurrences | 266 (571 total, 305 CLI-UX exempt) |
| hotpath console.error/warn files | 82 |
| files with createLogger | 65/753 |
| files with createLogger | 65/754 |
| subdomains with zero createLogger | 15 (api, bin, channels, cliproxy, cliproxy/accounts, cliproxy/ai-providers, cliproxy/binary, cliproxy/config, cliproxy/management, cliproxy/sync, cliproxy/types, config, dispatcher, shared, types) |
| files > 400 LOC | 91 |
| files > 600 LOC | 42 |
+2 -1
View File
@@ -10,7 +10,8 @@ For a third-party Claude launch, CCS:
1. suppresses the native `WebSearch` tool because the third-party backend cannot
execute it;
2. adds a short steering prompt that prefers the CCS MCP `WebSearch` tool;
2. when WebSearch is enabled, adds a short steering prompt that prefers the CCS
MCP `WebSearch` tool;
3. when WebSearch is enabled, installs the managed MCP server and adds the
`ccs-websearch` entry to the applicable Claude configuration;
4. searches enabled and ready providers in deterministic order.
+5 -5
View File
@@ -49,7 +49,7 @@
"src/cliproxy/executor/auth-coordinator.ts:176",
"src/cliproxy/executor/auth-coordinator.ts:317",
"src/cliproxy/executor/browser-launch-setup.ts:120",
"src/cliproxy/executor/index.ts:391",
"src/cliproxy/executor/index.ts:390",
"src/cliproxy/executor/lifecycle-manager.ts:129",
"src/cliproxy/executor/lifecycle-manager.ts:162",
"src/cliproxy/executor/lifecycle-manager.ts:59",
@@ -161,14 +161,14 @@
"src/cursor/cursor-translator.ts:189",
"src/delegation/headless-executor.ts:126",
"src/delegation/headless-executor.ts:141",
"src/delegation/headless-executor.ts:270",
"src/delegation/headless-executor.ts:679",
"src/delegation/headless-executor.ts:272",
"src/delegation/headless-executor.ts:683",
"src/dispatcher/cli-argument-parser.ts:310",
"src/dispatcher/cli-argument-parser.ts:314",
"src/dispatcher/cli-argument-parser.ts:324",
"src/dispatcher/cli-argument-parser.ts:328",
"src/dispatcher/flows/default-flow.ts:76",
"src/dispatcher/flows/settings-flow.ts:135",
"src/dispatcher/flows/settings-flow.ts:133",
"src/docker/docker-assets.ts:35",
"src/docker/docker-executor.ts:154",
"src/docker/docker-executor.ts:435",
@@ -280,7 +280,7 @@
"src/utils/shell-completion.ts:269",
"src/utils/shell-completion.ts:288",
"src/utils/shell-completion.ts:74",
"src/utils/websearch/mcp-installer.ts:438",
"src/utils/websearch/mcp-installer.ts:443",
"src/utils/websearch/profile-hook-injector.ts:206",
"src/web-server/index.ts:266",
"src/web-server/services/claude-extension-binding-service.ts:144",
@@ -47,7 +47,10 @@ mock.module('../../config/config-generator', () => ({
}));
mock.module('../../../utils/websearch-manager', () => ({
appendThirdPartyWebSearchToolArgs: (args: string[]) => args,
appendThirdPartyWebSearchToolArgs: (args: string[], enabled = true) =>
enabled
? [...args, '--append-system-prompt', 'CCS WebSearch steering']
: [...args, '--disallowedTools', 'WebSearch'],
createWebSearchTraceContext: jest.fn().mockReturnValue({}),
ensureWebSearchMcpOrThrow: jest.fn(),
displayWebSearchStatus: jest.fn(),
@@ -124,6 +127,11 @@ function baseContext(overrides: object = {}) {
skipLocalAuth: true,
sessionId: undefined,
imageAnalysisMcpReady: false,
webSearchLaunch: {
config: { enabled: true },
enabled: true,
hookEnv: { CCS_WEBSEARCH_ENABLED: '1' },
},
browserRuntimeEnv: undefined,
inheritedClaudeConfigDir: undefined,
codexReasoningProxy: null,
@@ -193,6 +201,22 @@ describe('launchClaude', () => {
expect(spawnArgs).toContain('--browser');
});
it('disallows native WebSearch without CCS steering when the launch snapshot is disabled', async () => {
await launchClaude(
baseContext({
webSearchLaunch: {
config: { enabled: false },
enabled: false,
hookEnv: { CCS_WEBSEARCH_ENABLED: '0', CCS_WEBSEARCH_SKIP: '1' },
},
})
);
const spawnArgs = mockSpawn.mock.calls[0][1] as string[];
expect(spawnArgs).toContain('--disallowedTools');
expect(spawnArgs).toContain('WebSearch');
expect(spawnArgs).not.toContain('CCS WebSearch steering');
});
it('registers cleanup handlers after spawn', async () => {
await launchClaude(baseContext());
expect(mockSetupCleanupHandlers).toHaveBeenCalledTimes(1);
+10 -2
View File
@@ -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<ChildP
skipLocalAuth,
sessionId,
imageAnalysisMcpReady,
webSearchLaunch,
browserRuntimeEnv,
inheritedClaudeConfigDir,
codexReasoningProxy,
@@ -124,8 +128,12 @@ export async function launchClaude(context: ClaudeLaunchContext): Promise<ChildP
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(browserArgs)
: ['--settings', settingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)];
? appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled)
: [
'--settings',
settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
// Inject web search trace context into env
const traceEnv = createWebSearchTraceContext({
+3 -2
View File
@@ -19,7 +19,6 @@ import {
import { applyExtendedContextConfig } from '../config/extended-context-config';
import { CLIProxyProvider } from '../types';
import { CompositeTierConfig } from '../../config/unified-config-types';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import {
applyImageAnalysisRuntimeOverrides,
getImageAnalysisHookEnv,
@@ -78,6 +77,8 @@ export interface ProxyChainConfig {
claudeConfigDir?: string;
/** Execution-aware image analysis env prepared by the caller */
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. */
browserRuntimeEnv?: Record<string, string>;
}
@@ -246,6 +247,7 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
compositeDefaultTier,
claudeConfigDir,
imageAnalysisEnv: resolvedImageAnalysisEnv,
webSearchEnv = {},
browserRuntimeEnv,
} = config;
@@ -386,7 +388,6 @@ export function buildClaudeEnvironment(config: ProxyChainConfig): Record<string,
};
// Add hook environment variables
const webSearchEnv = getWebSearchHookEnv();
const imageAnalysisEnv = resolvedImageAnalysisEnv ?? getImageAnalysisHookEnv(provider);
// Merge all environment variables (filter undefined values)
+8 -6
View File
@@ -52,7 +52,7 @@ import {
resolveCliproxyImageAnalysisEnv,
} from './env-resolver';
import { checkOrJoinProxy, registerProxySession } from './session-bridge';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
import { resolveWebSearchLaunchState } from '../../utils/websearch-manager';
import {
handleLogout,
handleImport,
@@ -276,10 +276,9 @@ export async function execClaudeWithCLIProxy(
await handleImport(authCtx);
// Setup first-class CCS WebSearch runtime for non-strict user launches.
const shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch();
if (shouldDisplayWebSearchStatus) {
displayWebSearchStatus();
}
const webSearchLaunch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
displayWebSearchStatus(webSearchLaunch.config);
// 3. Ensure OAuth completed (if provider requires it)
const remoteAuthToken = proxyConfig.authToken?.trim();
@@ -483,6 +482,7 @@ export async function execClaudeWithCLIProxy(
compositeDefaultTier: cfg.compositeDefaultTier,
claudeConfigDir: inheritedClaudeConfigDir,
imageAnalysisEnv,
webSearchEnv: webSearchLaunch.hookEnv,
});
// 9b. Build env-dependent proxy chain (tool-sanitization + codex-reasoning)
@@ -527,6 +527,7 @@ export async function execClaudeWithCLIProxy(
compositeDefaultTier: cfg.compositeDefaultTier,
claudeConfigDir: inheritedClaudeConfigDir,
imageAnalysisEnv,
webSearchEnv: webSearchLaunch.hookEnv,
browserRuntimeEnv,
});
@@ -542,7 +543,7 @@ export async function execClaudeWithCLIProxy(
);
}
const webSearchEnv = getWebSearchHookEnv();
const webSearchEnv = webSearchLaunch.hookEnv;
if (process.env.CCS_DEBUG) {
logger.info('browser-env-keys', 'CCS_BROWSER_* keys in environment', {
keys: Object.keys(env)
@@ -580,6 +581,7 @@ export async function execClaudeWithCLIProxy(
skipLocalAuth,
sessionId,
imageAnalysisMcpReady,
webSearchLaunch,
browserRuntimeEnv,
inheritedClaudeConfigDir,
codexReasoningProxy,
+20 -7
View File
@@ -18,11 +18,11 @@ import { normalizeCopilotConfigWithWarnings } from './copilot-model-normalizer';
import { CopilotStatus } from './types';
import { fail, info, ok, warn } from '../utils/ui';
import {
getWebSearchHookEnv,
appendThirdPartyWebSearchToolArgs,
createWebSearchTraceContext,
syncWebSearchMcpToConfigDir,
} from '../utils/websearch-manager';
import type { WebSearchLaunchState } from '../utils/websearch-manager';
import {
appendThirdPartyImageAnalysisToolArgs,
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.
* Uses model mapping for opus/sonnet/haiku tiers if configured.
@@ -185,7 +196,8 @@ export async function executeCopilotProfile(
config: CopilotConfig,
claudeArgs: string[],
claudeConfigDir?: string,
claudeCliPath: string = 'claude'
claudeCliPath: string = 'claude',
webSearchLaunch?: WebSearchLaunchState
): Promise<number> {
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,
+12 -4
View File
@@ -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<number> {
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,
+8 -4
View File
@@ -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,
+8 -3
View File
@@ -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<void>
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<void>
copilotConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
claudeCli,
webSearchLaunch
);
process.exit(exitCode);
}
+8 -3
View File
@@ -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<void>
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<void>
cursorConfig,
remainingArgs,
continuityInheritance.claudeConfigDir,
claudeCli
claudeCli,
webSearchLaunch
);
process.exit(exitCode);
}
+13 -11
View File
@@ -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<void
if (browserAttachRuntime?.warning) {
process.stderr.write(`${warn(browserAttachRuntime.warning)}\n`);
}
let shouldDisplayWebSearchStatus = true;
const webSearchLaunch = resolveWebSearchLaunchState();
if (resolvedTarget === 'claude') {
shouldDisplayWebSearchStatus = ensureWebSearchMcpForLaunch();
ensureWebSearchMcpForLaunch(webSearchLaunch.config);
if (browserRuntimeEnv) {
ensureBrowserMcpOrThrow();
}
}
// Display WebSearch status (single line, equilibrium UX)
if (shouldDisplayWebSearchStatus) {
displayWebSearchStatus();
}
displayWebSearchStatus(webSearchLaunch.config);
const continuityInheritance =
resolvedTarget === 'claude'
@@ -275,7 +273,7 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
targetRemainingArgs,
});
const webSearchEnv = getWebSearchHookEnv();
const webSearchEnv = webSearchLaunch.hookEnv;
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
const globalEnvConfig = getGlobalEnvConfig();
@@ -395,11 +393,11 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
? stripClaudeSubcommandSessionArgs(browserArgs)
: browserArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(subcommandArgs)
? appendThirdPartyWebSearchToolArgs(subcommandArgs, webSearchLaunch.enabled)
: [
'--settings',
launchSettings.settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile.proxy',
@@ -418,8 +416,12 @@ export async function runSettingsFlow(ctx: ProfileDispatchContext): Promise<void
const isSubcommand = isClaudeSubcommandInvocation(browserArgs);
const subcommandArgs = isSubcommand ? stripClaudeSubcommandSessionArgs(browserArgs) : browserArgs;
const launchArgs = isSubcommand
? appendThirdPartyWebSearchToolArgs(subcommandArgs)
: ['--settings', expandedSettingsPath, ...appendThirdPartyWebSearchToolArgs(browserArgs)];
? appendThirdPartyWebSearchToolArgs(subcommandArgs, webSearchLaunch.enabled)
: [
'--settings',
expandedSettingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs, webSearchLaunch.enabled),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile',
args: launchArgs,
+2
View File
@@ -73,6 +73,8 @@ export {
// Re-export Claude launch arg helpers
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
export {
+7 -2
View File
@@ -135,9 +135,14 @@ function ensureWebSearchSteeringPrompt(args: string[]): string[] {
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
// like `--append-system-prompt` and `--disallowedTools`. Issue #1218.
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 { isCcsWebSearchHook, deduplicateCcsHooks } from './hook-utils';
import { getWebSearchConfig } from '../../config/config-loader-facade';
import type { WebSearchConfigSnapshot } from './launch-state';
// Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
@@ -35,9 +36,10 @@ export function getHookPath(): string {
* Get WebSearch hook configuration for settings.json
* 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 wsConfig = getWebSearchConfig();
// Compute max timeout from enabled providers
const timeouts: number[] = [];
+4 -2
View File
@@ -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<string, string> {
const wsConfig = getWebSearchConfig();
export function getWebSearchHookEnv(
wsConfig: WebSearchConfigSnapshot = getWebSearchConfig()
): Record<string, string> {
const env: Record<string, string> = {
CCS_WEBSEARCH_ENABLED: '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 { 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) {
+2
View File
@@ -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 {
+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,
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);
}
+14 -10
View File
@@ -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':
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'bun:test';
import {
buildCopilotClaudeLaunchArgs,
generateCopilotEnv,
resolveCopilotImageAnalysisEnv,
} from '../../../src/copilot/copilot-executor';
@@ -15,6 +16,13 @@ const baseConfig: CopilotConfig = {
model: 'gpt-4.1',
};
it('disallows native WebSearch without CCS steering when the Copilot snapshot is disabled', () => {
const args = buildCopilotClaudeLaunchArgs(['smoke'], false, false);
expect(args).toContain('--disallowedTools');
expect(args).toContain('WebSearch');
expect(args).not.toContain('--append-system-prompt');
});
describe('generateCopilotEnv', () => {
it('normalizes deprecated raptor-mini model selections to the safe default', () => {
const env = generateCopilotEnv({
@@ -4,6 +4,7 @@ import * as os from 'os';
import * as path from 'path';
import {
buildCursorClaudeLaunchArgs,
executeCursorProfile,
generateCursorEnv,
resolveCursorImageAnalysisEnv,
@@ -19,6 +20,13 @@ const BASE_CONFIG: CursorConfig = {
model: 'gpt-5.3-codex',
};
it('disallows native WebSearch without CCS steering when the Cursor snapshot is disabled', () => {
const args = buildCursorClaudeLaunchArgs(['smoke'], false);
expect(args).toContain('--disallowedTools');
expect(args).toContain('WebSearch');
expect(args).not.toContain('--append-system-prompt');
});
describe('cursor-profile-executor', () => {
let originalCcsHome: string | undefined;
let tempDir: string;
@@ -136,7 +136,7 @@ exit 0
expect(fs.existsSync(claudeArgsLogPath)).toBe(false);
});
it('keeps launch non-fatal when WebSearch is disabled', () => {
it('keeps native WebSearch disallowed without CCS steering when WebSearch is disabled', () => {
if (process.platform === 'win32') return;
fs.writeFileSync(
@@ -154,8 +154,26 @@ exit 0
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
expect(launchedArgs).toContain('--disallowedTools');
expect(launchedArgs).toContain('WebSearch');
expect(launchedArgs).toContain('--append-system-prompt');
expect(launchedArgs).toContain(STEERING_PROMPT_SNIPPET);
expect(launchedArgs).not.toContain(STEERING_PROMPT_SNIPPET);
});
it('keeps native WebSearch disallowed without CCS steering for disabled headless launches', () => {
if (process.platform === 'win32') return;
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
['version: 12', 'websearch:', ' enabled: false', ''].join('\n'),
'utf8'
);
const result = runCcs(['glm', '-p', 'smoke'], baseEnv);
expect(result.status).toBe(0);
expect(fs.existsSync(claudeArgsLogPath)).toBe(true);
const launchedArgs = fs.readFileSync(claudeArgsLogPath, 'utf8');
expect(launchedArgs).toContain('--disallowedTools');
expect(launchedArgs).toContain('WebSearch');
expect(launchedArgs).not.toContain(STEERING_PROMPT_SNIPPET);
});
it('writes a source-side launch trace for settings profiles when tracing is enabled', () => {
@@ -187,7 +205,7 @@ exit 0
expect(launchEvent?.launcher).toBe('ccs.settings-profile');
expect(launchEvent?.nativeWebSearchDisallowed).toBe(true);
expect(launchEvent?.steeringPromptApplied).toBe(true);
expect(launchEvent?.steeringPromptApplied).toBe(false);
expect(launchEvent?.settingsPath).toBe(settingsPath);
});
});
@@ -4,9 +4,13 @@ import { appendThirdPartyWebSearchToolArgs } from '../../../../src/utils/websear
const STEERING_PROMPT =
'For web lookup or current-information requests, prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches. If the user explicitly wants shell commands, or WebSearch is unavailable or fails, you may fall back to Bash/network tools.';
function appendEnabledWebSearchArgs(args: string[]): string[] {
return appendThirdPartyWebSearchToolArgs(args, true);
}
describe('appendThirdPartyWebSearchToolArgs', () => {
it('appends native WebSearch suppression and inline steering prompt when no prompt flags are present', () => {
expect(appendThirdPartyWebSearchToolArgs(['smoke'])).toEqual([
expect(appendEnabledWebSearchArgs(['smoke'])).toEqual([
'smoke',
'--disallowedTools',
'WebSearch',
@@ -17,7 +21,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
it('does not append duplicate suppression or steering prompt when both are already present', () => {
expect(
appendThirdPartyWebSearchToolArgs([
appendEnabledWebSearchArgs([
'smoke',
'--disallowedTools',
'WebSearch',
@@ -34,9 +38,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('detects comma-separated disallowed tool values', () => {
expect(
appendThirdPartyWebSearchToolArgs(['smoke', '--disallowedTools=Read,WebSearch'])
).toEqual([
expect(appendEnabledWebSearchArgs(['smoke', '--disallowedTools=Read,WebSearch'])).toEqual([
'smoke',
'--disallowedTools=Read,WebSearch',
'--append-system-prompt',
@@ -45,7 +47,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('merges WebSearch into an existing space-separated disallowed tool flag', () => {
expect(appendThirdPartyWebSearchToolArgs(['smoke', '--disallowedTools', 'Read'])).toEqual([
expect(appendEnabledWebSearchArgs(['smoke', '--disallowedTools', 'Read'])).toEqual([
'smoke',
'--disallowedTools',
'Read,WebSearch',
@@ -55,7 +57,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('merges WebSearch into an existing equals-form disallowed tool flag', () => {
expect(appendThirdPartyWebSearchToolArgs(['smoke', '--disallowedTools=Read'])).toEqual([
expect(appendEnabledWebSearchArgs(['smoke', '--disallowedTools=Read'])).toEqual([
'smoke',
'--disallowedTools=Read,WebSearch',
'--append-system-prompt',
@@ -65,11 +67,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
it('preserves user-supplied append-system-prompt values and adds the CCS steering hint once', () => {
expect(
appendThirdPartyWebSearchToolArgs([
'smoke',
'--append-system-prompt',
'User-provided instruction',
])
appendEnabledWebSearchArgs(['smoke', '--append-system-prompt', 'User-provided instruction'])
).toEqual([
'smoke',
'--append-system-prompt',
@@ -83,7 +81,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
it('does not duplicate the steering prompt when it already exists in equals form', () => {
expect(
appendThirdPartyWebSearchToolArgs([
appendEnabledWebSearchArgs([
'smoke',
'--disallowedTools',
'WebSearch',
@@ -98,9 +96,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('does not consume positional args after a disallowed-tools flag value', () => {
expect(
appendThirdPartyWebSearchToolArgs(['--disallowedTools', 'Read', 'latest AI news'])
).toEqual([
expect(appendEnabledWebSearchArgs(['--disallowedTools', 'Read', 'latest AI news'])).toEqual([
'--disallowedTools',
'Read,WebSearch',
'latest AI news',
@@ -110,7 +106,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('injects synthetic flags before an end-of-options marker', () => {
expect(appendThirdPartyWebSearchToolArgs(['--', 'latest AI news'])).toEqual([
expect(appendEnabledWebSearchArgs(['--', 'latest AI news'])).toEqual([
'--disallowedTools',
'WebSearch',
'--append-system-prompt',
@@ -121,7 +117,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('inserts the WebSearch disallow value when the flag is present without one', () => {
expect(appendThirdPartyWebSearchToolArgs(['--disallowedTools', '--verbose'])).toEqual([
expect(appendEnabledWebSearchArgs(['--disallowedTools', '--verbose'])).toEqual([
'--disallowedTools',
'WebSearch',
'--verbose',
@@ -133,7 +129,7 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
// File mode: --append-system-prompt-file when user passes --append-system-prompt-file
it('uses --append-system-prompt-file when user passes --append-system-prompt-file', () => {
const result = appendThirdPartyWebSearchToolArgs([
const result = appendEnabledWebSearchArgs([
'smoke',
'--append-system-prompt-file',
'/tmp/user-prompt.txt',
@@ -147,26 +143,54 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
});
it('uses --append-system-prompt-file when user passes --append-system-prompt-file= form', () => {
const result = appendThirdPartyWebSearchToolArgs([
const result = appendEnabledWebSearchArgs([
'smoke',
'--append-system-prompt-file=/tmp/user-prompt.txt',
]);
const fileFlags = result.filter(
(arg) => arg === '--append-system-prompt-file' || arg.startsWith('--append-system-prompt-file=')
(arg) =>
arg === '--append-system-prompt-file' || arg.startsWith('--append-system-prompt-file=')
);
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
expect(result).not.toContain('--append-system-prompt');
});
it('does not treat unrelated user prompt files as the managed CCS steering prompt', () => {
const result = appendThirdPartyWebSearchToolArgs([
const result = appendEnabledWebSearchArgs([
'smoke',
'--append-system-prompt-file',
'/tmp/user-ccs-prompt-websearch-tool-notes.txt',
]);
const filePaths = result.filter((arg, index) => result[index - 1] === '--append-system-prompt-file');
const filePaths = result.filter(
(arg, index) => result[index - 1] === '--append-system-prompt-file'
);
expect(filePaths).toContain('/tmp/user-ccs-prompt-websearch-tool-notes.txt');
expect(filePaths.some((filePath) => filePath.endsWith('/ccs-prompt-websearch-tool.txt'))).toBe(true);
expect(filePaths.some((filePath) => filePath.endsWith('/ccs-prompt-websearch-tool.txt'))).toBe(
true
);
});
it('keeps native WebSearch disallowed but skips CCS steering when WebSearch is disabled', () => {
expect(appendThirdPartyWebSearchToolArgs(['smoke'], false)).toEqual([
'smoke',
'--disallowedTools',
'WebSearch',
]);
});
it('preserves user prompt args when WebSearch is disabled', () => {
expect(
appendThirdPartyWebSearchToolArgs(
['smoke', '--append-system-prompt', 'User-provided instruction'],
false
)
).toEqual([
'smoke',
'--append-system-prompt',
'User-provided instruction',
'--disallowedTools',
'WebSearch',
]);
});
});
@@ -0,0 +1,48 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { appendThirdPartyWebSearchToolArgs } from '../../../../src/utils/websearch/claude-tool-args';
import { resolveWebSearchLaunchState } from '../../../../src/utils/websearch/launch-state';
import { ensureWebSearchMcpForLaunch } from '../../../../src/utils/websearch/mcp-installer';
describe('WebSearch launch state', () => {
let originalCcsHome: string | undefined;
let tempHome = '';
let configPath = '';
beforeEach(() => {
originalCcsHome = process.env.CCS_HOME;
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-websearch-launch-state-'));
const ccsDir = path.join(tempHome, '.ccs');
configPath = path.join(ccsDir, 'config.yaml');
fs.mkdirSync(ccsDir, { recursive: true });
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
fs.rmSync(tempHome, { recursive: true, force: true });
});
it('keeps provisioning, hook env, and steering on one snapshot when config changes', () => {
fs.writeFileSync(configPath, 'version: 12\nwebsearch:\n enabled: false\n', 'utf8');
const launch = resolveWebSearchLaunchState();
ensureWebSearchMcpForLaunch(launch.config);
fs.writeFileSync(configPath, 'version: 12\nwebsearch:\n enabled: true\n', 'utf8');
const args = appendThirdPartyWebSearchToolArgs(['smoke'], launch.enabled);
expect(launch.enabled).toBe(false);
expect(launch.hookEnv.CCS_WEBSEARCH_ENABLED).toBe('0');
expect(launch.hookEnv.CCS_WEBSEARCH_SKIP).toBe('1');
expect(args).toContain('--disallowedTools');
expect(args).toContain('WebSearch');
expect(args).not.toContain('--append-system-prompt');
});
});