From 8baffb4ad3b890afd04924629501fb78cf3c4c86 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 5 Apr 2026 13:59:42 -0700 Subject: [PATCH] fix: harden install target filtering and MCP health probes --- README.md | 2 +- manifests/install-modules.json | 1 - scripts/hooks/mcp-health-check.js | 2 +- .../lib/install-targets/codebuddy-project.js | 25 ++++--- scripts/lib/install-targets/cursor-project.js | 25 ++++--- scripts/lib/install-targets/helpers.js | 46 +++++++++--- tests/hooks/mcp-health-check.test.js | 73 ++++++++++++++++++- 7 files changed, 138 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4e3a62c3..f9e4bfd9 100644 --- a/README.md +++ b/README.md @@ -905,7 +905,7 @@ Each component is fully independent. Yes. ECC is cross-platform: - **Cursor**: Pre-translated configs in `.cursor/`. See [Cursor IDE Support](#cursor-ide-support). - **Gemini CLI**: Experimental project-local support via `.gemini/GEMINI.md` and shared installer plumbing. -- **OpenCode**: Full plugin support in `.opencode/`. See [OpenCode Support](#-opencode-support). +- **OpenCode**: Full plugin support in `.opencode/`. See [OpenCode Support](#opencode-support). - **Codex**: First-class support for both macOS app and CLI, with adapter drift guards and SessionStart fallback. See PR [#257](https://github.com/affaan-m/everything-claude-code/pull/257). - **Antigravity**: Tightly integrated setup for workflows, skills, and flattened rules in `.agent/`. See [Antigravity Guide](docs/ANTIGRAVITY-GUIDE.md). - **Claude Code**: Native — this is the primary target. diff --git a/manifests/install-modules.json b/manifests/install-modules.json index 8b46fd39..6e5fafb8 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -33,7 +33,6 @@ "cursor", "antigravity", "codex", - "opencode", "codebuddy" ], "dependencies": [], diff --git a/scripts/hooks/mcp-health-check.js b/scripts/hooks/mcp-health-check.js index 80a535e2..ec425148 100644 --- a/scripts/hooks/mcp-health-check.js +++ b/scripts/hooks/mcp-health-check.js @@ -24,7 +24,7 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 5000; const DEFAULT_BACKOFF_MS = 30 * 1000; const MAX_BACKOFF_MS = 10 * 60 * 1000; -const HEALTHY_HTTP_CODES = new Set([200, 201, 202, 204, 301, 302, 303, 304, 307, 308, 405]); +const HEALTHY_HTTP_CODES = new Set([200, 201, 202, 204, 301, 302, 303, 304, 307, 308, 400, 405]); const RECONNECT_STATUS_CODES = new Set([401, 403, 429, 503]); const FAILURE_PATTERNS = [ { code: 401, pattern: /\b401\b|unauthori[sz]ed|auth(?:entication)?\s+(?:failed|expired|invalid)/i }, diff --git a/scripts/lib/install-targets/codebuddy-project.js b/scripts/lib/install-targets/codebuddy-project.js index 7e1d71fa..100a133a 100644 --- a/scripts/lib/install-targets/codebuddy-project.js +++ b/scripts/lib/install-targets/codebuddy-project.js @@ -3,6 +3,7 @@ const path = require('path'); const { createFlatRuleOperations, createInstallTargetAdapter, + isForeignPlatformPath, } = require('./helpers'); module.exports = createInstallTargetAdapter({ @@ -30,18 +31,20 @@ module.exports = createInstallTargetAdapter({ return modules.flatMap(module => { const paths = Array.isArray(module.paths) ? module.paths : []; - return paths.flatMap(sourceRelativePath => { - if (sourceRelativePath === 'rules') { - return createFlatRuleOperations({ - moduleId: module.id, - repoRoot, - sourceRelativePath, - destinationDir: path.join(targetRoot, 'rules'), - }); - } + return paths + .filter(p => !isForeignPlatformPath(p, adapter.target)) + .flatMap(sourceRelativePath => { + if (sourceRelativePath === 'rules') { + return createFlatRuleOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath, + destinationDir: path.join(targetRoot, 'rules'), + }); + } - return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; - }); + return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; + }); }); }, }); diff --git a/scripts/lib/install-targets/cursor-project.js b/scripts/lib/install-targets/cursor-project.js index d3dd4e7b..527ba2a6 100644 --- a/scripts/lib/install-targets/cursor-project.js +++ b/scripts/lib/install-targets/cursor-project.js @@ -3,6 +3,7 @@ const path = require('path'); const { createFlatRuleOperations, createInstallTargetAdapter, + isForeignPlatformPath, } = require('./helpers'); module.exports = createInstallTargetAdapter({ @@ -30,18 +31,20 @@ module.exports = createInstallTargetAdapter({ return modules.flatMap(module => { const paths = Array.isArray(module.paths) ? module.paths : []; - return paths.flatMap(sourceRelativePath => { - if (sourceRelativePath === 'rules') { - return createFlatRuleOperations({ - moduleId: module.id, - repoRoot, - sourceRelativePath, - destinationDir: path.join(targetRoot, 'rules'), - }); - } + return paths + .filter(p => !isForeignPlatformPath(p, adapter.target)) + .flatMap(sourceRelativePath => { + if (sourceRelativePath === 'rules') { + return createFlatRuleOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath, + destinationDir: path.join(targetRoot, 'rules'), + }); + } - return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; - }); + return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; + }); }); }, }); diff --git a/scripts/lib/install-targets/helpers.js b/scripts/lib/install-targets/helpers.js index ceb0d903..fd959aa7 100644 --- a/scripts/lib/install-targets/helpers.js +++ b/scripts/lib/install-targets/helpers.js @@ -2,6 +2,15 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); +const PLATFORM_SOURCE_PATH_OWNERS = Object.freeze({ + '.claude-plugin': 'claude', + '.codex': 'codex', + '.cursor': 'cursor', + '.gemini': 'gemini', + '.opencode': 'opencode', + '.codebuddy': 'codebuddy', +}); + function normalizeRelativePath(relativePath) { return String(relativePath || '') .replace(/\\/g, '/') @@ -9,6 +18,18 @@ function normalizeRelativePath(relativePath) { .replace(/\/+$/, ''); } +function isForeignPlatformPath(sourceRelativePath, adapterTarget) { + const normalizedPath = normalizeRelativePath(sourceRelativePath); + + for (const [prefix, ownerTarget] of Object.entries(PLATFORM_SOURCE_PATH_OWNERS)) { + if (normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`)) { + return ownerTarget !== adapterTarget; + } + } + + return false; +} + function resolveBaseRoot(scope, input = {}) { if (scope === 'home') { return input.homeDir || os.homedir(); @@ -260,21 +281,25 @@ function createInstallTargetAdapter(config) { if (Array.isArray(input.modules)) { return input.modules.flatMap(module => { const paths = Array.isArray(module.paths) ? module.paths : []; - return paths.map(sourceRelativePath => adapter.createScaffoldOperation( - module.id, - sourceRelativePath, - input - )); + return paths + .filter(p => !isForeignPlatformPath(p, config.target)) + .map(sourceRelativePath => adapter.createScaffoldOperation( + module.id, + sourceRelativePath, + input + )); }); } const module = input.module || {}; const paths = Array.isArray(module.paths) ? module.paths : []; - return paths.map(sourceRelativePath => adapter.createScaffoldOperation( - module.id, - sourceRelativePath, - input - )); + return paths + .filter(p => !isForeignPlatformPath(p, config.target)) + .map(sourceRelativePath => adapter.createScaffoldOperation( + module.id, + sourceRelativePath, + input + )); }, supportsModule(module, input = {}) { if (typeof config.supportsModule === 'function') { @@ -310,5 +335,6 @@ module.exports = { ), createNamespacedFlatRuleOperations, createRemappedOperation, + isForeignPlatformPath, normalizeRelativePath, }; diff --git a/tests/hooks/mcp-health-check.test.js b/tests/hooks/mcp-health-check.test.js index 4404002a..90b62d40 100644 --- a/tests/hooks/mcp-health-check.test.js +++ b/tests/hooks/mcp-health-check.test.js @@ -6,9 +6,10 @@ const assert = require('assert'); const fs = require('fs'); +const http = require('http'); const os = require('os'); const path = require('path'); -const { spawnSync } = require('child_process'); +const { spawn, spawnSync } = require('child_process'); const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'mcp-health-check.js'); @@ -98,6 +99,17 @@ function runRawHook(rawInput, env = {}) { stderr: result.stderr || '' }; } + +function waitForFile(filePath, timeoutMs = 5000) { + const started = Date.now(); + while (Date.now() - started < timeoutMs) { + if (fs.existsSync(filePath)) { + return fs.readFileSync(filePath, 'utf8'); + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25); + } + throw new Error(`Timed out waiting for ${filePath}`); +} async function runTests() { console.log('\n=== Testing mcp-health-check.js ===\n'); @@ -288,6 +300,65 @@ async function runTests() { } })) passed++; else failed++; + if (await asyncTest('treats HTTP 400 probe responses as healthy reachable servers', async () => { + const tempDir = createTempDir(); + const configPath = path.join(tempDir, 'claude.json'); + const statePath = path.join(tempDir, 'mcp-health.json'); + const serverScript = path.join(tempDir, 'http-400-server.js'); + const portFile = path.join(tempDir, 'server-port.txt'); + + fs.writeFileSync( + serverScript, + [ + "const fs = require('fs');", + "const http = require('http');", + "const portFile = process.argv[2];", + "const server = http.createServer((_req, res) => {", + " res.writeHead(400, { 'Content-Type': 'application/json' });", + " res.end(JSON.stringify({ error: 'invalid MCP request' }));", + "});", + "server.listen(0, '127.0.0.1', () => {", + " fs.writeFileSync(portFile, String(server.address().port));", + "});", + "setInterval(() => {}, 1000);" + ].join('\n') + ); + + const serverProcess = spawn(process.execPath, [serverScript, portFile], { + stdio: 'ignore' + }); + + try { + const port = waitForFile(portFile).trim(); + + writeConfig(configPath, { + mcpServers: { + github: { + type: 'http', + url: `http://127.0.0.1:${port}/mcp` + } + } + }); + + const input = { tool_name: 'mcp__github__search_repositories', tool_input: {} }; + const result = runHook(input, { + CLAUDE_HOOK_EVENT_NAME: 'PreToolUse', + ECC_MCP_CONFIG_PATH: configPath, + ECC_MCP_HEALTH_STATE_PATH: statePath, + ECC_MCP_HEALTH_TIMEOUT_MS: '500' + }); + + assert.strictEqual(result.code, 0, `Expected HTTP 400 probe to be treated as healthy, got ${result.code}`); + assert.strictEqual(result.stdout.trim(), JSON.stringify(input), 'Expected original JSON on stdout'); + + const state = readState(statePath); + assert.strictEqual(state.servers.github.status, 'healthy', 'Expected HTTP MCP server to be marked healthy'); + } finally { + serverProcess.kill('SIGTERM'); + cleanupTempDir(tempDir); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }