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.
Files changed (49) hide show
  1. {regcode-0.10.0 → regcode-0.11.0}/PKG-INFO +1 -1
  2. {regcode-0.10.0 → regcode-0.11.0}/pyproject.toml +1 -1
  3. {regcode-0.10.0 → regcode-0.11.0}/regcode/main.py +3 -1
  4. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/builtins.py +89 -44
  5. {regcode-0.10.0 → regcode-0.11.0}/uv.lock +1 -1
  6. {regcode-0.10.0 → regcode-0.11.0}/.gitignore +0 -0
  7. {regcode-0.10.0 → regcode-0.11.0}/.gitlab-ci.yml +0 -0
  8. {regcode-0.10.0 → regcode-0.11.0}/.python-version +0 -0
  9. {regcode-0.10.0 → regcode-0.11.0}/.qwen/settings.json +0 -0
  10. {regcode-0.10.0 → regcode-0.11.0}/AGENTS.md +0 -0
  11. {regcode-0.10.0 → regcode-0.11.0}/GOALS.md +0 -0
  12. {regcode-0.10.0 → regcode-0.11.0}/README.md +0 -0
  13. {regcode-0.10.0 → regcode-0.11.0}/config.yaml.template +0 -0
  14. {regcode-0.10.0 → regcode-0.11.0}/main.py +0 -0
  15. {regcode-0.10.0 → regcode-0.11.0}/regcode/__init__.py +0 -0
  16. {regcode-0.10.0 → regcode-0.11.0}/regcode/cli.py +0 -0
  17. {regcode-0.10.0 → regcode-0.11.0}/regcode/clipboard.py +0 -0
  18. {regcode-0.10.0 → regcode-0.11.0}/regcode/config.py +0 -0
  19. {regcode-0.10.0 → regcode-0.11.0}/regcode/conversation_manager.py +0 -0
  20. {regcode-0.10.0 → regcode-0.11.0}/regcode/dependencies.py +0 -0
  21. {regcode-0.10.0 → regcode-0.11.0}/regcode/mcp_client.py +0 -0
  22. {regcode-0.10.0 → regcode-0.11.0}/regcode/monty_sandbox.py +0 -0
  23. {regcode-0.10.0 → regcode-0.11.0}/regcode/paste.py +0 -0
  24. {regcode-0.10.0 → regcode-0.11.0}/regcode/permissions.py +0 -0
  25. {regcode-0.10.0 → regcode-0.11.0}/regcode/sandbox.py +0 -0
  26. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/__init__.py +0 -0
  27. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/base.py +0 -0
  28. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/registry.py +0 -0
  29. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/review_notes.py +0 -0
  30. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/web_crawl.py +0 -0
  31. {regcode-0.10.0 → regcode-0.11.0}/regcode/tools/web_search.py +0 -0
  32. {regcode-0.10.0 → regcode-0.11.0}/regcode/tracing.py +0 -0
  33. {regcode-0.10.0 → regcode-0.11.0}/regcode/tui.py +0 -0
  34. {regcode-0.10.0 → regcode-0.11.0}/tests/__init__.py +0 -0
  35. {regcode-0.10.0 → regcode-0.11.0}/tests/conftest.py +0 -0
  36. {regcode-0.10.0 → regcode-0.11.0}/tests/test_api.py +0 -0
  37. {regcode-0.10.0 → regcode-0.11.0}/tests/test_cli.py +0 -0
  38. {regcode-0.10.0 → regcode-0.11.0}/tests/test_clipboard.py +0 -0
  39. {regcode-0.10.0 → regcode-0.11.0}/tests/test_config.py +0 -0
  40. {regcode-0.10.0 → regcode-0.11.0}/tests/test_context_manager.py +0 -0
  41. {regcode-0.10.0 → regcode-0.11.0}/tests/test_dependencies.py +0 -0
  42. {regcode-0.10.0 → regcode-0.11.0}/tests/test_main.py +0 -0
  43. {regcode-0.10.0 → regcode-0.11.0}/tests/test_mcp.py +0 -0
  44. {regcode-0.10.0 → regcode-0.11.0}/tests/test_mcp_status.py +0 -0
  45. {regcode-0.10.0 → regcode-0.11.0}/tests/test_monty_sandbox.py +0 -0
  46. {regcode-0.10.0 → regcode-0.11.0}/tests/test_osv_cve.py +0 -0
  47. {regcode-0.10.0 → regcode-0.11.0}/tests/test_provider_extra.py +0 -0
  48. {regcode-0.10.0 → regcode-0.11.0}/tests/test_review_notes.py +0 -0
  49. {regcode-0.10.0 → regcode-0.11.0}/tests/test_web_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regcode
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Minimalistic coding agent with Python API and CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: boto3>=1.43.40
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regcode"
3
- version = "0.10.0"
3
+ version = "0.11.0"
4
4
  description = "Minimalistic coding agent with Python API and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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 = self._find_git_root(p)
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.resolve().relative_to(repo_root.resolve())
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
- p = Path(path)
514
- # Block path traversal
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
- # Resolve the path to get absolute path
522
- resolved = p.resolve()
571
+ p = resolve_path(path)
523
572
  try:
524
- resolved.parent.mkdir(parents=True, exist_ok=True)
525
- resolved.write_text(content)
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
- p = Path(path)
559
- # Block path traversal
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
- # Resolve the path to get absolute path
567
- resolved = p.resolve()
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
- resolved.rmdir()
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
- p = Path(path)
607
- # Block path traversal
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
- # Resolve the path to get absolute path
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(resolved):
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
- resolved.parent.mkdir(parents=True, exist_ok=True)
625
- resolved.unlink()
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 = Path(path)
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
- resolved = p.resolve()
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 = resolved.read_text()
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
- resolved.write_text(new_content)
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 = Path(path)
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 = Path(path)
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 = Path(path)
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 = Path(path)
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 = Path(script_path)
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 = Path("pyproject.toml")
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 = Path("package.json")
1361
+ pj = resolve_path("package.json")
1317
1362
  if pj.exists():
1318
1363
  try:
1319
1364
  deps.extend(self._parse_package_json(pj))
@@ -3935,7 +3935,7 @@ wheels = [
3935
3935
 
3936
3936
  [[package]]
3937
3937
  name = "regcode"
3938
- version = "0.10.0"
3938
+ version = "0.11.0"
3939
3939
  source = { editable = "." }
3940
3940
  dependencies = [
3941
3941
  { name = "boto3" },
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