regcode 0.10.0__tar.gz → 0.11.0__tar.gz
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-0.10.0 → regcode-0.11.0}/PKG-INFO +1 -1
- {regcode-0.10.0 → regcode-0.11.0}/pyproject.toml +1 -1
- {regcode-0.10.0 → regcode-0.11.0}/regcode/main.py +3 -1
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/builtins.py +89 -44
- {regcode-0.10.0 → regcode-0.11.0}/uv.lock +1 -1
- {regcode-0.10.0 → regcode-0.11.0}/.gitignore +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/.gitlab-ci.yml +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/.python-version +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/.qwen/settings.json +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/AGENTS.md +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/GOALS.md +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/README.md +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/config.yaml.template +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/main.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/__init__.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/cli.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/clipboard.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/config.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/conversation_manager.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/dependencies.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/mcp_client.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/monty_sandbox.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/paste.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/permissions.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/sandbox.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/__init__.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/base.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/registry.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/review_notes.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/web_crawl.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/web_search.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tracing.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/regcode/tui.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/__init__.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/conftest.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_api.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_cli.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_clipboard.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_config.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_context_manager.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_dependencies.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_main.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_mcp.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_mcp_status.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_monty_sandbox.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_osv_cve.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_provider_extra.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_review_notes.py +0 -0
- {regcode-0.10.0 → regcode-0.11.0}/tests/test_web_search.py +0 -0
|
@@ -19,7 +19,7 @@ from regcode.monty_sandbox import MontySandbox, MontySandboxConfig
|
|
|
19
19
|
from regcode.permissions import _TOOL_PERMISSIONS, AgentPermission
|
|
20
20
|
from regcode.sandbox import Sandbox, SandboxConfig
|
|
21
21
|
from regcode.tools.base import ToolResult
|
|
22
|
-
from regcode.tools.builtins import create_default_tools
|
|
22
|
+
from regcode.tools.builtins import create_default_tools, set_working_dir
|
|
23
23
|
from regcode.tools.registry import ToolRegistry
|
|
24
24
|
from regcode.tools.review_notes import AddReviewNoteTool, ReadReviewNotesTool
|
|
25
25
|
from regcode.tracing import setup_mlflow_tracing
|
|
@@ -83,6 +83,8 @@ class Agent:
|
|
|
83
83
|
self._context_callback = context_callback
|
|
84
84
|
self._cost_callback = cost_callback
|
|
85
85
|
self._allowed_dir = directory
|
|
86
|
+
# Set the working directory context variable for all filesystem tools
|
|
87
|
+
set_working_dir(directory or ".")
|
|
86
88
|
self._tool_budget = self.config.tool_budget
|
|
87
89
|
self._tool_budget_remaining = self._tool_budget
|
|
88
90
|
self._tool_budget_exhausted = False
|
|
@@ -10,8 +10,10 @@ import json
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
12
|
import subprocess
|
|
13
|
+
import sys
|
|
13
14
|
import time
|
|
14
15
|
import urllib.request
|
|
16
|
+
from contextvars import ContextVar
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
from typing import Any
|
|
17
19
|
|
|
@@ -23,6 +25,54 @@ from regcode.tools.base import BaseTool, ToolParam, ToolResult
|
|
|
23
25
|
from regcode.tools.web_crawl import WebCrawlerTool
|
|
24
26
|
from regcode.tools.web_search import WebSearchTool
|
|
25
27
|
|
|
28
|
+
# Context variable to store the working directory for directory isolation.
|
|
29
|
+
# All filesystem tools resolve paths relative to this directory.
|
|
30
|
+
# Default is "." (current working directory).
|
|
31
|
+
_working_dir: ContextVar[str] = ContextVar("_working_dir", default=".")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def set_working_dir(directory: str) -> None:
|
|
35
|
+
"""Set the working directory for all filesystem tools.
|
|
36
|
+
|
|
37
|
+
This uses a context variable so the directory is propagated
|
|
38
|
+
across all tool calls within the same async context.
|
|
39
|
+
"""
|
|
40
|
+
_working_dir.set(directory)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_working_dir() -> str:
|
|
44
|
+
"""Get the current working directory from context.
|
|
45
|
+
|
|
46
|
+
Returns the configured directory or "." if not set.
|
|
47
|
+
"""
|
|
48
|
+
return _working_dir.get()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_path(path: str) -> Path:
|
|
52
|
+
"""Resolve a path relative to the working directory.
|
|
53
|
+
|
|
54
|
+
If the path is absolute, it is returned as-is.
|
|
55
|
+
Otherwise, it is resolved relative to the working directory.
|
|
56
|
+
"""
|
|
57
|
+
p = Path(path)
|
|
58
|
+
if p.is_absolute():
|
|
59
|
+
return p
|
|
60
|
+
base = Path(get_working_dir())
|
|
61
|
+
return (base / p).resolve()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_git_root_from_working_dir() -> Path | None:
|
|
65
|
+
"""Find the git repository root relative to the working directory.
|
|
66
|
+
|
|
67
|
+
Walks up from the working directory looking for .git directory.
|
|
68
|
+
Returns the repository root, or None if not found.
|
|
69
|
+
"""
|
|
70
|
+
base = Path(get_working_dir()).resolve()
|
|
71
|
+
for parent in base.parents:
|
|
72
|
+
if (parent / ".git").exists():
|
|
73
|
+
return parent
|
|
74
|
+
return None
|
|
75
|
+
|
|
26
76
|
# Files and patterns that are too large/toxic for context — never read them
|
|
27
77
|
BLOCKED_NAMES: frozenset[str] = frozenset((
|
|
28
78
|
# Package lock files
|
|
@@ -373,8 +423,9 @@ class ReadFileTool(BaseTool):
|
|
|
373
423
|
output="Missing required parameter: path",
|
|
374
424
|
error=True,
|
|
375
425
|
)
|
|
426
|
+
# Resolve path relative to working directory
|
|
427
|
+
p = resolve_path(path)
|
|
376
428
|
# Check for blocked paths
|
|
377
|
-
p = Path(path)
|
|
378
429
|
# Walk all parts of the path — if any matches a blocked name, reject
|
|
379
430
|
for part in p.parts:
|
|
380
431
|
if part in BLOCKED_NAMES:
|
|
@@ -406,13 +457,13 @@ class ReadFileTool(BaseTool):
|
|
|
406
457
|
|
|
407
458
|
# Check against .gitignore patterns
|
|
408
459
|
# Find the git repository root (or project root)
|
|
409
|
-
repo_root =
|
|
460
|
+
repo_root = _get_git_root_from_working_dir()
|
|
410
461
|
if repo_root is not None:
|
|
411
462
|
spec = self._load_gitignore_spec(repo_root)
|
|
412
463
|
if spec is not None:
|
|
413
464
|
# Get relative path from repo_root
|
|
414
465
|
try:
|
|
415
|
-
rel_path = p.
|
|
466
|
+
rel_path = p.relative_to(repo_root)
|
|
416
467
|
# pathspec expects forward slashes
|
|
417
468
|
rel_path_str = str(rel_path).replace("\\", "/")
|
|
418
469
|
if spec.match_file(rel_path_str):
|
|
@@ -510,19 +561,17 @@ class WriteFileTool(BaseTool):
|
|
|
510
561
|
output="Missing required parameter: content",
|
|
511
562
|
error=True,
|
|
512
563
|
)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if ".." in p.parts:
|
|
564
|
+
# Block path traversal BEFORE resolving
|
|
565
|
+
if ".." in path:
|
|
516
566
|
return ToolResult(
|
|
517
567
|
output="Error writing file: Path traversal ('..') is not allowed",
|
|
518
568
|
error=True,
|
|
519
569
|
exit_code=1,
|
|
520
570
|
)
|
|
521
|
-
|
|
522
|
-
resolved = p.resolve()
|
|
571
|
+
p = resolve_path(path)
|
|
523
572
|
try:
|
|
524
|
-
|
|
525
|
-
|
|
573
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
574
|
+
p.write_text(content)
|
|
526
575
|
return ToolResult(
|
|
527
576
|
output=f"Written {len(content)} bytes to {path}",
|
|
528
577
|
exit_code=0,
|
|
@@ -555,24 +604,22 @@ class DeleteDirectoryTool(BaseTool):
|
|
|
555
604
|
output="Missing required parameter: path",
|
|
556
605
|
error=True,
|
|
557
606
|
)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if ".." in p.parts:
|
|
607
|
+
# Block path traversal BEFORE resolving
|
|
608
|
+
if ".." in path:
|
|
561
609
|
return ToolResult(
|
|
562
610
|
output="Error deleting directory: Path traversal ('..') is not allowed",
|
|
563
611
|
error=True,
|
|
564
612
|
exit_code=1,
|
|
565
613
|
)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if not resolved.is_dir():
|
|
614
|
+
p = resolve_path(path)
|
|
615
|
+
if not p.is_dir():
|
|
569
616
|
return ToolResult(
|
|
570
617
|
output=f"Error deleting: {path} is not a directory or does not exist",
|
|
571
618
|
error=True,
|
|
572
619
|
exit_code=1,
|
|
573
620
|
)
|
|
574
621
|
try:
|
|
575
|
-
|
|
622
|
+
p.rmdir()
|
|
576
623
|
return ToolResult(
|
|
577
624
|
output=f"Deleted directory: {path}",
|
|
578
625
|
exit_code=0,
|
|
@@ -603,26 +650,24 @@ class DeleteFileTool(BaseTool):
|
|
|
603
650
|
output="Missing required parameter: path",
|
|
604
651
|
error=True,
|
|
605
652
|
)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if ".." in p.parts:
|
|
653
|
+
# Block path traversal BEFORE resolving
|
|
654
|
+
if ".." in path:
|
|
609
655
|
return ToolResult(
|
|
610
656
|
output="Error deleting file: Path traversal ('..') is not allowed",
|
|
611
657
|
error=True,
|
|
612
658
|
exit_code=1,
|
|
613
659
|
)
|
|
614
|
-
|
|
615
|
-
resolved = p.resolve()
|
|
660
|
+
p = resolve_path(path)
|
|
616
661
|
# check if delete is allowed for this path
|
|
617
|
-
if not self._is_delete_allowed(
|
|
662
|
+
if not self._is_delete_allowed(p):
|
|
618
663
|
return ToolResult(
|
|
619
664
|
output=f"Error deleting file: Deletion not allowed for path {path}",
|
|
620
665
|
error=True,
|
|
621
666
|
exit_code=1,
|
|
622
667
|
)
|
|
623
668
|
try:
|
|
624
|
-
|
|
625
|
-
|
|
669
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
670
|
+
p.unlink()
|
|
626
671
|
return ToolResult(
|
|
627
672
|
output=f"Deleted file: {path}",
|
|
628
673
|
exit_code=0,
|
|
@@ -706,6 +751,14 @@ class PatchFileTool(BaseTool):
|
|
|
706
751
|
error=True,
|
|
707
752
|
)
|
|
708
753
|
|
|
754
|
+
# Block path traversal BEFORE resolving
|
|
755
|
+
if ".." in path:
|
|
756
|
+
return ToolResult(
|
|
757
|
+
output="Error: Path traversal ('..') is not allowed",
|
|
758
|
+
error=True,
|
|
759
|
+
exit_code=1,
|
|
760
|
+
)
|
|
761
|
+
|
|
709
762
|
# Coerce line numbers
|
|
710
763
|
try:
|
|
711
764
|
start_line = int(start_line)
|
|
@@ -724,16 +777,9 @@ class PatchFileTool(BaseTool):
|
|
|
724
777
|
exit_code=1,
|
|
725
778
|
)
|
|
726
779
|
|
|
727
|
-
p =
|
|
728
|
-
if ".." in p.parts:
|
|
729
|
-
return ToolResult(
|
|
730
|
-
output="Error: Path traversal ('..') is not allowed",
|
|
731
|
-
error=True,
|
|
732
|
-
exit_code=1,
|
|
733
|
-
)
|
|
780
|
+
p = resolve_path(path)
|
|
734
781
|
|
|
735
|
-
|
|
736
|
-
if not resolved.exists():
|
|
782
|
+
if not p.exists():
|
|
737
783
|
return ToolResult(
|
|
738
784
|
output=f"File not found: {path}",
|
|
739
785
|
error=True,
|
|
@@ -741,7 +787,7 @@ class PatchFileTool(BaseTool):
|
|
|
741
787
|
)
|
|
742
788
|
|
|
743
789
|
try:
|
|
744
|
-
raw_content =
|
|
790
|
+
raw_content = p.read_text()
|
|
745
791
|
trailing_newline = raw_content.endswith("\n") if raw_content else False
|
|
746
792
|
lines = raw_content.splitlines()
|
|
747
793
|
total = len(lines)
|
|
@@ -783,7 +829,7 @@ class PatchFileTool(BaseTool):
|
|
|
783
829
|
if trailing_newline:
|
|
784
830
|
new_content += "\n"
|
|
785
831
|
|
|
786
|
-
|
|
832
|
+
p.write_text(new_content)
|
|
787
833
|
return ToolResult(
|
|
788
834
|
output=(
|
|
789
835
|
f"Replaced lines {start_line}-{end_line} in {path}\n"
|
|
@@ -820,7 +866,7 @@ class ListDirTool(BaseTool):
|
|
|
820
866
|
def execute(self, **kwargs: Any) -> ToolResult:
|
|
821
867
|
path = kwargs.get("path", ".")
|
|
822
868
|
# List from host filesystem
|
|
823
|
-
p =
|
|
869
|
+
p = resolve_path(path)
|
|
824
870
|
if not p.exists():
|
|
825
871
|
return ToolResult(
|
|
826
872
|
output=f"Directory not found: {path}",
|
|
@@ -883,7 +929,7 @@ class SearchFilesTool(BaseTool):
|
|
|
883
929
|
error=True,
|
|
884
930
|
)
|
|
885
931
|
# Search on host filesystem
|
|
886
|
-
p =
|
|
932
|
+
p = resolve_path(path)
|
|
887
933
|
if not p.exists():
|
|
888
934
|
return ToolResult(
|
|
889
935
|
output=f"Directory not found: {path}",
|
|
@@ -934,7 +980,7 @@ class BrowseDirTool(BaseTool):
|
|
|
934
980
|
def execute(self, **kwargs: Any) -> ToolResult:
|
|
935
981
|
path = kwargs.get("path", ".")
|
|
936
982
|
try:
|
|
937
|
-
p =
|
|
983
|
+
p = resolve_path(path)
|
|
938
984
|
if not p.exists():
|
|
939
985
|
return ToolResult(
|
|
940
986
|
output=f"Path not found: {path}",
|
|
@@ -1024,7 +1070,7 @@ class SearchDirTool(BaseTool):
|
|
|
1024
1070
|
error=True,
|
|
1025
1071
|
)
|
|
1026
1072
|
try:
|
|
1027
|
-
p =
|
|
1073
|
+
p = resolve_path(path)
|
|
1028
1074
|
if not p.exists():
|
|
1029
1075
|
return ToolResult(
|
|
1030
1076
|
output=f"Directory not found: {path}",
|
|
@@ -1140,7 +1186,7 @@ class RunPythonScriptTool(BaseTool):
|
|
|
1140
1186
|
sandbox = MontySandbox()
|
|
1141
1187
|
try:
|
|
1142
1188
|
# Read script file from the host filesystem
|
|
1143
|
-
p =
|
|
1189
|
+
p = resolve_path(script_path)
|
|
1144
1190
|
if not p.exists():
|
|
1145
1191
|
return ToolResult(
|
|
1146
1192
|
output=f"Script not found: {script_path}",
|
|
@@ -1188,7 +1234,6 @@ class SystemInfoTool(BaseTool):
|
|
|
1188
1234
|
|
|
1189
1235
|
def execute(self, **kwargs: Any) -> ToolResult:
|
|
1190
1236
|
import platform
|
|
1191
|
-
import sys
|
|
1192
1237
|
|
|
1193
1238
|
try:
|
|
1194
1239
|
python_version = sys.version
|
|
@@ -1305,7 +1350,7 @@ class OSVCVETool(BaseTool):
|
|
|
1305
1350
|
deps: list[dict[str, str]] = []
|
|
1306
1351
|
|
|
1307
1352
|
# Try pyproject.toml
|
|
1308
|
-
p =
|
|
1353
|
+
p = resolve_path("pyproject.toml")
|
|
1309
1354
|
if p.exists():
|
|
1310
1355
|
try:
|
|
1311
1356
|
deps.extend(self._parse_pyproject(p))
|
|
@@ -1313,7 +1358,7 @@ class OSVCVETool(BaseTool):
|
|
|
1313
1358
|
pass
|
|
1314
1359
|
|
|
1315
1360
|
# Try package.json
|
|
1316
|
-
pj =
|
|
1361
|
+
pj = resolve_path("package.json")
|
|
1317
1362
|
if pj.exists():
|
|
1318
1363
|
try:
|
|
1319
1364
|
deps.extend(self._parse_package_json(pj))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|