atabey 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +8 -14
- package/README.md +163 -14
- package/bin/cli.js +72 -5
- package/dist/framework-mcp/src/index.js +76 -3
- package/dist/framework-mcp/src/index.js.map +1 -1
- package/dist/framework-mcp/src/tools/messaging/approve_operation.d.ts +8 -8
- package/dist/framework-mcp/src/tools/messaging/approve_operation.js +11 -11
- package/dist/framework-mcp/src/tools/messaging/approve_operation.js.map +1 -1
- package/dist/framework-mcp/src/tools/messaging/ask_human.d.ts +8 -8
- package/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
- package/dist/framework-mcp/src/tools/observability/check_ports.d.ts +1 -0
- package/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
- package/dist/framework-mcp/src/tools/observability/check_ports.js.map +1 -1
- package/dist/framework-mcp/src/tools/shell/run_command.js +138 -92
- package/dist/framework-mcp/src/tools/shell/run_command.js.map +1 -1
- package/dist/framework-mcp/src/utils/auth.js +4 -4
- package/dist/framework-mcp/src/utils/auth.js.map +1 -1
- package/dist/framework-mcp/src/utils/discipline.js +1 -1
- package/dist/framework-mcp/src/utils/discipline.js.map +1 -1
- package/dist/framework-mcp/src/utils/human-in-loop.js +1 -1
- package/dist/framework-mcp/src/utils/human-in-loop.js.map +1 -1
- package/dist/framework-mcp/src/utils/loop-detector.js +3 -3
- package/dist/framework-mcp/src/utils/loop-detector.js.map +1 -1
- package/dist/framework-mcp/src/utils/quality.js +4 -3
- package/dist/framework-mcp/src/utils/quality.js.map +1 -1
- package/dist/framework-mcp/src/utils/silent-router.d.ts +4 -2
- package/dist/framework-mcp/src/utils/silent-router.js +35 -9
- package/dist/framework-mcp/src/utils/silent-router.js.map +1 -1
- package/dist/framework-mcp/tests/tools/observability/check_ports.test.js +26 -12
- package/dist/framework-mcp/tests/tools/observability/check_ports.test.js.map +1 -1
- package/dist/framework-mcp/tests/tools/shell/run_command.test.js +38 -15
- package/dist/framework-mcp/tests/tools/shell/run_command.test.js.map +1 -1
- package/dist/src/cli/commands/init.js +14 -7
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/mcp.js +3 -1
- package/dist/src/cli/commands/mcp.js.map +1 -1
- package/dist/src/cli/commands/script.js +8 -1
- package/dist/src/cli/commands/script.js.map +1 -1
- package/dist/src/modules/engines/agent-executor.js +11 -1
- package/dist/src/modules/engines/agent-executor.js.map +1 -1
- package/dist/src/modules/engines/agent-loop.d.ts +5 -5
- package/dist/src/modules/engines/agent-loop.js +5 -5
- package/dist/src/modules/engines/evaluation-engine.d.ts +12 -2
- package/dist/src/modules/engines/evaluation-engine.js +76 -9
- package/dist/src/modules/engines/evaluation-engine.js.map +1 -1
- package/dist/src/shared/pii.js +1 -1
- package/dist/src/shared/pii.js.map +1 -1
- package/dist/src/shared/storage.d.ts +43 -0
- package/dist/src/shared/storage.js +1 -1
- package/dist/src/shared/storage.js.map +1 -1
- package/dist/tests/modules/engines/agent-executor.test.js +1 -1
- package/dist/tests/modules/engines/agent-executor.test.js.map +1 -1
- package/framework-mcp/dist/dashboard/assets/{index-B2mYld0c.js → index-B_rK57vi.js} +17 -17
- package/framework-mcp/dist/dashboard/index.html +1 -1
- package/framework-mcp/dist/framework-mcp/src/index.js +76 -3
- package/framework-mcp/dist/framework-mcp/src/tools/messaging/approve_operation.js +11 -11
- package/framework-mcp/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
- package/framework-mcp/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
- package/framework-mcp/dist/framework-mcp/src/tools/shell/run_command.js +138 -92
- package/framework-mcp/dist/framework-mcp/src/utils/auth.js +4 -4
- package/framework-mcp/dist/framework-mcp/src/utils/discipline.js +1 -1
- package/framework-mcp/dist/framework-mcp/src/utils/human-in-loop.js +1 -1
- package/framework-mcp/dist/framework-mcp/src/utils/loop-detector.js +3 -3
- package/framework-mcp/dist/framework-mcp/src/utils/quality.js +4 -3
- package/framework-mcp/dist/framework-mcp/src/utils/silent-router.js +35 -9
- package/framework-mcp/dist/src/modules/engines/evaluation-engine.js +169 -0
- package/framework-mcp/dist/src/shared/pii.js +1 -1
- package/framework-mcp/dist/src/shared/storage.js +1 -1
- package/framework-mcp/package.json +1 -1
- package/mcp.json +3 -2
- package/package.json +3 -4
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-B_rK57vi.js"></script>
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -16,6 +16,28 @@ import { RESOURCES, handleReadResource } from "./resources/index.js";
|
|
|
16
16
|
import { TOOLS, toolHandlers, toolSchemas } from "./tools/index.js";
|
|
17
17
|
// ─── Paths ────────────────────────────────────────────────────────
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
// ─── Friendly Startup Validation ──────────────────────────────────
|
|
20
|
+
function validateEnvironment() {
|
|
21
|
+
// Node.js version check
|
|
22
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
23
|
+
if (nodeMajor < 18) {
|
|
24
|
+
process.stderr.write(`
|
|
25
|
+
╔══════════════════════════════════════════════════════════╗
|
|
26
|
+
║ ⚠️ Unsupported Node.js version ║
|
|
27
|
+
╠══════════════════════════════════════════════════════════╣
|
|
28
|
+
║ Atabey MCP requires Node.js >= 18.0.0 ║
|
|
29
|
+
║ ║
|
|
30
|
+
║ Current version: ${process.versions.node} ║
|
|
31
|
+
║ ║
|
|
32
|
+
║ 📋 To fix this: ║
|
|
33
|
+
║ nvm install 18 && nvm use 18 (if using nvm) ║
|
|
34
|
+
║ brew install node (if using Homebrew) ║
|
|
35
|
+
║ https://nodejs.org (official installer) ║
|
|
36
|
+
╚══════════════════════════════════════════════════════════╝
|
|
37
|
+
`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
19
41
|
function findPackageJson(startDir) {
|
|
20
42
|
let currentDir = startDir;
|
|
21
43
|
while (currentDir !== path.parse(currentDir).root) {
|
|
@@ -24,16 +46,68 @@ function findPackageJson(startDir) {
|
|
|
24
46
|
return pkgPath;
|
|
25
47
|
currentDir = path.dirname(currentDir);
|
|
26
48
|
}
|
|
27
|
-
|
|
49
|
+
process.stderr.write(`
|
|
50
|
+
╔══════════════════════════════════════════════════════════╗
|
|
51
|
+
║ 🚧 Package not found ║
|
|
52
|
+
╠══════════════════════════════════════════════════════════╣
|
|
53
|
+
║ Atabey MCP package.json could not be located. ║
|
|
54
|
+
║ ║
|
|
55
|
+
║ This usually means: ║
|
|
56
|
+
║ 1. The package was not installed correctly ║
|
|
57
|
+
║ 2. Node modules are missing ║
|
|
58
|
+
║ ║
|
|
59
|
+
║ 📋 To fix this: ║
|
|
60
|
+
║ npm install -g atabey (global install) ║
|
|
61
|
+
║ npm install (local install) ║
|
|
62
|
+
║ npm run build (if developing) ║
|
|
63
|
+
╚══════════════════════════════════════════════════════════╝
|
|
64
|
+
`);
|
|
65
|
+
process.exit(1);
|
|
28
66
|
}
|
|
67
|
+
validateEnvironment();
|
|
29
68
|
const pkgPath = findPackageJson(__dirname);
|
|
30
69
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
31
70
|
const serverVersion = pkg.version;
|
|
71
|
+
// Validate environment variables with friendly messages
|
|
32
72
|
const PROJECT_ROOT = process.env.ATABEY_PROJECT_ROOT || process.cwd();
|
|
73
|
+
if (!process.env.ATABEY_PROJECT_ROOT) {
|
|
74
|
+
const transportMode = process.env.MCP_TRANSPORT || "unified";
|
|
75
|
+
if (transportMode !== "stdio") {
|
|
76
|
+
process.stderr.write(`
|
|
77
|
+
╔══════════════════════════════════════════════════════════╗
|
|
78
|
+
║ 💡 Tip: Set ATABEY_PROJECT_ROOT ║
|
|
79
|
+
╠══════════════════════════════════════════════════════════╣
|
|
80
|
+
║ In HTTP/SSE mode, it's recommended to explicitly set ║
|
|
81
|
+
║ the project root to avoid confusion. ║
|
|
82
|
+
║ ║
|
|
83
|
+
║ Example: ║
|
|
84
|
+
║ export ATABEY_PROJECT_ROOT=/path/to/your/project ║
|
|
85
|
+
║ ║
|
|
86
|
+
║ Using current directory: ${process.cwd()} ║
|
|
87
|
+
╚══════════════════════════════════════════════════════════╝
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
33
91
|
const FRAMEWORK_DIR = process.env.ATABEY_FRAMEWORK_DIR || path.join(PROJECT_ROOT, ".atabey");
|
|
34
|
-
const UI_DIST_PATH = path.join(__dirname, "../../dist
|
|
92
|
+
const UI_DIST_PATH = path.join(__dirname, "../../dashboard/dist");
|
|
35
93
|
// ─── Ports ────────────────────────────────────────────────────────
|
|
36
94
|
const PORT = parseInt(process.env.MCP_PORT || "5858", 10);
|
|
95
|
+
if (isNaN(PORT) || PORT < 1 || PORT > 65535) {
|
|
96
|
+
process.stderr.write(`
|
|
97
|
+
╔══════════════════════════════════════════════════════════╗
|
|
98
|
+
║ ❌ Invalid MCP_PORT ║
|
|
99
|
+
╠══════════════════════════════════════════════════════════╣
|
|
100
|
+
║ The port must be a number between 1 and 65535. ║
|
|
101
|
+
║ ║
|
|
102
|
+
║ You set: MCP_PORT=${process.env.MCP_PORT || "(empty)"} ║
|
|
103
|
+
║ ║
|
|
104
|
+
║ 📋 To fix this: ║
|
|
105
|
+
║ export MCP_PORT=5858 (default) ║
|
|
106
|
+
║ export MCP_PORT=8080 (alternative) ║
|
|
107
|
+
╚══════════════════════════════════════════════════════════╝
|
|
108
|
+
`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
37
111
|
const HOST = process.env.MCP_HOST || "0.0.0.0";
|
|
38
112
|
// ─── MCP Server ───────────────────────────────────────────────────
|
|
39
113
|
const server = new Server({ name: "atabey-mcp", version: serverVersion }, { capabilities: { tools: {}, resources: {} } });
|
|
@@ -242,7 +316,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
242
316
|
// [AUTO-ROLLBACK] Check write results for governance violations
|
|
243
317
|
if (name === "write_file" || name === "replace_text" || name === "patch_file") {
|
|
244
318
|
try {
|
|
245
|
-
const responseText = result.content?.filter(b => b.type === "text").map(b => b.text).join(" ") || "";
|
|
246
319
|
const filePath = maskedArgs.path || "";
|
|
247
320
|
const content = maskedArgs.content || "";
|
|
248
321
|
if (filePath && content) {
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { approveOperation,
|
|
1
|
+
import { approveOperation, getPendingApprovals, rejectOperation } from "../../utils/human-in-loop.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* MCP Tool: approve_operation
|
|
4
4
|
*
|
|
5
|
-
* AI CLI
|
|
6
|
-
*
|
|
5
|
+
* In AI CLI (Claude Code, Gemini CLI), running "atabey approve <traceId>"
|
|
6
|
+
* is not possible — anything typed in chat is treated as a message, not a CLI command.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* This tool moves the risk gate approval flow directly into an MCP tool call:
|
|
9
9
|
*
|
|
10
|
-
* 1. Atabey
|
|
11
|
-
* 2.
|
|
12
|
-
* 3. AI
|
|
10
|
+
* 1. Atabey blocks a high-risk operation
|
|
11
|
+
* 2. Tells the developer: "call the approve_operation tool"
|
|
12
|
+
* 3. AI calls this tool → operation is approved/rejected
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Developer never has to switch to terminal.
|
|
15
15
|
*/
|
|
16
16
|
export async function handleApproveOperation(_root, args) {
|
|
17
17
|
const { action, traceId, reason } = args;
|
|
18
|
-
// LIST:
|
|
18
|
+
// LIST: Show pending approvals
|
|
19
19
|
if (action === "list") {
|
|
20
20
|
const pending = getPendingApprovals();
|
|
21
21
|
if (pending.length === 0) {
|
|
@@ -42,7 +42,7 @@ export async function handleApproveOperation(_root, args) {
|
|
|
42
42
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
// APPROVE or REJECT: traceId
|
|
45
|
+
// APPROVE or REJECT: traceId is required
|
|
46
46
|
if (!traceId) {
|
|
47
47
|
return {
|
|
48
48
|
isError: true,
|
|
@@ -3,16 +3,16 @@ import path from "path";
|
|
|
3
3
|
/**
|
|
4
4
|
* ─── ASK HUMAN — File Bridge ─────────────────────────────────────────
|
|
5
5
|
*
|
|
6
|
-
* AI
|
|
7
|
-
*
|
|
6
|
+
* AI CLIs (Claude Code, Gemini CLI) start the MCP server as a stdio subprocess.
|
|
7
|
+
* In this subprocess, stdin is isTTY=false (used for MCP JSON-RPC).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* 1.
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
9
|
+
* Solution: File-based Q&A bridge.
|
|
10
|
+
* 1. Question → written to .atabey/hitl/question.txt
|
|
11
|
+
* 2. Developer answers via `atabey hitl` command
|
|
12
|
+
* 3. Answer → written to .atabey/hitl/answer.txt
|
|
13
|
+
* 4. This handler reads the answer and returns it to the AI
|
|
14
14
|
*
|
|
15
|
-
* Dashboard: /api/approvals endpoint
|
|
15
|
+
* Dashboard: Can also be answered via /api/approvals endpoint on the web dashboard.
|
|
16
16
|
*/
|
|
17
17
|
export async function handleAskHuman(root, args) {
|
|
18
18
|
const timeoutSeconds = args.timeoutSeconds ?? 120;
|
|
@@ -1,18 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
2
|
/**
|
|
3
3
|
* Checks for active network ports and their status.
|
|
4
|
+
* Uses spawnSync instead of execSync to prevent shell injection.
|
|
4
5
|
*/
|
|
5
6
|
export function handleCheckPorts(projectRoot, args) {
|
|
6
7
|
const rawFilter = args.filter || "";
|
|
7
8
|
// Sanitize filter to prevent command injection (allow only alphanumeric, dots, colons, dashes, underscores)
|
|
8
9
|
const filter = rawFilter.replace(/[^a-zA-Z0-9.:_-]/g, "");
|
|
9
10
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
let output;
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
const result = spawnSync("netstat", ["-ano"], { encoding: "utf8" });
|
|
14
|
+
if (result.error)
|
|
15
|
+
throw result.error;
|
|
16
|
+
const lines = (result.stdout || "").split("\n").filter(l => l.includes("LISTENING"));
|
|
17
|
+
output = filter
|
|
18
|
+
? lines.filter(l => l.includes(filter)).join("\n")
|
|
19
|
+
: lines.join("\n");
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const result = spawnSync("lsof", ["-i", "-P", "-n"], { encoding: "utf8" });
|
|
23
|
+
if (result.error)
|
|
24
|
+
throw result.error;
|
|
25
|
+
const lines = (result.stdout || "").split("\n").filter(l => l.includes("LISTEN"));
|
|
26
|
+
output = filter
|
|
27
|
+
? lines.filter(l => l.includes(filter)).join("\n")
|
|
28
|
+
: lines.join("\n");
|
|
29
|
+
}
|
|
16
30
|
return {
|
|
17
31
|
content: [{
|
|
18
32
|
type: "text",
|
|
@@ -1,102 +1,129 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
2
|
import { Metrics } from "../../utils/metrics.js";
|
|
3
|
+
// Each entry defines the executable and its allowed args prefix
|
|
3
4
|
const COMMAND_ALLOW_LIST = [
|
|
4
5
|
// Package managers
|
|
5
|
-
"npm test",
|
|
6
|
-
"npm run",
|
|
7
|
-
"npm install",
|
|
8
|
-
"npm ci",
|
|
9
|
-
"npm audit",
|
|
10
|
-
"npx vitest",
|
|
11
|
-
"npx tsc",
|
|
12
|
-
"npx eslint",
|
|
13
|
-
"yarn test",
|
|
14
|
-
"yarn run",
|
|
15
|
-
"yarn install",
|
|
16
|
-
"pnpm test",
|
|
17
|
-
"pnpm run",
|
|
18
|
-
"pnpm install",
|
|
6
|
+
{ cmd: "npm", args: ["test"] },
|
|
7
|
+
{ cmd: "npm", args: ["run"] },
|
|
8
|
+
{ cmd: "npm", args: ["install"] },
|
|
9
|
+
{ cmd: "npm", args: ["ci"] },
|
|
10
|
+
{ cmd: "npm", args: ["audit"] },
|
|
11
|
+
{ cmd: "npx", args: ["vitest"] },
|
|
12
|
+
{ cmd: "npx", args: ["tsc"] },
|
|
13
|
+
{ cmd: "npx", args: ["eslint"] },
|
|
14
|
+
{ cmd: "yarn", args: ["test"] },
|
|
15
|
+
{ cmd: "yarn", args: ["run"] },
|
|
16
|
+
{ cmd: "yarn", args: ["install"] },
|
|
17
|
+
{ cmd: "pnpm", args: ["test"] },
|
|
18
|
+
{ cmd: "pnpm", args: ["run"] },
|
|
19
|
+
{ cmd: "pnpm", args: ["install"] },
|
|
19
20
|
// Git
|
|
20
|
-
"git status",
|
|
21
|
-
"git diff",
|
|
22
|
-
"git log",
|
|
23
|
-
"git branch",
|
|
24
|
-
"git stash",
|
|
25
|
-
"git add",
|
|
26
|
-
"git commit",
|
|
27
|
-
"git push",
|
|
28
|
-
"git pull",
|
|
29
|
-
"git fetch",
|
|
30
|
-
"git merge",
|
|
31
|
-
"git rebase",
|
|
32
|
-
"git checkout",
|
|
33
|
-
"git reset",
|
|
21
|
+
{ cmd: "git", args: ["status"] },
|
|
22
|
+
{ cmd: "git", args: ["diff"] },
|
|
23
|
+
{ cmd: "git", args: ["log"] },
|
|
24
|
+
{ cmd: "git", args: ["branch"] },
|
|
25
|
+
{ cmd: "git", args: ["stash"] },
|
|
26
|
+
{ cmd: "git", args: ["add"] },
|
|
27
|
+
{ cmd: "git", args: ["commit"] },
|
|
28
|
+
{ cmd: "git", args: ["push"] },
|
|
29
|
+
{ cmd: "git", args: ["pull"] },
|
|
30
|
+
{ cmd: "git", args: ["fetch"] },
|
|
31
|
+
{ cmd: "git", args: ["merge"] },
|
|
32
|
+
{ cmd: "git", args: ["rebase"] },
|
|
33
|
+
{ cmd: "git", args: ["checkout"] },
|
|
34
|
+
{ cmd: "git", args: ["reset"] },
|
|
34
35
|
// Build tools
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"uniq",
|
|
79
|
-
"echo",
|
|
80
|
-
"pwd",
|
|
81
|
-
"which",
|
|
82
|
-
"file",
|
|
83
|
-
"du",
|
|
84
|
-
"df",
|
|
36
|
+
{ cmd: "make" },
|
|
37
|
+
{ cmd: "cmake" },
|
|
38
|
+
{ cmd: "dotnet", args: ["test"] },
|
|
39
|
+
{ cmd: "dotnet", args: ["format"] },
|
|
40
|
+
{ cmd: "dotnet", args: ["build"] },
|
|
41
|
+
{ cmd: "dotnet", args: ["run"] },
|
|
42
|
+
{ cmd: "dotnet", args: ["restore"] },
|
|
43
|
+
{ cmd: "go", args: ["test"] },
|
|
44
|
+
{ cmd: "go", args: ["fmt"] },
|
|
45
|
+
{ cmd: "go", args: ["build"] },
|
|
46
|
+
{ cmd: "go", args: ["run"] },
|
|
47
|
+
{ cmd: "go", args: ["vet"] },
|
|
48
|
+
{ cmd: "go", args: ["mod"] },
|
|
49
|
+
{ cmd: "pytest" },
|
|
50
|
+
{ cmd: "ruff", args: ["check"] },
|
|
51
|
+
{ cmd: "ruff", args: ["format"] },
|
|
52
|
+
{ cmd: "mvn" },
|
|
53
|
+
{ cmd: "./gradlew" },
|
|
54
|
+
{ cmd: "cargo", args: ["test"] },
|
|
55
|
+
{ cmd: "cargo", args: ["build"] },
|
|
56
|
+
{ cmd: "cargo", args: ["check"] },
|
|
57
|
+
{ cmd: "cargo", args: ["fmt"] },
|
|
58
|
+
// File operations (safe) — spawn prevents shell injection
|
|
59
|
+
{ cmd: "mkdir" },
|
|
60
|
+
{ cmd: "cp" },
|
|
61
|
+
{ cmd: "mv" },
|
|
62
|
+
{ cmd: "rmdir" },
|
|
63
|
+
{ cmd: "cat" },
|
|
64
|
+
{ cmd: "ls" },
|
|
65
|
+
{ cmd: "touch" },
|
|
66
|
+
{ cmd: "head" },
|
|
67
|
+
{ cmd: "tail" },
|
|
68
|
+
{ cmd: "wc" },
|
|
69
|
+
{ cmd: "grep" },
|
|
70
|
+
{ cmd: "find" },
|
|
71
|
+
{ cmd: "sort" },
|
|
72
|
+
{ cmd: "uniq" },
|
|
73
|
+
{ cmd: "echo" },
|
|
74
|
+
{ cmd: "pwd" },
|
|
75
|
+
{ cmd: "which" },
|
|
76
|
+
{ cmd: "file" },
|
|
77
|
+
{ cmd: "du" },
|
|
78
|
+
{ cmd: "df" },
|
|
85
79
|
];
|
|
86
80
|
const TIMEOUT = 30000; // 30 seconds
|
|
81
|
+
/**
|
|
82
|
+
* Parse a command string into executable + args array.
|
|
83
|
+
* Uses spawn for safety (no shell injection).
|
|
84
|
+
*/
|
|
85
|
+
function parseCommand(cmdString) {
|
|
86
|
+
const parts = cmdString.trim().split(/\s+/);
|
|
87
|
+
if (parts.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
return { cmd: parts[0], args: parts.slice(1) };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if a parsed command matches the allow list.
|
|
93
|
+
*/
|
|
94
|
+
function isAllowed(parsed) {
|
|
95
|
+
return COMMAND_ALLOW_LIST.some(entry => {
|
|
96
|
+
if (entry.cmd !== parsed.cmd)
|
|
97
|
+
return false;
|
|
98
|
+
if (!entry.args)
|
|
99
|
+
return true; // Any args allowed for this cmd
|
|
100
|
+
if (!parsed.args || parsed.args.length < entry.args.length)
|
|
101
|
+
return false;
|
|
102
|
+
// Check that args start with the allowed prefix
|
|
103
|
+
return entry.args.every((arg, i) => parsed.args[i] === arg);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
87
106
|
export function handleRunCommand(projectRoot, args) {
|
|
88
107
|
const command = args.command;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
// Parse command into executable + args
|
|
109
|
+
const parsed = parseCommand(command);
|
|
110
|
+
if (!parsed) {
|
|
111
|
+
return Promise.resolve({
|
|
112
|
+
content: [{ type: "text", text: "ERROR: Empty command." }],
|
|
113
|
+
isError: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (!isAllowed(parsed)) {
|
|
117
|
+
const allowedList = COMMAND_ALLOW_LIST.map(e => e.args ? `${e.cmd} ${e.args.join(" ")}` : e.cmd);
|
|
118
|
+
const errorMsg = `Command not allowed: "${command}". Only the following are allowed: ${allowedList.join(", ")}`;
|
|
92
119
|
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied)`, errorMsg);
|
|
93
120
|
return Promise.resolve({
|
|
94
121
|
content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
|
|
95
122
|
isError: true,
|
|
96
123
|
});
|
|
97
124
|
}
|
|
98
|
-
//
|
|
99
|
-
const hasShellMetacharacters = /[;&|><$`\n\r]/.test(
|
|
125
|
+
// Reject any args containing shell metacharacters
|
|
126
|
+
const hasShellMetacharacters = parsed.args.some(a => /[;&|><$`\n\r]/.test(a));
|
|
100
127
|
if (hasShellMetacharacters) {
|
|
101
128
|
const errorMsg = "Command rejected: Shell metacharacters are forbidden to prevent command injection.";
|
|
102
129
|
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied: metacharacters)`, errorMsg);
|
|
@@ -106,12 +133,30 @@ export function handleRunCommand(projectRoot, args) {
|
|
|
106
133
|
});
|
|
107
134
|
}
|
|
108
135
|
return new Promise((resolve) => {
|
|
109
|
-
|
|
136
|
+
const child = spawn(parsed.cmd, parsed.args, {
|
|
137
|
+
cwd: projectRoot,
|
|
138
|
+
timeout: TIMEOUT,
|
|
139
|
+
shell: false,
|
|
140
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
+
});
|
|
142
|
+
let stdout = "";
|
|
143
|
+
let stderr = "";
|
|
144
|
+
child.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
145
|
+
child.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
146
|
+
child.on("error", (err) => {
|
|
147
|
+
const errorMsg = `Failed to start command: ${err.message}`;
|
|
148
|
+
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
|
|
149
|
+
resolve({
|
|
150
|
+
content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
|
|
151
|
+
isError: true,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
child.on("close", (code) => {
|
|
110
155
|
const output = stdout + stderr;
|
|
111
156
|
const tokens = Metrics.estimateTokens(output);
|
|
112
157
|
Metrics.logUsage(projectRoot, "@mcp", `run_shell_command: ${command}`, tokens);
|
|
113
|
-
if (
|
|
114
|
-
const errorMsg = `Command failed with exit code ${
|
|
158
|
+
if (code !== 0) {
|
|
159
|
+
const errorMsg = `Command failed with exit code ${code}.`;
|
|
115
160
|
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
|
|
116
161
|
resolve({
|
|
117
162
|
content: [{ type: "text", text: `ERROR: ${errorMsg}. Output: ${output}` }],
|
|
@@ -121,11 +166,12 @@ export function handleRunCommand(projectRoot, args) {
|
|
|
121
166
|
}
|
|
122
167
|
// Truncate long outputs
|
|
123
168
|
const MAX_OUTPUT_LENGTH = 5000;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
169
|
+
const truncatedOutput = output.length > MAX_OUTPUT_LENGTH
|
|
170
|
+
? output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."
|
|
171
|
+
: output;
|
|
172
|
+
resolve({
|
|
173
|
+
content: [{ type: "text", text: truncatedOutput }],
|
|
174
|
+
});
|
|
129
175
|
});
|
|
130
176
|
});
|
|
131
177
|
}
|
|
@@ -27,14 +27,14 @@ export function initAuth() {
|
|
|
27
27
|
}).filter(u => u.token);
|
|
28
28
|
}
|
|
29
29
|
if (masterToken) {
|
|
30
|
-
|
|
30
|
+
process.stderr.write(`[AUTH] Master token configured (${masterToken.length} chars)\n`);
|
|
31
31
|
}
|
|
32
32
|
if (authUsers.length > 0) {
|
|
33
|
-
|
|
33
|
+
process.stderr.write(`[AUTH] ${authUsers.length} user(s) configured: ${authUsers.map(u => u.name).join(", ")}\n`);
|
|
34
34
|
}
|
|
35
35
|
if (!masterToken && authUsers.length === 0) {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
process.stderr.write("[AUTH] No authentication configured - OPEN ACCESS\n");
|
|
37
|
+
process.stderr.write("[AUTH] Set MCP_AUTH_TOKEN=<key> or MCP_AUTH_USERS=user1:key1,user2:key2 to enable\n");
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
@@ -202,7 +202,7 @@ export function getDisciplineStats(agent) {
|
|
|
202
202
|
*/
|
|
203
203
|
export function getAllDisciplineStats() {
|
|
204
204
|
const stats = {};
|
|
205
|
-
for (const [agent
|
|
205
|
+
for (const [agent] of agentDiscipline) {
|
|
206
206
|
stats[agent] = getDisciplineStats(agent);
|
|
207
207
|
}
|
|
208
208
|
return stats;
|
|
@@ -196,7 +196,7 @@ export function getPendingApprovals() {
|
|
|
196
196
|
const now = Date.now();
|
|
197
197
|
const pending = [];
|
|
198
198
|
// Clean expired
|
|
199
|
-
for (const [
|
|
199
|
+
for (const [, request] of activeApprovals) {
|
|
200
200
|
if (request.status === "PENDING" && now > request.expiresAt) {
|
|
201
201
|
request.status = "EXPIRED";
|
|
202
202
|
}
|
|
@@ -274,7 +274,7 @@ export function recordAndCheck(agent, toolName, args) {
|
|
|
274
274
|
/**
|
|
275
275
|
* Apply cooldown to an agent.
|
|
276
276
|
*/
|
|
277
|
-
function applyCooldown(history, alert,
|
|
277
|
+
function applyCooldown(history, alert, _agent) {
|
|
278
278
|
history.inCooldown = true;
|
|
279
279
|
history.cooldownUntil = alert.cooldownUntil || Date.now() + CONFIG.COOLDOWN_MS;
|
|
280
280
|
history.cooldownCount++;
|
|
@@ -347,8 +347,8 @@ export function getLoopStats(agent) {
|
|
|
347
347
|
*/
|
|
348
348
|
export function getAllLoopStats() {
|
|
349
349
|
const stats = {};
|
|
350
|
-
for (const [
|
|
351
|
-
stats[
|
|
350
|
+
for (const [agentName] of agentHistories) {
|
|
351
|
+
stats[agentName] = getLoopStats(agentName);
|
|
352
352
|
}
|
|
353
353
|
return stats;
|
|
354
354
|
}
|
|
@@ -93,9 +93,10 @@ export function analyzePathQuality(projectRoot, targetPath) {
|
|
|
93
93
|
}
|
|
94
94
|
// Check nesting depth
|
|
95
95
|
lines.forEach((line, idx) => {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
const
|
|
96
|
+
const leadingSpaces = line.match(/^(\s*)/)?.[0].length || 0;
|
|
97
|
+
if (leadingSpaces >= 10 && !line.trim().startsWith("//") && !line.trim().startsWith("*")) {
|
|
98
|
+
const indentSize = (leadingSpaces % 4 === 0) ? 4 : 2;
|
|
99
|
+
const depth = Math.floor(leadingSpaces / indentSize);
|
|
99
100
|
if (depth > 4) {
|
|
100
101
|
deepNesting++;
|
|
101
102
|
issues.push({
|