fix: fail closed on websearch launch provisioning (#1621)

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-30 13:00:41 -04:00
1 parent 0527e276b1
commit f2843ad2ce
3 files changed
+49 -58

No files matched your search

@@ -141,7 +141,7 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
} }
}); });
it('degrades WebSearch provisioning failures for CLIProxy launches', async () => { it('fails closed on WebSearch provisioning failures for CLIProxy launches', async () => {
makeWebSearchProvisioningFail(); makeWebSearchProvisioningFail();
const markerPath = path.join(tmpHome, 'fake-claude-launched'); const markerPath = path.join(tmpHome, 'fake-claude-launched');
@@ -176,26 +176,30 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
try { try {
await execClaudeWithCLIProxy( await expect(
fakeClaudePath, execClaudeWithCLIProxy(
'gemini', fakeClaudePath,
[ 'gemini',
'--proxy-host', [
'127.0.0.1', '--proxy-host',
'--proxy-port', '127.0.0.1',
String(address.port), '--proxy-port',
'--proxy-auth-token', String(address.port),
'SECRET_TOKEN_FOR_VALIDATION', '--proxy-auth-token',
'--remote-only', 'SECRET_TOKEN_FOR_VALIDATION',
'--print', '--remote-only',
'hello', '--print',
], 'hello',
{} ],
{}
)
).rejects.toThrow(
'WebSearch is enabled, but CCS could not prepare the local WebSearch tool.'
); );
expect(await waitForFile(markerPath)).toBe(true); expect(await waitForFile(markerPath)).toBe(false);
expect(requestCount).toBeGreaterThan(0); expect(requestCount).toBeGreaterThan(0);
expect(exitSpy).toHaveBeenCalledWith(0); expect(exitSpy).not.toHaveBeenCalledWith(0);
} finally { } finally {
exitSpy.mockRestore(); exitSpy.mockRestore();
logSpy.mockRestore(); logSpy.mockRestore();
+7 -21
View File
@@ -440,28 +440,14 @@ export function ensureWebSearchMcpOrThrow(): void {
} }
/** /**
* Prepare WebSearch for a user launch without blocking Claude startup. * Prepare WebSearch for a user launch.
* *
* Returns true when the normal WebSearch status line is still accurate. A * WebSearch-enabled launches must fail closed when the managed local MCP
* failed MCP prepare already prints a degraded-path warning, so callers should * runtime cannot be prepared. Otherwise CCS would still suppress Claude's
* skip the ready/status line when this returns false. * native WebSearch and inject fallback steering while the constrained MCP
* search path is unavailable.
*/ */
export function ensureWebSearchMcpForLaunch(): boolean { export function ensureWebSearchMcpForLaunch(): boolean {
const wsConfig = getWebSearchConfig(); ensureWebSearchMcpOrThrow();
if (!wsConfig.enabled) { return true;
return true;
}
const ready = ensureWebSearchMcp();
if (!ready) {
process.stderr.write(
String(
warn(
'WebSearch is enabled, but CCS could not prepare the local WebSearch tool. This session will continue without local WebSearch.'
)
) + '\n'
);
}
return ready;
} }
@@ -61,6 +61,11 @@ describe('settings profile WebSearch launch', () => {
path.join(ccsDir, 'config.json'), path.join(ccsDir, 'config.json'),
JSON.stringify({ profiles: { glm: settingsPath } }, null, 2) + '\n' JSON.stringify({ profiles: { glm: settingsPath } }, null, 2) + '\n'
); );
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
['version: 12', 'websearch:', ' enabled: true', ''].join('\n'),
'utf8'
);
fs.writeFileSync( fs.writeFileSync(
settingsPath, settingsPath,
JSON.stringify( JSON.stringify(
@@ -93,6 +98,7 @@ exit 0
CCS_HOME: tmpHome, CCS_HOME: tmpHome,
CCS_CLAUDE_PATH: fakeClaudePath, CCS_CLAUDE_PATH: fakeClaudePath,
CCS_DEBUG: '1', CCS_DEBUG: '1',
CCS_SKIP_PREFLIGHT: '1',
}; };
}); });
@@ -104,40 +110,30 @@ exit 0
fs.rmSync(tmpHome, { recursive: true, force: true }); fs.rmSync(tmpHome, { recursive: true, force: true });
}); });
it('continues without local WebSearch when the tool runtime cannot be prepared', () => { it('fails closed when the local WebSearch tool runtime cannot be prepared', () => {
if (process.platform === 'win32') return; if (process.platform === 'win32') return;
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8'); fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
const result = runCcs(['glm', 'smoke'], baseEnv); const result = runCcs(['glm', 'smoke'], baseEnv);
expect(result.status).toBe(0); expect(result.status).not.toBe(0);
expect(result.stderr).toContain('could not prepare the local WebSearch tool'); expect(result.stderr).toContain('could not prepare the local WebSearch tool');
expect(result.stderr).toContain('This session will continue without local WebSearch'); expect(result.stderr).not.toContain('This session will continue without local WebSearch');
expect(fs.existsSync(claudeArgsLogPath)).toBe(true); expect(fs.existsSync(claudeArgsLogPath)).toBe(false);
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);
}); });
it('continues delegated headless launch when the local WebSearch tool runtime cannot be prepared', () => { it('fails closed for delegated headless launch when the local WebSearch tool runtime cannot be prepared', () => {
if (process.platform === 'win32') return; if (process.platform === 'win32') return;
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8'); fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
const result = runCcs(['glm', '-p', 'smoke'], baseEnv); const result = runCcs(['glm', '-p', 'smoke'], baseEnv);
expect(result.status).toBe(0); expect(result.status).not.toBe(0);
expect(result.stderr).toContain('could not prepare the local WebSearch tool'); expect(result.stderr).toContain('could not prepare the local WebSearch tool');
expect(result.stderr).toContain('This session will continue without local WebSearch'); expect(result.stderr).not.toContain('This session will continue without local WebSearch');
expect(fs.existsSync(claudeArgsLogPath)).toBe(true); expect(fs.existsSync(claudeArgsLogPath)).toBe(false);
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);
}); });
it('keeps launch non-fatal when WebSearch is disabled', () => { it('keeps launch non-fatal when WebSearch is disabled', () => {
@@ -145,7 +141,7 @@ exit 0
fs.writeFileSync( fs.writeFileSync(
path.join(ccsDir, 'config.yaml'), path.join(ccsDir, 'config.yaml'),
'version: 12\nwebsearch:\n enabled: false\n', ['version: 12', 'websearch:', ' enabled: false', ''].join('\n'),
'utf8' 'utf8'
); );
fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8'); fs.writeFileSync(path.join(ccsDir, 'hooks'), 'not-a-directory', 'utf8');
@@ -165,6 +161,11 @@ exit 0
it('writes a source-side launch trace for settings profiles when tracing is enabled', () => { it('writes a source-side launch trace for settings profiles when tracing is enabled', () => {
if (process.platform === 'win32') return; if (process.platform === 'win32') return;
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
['version: 12', 'websearch:', ' enabled: false', ''].join('\n'),
'utf8'
);
const tracePath = path.join(ccsDir, 'logs', 'websearch-trace.jsonl'); const tracePath = path.join(ccsDir, 'logs', 'websearch-trace.jsonl');
const result = runCcs(['glm', 'smoke'], { const result = runCcs(['glm', 'smoke'], {
...baseEnv, ...baseEnv,