regcode 0.1.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.
- regcode/__init__.py +5 -0
- regcode/cli.py +180 -0
- regcode/config.py +153 -0
- regcode/conversation_manager.py +154 -0
- regcode/main.py +893 -0
- regcode/monty_sandbox.py +415 -0
- regcode/permissions.py +27 -0
- regcode/sandbox.py +382 -0
- regcode/tools/__init__.py +13 -0
- regcode/tools/base.py +125 -0
- regcode/tools/builtins.py +947 -0
- regcode/tools/registry.py +78 -0
- regcode/tools/review_notes.py +122 -0
- regcode/tui.py +331 -0
- regcode-0.1.0.dist-info/METADATA +163 -0
- regcode-0.1.0.dist-info/RECORD +18 -0
- regcode-0.1.0.dist-info/WHEEL +4 -0
- regcode-0.1.0.dist-info/entry_points.txt +2 -0
regcode/monty_sandbox.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Sandbox for secure, isolated code execution using pydantic-monty.
|
|
2
|
+
|
|
3
|
+
Provides Monty-based code execution with:
|
|
4
|
+
- Rust-based secure Python interpreter
|
|
5
|
+
- Resource limits (memory, CPU, recursion)
|
|
6
|
+
- External function registration
|
|
7
|
+
- Type checking
|
|
8
|
+
- Stream collection
|
|
9
|
+
- File system sandboxing
|
|
10
|
+
|
|
11
|
+
Designed as a drop-in replacement for the subprocess-based Sandbox class.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import shutil
|
|
18
|
+
import tempfile
|
|
19
|
+
import textwrap
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
from pydantic_monty import (
|
|
26
|
+
CollectString,
|
|
27
|
+
Monty,
|
|
28
|
+
MontyRuntimeError,
|
|
29
|
+
MontySyntaxError,
|
|
30
|
+
MontyTypingError,
|
|
31
|
+
ResourceLimits,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class MontySandboxConfig:
|
|
37
|
+
"""Configuration for the Monty sandbox."""
|
|
38
|
+
|
|
39
|
+
# Resource limits
|
|
40
|
+
max_duration_secs: float = 10.0
|
|
41
|
+
max_memory_mb: int = 64
|
|
42
|
+
max_recursion_depth: int = 100
|
|
43
|
+
max_allocations: int = 10000
|
|
44
|
+
|
|
45
|
+
# Type checking
|
|
46
|
+
type_check: bool = True
|
|
47
|
+
|
|
48
|
+
# Working directory
|
|
49
|
+
sandbox_dir: Path | None = None
|
|
50
|
+
|
|
51
|
+
# Max output size
|
|
52
|
+
max_output_bytes: int = 65536
|
|
53
|
+
|
|
54
|
+
# Allowed imports (whitelist)
|
|
55
|
+
allowed_imports: list[str] = field(default_factory=lambda: [
|
|
56
|
+
"os", "sys", "json", "re", "math", "datetime",
|
|
57
|
+
"collections", "itertools", "functools", "pathlib",
|
|
58
|
+
"subprocess", "asyncio", "http", "urllib", "csv",
|
|
59
|
+
"io", "string", "textwrap", "copy", "pprint",
|
|
60
|
+
"time", "random", "uuid", "hashlib", "base64",
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MontySandboxError(Exception):
|
|
65
|
+
"""Raised when Monty sandbox execution fails."""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
message: str,
|
|
70
|
+
stdout: str = "",
|
|
71
|
+
stderr: str = "",
|
|
72
|
+
exit_code: int = -1,
|
|
73
|
+
):
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
self.stdout = stdout
|
|
76
|
+
self.stderr = stderr
|
|
77
|
+
self.exit_code = exit_code
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class MontySandboxResult:
|
|
82
|
+
"""Result from a Monty sandboxed execution."""
|
|
83
|
+
|
|
84
|
+
stdout: str
|
|
85
|
+
stderr: str
|
|
86
|
+
exit_code: int
|
|
87
|
+
execution_time: float
|
|
88
|
+
result: Any = None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def success(self) -> bool:
|
|
92
|
+
return self.exit_code == 0 and not self.stdout.startswith("SANDBOX_ERROR")
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict[str, Any]:
|
|
95
|
+
return {
|
|
96
|
+
"stdout": self.stdout,
|
|
97
|
+
"stderr": self.stderr,
|
|
98
|
+
"exit_code": self.exit_code,
|
|
99
|
+
"execution_time": round(self.execution_time, 3),
|
|
100
|
+
"result": self.result,
|
|
101
|
+
"success": self.success,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MontySandbox:
|
|
106
|
+
"""Monty-based execution sandbox for secure Python code.
|
|
107
|
+
|
|
108
|
+
Provides isolation using pydantic-monty:
|
|
109
|
+
- Rust-based secure Python interpreter
|
|
110
|
+
- Resource limits (memory, CPU, recursion)
|
|
111
|
+
- External function registration
|
|
112
|
+
- Type checking
|
|
113
|
+
- Stream collection
|
|
114
|
+
|
|
115
|
+
Architecture note: Uses pydantic-monty for secure code execution.
|
|
116
|
+
Provides a drop-in replacement for the existing Sandbox class.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, config: MontySandboxConfig | None = None):
|
|
120
|
+
self.config = config or MontySandboxConfig()
|
|
121
|
+
self._sandbox_root: Path | None = None
|
|
122
|
+
self._external_functions: dict[str, Callable] = {}
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def sandbox_root(self) -> Path:
|
|
126
|
+
"""Get or create the sandbox working directory."""
|
|
127
|
+
if self._sandbox_root is None:
|
|
128
|
+
base = self.config.sandbox_dir or Path(
|
|
129
|
+
tempfile.mkdtemp(prefix="regcode_monty_")
|
|
130
|
+
)
|
|
131
|
+
self._sandbox_root = base / f"session_{int(time.time())}"
|
|
132
|
+
self._sandbox_root.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
return self._sandbox_root
|
|
134
|
+
|
|
135
|
+
def register_function(self, name: str, func: Callable) -> None:
|
|
136
|
+
"""Register an external function for use in Monty code."""
|
|
137
|
+
self._external_functions[name] = func
|
|
138
|
+
|
|
139
|
+
def register_functions(self, functions: dict[str, Callable]) -> None:
|
|
140
|
+
"""Register multiple external functions."""
|
|
141
|
+
self._external_functions.update(functions)
|
|
142
|
+
|
|
143
|
+
def clear_functions(self) -> None:
|
|
144
|
+
"""Clear all registered external functions."""
|
|
145
|
+
self._external_functions.clear()
|
|
146
|
+
|
|
147
|
+
def cleanup(self) -> None:
|
|
148
|
+
"""Clean up sandbox resources."""
|
|
149
|
+
if self._sandbox_root and self._sandbox_root.exists():
|
|
150
|
+
shutil.rmtree(self._sandbox_root, ignore_errors=True)
|
|
151
|
+
self._sandbox_root = None
|
|
152
|
+
self._external_functions.clear()
|
|
153
|
+
|
|
154
|
+
def __del__(self) -> None:
|
|
155
|
+
self.cleanup()
|
|
156
|
+
|
|
157
|
+
def _get_event_loop(self):
|
|
158
|
+
"""Get/create event loop, handling running and non-running contexts."""
|
|
159
|
+
try:
|
|
160
|
+
loop = asyncio.get_running_loop()
|
|
161
|
+
# A loop is already running (e.g., from pytest)
|
|
162
|
+
return loop, False
|
|
163
|
+
except RuntimeError:
|
|
164
|
+
# No loop running - create a new one
|
|
165
|
+
loop = asyncio.new_event_loop()
|
|
166
|
+
return loop, True
|
|
167
|
+
|
|
168
|
+
def run(
|
|
169
|
+
self,
|
|
170
|
+
code: str,
|
|
171
|
+
timeout: float | None = None,
|
|
172
|
+
external_functions: dict[str, Callable] | None = None,
|
|
173
|
+
) -> MontySandboxResult:
|
|
174
|
+
"""Execute Python code in the Monty sandbox.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
code: Python code string to execute.
|
|
178
|
+
timeout: Override timeout for this execution.
|
|
179
|
+
external_functions: External functions to register for this execution.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
MontySandboxResult with stdout, stderr, exit_code, result.
|
|
183
|
+
"""
|
|
184
|
+
timeout = timeout or self.config.max_duration_secs
|
|
185
|
+
|
|
186
|
+
# Merge external functions
|
|
187
|
+
funcs = self._external_functions.copy()
|
|
188
|
+
if external_functions:
|
|
189
|
+
funcs.update(external_functions)
|
|
190
|
+
|
|
191
|
+
# Set up resource limits
|
|
192
|
+
limits = ResourceLimits(
|
|
193
|
+
max_duration_secs=timeout,
|
|
194
|
+
max_memory=self.config.max_memory_mb * 1024 * 1024,
|
|
195
|
+
max_recursion_depth=self.config.max_recursion_depth,
|
|
196
|
+
max_allocations=self.config.max_allocations,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Create Monty runner
|
|
201
|
+
monty = Monty(code)
|
|
202
|
+
|
|
203
|
+
# Execute code - always use asyncio.run() for full isolation
|
|
204
|
+
# This avoids "no running event loop" errors that occur when
|
|
205
|
+
# the Monty library checks for a running loop at the process level
|
|
206
|
+
# Create callback outside async scope so we can access .output
|
|
207
|
+
cb = CollectString()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
async def _async_run():
|
|
211
|
+
return await monty.run_async(
|
|
212
|
+
external_functions=funcs,
|
|
213
|
+
limits=limits,
|
|
214
|
+
print_callback=cb,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
result = asyncio.run(_async_run())
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return MontySandboxResult(
|
|
220
|
+
stdout=f"SANDBOX_ERROR: {str(e)}",
|
|
221
|
+
stderr="",
|
|
222
|
+
exit_code=1,
|
|
223
|
+
execution_time=0,
|
|
224
|
+
result=None,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Extract stdout from callback, result is the last expression value
|
|
228
|
+
stdout = cb.output or ""
|
|
229
|
+
actual_result = result
|
|
230
|
+
|
|
231
|
+
return MontySandboxResult(
|
|
232
|
+
stdout=stdout[: self.config.max_output_bytes] if stdout else "",
|
|
233
|
+
stderr="",
|
|
234
|
+
exit_code=0,
|
|
235
|
+
execution_time=timeout,
|
|
236
|
+
result=actual_result,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
except (MontyRuntimeError, MontySyntaxError, MontyTypingError) as e:
|
|
240
|
+
return MontySandboxResult(
|
|
241
|
+
stdout=f"SANDBOX_ERROR: {str(e)}",
|
|
242
|
+
stderr="",
|
|
243
|
+
exit_code=1,
|
|
244
|
+
execution_time=0,
|
|
245
|
+
result=None,
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
return MontySandboxResult(
|
|
249
|
+
stdout=f"SANDBOX_ERROR: {str(e)}",
|
|
250
|
+
stderr="",
|
|
251
|
+
exit_code=1,
|
|
252
|
+
execution_time=0,
|
|
253
|
+
result=None,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def run_python(
|
|
257
|
+
self,
|
|
258
|
+
code: str,
|
|
259
|
+
timeout: float | None = None,
|
|
260
|
+
stdin: str | None = None,
|
|
261
|
+
) -> MontySandboxResult:
|
|
262
|
+
"""Execute Python code in the Monty sandbox.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
code: Python code string to execute.
|
|
266
|
+
timeout: Override timeout.
|
|
267
|
+
stdin: Optional stdin (not used in Monty, kept for compatibility).
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
MontySandboxResult.
|
|
271
|
+
"""
|
|
272
|
+
return self.run(code, timeout=timeout)
|
|
273
|
+
|
|
274
|
+
def run_shell(
|
|
275
|
+
self,
|
|
276
|
+
command: str,
|
|
277
|
+
timeout: float | None = None,
|
|
278
|
+
) -> MontySandboxResult:
|
|
279
|
+
"""Execute a shell command in the Monty sandbox.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
command: Shell command string.
|
|
283
|
+
timeout: Override timeout.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
MontySandboxResult.
|
|
287
|
+
"""
|
|
288
|
+
# Use textwrap.dedent so Monty doesn't strip leading indentation
|
|
289
|
+
# and pass the command as a separate argument to avoid injection
|
|
290
|
+
code = textwrap.dedent(
|
|
291
|
+
"""\
|
|
292
|
+
import subprocess
|
|
293
|
+
result = subprocess.run(
|
|
294
|
+
['bash', '-c', user_command],
|
|
295
|
+
capture_output=True,
|
|
296
|
+
text=True,
|
|
297
|
+
timeout=user_timeout,
|
|
298
|
+
)
|
|
299
|
+
result.stdout if result.returncode == 0 else f'Error: {{result.stderr}}'
|
|
300
|
+
"""
|
|
301
|
+
)
|
|
302
|
+
return self.run(
|
|
303
|
+
code,
|
|
304
|
+
timeout=timeout,
|
|
305
|
+
external_functions={
|
|
306
|
+
"user_command": lambda: command,
|
|
307
|
+
"user_timeout": lambda: timeout or self.config.max_duration_secs,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def read_file(self, path: str) -> MontySandboxResult:
|
|
312
|
+
"""Read a file from the sandbox filesystem.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
path: Path to read.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
MontySandboxResult with file content.
|
|
319
|
+
"""
|
|
320
|
+
sandbox_path = self.sandbox_root / path
|
|
321
|
+
try:
|
|
322
|
+
if not sandbox_path.exists():
|
|
323
|
+
return MontySandboxResult(
|
|
324
|
+
stdout=f"File not found: {path}",
|
|
325
|
+
stderr="",
|
|
326
|
+
exit_code=1,
|
|
327
|
+
execution_time=0,
|
|
328
|
+
result=None,
|
|
329
|
+
)
|
|
330
|
+
content = sandbox_path.read_text()
|
|
331
|
+
return MontySandboxResult(
|
|
332
|
+
stdout=content,
|
|
333
|
+
stderr="",
|
|
334
|
+
exit_code=0,
|
|
335
|
+
execution_time=0,
|
|
336
|
+
result=content,
|
|
337
|
+
)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
return MontySandboxResult(
|
|
340
|
+
stdout=f"Error reading file: {str(e)}",
|
|
341
|
+
stderr="",
|
|
342
|
+
exit_code=1,
|
|
343
|
+
execution_time=0,
|
|
344
|
+
result=None,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def write_file(self, path: str, content: str) -> MontySandboxResult:
|
|
348
|
+
"""Write content to a file in the sandbox.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
path: Path to write to.
|
|
352
|
+
content: Content to write.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
MontySandboxResult.
|
|
356
|
+
"""
|
|
357
|
+
sandbox_path = self.sandbox_root / path
|
|
358
|
+
try:
|
|
359
|
+
sandbox_path.parent.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
sandbox_path.write_text(content)
|
|
361
|
+
return MontySandboxResult(
|
|
362
|
+
stdout=f"Written {len(content)} bytes to {path}",
|
|
363
|
+
stderr="",
|
|
364
|
+
exit_code=0,
|
|
365
|
+
execution_time=0,
|
|
366
|
+
result=None,
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
return MontySandboxResult(
|
|
370
|
+
stdout=f"Error writing file: {str(e)}",
|
|
371
|
+
stderr="",
|
|
372
|
+
exit_code=1,
|
|
373
|
+
execution_time=0,
|
|
374
|
+
result=None,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def list_dir(self, path: str = ".") -> MontySandboxResult:
|
|
378
|
+
"""List contents of a directory in the sandbox.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
path: Directory path.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
MontySandboxResult with listing.
|
|
385
|
+
"""
|
|
386
|
+
sandbox_path = self.sandbox_root / path
|
|
387
|
+
try:
|
|
388
|
+
if not sandbox_path.exists():
|
|
389
|
+
return MontySandboxResult(
|
|
390
|
+
stdout=f"Directory not found: {path}",
|
|
391
|
+
stderr="",
|
|
392
|
+
exit_code=1,
|
|
393
|
+
execution_time=0,
|
|
394
|
+
result=None,
|
|
395
|
+
)
|
|
396
|
+
entries = sorted(sandbox_path.iterdir())
|
|
397
|
+
lines = []
|
|
398
|
+
for entry in entries:
|
|
399
|
+
marker = "/" if entry.is_dir() else ""
|
|
400
|
+
lines.append(f"{entry.name}{marker}")
|
|
401
|
+
return MontySandboxResult(
|
|
402
|
+
stdout="\n".join(lines) if lines else "(empty)",
|
|
403
|
+
stderr="",
|
|
404
|
+
exit_code=0,
|
|
405
|
+
execution_time=0,
|
|
406
|
+
result=lines,
|
|
407
|
+
)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
return MontySandboxResult(
|
|
410
|
+
stdout=f"Error listing directory: {str(e)}",
|
|
411
|
+
stderr="",
|
|
412
|
+
exit_code=1,
|
|
413
|
+
execution_time=0,
|
|
414
|
+
result=None,
|
|
415
|
+
)
|
regcode/permissions.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Agent permission definitions for controlling tool access."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentPermission(str, Enum):
|
|
7
|
+
"""Permissions that control which tools the agent can use."""
|
|
8
|
+
|
|
9
|
+
READ = "read"
|
|
10
|
+
WRITE = "write"
|
|
11
|
+
DELETE = "delete"
|
|
12
|
+
EXECUTE = "execute"
|
|
13
|
+
ALL = "all"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_TOOL_PERMISSIONS: dict[str, list[AgentPermission]] = {
|
|
17
|
+
"python_code": [AgentPermission.EXECUTE],
|
|
18
|
+
"shell_command": [AgentPermission.EXECUTE],
|
|
19
|
+
"run_script": [AgentPermission.EXECUTE],
|
|
20
|
+
"read_file": [AgentPermission.READ],
|
|
21
|
+
"write_file": [AgentPermission.WRITE],
|
|
22
|
+
"patch_file": [AgentPermission.WRITE],
|
|
23
|
+
"list_dir": [AgentPermission.READ],
|
|
24
|
+
"search_files": [AgentPermission.READ],
|
|
25
|
+
"browse_dir": [AgentPermission.READ],
|
|
26
|
+
"search_dir": [AgentPermission.READ],
|
|
27
|
+
}
|