fix(websearch): harden settings-profile hook setup

- fail enabled settings-profile create and launch flows when hook preparation is still unusable

- refresh stale shared hook binaries without rewriting unchanged installs or failing benign races

- add rollback and regression coverage for disabled, stale, invalid, copy, import, and launch paths
This commit is contained in:
Tam Nhu Tran committed 2026-03-28 09:51:13 -04:00
1 parent fbbdd80830
commit 0e6965d205
12 files changed
+549 -46

No files matched your search

+14 -4
View File
@@ -9,7 +9,7 @@ import * as path from 'path';
import type { Config, Settings } from '../../types';
import type { TargetType } from '../../targets/target-adapter';
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector';
import { ensureProfileHooksOrThrow } from '../../utils/websearch/profile-hook-injector';
import { isSensitiveKey } from '../../utils/sensitive-keys';
import { isReservedName } from '../../config/reserved-names';
import { isUnifiedMode, mutateUnifiedConfig } from '../../config/unified-config-loader';
@@ -216,8 +216,8 @@ export function registerApiProfileOrphans(options?: {
}
try {
ensureProfileHooksOrThrow(orphan.name);
registerApiProfileInConfig(orphan.name, options?.target || 'claude', options?.force || false);
ensureProfileHooks(orphan.name);
result.registered.push(orphan.name);
} catch (error) {
result.skipped.push({ name: orphan.name, reason: (error as Error).message });
@@ -264,7 +264,12 @@ export function copyApiProfile(
: null;
writeJsonObjectAtomically(destinationSettingsPath, sourceSettings);
ensureProfileHooks(destination);
try {
ensureProfileHooksOrThrow(destination);
} catch (hookError) {
rollbackSettingsFile(destinationSettingsPath, previousDestinationContent, destinationExisted);
throw hookError;
}
try {
registerApiProfileInConfig(
destination,
@@ -384,7 +389,12 @@ export function importApiProfileBundle(
const previousSettingsContent = settingsExisted ? fs.readFileSync(settingsPath, 'utf8') : null;
writeJsonObjectAtomically(settingsPath, settings);
ensureProfileHooks(name);
try {
ensureProfileHooksOrThrow(name);
} catch (hookError) {
rollbackSettingsFile(settingsPath, previousSettingsContent, settingsExisted);
throw hookError;
}
try {
registerApiProfileInConfig(name, options?.target || bundleTarget || 'claude', options?.force);
} catch (registrationError) {
+34 -5
View File
@@ -8,7 +8,7 @@ import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-man
import { expandPath } from '../../utils/helpers';
import { validateApiName } from './validation-service';
import { mutateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector';
import { ensureProfileHooksOrThrow } from '../../utils/websearch/profile-hook-injector';
import type { TargetType } from '../../targets/target-adapter';
import { resolveDroidProvider } from '../../targets/droid-provider';
import { isReservedName } from '../../config/reserved-names';
@@ -69,6 +69,21 @@ function getDeniedModelReason(baseUrl: string, models: ModelMapping): string | n
return null;
}
function rollbackSettingsFile(
filePath: string,
previousContent: string | null,
existedBefore: boolean
): void {
if (existedBefore && previousContent !== null) {
fs.writeFileSync(filePath, previousContent, 'utf8');
return;
}
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
/** Create settings.json file for API profile (legacy format) */
function createSettingsFile(
name: string,
@@ -105,11 +120,18 @@ function createSettingsFile(
},
};
const settingsExisted = fs.existsSync(settingsPath);
const previousSettingsContent = settingsExisted ? fs.readFileSync(settingsPath, 'utf8') : null;
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
// Inject WebSearch hooks into profile settings
ensureProfileHooks(name);
try {
// Inject WebSearch hooks into profile settings
ensureProfileHooksOrThrow(name);
} catch (error) {
rollbackSettingsFile(settingsPath, previousSettingsContent, settingsExisted);
throw error;
}
return settingsPath;
}
@@ -189,10 +211,17 @@ function createApiProfileUnified(
fs.mkdirSync(ccsDir, { recursive: true });
}
const settingsExisted = fs.existsSync(settingsPath);
const previousSettingsContent = settingsExisted ? fs.readFileSync(settingsPath, 'utf8') : null;
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
// Inject WebSearch hooks into profile settings
ensureProfileHooks(name);
try {
// Inject WebSearch hooks into profile settings
ensureProfileHooksOrThrow(name);
} catch (error) {
rollbackSettingsFile(settingsPath, previousSettingsContent, settingsExisted);
throw error;
}
mutateUnifiedConfig((config) => {
config.profiles[name] = {