hanzo-mcp 0.8.11__py3-none-any.whl → 0.9.0__py3-none-any.whl
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.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +3 -9
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +6 -15
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +1 -3
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +2 -6
- hanzo_mcp/tools/__init__.py +26 -27
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +22 -15
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +75 -74
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +6 -18
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +1 -3
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +10 -27
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +6 -18
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +24 -78
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +7 -27
- hanzo_mcp/tools/vector/mock_infinity.py +1 -3
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -718
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"""Sandboxed multi-language compiler and execution tool.
|
|
2
|
+
|
|
3
|
+
This tool provides safe compilation and execution of code in multiple
|
|
4
|
+
languages with sandboxing, resource limits, and timeout controls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import asyncio
|
|
11
|
+
import tempfile
|
|
12
|
+
import shutil
|
|
13
|
+
import logging
|
|
14
|
+
import resource
|
|
15
|
+
import signal
|
|
16
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
from hanzo_mcp.types import MCPResourceDocument
|
|
22
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Language(Enum):
|
|
26
|
+
"""Supported programming languages."""
|
|
27
|
+
C = "c"
|
|
28
|
+
CPP = "cpp"
|
|
29
|
+
GO = "go"
|
|
30
|
+
JAVASCRIPT = "javascript"
|
|
31
|
+
TYPESCRIPT = "typescript"
|
|
32
|
+
RUST = "rust"
|
|
33
|
+
ZIG = "zig"
|
|
34
|
+
PYTHON = "python"
|
|
35
|
+
JAVA = "java"
|
|
36
|
+
KOTLIN = "kotlin"
|
|
37
|
+
SWIFT = "swift"
|
|
38
|
+
RUBY = "ruby"
|
|
39
|
+
PHP = "php"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class CompilerConfig:
|
|
44
|
+
"""Compiler configuration for a language."""
|
|
45
|
+
language: Language
|
|
46
|
+
compiler_cmd: List[str]
|
|
47
|
+
run_cmd: List[str]
|
|
48
|
+
file_extension: str
|
|
49
|
+
needs_compilation: bool
|
|
50
|
+
docker_image: Optional[str] = None
|
|
51
|
+
sandbox_cmd: Optional[List[str]] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Language compiler configurations
|
|
55
|
+
COMPILER_CONFIGS = {
|
|
56
|
+
Language.C: CompilerConfig(
|
|
57
|
+
language=Language.C,
|
|
58
|
+
compiler_cmd=["clang", "-O2", "-o", "{output}", "{input}"],
|
|
59
|
+
run_cmd=["./{output}"],
|
|
60
|
+
file_extension=".c",
|
|
61
|
+
needs_compilation=True,
|
|
62
|
+
docker_image="gcc:latest",
|
|
63
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "gcc:latest"],
|
|
64
|
+
),
|
|
65
|
+
|
|
66
|
+
Language.CPP: CompilerConfig(
|
|
67
|
+
language=Language.CPP,
|
|
68
|
+
compiler_cmd=["clang++", "-std=c++17", "-O2", "-o", "{output}", "{input}"],
|
|
69
|
+
run_cmd=["./{output}"],
|
|
70
|
+
file_extension=".cpp",
|
|
71
|
+
needs_compilation=True,
|
|
72
|
+
docker_image="gcc:latest",
|
|
73
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "gcc:latest"],
|
|
74
|
+
),
|
|
75
|
+
|
|
76
|
+
Language.GO: CompilerConfig(
|
|
77
|
+
language=Language.GO,
|
|
78
|
+
compiler_cmd=["go", "build", "-o", "{output}", "{input}"],
|
|
79
|
+
run_cmd=["./{output}"],
|
|
80
|
+
file_extension=".go",
|
|
81
|
+
needs_compilation=True,
|
|
82
|
+
docker_image="golang:latest",
|
|
83
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "golang:latest"],
|
|
84
|
+
),
|
|
85
|
+
|
|
86
|
+
Language.JAVASCRIPT: CompilerConfig(
|
|
87
|
+
language=Language.JAVASCRIPT,
|
|
88
|
+
compiler_cmd=[],
|
|
89
|
+
run_cmd=["node", "{input}"],
|
|
90
|
+
file_extension=".js",
|
|
91
|
+
needs_compilation=False,
|
|
92
|
+
docker_image="node:latest",
|
|
93
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "node:latest"],
|
|
94
|
+
),
|
|
95
|
+
|
|
96
|
+
Language.TYPESCRIPT: CompilerConfig(
|
|
97
|
+
language=Language.TYPESCRIPT,
|
|
98
|
+
compiler_cmd=["tsc", "{input}", "--outFile", "{output}.js"],
|
|
99
|
+
run_cmd=["node", "{output}.js"],
|
|
100
|
+
file_extension=".ts",
|
|
101
|
+
needs_compilation=True,
|
|
102
|
+
docker_image="node:latest",
|
|
103
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "node:latest"],
|
|
104
|
+
),
|
|
105
|
+
|
|
106
|
+
Language.RUST: CompilerConfig(
|
|
107
|
+
language=Language.RUST,
|
|
108
|
+
compiler_cmd=["rustc", "-O", "-o", "{output}", "{input}"],
|
|
109
|
+
run_cmd=["./{output}"],
|
|
110
|
+
file_extension=".rs",
|
|
111
|
+
needs_compilation=True,
|
|
112
|
+
docker_image="rust:latest",
|
|
113
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "rust:latest"],
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
Language.ZIG: CompilerConfig(
|
|
117
|
+
language=Language.ZIG,
|
|
118
|
+
compiler_cmd=["zig", "build-exe", "{input}", "-femit-bin={output}"],
|
|
119
|
+
run_cmd=["./{output}"],
|
|
120
|
+
file_extension=".zig",
|
|
121
|
+
needs_compilation=True,
|
|
122
|
+
docker_image="euantorano/zig:latest",
|
|
123
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "euantorano/zig:latest"],
|
|
124
|
+
),
|
|
125
|
+
|
|
126
|
+
Language.PYTHON: CompilerConfig(
|
|
127
|
+
language=Language.PYTHON,
|
|
128
|
+
compiler_cmd=[],
|
|
129
|
+
run_cmd=["python", "{input}"],
|
|
130
|
+
file_extension=".py",
|
|
131
|
+
needs_compilation=False,
|
|
132
|
+
docker_image="python:latest",
|
|
133
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "python:latest"],
|
|
134
|
+
),
|
|
135
|
+
|
|
136
|
+
Language.JAVA: CompilerConfig(
|
|
137
|
+
language=Language.JAVA,
|
|
138
|
+
compiler_cmd=["javac", "{input}"],
|
|
139
|
+
run_cmd=["java", "{classname}"],
|
|
140
|
+
file_extension=".java",
|
|
141
|
+
needs_compilation=True,
|
|
142
|
+
docker_image="openjdk:latest",
|
|
143
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "openjdk:latest"],
|
|
144
|
+
),
|
|
145
|
+
|
|
146
|
+
Language.RUBY: CompilerConfig(
|
|
147
|
+
language=Language.RUBY,
|
|
148
|
+
compiler_cmd=[],
|
|
149
|
+
run_cmd=["ruby", "{input}"],
|
|
150
|
+
file_extension=".rb",
|
|
151
|
+
needs_compilation=False,
|
|
152
|
+
docker_image="ruby:latest",
|
|
153
|
+
sandbox_cmd=["docker", "run", "--rm", "-v", "{mount}:/code", "ruby:latest"],
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SandboxExecutor:
|
|
159
|
+
"""Executes code in a sandboxed environment."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, use_docker: bool = False):
|
|
162
|
+
self.use_docker = use_docker
|
|
163
|
+
self.logger = logging.getLogger(__name__)
|
|
164
|
+
|
|
165
|
+
async def execute(
|
|
166
|
+
self,
|
|
167
|
+
cmd: List[str],
|
|
168
|
+
cwd: str,
|
|
169
|
+
timeout: int = 10,
|
|
170
|
+
memory_limit: int = 256 * 1024 * 1024, # 256 MB
|
|
171
|
+
stdin_data: Optional[str] = None,
|
|
172
|
+
) -> Tuple[int, str, str]:
|
|
173
|
+
"""Execute command with sandboxing."""
|
|
174
|
+
|
|
175
|
+
if self.use_docker:
|
|
176
|
+
return await self._execute_docker(cmd, cwd, timeout, stdin_data)
|
|
177
|
+
else:
|
|
178
|
+
return await self._execute_native(cmd, cwd, timeout, memory_limit, stdin_data)
|
|
179
|
+
|
|
180
|
+
async def _execute_native(
|
|
181
|
+
self,
|
|
182
|
+
cmd: List[str],
|
|
183
|
+
cwd: str,
|
|
184
|
+
timeout: int,
|
|
185
|
+
memory_limit: int,
|
|
186
|
+
stdin_data: Optional[str],
|
|
187
|
+
) -> Tuple[int, str, str]:
|
|
188
|
+
"""Execute with native sandboxing."""
|
|
189
|
+
|
|
190
|
+
# Create process with resource limits
|
|
191
|
+
try:
|
|
192
|
+
process = await asyncio.create_subprocess_exec(
|
|
193
|
+
*cmd,
|
|
194
|
+
stdin=asyncio.subprocess.PIPE if stdin_data else None,
|
|
195
|
+
stdout=asyncio.subprocess.PIPE,
|
|
196
|
+
stderr=asyncio.subprocess.PIPE,
|
|
197
|
+
cwd=cwd,
|
|
198
|
+
preexec_fn=lambda: self._set_limits(memory_limit) if sys.platform != "win32" else None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Send input and get output
|
|
202
|
+
stdout, stderr = await asyncio.wait_for(
|
|
203
|
+
process.communicate(stdin_data.encode() if stdin_data else None),
|
|
204
|
+
timeout=timeout,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return process.returncode, stdout.decode(), stderr.decode()
|
|
208
|
+
|
|
209
|
+
except asyncio.TimeoutError:
|
|
210
|
+
if process:
|
|
211
|
+
process.kill()
|
|
212
|
+
await process.wait()
|
|
213
|
+
return -1, "", "Execution timed out"
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
return -1, "", str(e)
|
|
217
|
+
|
|
218
|
+
def _set_limits(self, memory_limit: int):
|
|
219
|
+
"""Set resource limits for process."""
|
|
220
|
+
# Memory limit
|
|
221
|
+
resource.setrlimit(resource.RLIMIT_AS, (memory_limit, memory_limit))
|
|
222
|
+
|
|
223
|
+
# CPU time limit (10 seconds)
|
|
224
|
+
resource.setrlimit(resource.RLIMIT_CPU, (10, 10))
|
|
225
|
+
|
|
226
|
+
# No file creation
|
|
227
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (0, 0))
|
|
228
|
+
|
|
229
|
+
# Limited number of processes
|
|
230
|
+
resource.setrlimit(resource.RLIMIT_NPROC, (10, 10))
|
|
231
|
+
|
|
232
|
+
async def _execute_docker(
|
|
233
|
+
self,
|
|
234
|
+
cmd: List[str],
|
|
235
|
+
cwd: str,
|
|
236
|
+
timeout: int,
|
|
237
|
+
stdin_data: Optional[str],
|
|
238
|
+
) -> Tuple[int, str, str]:
|
|
239
|
+
"""Execute with Docker sandboxing."""
|
|
240
|
+
|
|
241
|
+
# Docker execution with limits
|
|
242
|
+
docker_cmd = [
|
|
243
|
+
"docker", "run",
|
|
244
|
+
"--rm",
|
|
245
|
+
"--network", "none", # No network access
|
|
246
|
+
"--memory", "256m", # Memory limit
|
|
247
|
+
"--cpus", "0.5", # CPU limit
|
|
248
|
+
"--read-only", # Read-only filesystem
|
|
249
|
+
"-v", f"{cwd}:/code:ro", # Mount code directory as read-only
|
|
250
|
+
"-w", "/code",
|
|
251
|
+
"--timeout", str(timeout),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
# Add the actual command
|
|
255
|
+
docker_cmd.extend(cmd)
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
process = await asyncio.create_subprocess_exec(
|
|
259
|
+
*docker_cmd,
|
|
260
|
+
stdin=asyncio.subprocess.PIPE if stdin_data else None,
|
|
261
|
+
stdout=asyncio.subprocess.PIPE,
|
|
262
|
+
stderr=asyncio.subprocess.PIPE,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
stdout, stderr = await asyncio.wait_for(
|
|
266
|
+
process.communicate(stdin_data.encode() if stdin_data else None),
|
|
267
|
+
timeout=timeout + 2, # Give Docker a bit more time
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return process.returncode, stdout.decode(), stderr.decode()
|
|
271
|
+
|
|
272
|
+
except asyncio.TimeoutError:
|
|
273
|
+
return -1, "", "Docker execution timed out"
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
return -1, "", str(e)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class SandboxedCompiler(BaseTool):
|
|
280
|
+
"""Sandboxed compiler and execution tool."""
|
|
281
|
+
|
|
282
|
+
name = "compile"
|
|
283
|
+
description = """Compile and run code in multiple languages with sandboxing.
|
|
284
|
+
|
|
285
|
+
Actions:
|
|
286
|
+
- compile: Compile source code
|
|
287
|
+
- run: Compile and run code
|
|
288
|
+
- evaluate: Evaluate expression
|
|
289
|
+
- test: Run with test cases
|
|
290
|
+
- benchmark: Run performance benchmark
|
|
291
|
+
|
|
292
|
+
Supported languages: C, C++, Go, JavaScript, TypeScript, Rust, Zig, Python, Java, Ruby
|
|
293
|
+
|
|
294
|
+
Safety features:
|
|
295
|
+
- Memory limits (256MB default)
|
|
296
|
+
- CPU time limits (10s default)
|
|
297
|
+
- No network access in sandbox mode
|
|
298
|
+
- Read-only filesystem in Docker mode
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(self, use_docker: bool = False):
|
|
302
|
+
super().__init__()
|
|
303
|
+
self.logger = logging.getLogger(__name__)
|
|
304
|
+
self.executor = SandboxExecutor(use_docker)
|
|
305
|
+
self.use_docker = use_docker
|
|
306
|
+
|
|
307
|
+
def _detect_language(self, code: str, language: Optional[str] = None) -> Optional[Language]:
|
|
308
|
+
"""Detect language from code or hint."""
|
|
309
|
+
|
|
310
|
+
if language:
|
|
311
|
+
# Try to match language string
|
|
312
|
+
for lang in Language:
|
|
313
|
+
if language.lower() in [lang.value, lang.name.lower()]:
|
|
314
|
+
return lang
|
|
315
|
+
|
|
316
|
+
# Auto-detect from code patterns
|
|
317
|
+
patterns = {
|
|
318
|
+
Language.C: ["#include <stdio.h>", "int main("],
|
|
319
|
+
Language.CPP: ["#include <iostream>", "using namespace std", "std::"],
|
|
320
|
+
Language.GO: ["package main", "func main()", "import ("],
|
|
321
|
+
Language.JAVASCRIPT: ["console.log", "function ", "const ", "let ", "var "],
|
|
322
|
+
Language.TYPESCRIPT: ["interface ", "type ", ": string", ": number"],
|
|
323
|
+
Language.RUST: ["fn main()", "let mut ", "impl ", "trait "],
|
|
324
|
+
Language.ZIG: ["pub fn main()", "const std ="],
|
|
325
|
+
Language.PYTHON: ["def ", "import ", "from ", "print("],
|
|
326
|
+
Language.JAVA: ["public class ", "public static void main"],
|
|
327
|
+
Language.RUBY: ["puts ", "def ", "class ", "require "],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for lang, markers in patterns.items():
|
|
331
|
+
if any(marker in code for marker in markers):
|
|
332
|
+
return lang
|
|
333
|
+
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
async def _compile_code(
|
|
337
|
+
self,
|
|
338
|
+
code: str,
|
|
339
|
+
language: Language,
|
|
340
|
+
output_dir: Path,
|
|
341
|
+
) -> Tuple[bool, str, str]:
|
|
342
|
+
"""Compile source code."""
|
|
343
|
+
|
|
344
|
+
config = COMPILER_CONFIGS[language]
|
|
345
|
+
|
|
346
|
+
# Write source file
|
|
347
|
+
source_file = output_dir / f"main{config.file_extension}"
|
|
348
|
+
source_file.write_text(code)
|
|
349
|
+
|
|
350
|
+
if not config.needs_compilation:
|
|
351
|
+
return True, "", ""
|
|
352
|
+
|
|
353
|
+
# Prepare compile command
|
|
354
|
+
output_file = output_dir / "program"
|
|
355
|
+
compile_cmd = [
|
|
356
|
+
part.replace("{input}", str(source_file))
|
|
357
|
+
.replace("{output}", str(output_file))
|
|
358
|
+
for part in config.compiler_cmd
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
# Compile
|
|
362
|
+
returncode, stdout, stderr = await self.executor.execute(
|
|
363
|
+
compile_cmd,
|
|
364
|
+
str(output_dir),
|
|
365
|
+
timeout=30, # Longer timeout for compilation
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return returncode == 0, stdout, stderr
|
|
369
|
+
|
|
370
|
+
async def _run_code(
|
|
371
|
+
self,
|
|
372
|
+
language: Language,
|
|
373
|
+
output_dir: Path,
|
|
374
|
+
stdin_data: Optional[str] = None,
|
|
375
|
+
timeout: int = 10,
|
|
376
|
+
) -> Tuple[int, str, str]:
|
|
377
|
+
"""Run compiled code."""
|
|
378
|
+
|
|
379
|
+
config = COMPILER_CONFIGS[language]
|
|
380
|
+
|
|
381
|
+
if config.needs_compilation:
|
|
382
|
+
output_file = "program"
|
|
383
|
+
else:
|
|
384
|
+
output_file = f"main{config.file_extension}"
|
|
385
|
+
|
|
386
|
+
# Prepare run command
|
|
387
|
+
run_cmd = [
|
|
388
|
+
part.replace("{input}", output_file)
|
|
389
|
+
.replace("{output}", output_file)
|
|
390
|
+
.replace("{classname}", "main") # For Java
|
|
391
|
+
for part in config.run_cmd
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
# Run
|
|
395
|
+
return await self.executor.execute(
|
|
396
|
+
run_cmd,
|
|
397
|
+
str(output_dir),
|
|
398
|
+
timeout=timeout,
|
|
399
|
+
stdin_data=stdin_data,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
async def _run_test_cases(
|
|
403
|
+
self,
|
|
404
|
+
code: str,
|
|
405
|
+
language: Language,
|
|
406
|
+
test_cases: List[Dict[str, str]],
|
|
407
|
+
) -> List[Dict[str, Any]]:
|
|
408
|
+
"""Run code with multiple test cases."""
|
|
409
|
+
|
|
410
|
+
results = []
|
|
411
|
+
|
|
412
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
413
|
+
output_dir = Path(tmpdir)
|
|
414
|
+
|
|
415
|
+
# Compile once
|
|
416
|
+
success, compile_out, compile_err = await self._compile_code(
|
|
417
|
+
code, language, output_dir
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if not success:
|
|
421
|
+
return [{
|
|
422
|
+
"error": "Compilation failed",
|
|
423
|
+
"stdout": compile_out,
|
|
424
|
+
"stderr": compile_err,
|
|
425
|
+
}]
|
|
426
|
+
|
|
427
|
+
# Run test cases
|
|
428
|
+
for i, test_case in enumerate(test_cases):
|
|
429
|
+
stdin = test_case.get("input", "")
|
|
430
|
+
expected = test_case.get("expected", "")
|
|
431
|
+
|
|
432
|
+
returncode, stdout, stderr = await self._run_code(
|
|
433
|
+
language, output_dir, stdin
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Check result
|
|
437
|
+
passed = (
|
|
438
|
+
returncode == 0 and
|
|
439
|
+
(not expected or stdout.strip() == expected.strip())
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
results.append({
|
|
443
|
+
"test_case": i + 1,
|
|
444
|
+
"input": stdin,
|
|
445
|
+
"expected": expected,
|
|
446
|
+
"actual": stdout.strip(),
|
|
447
|
+
"stderr": stderr,
|
|
448
|
+
"passed": passed,
|
|
449
|
+
"exit_code": returncode,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return results
|
|
453
|
+
|
|
454
|
+
async def _benchmark_code(
|
|
455
|
+
self,
|
|
456
|
+
code: str,
|
|
457
|
+
language: Language,
|
|
458
|
+
iterations: int = 10,
|
|
459
|
+
) -> Dict[str, Any]:
|
|
460
|
+
"""Benchmark code performance."""
|
|
461
|
+
|
|
462
|
+
import time
|
|
463
|
+
|
|
464
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
465
|
+
output_dir = Path(tmpdir)
|
|
466
|
+
|
|
467
|
+
# Compile
|
|
468
|
+
success, compile_out, compile_err = await self._compile_code(
|
|
469
|
+
code, language, output_dir
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if not success:
|
|
473
|
+
return {
|
|
474
|
+
"error": "Compilation failed",
|
|
475
|
+
"stdout": compile_out,
|
|
476
|
+
"stderr": compile_err,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# Run multiple times
|
|
480
|
+
times = []
|
|
481
|
+
for _ in range(iterations):
|
|
482
|
+
start = time.perf_counter()
|
|
483
|
+
returncode, stdout, stderr = await self._run_code(
|
|
484
|
+
language, output_dir
|
|
485
|
+
)
|
|
486
|
+
elapsed = time.perf_counter() - start
|
|
487
|
+
|
|
488
|
+
if returncode != 0:
|
|
489
|
+
return {
|
|
490
|
+
"error": "Execution failed",
|
|
491
|
+
"stdout": stdout,
|
|
492
|
+
"stderr": stderr,
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
times.append(elapsed)
|
|
496
|
+
|
|
497
|
+
# Calculate statistics
|
|
498
|
+
avg_time = sum(times) / len(times)
|
|
499
|
+
min_time = min(times)
|
|
500
|
+
max_time = max(times)
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
"iterations": iterations,
|
|
504
|
+
"average_time": avg_time,
|
|
505
|
+
"min_time": min_time,
|
|
506
|
+
"max_time": max_time,
|
|
507
|
+
"times": times,
|
|
508
|
+
"unit": "seconds",
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async def run(
|
|
512
|
+
self,
|
|
513
|
+
action: str,
|
|
514
|
+
code: str,
|
|
515
|
+
language: Optional[str] = None,
|
|
516
|
+
input: Optional[str] = None,
|
|
517
|
+
test_cases: Optional[List[Dict[str, str]]] = None,
|
|
518
|
+
timeout: int = 10,
|
|
519
|
+
iterations: int = 10,
|
|
520
|
+
**kwargs,
|
|
521
|
+
) -> MCPResourceDocument:
|
|
522
|
+
"""Execute compiler action."""
|
|
523
|
+
|
|
524
|
+
# Detect language
|
|
525
|
+
lang = self._detect_language(code, language)
|
|
526
|
+
|
|
527
|
+
if not lang:
|
|
528
|
+
return MCPResourceDocument(
|
|
529
|
+
data={
|
|
530
|
+
"error": "Could not detect language",
|
|
531
|
+
"hint": "Specify language or use clearer code patterns",
|
|
532
|
+
"supported_languages": [l.value for l in Language],
|
|
533
|
+
}
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if action == "compile":
|
|
537
|
+
# Compile only
|
|
538
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
539
|
+
output_dir = Path(tmpdir)
|
|
540
|
+
success, stdout, stderr = await self._compile_code(
|
|
541
|
+
code, lang, output_dir
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
return MCPResourceDocument(
|
|
545
|
+
data={
|
|
546
|
+
"success": success,
|
|
547
|
+
"language": lang.value,
|
|
548
|
+
"stdout": stdout,
|
|
549
|
+
"stderr": stderr,
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
elif action == "run":
|
|
554
|
+
# Compile and run
|
|
555
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
556
|
+
output_dir = Path(tmpdir)
|
|
557
|
+
|
|
558
|
+
# Compile
|
|
559
|
+
success, compile_out, compile_err = await self._compile_code(
|
|
560
|
+
code, lang, output_dir
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if not success:
|
|
564
|
+
return MCPResourceDocument(
|
|
565
|
+
data={
|
|
566
|
+
"success": False,
|
|
567
|
+
"language": lang.value,
|
|
568
|
+
"phase": "compilation",
|
|
569
|
+
"stdout": compile_out,
|
|
570
|
+
"stderr": compile_err,
|
|
571
|
+
}
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# Run
|
|
575
|
+
returncode, stdout, stderr = await self._run_code(
|
|
576
|
+
lang, output_dir, input, timeout
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
return MCPResourceDocument(
|
|
580
|
+
data={
|
|
581
|
+
"success": returncode == 0,
|
|
582
|
+
"language": lang.value,
|
|
583
|
+
"exit_code": returncode,
|
|
584
|
+
"stdout": stdout,
|
|
585
|
+
"stderr": stderr,
|
|
586
|
+
"compile_output": compile_out,
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
elif action == "evaluate":
|
|
591
|
+
# Quick evaluation (for expressions)
|
|
592
|
+
# Wrap code in minimal boilerplate
|
|
593
|
+
if lang == Language.PYTHON:
|
|
594
|
+
wrapped = f"print({code})"
|
|
595
|
+
elif lang == Language.JAVASCRIPT:
|
|
596
|
+
wrapped = f"console.log({code})"
|
|
597
|
+
elif lang == Language.GO:
|
|
598
|
+
wrapped = f"package main\nimport \"fmt\"\nfunc main() {{ fmt.Println({code}) }}"
|
|
599
|
+
else:
|
|
600
|
+
wrapped = code
|
|
601
|
+
|
|
602
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
603
|
+
output_dir = Path(tmpdir)
|
|
604
|
+
|
|
605
|
+
success, compile_out, compile_err = await self._compile_code(
|
|
606
|
+
wrapped, lang, output_dir
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
if not success:
|
|
610
|
+
return MCPResourceDocument(
|
|
611
|
+
data={
|
|
612
|
+
"success": False,
|
|
613
|
+
"language": lang.value,
|
|
614
|
+
"error": compile_err,
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
returncode, stdout, stderr = await self._run_code(
|
|
619
|
+
lang, output_dir, timeout=5
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
return MCPResourceDocument(
|
|
623
|
+
data={
|
|
624
|
+
"success": returncode == 0,
|
|
625
|
+
"language": lang.value,
|
|
626
|
+
"result": stdout.strip(),
|
|
627
|
+
"stderr": stderr,
|
|
628
|
+
}
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
elif action == "test":
|
|
632
|
+
# Run with test cases
|
|
633
|
+
if not test_cases:
|
|
634
|
+
return MCPResourceDocument(
|
|
635
|
+
data={"error": "Test cases required for test action"}
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
results = await self._run_test_cases(code, lang, test_cases)
|
|
639
|
+
|
|
640
|
+
passed = sum(1 for r in results if r.get("passed", False))
|
|
641
|
+
total = len(results)
|
|
642
|
+
|
|
643
|
+
return MCPResourceDocument(
|
|
644
|
+
data={
|
|
645
|
+
"language": lang.value,
|
|
646
|
+
"passed": passed,
|
|
647
|
+
"total": total,
|
|
648
|
+
"success": passed == total,
|
|
649
|
+
"results": results,
|
|
650
|
+
}
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
elif action == "benchmark":
|
|
654
|
+
# Benchmark performance
|
|
655
|
+
result = await self._benchmark_code(code, lang, iterations)
|
|
656
|
+
|
|
657
|
+
return MCPResourceDocument(
|
|
658
|
+
data={
|
|
659
|
+
"language": lang.value,
|
|
660
|
+
**result,
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
else:
|
|
665
|
+
return MCPResourceDocument(
|
|
666
|
+
data={
|
|
667
|
+
"error": f"Unknown action: {action}",
|
|
668
|
+
"valid_actions": ["compile", "run", "evaluate", "test", "benchmark"],
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
async def call(self, **kwargs) -> str:
|
|
673
|
+
"""Tool interface for MCP."""
|
|
674
|
+
result = await self.run(**kwargs)
|
|
675
|
+
return result.to_json_string()
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# Factory function
|
|
679
|
+
def create_sandboxed_compiler(use_docker: bool = False):
|
|
680
|
+
"""Create sandboxed compiler tool."""
|
|
681
|
+
return SandboxedCompiler(use_docker)
|