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.
@@ -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
+ }