mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 20:13:30 +08:00
fix: harden install target filtering and MCP health probes
This commit is contained in:
@@ -905,7 +905,7 @@ Each component is fully independent.
|
|||||||
Yes. ECC is cross-platform:
|
Yes. ECC is cross-platform:
|
||||||
- **Cursor**: Pre-translated configs in `.cursor/`. See [Cursor IDE Support](#cursor-ide-support).
|
- **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.
|
- **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).
|
- **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).
|
- **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.
|
- **Claude Code**: Native — this is the primary target.
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"cursor",
|
"cursor",
|
||||||
"antigravity",
|
"antigravity",
|
||||||
"codex",
|
"codex",
|
||||||
"opencode",
|
|
||||||
"codebuddy"
|
"codebuddy"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000;
|
|||||||
const DEFAULT_TIMEOUT_MS = 5000;
|
const DEFAULT_TIMEOUT_MS = 5000;
|
||||||
const DEFAULT_BACKOFF_MS = 30 * 1000;
|
const DEFAULT_BACKOFF_MS = 30 * 1000;
|
||||||
const MAX_BACKOFF_MS = 10 * 60 * 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 RECONNECT_STATUS_CODES = new Set([401, 403, 429, 503]);
|
||||||
const FAILURE_PATTERNS = [
|
const FAILURE_PATTERNS = [
|
||||||
{ code: 401, pattern: /\b401\b|unauthori[sz]ed|auth(?:entication)?\s+(?:failed|expired|invalid)/i },
|
{ code: 401, pattern: /\b401\b|unauthori[sz]ed|auth(?:entication)?\s+(?:failed|expired|invalid)/i },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const path = require('path');
|
|||||||
const {
|
const {
|
||||||
createFlatRuleOperations,
|
createFlatRuleOperations,
|
||||||
createInstallTargetAdapter,
|
createInstallTargetAdapter,
|
||||||
|
isForeignPlatformPath,
|
||||||
} = require('./helpers');
|
} = require('./helpers');
|
||||||
|
|
||||||
module.exports = createInstallTargetAdapter({
|
module.exports = createInstallTargetAdapter({
|
||||||
@@ -30,18 +31,20 @@ module.exports = createInstallTargetAdapter({
|
|||||||
|
|
||||||
return modules.flatMap(module => {
|
return modules.flatMap(module => {
|
||||||
const paths = Array.isArray(module.paths) ? module.paths : [];
|
const paths = Array.isArray(module.paths) ? module.paths : [];
|
||||||
return paths.flatMap(sourceRelativePath => {
|
return paths
|
||||||
if (sourceRelativePath === 'rules') {
|
.filter(p => !isForeignPlatformPath(p, adapter.target))
|
||||||
return createFlatRuleOperations({
|
.flatMap(sourceRelativePath => {
|
||||||
moduleId: module.id,
|
if (sourceRelativePath === 'rules') {
|
||||||
repoRoot,
|
return createFlatRuleOperations({
|
||||||
sourceRelativePath,
|
moduleId: module.id,
|
||||||
destinationDir: path.join(targetRoot, 'rules'),
|
repoRoot,
|
||||||
});
|
sourceRelativePath,
|
||||||
}
|
destinationDir: path.join(targetRoot, 'rules'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
|
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const path = require('path');
|
|||||||
const {
|
const {
|
||||||
createFlatRuleOperations,
|
createFlatRuleOperations,
|
||||||
createInstallTargetAdapter,
|
createInstallTargetAdapter,
|
||||||
|
isForeignPlatformPath,
|
||||||
} = require('./helpers');
|
} = require('./helpers');
|
||||||
|
|
||||||
module.exports = createInstallTargetAdapter({
|
module.exports = createInstallTargetAdapter({
|
||||||
@@ -30,18 +31,20 @@ module.exports = createInstallTargetAdapter({
|
|||||||
|
|
||||||
return modules.flatMap(module => {
|
return modules.flatMap(module => {
|
||||||
const paths = Array.isArray(module.paths) ? module.paths : [];
|
const paths = Array.isArray(module.paths) ? module.paths : [];
|
||||||
return paths.flatMap(sourceRelativePath => {
|
return paths
|
||||||
if (sourceRelativePath === 'rules') {
|
.filter(p => !isForeignPlatformPath(p, adapter.target))
|
||||||
return createFlatRuleOperations({
|
.flatMap(sourceRelativePath => {
|
||||||
moduleId: module.id,
|
if (sourceRelativePath === 'rules') {
|
||||||
repoRoot,
|
return createFlatRuleOperations({
|
||||||
sourceRelativePath,
|
moduleId: module.id,
|
||||||
destinationDir: path.join(targetRoot, 'rules'),
|
repoRoot,
|
||||||
});
|
sourceRelativePath,
|
||||||
}
|
destinationDir: path.join(targetRoot, 'rules'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
|
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ const fs = require('fs');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
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) {
|
function normalizeRelativePath(relativePath) {
|
||||||
return String(relativePath || '')
|
return String(relativePath || '')
|
||||||
.replace(/\\/g, '/')
|
.replace(/\\/g, '/')
|
||||||
@@ -9,6 +18,18 @@ function normalizeRelativePath(relativePath) {
|
|||||||
.replace(/\/+$/, '');
|
.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 = {}) {
|
function resolveBaseRoot(scope, input = {}) {
|
||||||
if (scope === 'home') {
|
if (scope === 'home') {
|
||||||
return input.homeDir || os.homedir();
|
return input.homeDir || os.homedir();
|
||||||
@@ -260,21 +281,25 @@ function createInstallTargetAdapter(config) {
|
|||||||
if (Array.isArray(input.modules)) {
|
if (Array.isArray(input.modules)) {
|
||||||
return input.modules.flatMap(module => {
|
return input.modules.flatMap(module => {
|
||||||
const paths = Array.isArray(module.paths) ? module.paths : [];
|
const paths = Array.isArray(module.paths) ? module.paths : [];
|
||||||
return paths.map(sourceRelativePath => adapter.createScaffoldOperation(
|
return paths
|
||||||
module.id,
|
.filter(p => !isForeignPlatformPath(p, config.target))
|
||||||
sourceRelativePath,
|
.map(sourceRelativePath => adapter.createScaffoldOperation(
|
||||||
input
|
module.id,
|
||||||
));
|
sourceRelativePath,
|
||||||
|
input
|
||||||
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const module = input.module || {};
|
const module = input.module || {};
|
||||||
const paths = Array.isArray(module.paths) ? module.paths : [];
|
const paths = Array.isArray(module.paths) ? module.paths : [];
|
||||||
return paths.map(sourceRelativePath => adapter.createScaffoldOperation(
|
return paths
|
||||||
module.id,
|
.filter(p => !isForeignPlatformPath(p, config.target))
|
||||||
sourceRelativePath,
|
.map(sourceRelativePath => adapter.createScaffoldOperation(
|
||||||
input
|
module.id,
|
||||||
));
|
sourceRelativePath,
|
||||||
|
input
|
||||||
|
));
|
||||||
},
|
},
|
||||||
supportsModule(module, input = {}) {
|
supportsModule(module, input = {}) {
|
||||||
if (typeof config.supportsModule === 'function') {
|
if (typeof config.supportsModule === 'function') {
|
||||||
@@ -310,5 +335,6 @@ module.exports = {
|
|||||||
),
|
),
|
||||||
createNamespacedFlatRuleOperations,
|
createNamespacedFlatRuleOperations,
|
||||||
createRemappedOperation,
|
createRemappedOperation,
|
||||||
|
isForeignPlatformPath,
|
||||||
normalizeRelativePath,
|
normalizeRelativePath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
|
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const http = require('http');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
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');
|
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'mcp-health-check.js');
|
||||||
|
|
||||||
@@ -98,6 +99,17 @@ function runRawHook(rawInput, env = {}) {
|
|||||||
stderr: result.stderr || ''
|
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() {
|
async function runTests() {
|
||||||
console.log('\n=== Testing mcp-health-check.js ===\n');
|
console.log('\n=== Testing mcp-health-check.js ===\n');
|
||||||
|
|
||||||
@@ -288,6 +300,65 @@ async function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) 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}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user