mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 02:15:36 +00:00
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:
1 parent
fbbdd80830
commit
0e6965d205
12 files changed
+549
-46
No files matched your search
@@ -63,7 +63,7 @@ export {
|
||||
} from './websearch/status';
|
||||
|
||||
// Re-export profile hook injection
|
||||
export { ensureProfileHooks } from './websearch/profile-hook-injector';
|
||||
export { ensureProfileHooks, ensureProfileHooksOrThrow } from './websearch/profile-hook-injector';
|
||||
|
||||
// Import for local use
|
||||
import { clearGeminiCliCache, clearGrokCliCache, clearOpenCodeCliCache } from './websearch';
|
||||
|
||||
@@ -19,6 +19,21 @@ export { getHookPath, getWebSearchHookConfig } from './hook-config';
|
||||
// Hook file name
|
||||
const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
|
||||
|
||||
function hasMatchingHookContents(sourcePath: string, destinationPath: string): boolean {
|
||||
if (!fs.existsSync(destinationPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(sourcePath);
|
||||
const destination = fs.readFileSync(destinationPath);
|
||||
return source.equals(destination);
|
||||
}
|
||||
|
||||
function getTempHookPath(hookPath: string): string {
|
||||
const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
return `${hookPath}.${uniqueSuffix}.tmp`;
|
||||
}
|
||||
|
||||
export function getMigrationMarkerPath(): string {
|
||||
return path.join(getCcsDir(), '.hook-migrated');
|
||||
}
|
||||
@@ -94,9 +109,35 @@ export function installWebSearchHook(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy hook to ~/.ccs/hooks/
|
||||
fs.copyFileSync(sourcePath, hookPath);
|
||||
fs.chmodSync(hookPath, 0o755);
|
||||
// Avoid rewriting the shared hook binary when the bundled script is unchanged.
|
||||
if (hasMatchingHookContents(sourcePath, hookPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Copy hook to ~/.ccs/hooks/ via a unique temp path so concurrent installers
|
||||
// do not contend on the same file.
|
||||
const tempHookPath = getTempHookPath(hookPath);
|
||||
try {
|
||||
fs.copyFileSync(sourcePath, tempHookPath);
|
||||
fs.chmodSync(tempHookPath, 0o755);
|
||||
|
||||
try {
|
||||
fs.renameSync(tempHookPath, hookPath);
|
||||
} catch (renameError) {
|
||||
const errorCode = (renameError as NodeJS.ErrnoException).code;
|
||||
if (errorCode !== 'EEXIST' && errorCode !== 'EPERM') {
|
||||
throw renameError;
|
||||
}
|
||||
|
||||
fs.copyFileSync(tempHookPath, hookPath);
|
||||
fs.chmodSync(hookPath, 0o755);
|
||||
fs.unlinkSync(tempHookPath);
|
||||
}
|
||||
} finally {
|
||||
if (fs.existsSync(tempHookPath)) {
|
||||
fs.unlinkSync(tempHookPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(info(`Installed WebSearch hook: ${hookPath}`));
|
||||
|
||||
@@ -62,4 +62,4 @@ export {
|
||||
export { WEBSEARCH_API_KEY_PROVIDERS, getWebSearchApiKeyStates } from './provider-secrets';
|
||||
|
||||
// Profile Hook Injection
|
||||
export { ensureProfileHooks } from './profile-hook-injector';
|
||||
export { ensureProfileHooks, ensureProfileHooksOrThrow } from './profile-hook-injector';
|
||||
@@ -20,6 +20,21 @@ import { getMigrationMarkerPath, installWebSearchHook } from './hook-installer';
|
||||
// Valid profile name pattern (alphanumeric, dash, underscore only)
|
||||
const VALID_PROFILE_NAME = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
function hasUsableHookBinary(): boolean {
|
||||
try {
|
||||
const hookPath = getHookPath();
|
||||
const stat = fs.statSync(hookPath);
|
||||
if (!stat.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.accessSync(hookPath, fs.constants.R_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CCS WebSearch hook exists in settings
|
||||
*/
|
||||
@@ -83,25 +98,8 @@ export function ensureProfileHooks(profileName: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep the injected command target valid for all profile types, not just CLIProxy.
|
||||
if (!installWebSearchHook() && !fs.existsSync(getHookPath())) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn('WebSearch hook binary is missing and could not be installed'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// One-time migration from global settings
|
||||
migrateGlobalHook();
|
||||
|
||||
// Get CCS directory (respects CCS_HOME for test isolation)
|
||||
const ccsDir = getCcsDir();
|
||||
|
||||
// Ensure CCS dir exists
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
const settingsPath = path.join(ccsDir, `${profileName}.settings.json`);
|
||||
|
||||
// Read existing settings or create empty
|
||||
@@ -121,6 +119,25 @@ export function ensureProfileHooks(profileName: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the injected command target valid for all profile types, not just CLIProxy.
|
||||
// The installer already skips byte-identical copies, so we can always attempt a refresh
|
||||
// without rewriting unchanged hooks. Re-check existence after a failed install to tolerate
|
||||
// concurrent first-run installs that may have completed in another process.
|
||||
if (!installWebSearchHook() && !hasUsableHookBinary()) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error(warn('WebSearch hook binary is missing and could not be installed'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// One-time migration from global settings
|
||||
migrateGlobalHook();
|
||||
|
||||
// Ensure CCS dir exists before writing settings updates.
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
fs.mkdirSync(ccsDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
|
||||
// Check if CCS hook already present
|
||||
if (hasCcsHook(settings)) {
|
||||
// Clean up any duplicates that may have accumulated (Windows path bug fix)
|
||||
@@ -176,6 +193,19 @@ export function ensureProfileHooks(profileName: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureProfileHooksOrThrow(profileName: string): void {
|
||||
const wsConfig = getWebSearchConfig();
|
||||
if (!wsConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureProfileHooks(profileName)) {
|
||||
throw new Error(
|
||||
`WebSearch is enabled, but CCS could not prepare the profile hook for "${profileName}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update hook timeout if it differs from current config
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user