fix(proxy): clean up temp launch settings

This commit is contained in:
Tam Nhu Tran
2026-04-28 11:59:37 -04:00
parent 3dcf150978
commit 47fbe8bb3a
6 changed files with 93 additions and 15 deletions
+4 -4
View File
@@ -82,7 +82,7 @@ import { tryHandleRootCommand } from './commands/root-command-router';
// Import extracted utility functions // Import extracted utility functions
import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor'; import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor';
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation'; import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
import { createOpenAICompatLaunchSettingsPath } from './utils/openai-compat-launch-settings'; import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings';
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning'; import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
import { createLogger } from './services/logging'; import { createLogger } from './services/logging';
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides'; import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
@@ -1460,14 +1460,14 @@ async function main(): Promise<void> {
), ),
}; };
delete proxyEnv.ANTHROPIC_API_KEY; delete proxyEnv.ANTHROPIC_API_KEY;
const launchSettingsPath = createOpenAICompatLaunchSettingsPath( const launchSettings = createOpenAICompatLaunchSettings(
expandedSettingsPath, expandedSettingsPath,
settings settings
); );
const launchArgs = [ const launchArgs = [
'--settings', '--settings',
launchSettingsPath, launchSettings.settingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs), ...appendThirdPartyWebSearchToolArgs(browserArgs),
]; ];
const traceEnv = createWebSearchTraceContext({ const traceEnv = createWebSearchTraceContext({
@@ -1478,7 +1478,7 @@ async function main(): Promise<void> {
settingsPath: expandedSettingsPath, settingsPath: expandedSettingsPath,
}); });
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }); execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }, launchSettings.cleanup);
return; return;
} }
const launchArgs = [ const launchArgs = [
+18 -5
View File
@@ -23,7 +23,7 @@ import {
stripAnthropicRoutingEnv, stripAnthropicRoutingEnv,
stripClaudeCodeEnv, stripClaudeCodeEnv,
} from '../utils/shell-executor'; } from '../utils/shell-executor';
import { createOpenAICompatLaunchSettingsPath } from '../utils/openai-compat-launch-settings'; import { createOpenAICompatLaunchSettings } from '../utils/openai-compat-launch-settings';
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance'; import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
import { import {
appendThirdPartyImageAnalysisToolArgs, appendThirdPartyImageAnalysisToolArgs,
@@ -256,12 +256,12 @@ export class HeadlessExecutor {
// Smart slash command detection and preservation // Smart slash command detection and preservation
const processedPrompt = this._processSlashCommand(enhancedPrompt); const processedPrompt = this._processSlashCommand(enhancedPrompt);
const launchSettingsPath = openAICompatProfile const launchSettings = openAICompatProfile
? createOpenAICompatLaunchSettingsPath(settingsPath, settings) ? createOpenAICompatLaunchSettings(settingsPath, settings)
: settingsPath; : { settingsPath, cleanup: () => {} };
// Prepare arguments // Prepare arguments
const args: string[] = ['-p', processedPrompt, '--settings', launchSettingsPath]; const args: string[] = ['-p', processedPrompt, '--settings', launchSettings.settingsPath];
// Always use stream-json for real-time progress visibility // Always use stream-json for real-time progress visibility
args.push('--output-format', 'stream-json', '--verbose'); args.push('--output-format', 'stream-json', '--verbose');
@@ -373,6 +373,7 @@ export class HeadlessExecutor {
imageAnalysisEnv, imageAnalysisEnv,
runtimeEnvVars, runtimeEnvVars,
traceEnv, traceEnv,
launchCleanup: launchSettings.cleanup,
}); });
} }
@@ -393,6 +394,7 @@ export class HeadlessExecutor {
imageAnalysisEnv?: Record<string, string>; imageAnalysisEnv?: Record<string, string>;
runtimeEnvVars?: NodeJS.ProcessEnv; runtimeEnvVars?: NodeJS.ProcessEnv;
traceEnv?: Record<string, string>; traceEnv?: Record<string, string>;
launchCleanup?: () => void;
} }
): Promise<ExecutionResult> { ): Promise<ExecutionResult> {
const { const {
@@ -406,6 +408,7 @@ export class HeadlessExecutor {
imageAnalysisEnv = {}, imageAnalysisEnv = {},
runtimeEnvVars = {}, runtimeEnvVars = {},
traceEnv = {}, traceEnv = {},
launchCleanup = () => {},
} = ctx; } = ctx;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -443,6 +446,14 @@ export class HeadlessExecutor {
let progressInterval: NodeJS.Timeout | undefined; let progressInterval: NodeJS.Timeout | undefined;
const messages: StreamMessage[] = []; const messages: StreamMessage[] = [];
let timedOut = false; let timedOut = false;
let cleanedUp = false;
const cleanupLaunchArtifacts = () => {
if (cleanedUp) {
return;
}
cleanedUp = true;
launchCleanup();
};
// Setup signal handlers for cleanup // Setup signal handlers for cleanup
const cleanupHandler = () => { const cleanupHandler = () => {
@@ -501,6 +512,7 @@ export class HeadlessExecutor {
// Handle completion // Handle completion
proc.on('close', (exitCode: number | null) => { proc.on('close', (exitCode: number | null) => {
cleanupLaunchArtifacts();
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
if (progressInterval) { if (progressInterval) {
@@ -593,6 +605,7 @@ export class HeadlessExecutor {
// Handle errors // Handle errors
proc.on('error', (error: Error) => { proc.on('error', (error: Error) => {
cleanupLaunchArtifacts();
if (progressInterval) clearInterval(progressInterval); if (progressInterval) clearInterval(progressInterval);
reject(new Error(`Failed to execute Claude CLI: ${error.message}`)); reject(new Error(`Failed to execute Claude CLI: ${error.message}`));
}); });
+20 -3
View File
@@ -5,10 +5,15 @@ import * as path from 'path';
import type { Settings } from '../types/config'; import type { Settings } from '../types/config';
import { stripAnthropicRoutingEnv } from './shell-executor'; import { stripAnthropicRoutingEnv } from './shell-executor';
export function createOpenAICompatLaunchSettingsPath( export interface OpenAICompatLaunchSettings {
settingsPath: string;
cleanup: () => void;
}
export function createOpenAICompatLaunchSettings(
settingsPath: string, settingsPath: string,
settings: Settings settings: Settings
): string { ): OpenAICompatLaunchSettings {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-openai-compat-settings-')); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-openai-compat-settings-'));
fs.chmodSync(tempDir, 0o700); fs.chmodSync(tempDir, 0o700);
@@ -31,5 +36,17 @@ export function createOpenAICompatLaunchSettingsPath(
mode: 0o600, mode: 0o600,
}); });
return launchSettingsPath; let cleanedUp = false;
const cleanup = (): void => {
if (cleanedUp) {
return;
}
cleanedUp = true;
fs.rmSync(tempDir, { recursive: true, force: true });
};
return {
settingsPath: launchSettingsPath,
cleanup,
};
} }
+13 -1
View File
@@ -208,7 +208,8 @@ export function getWindowsEscapedCommandShell(): SpawnOptions['shell'] {
export function execClaude( export function execClaude(
claudeCli: string, claudeCli: string,
args: string[], args: string[],
envVars: NodeJS.ProcessEnv | null = null envVars: NodeJS.ProcessEnv | null = null,
onExitCleanup?: () => void
): void { ): void {
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli); const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
@@ -286,6 +287,17 @@ export function execClaude(
}); });
} }
let cleanedUp = false;
const runExitCleanup = (): void => {
if (cleanedUp) {
return;
}
cleanedUp = true;
onExitCleanup?.();
};
child.once('exit', runExitCleanup);
child.once('error', runExitCleanup);
wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => { wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
if (err.code === 'EACCES') { if (err.code === 'EACCES') {
console.error(`[X] Claude CLI is not executable: ${claudeCli}`); console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
@@ -92,6 +92,8 @@ describe('settings profile browser launch', () => {
let fakeClaudePath = ''; let fakeClaudePath = '';
let claudeArgsLogPath = ''; let claudeArgsLogPath = '';
let claudeEnvLogPath = ''; let claudeEnvLogPath = '';
let claudeSettingsPathLogPath = '';
let claudeSettingsSnapshotPath = '';
let browserProfileDir = ''; let browserProfileDir = '';
let devtoolsServer: ChildProcess | undefined; let devtoolsServer: ChildProcess | undefined;
let baseEnv: NodeJS.ProcessEnv; let baseEnv: NodeJS.ProcessEnv;
@@ -107,6 +109,8 @@ describe('settings profile browser launch', () => {
fakeClaudePath = path.join(tmpHome, 'fake-claude.sh'); fakeClaudePath = path.join(tmpHome, 'fake-claude.sh');
claudeArgsLogPath = path.join(tmpHome, 'claude-args.txt'); claudeArgsLogPath = path.join(tmpHome, 'claude-args.txt');
claudeEnvLogPath = path.join(tmpHome, 'claude-env.txt'); claudeEnvLogPath = path.join(tmpHome, 'claude-env.txt');
claudeSettingsPathLogPath = path.join(tmpHome, 'claude-settings-path.txt');
claudeSettingsSnapshotPath = path.join(tmpHome, 'claude-settings-snapshot.json');
browserProfileDir = path.join(tmpHome, 'chrome-user-data'); browserProfileDir = path.join(tmpHome, 'chrome-user-data');
fs.mkdirSync(ccsDir, { recursive: true }); fs.mkdirSync(ccsDir, { recursive: true });
@@ -138,6 +142,19 @@ describe('settings profile browser launch', () => {
fakeClaudePath, fakeClaudePath,
`#!/bin/sh `#!/bin/sh
printf "%s\n" "$@" > "${claudeArgsLogPath}" printf "%s\n" "$@" > "${claudeArgsLogPath}"
settings_path=""
prev=""
for arg in "$@"; do
if [ "$prev" = "--settings" ]; then
settings_path="$arg"
break
fi
prev="$arg"
done
printf "%s" "$settings_path" > "${claudeSettingsPathLogPath}"
if [ -n "$settings_path" ] && [ -f "$settings_path" ]; then
cat "$settings_path" > "${claudeSettingsSnapshotPath}"
fi
{ {
printf "userDataDir=%s\n" "$CCS_BROWSER_USER_DATA_DIR" printf "userDataDir=%s\n" "$CCS_BROWSER_USER_DATA_DIR"
printf "legacyProfileDir=%s\n" "$CCS_BROWSER_PROFILE_DIR" printf "legacyProfileDir=%s\n" "$CCS_BROWSER_PROFILE_DIR"
@@ -308,7 +325,7 @@ exit 0
expect(launchSettingsPath).not.toBe(settingsPath); expect(launchSettingsPath).not.toBe(settingsPath);
const persistedLaunchSettings = JSON.parse( const persistedLaunchSettings = JSON.parse(
fs.readFileSync(launchSettingsPath as string, 'utf8') fs.readFileSync(claudeSettingsSnapshotPath, 'utf8')
) as { ) as {
env?: Record<string, string>; env?: Record<string, string>;
hooks?: { hooks?: {
@@ -333,6 +350,8 @@ exit 0
expect(launchedEnv).not.toContain('anthropicBaseUrl=https://api.openai.com/v1'); expect(launchedEnv).not.toContain('anthropicBaseUrl=https://api.openai.com/v1');
expect(launchedEnv).toContain('anthropicModel=gpt-5.4'); expect(launchedEnv).toContain('anthropicModel=gpt-5.4');
expect(launchedEnv).toContain('maxOutputTokens=12345'); expect(launchedEnv).toContain('maxOutputTokens=12345');
expect(fs.readFileSync(claudeSettingsPathLogPath, 'utf8')).toBe(launchSettingsPath);
expect(fs.existsSync(launchSettingsPath as string)).toBe(false);
}); });
it('does not auto-enable browser reuse for settings-profile launches from env overrides alone', async () => { it('does not auto-enable browser reuse for settings-profile launches from env overrides alone', async () => {
@@ -31,6 +31,7 @@ const STEERING_PROMPT_SNIPPET =
'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches'; 'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches';
const spawnCalls: SpawnCall[] = []; const spawnCalls: SpawnCall[] = [];
const spawnSyncCalls: SpawnSyncCall[] = []; const spawnSyncCalls: SpawnSyncCall[] = [];
const launchSettingsSnapshots: Array<{ path: string; content: string }> = [];
const originalPlatform = process.platform; const originalPlatform = process.platform;
let baselineSigintListeners: Array<(...args: unknown[]) => void> = []; let baselineSigintListeners: Array<(...args: unknown[]) => void> = [];
let baselineSigtermListeners: Array<(...args: unknown[]) => void> = []; let baselineSigtermListeners: Array<(...args: unknown[]) => void> = [];
@@ -99,6 +100,16 @@ function registerChildProcessMock(): void {
} }
spawnCalls.push({ command, args, options }); spawnCalls.push({ command, args, options });
const settingsIndex = args.indexOf('--settings');
if (settingsIndex >= 0) {
const settingsPath = args[settingsIndex + 1];
if (settingsPath && fs.existsSync(settingsPath)) {
launchSettingsSnapshots.push({
path: settingsPath,
content: fs.readFileSync(settingsPath, 'utf8'),
});
}
}
const child = createMockChild(); const child = createMockChild();
setTimeout(() => child.emit('close', 0), 0); setTimeout(() => child.emit('close', 0), 0);
@@ -193,6 +204,7 @@ describe('CLAUDECODE environment stripping', () => {
beforeEach(() => { beforeEach(() => {
spawnCalls.length = 0; spawnCalls.length = 0;
spawnSyncCalls.length = 0; spawnSyncCalls.length = 0;
launchSettingsSnapshots.length = 0;
process.env.CCS_QUIET = '1'; process.env.CCS_QUIET = '1';
// Save original env values for restoration in afterEach // Save original env values for restoration in afterEach
@@ -695,8 +707,12 @@ describe('CLAUDECODE environment stripping', () => {
const launchSettingsPath = args[settingsIndex + 1]; const launchSettingsPath = args[settingsIndex + 1];
expect(launchSettingsPath).toBeDefined(); expect(launchSettingsPath).toBeDefined();
expect(launchSettingsPath).not.toBe(path.join(ccsDir, 'bridge.settings.json')); expect(launchSettingsPath).not.toBe(path.join(ccsDir, 'bridge.settings.json'));
const launchSettingsSnapshot = launchSettingsSnapshots.find(
(snapshot) => snapshot.path === launchSettingsPath
);
expect(launchSettingsSnapshot).toBeDefined();
const persistedLaunchSettings = JSON.parse(fs.readFileSync(launchSettingsPath, 'utf8')) as { const persistedLaunchSettings = JSON.parse(launchSettingsSnapshot?.content || '{}') as {
env?: Record<string, string>; env?: Record<string, string>;
hooks?: { hooks?: {
PreToolUse?: Array<{ PreToolUse?: Array<{
@@ -713,6 +729,7 @@ describe('CLAUDECODE environment stripping', () => {
expect( expect(
persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command
).toBe('echo headless-bridge-hook'); ).toBe('echo headless-bridge-hook');
expect(fs.existsSync(launchSettingsPath)).toBe(false);
}); });
it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => { it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => {