Project-Rivet / v2 /tools /file_tools.py
HumboldtJoker's picture
Upload folder using huggingface_hub
4554903 verified
Raw
History Blame Contribute Delete
4.19 kB
"""Git-aware file access for the campus repo.
Reads are jailed to the configured repo root. Writes additionally
require the target to be clean in git (no clobbering uncommitted work)
and are disabled entirely unless the operator turns them on in config —
Rivet is a colleague that suggests; writing into the repo is a
consensus-gated action.
"""
import re
from dataclasses import dataclass, field
from pathlib import Path
from tools.guard import ToolResult, check_path, run_checked
MAX_READ_BYTES = 200_000
@dataclass
class FileReadResult:
ok: bool
path: str = ""
content: str = ""
truncated: bool = False
git_status: str = "" # '' = clean/untracked-unknown, else porcelain code
error: str = ""
@dataclass
class RepoFiles:
repo_root: str
write_enabled: bool = False
files_read: list = field(default_factory=list)
def _jail(self, rel_path: str) -> tuple:
target = Path(self.repo_root) / rel_path
reason = check_path(target, [self.repo_root])
return target, reason
def read(self, rel_path: str) -> FileReadResult:
target, reason = self._jail(rel_path)
if reason:
return FileReadResult(ok=False, path=rel_path, error=reason)
if not target.exists():
return FileReadResult(ok=False, path=rel_path,
error=f"not found: {rel_path}")
if not target.is_file():
return FileReadResult(ok=False, path=rel_path,
error=f"not a file: {rel_path}")
raw = target.read_bytes()
truncated = len(raw) > MAX_READ_BYTES
content = raw[:MAX_READ_BYTES].decode("utf-8", errors="replace")
self.files_read.append(rel_path)
return FileReadResult(
ok=True, path=rel_path, content=content, truncated=truncated,
git_status=self._git_status(rel_path),
)
def write(self, rel_path: str, content: str) -> ToolResult:
if not self.write_enabled:
return ToolResult(
ok=False,
blocked_reason=("repo writes are disabled by config "
"(tools.repo_write_enabled) — Rivet suggests, "
"humans apply"),
)
target, reason = self._jail(rel_path)
if reason:
return ToolResult(ok=False, blocked_reason=reason)
status = self._git_status(rel_path)
if status and status != "??":
return ToolResult(
ok=False,
blocked_reason=(f"{rel_path} has uncommitted changes "
f"(git status '{status}') — refusing to clobber"),
)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content)
return ToolResult(ok=True, stdout=f"wrote {rel_path}")
def search(self, pattern: str, glob: str = "", max_results: int = 50) -> list:
"""Regex search across the repo via git grep (respects .gitignore)."""
argv = ["git", "grep", "-n", "-E", "--", pattern]
if glob:
argv += [glob]
result = run_checked(argv, cwd=self.repo_root, timeout=30)
if not result.ok and not result.stdout:
return []
hits = []
for line in result.stdout.splitlines()[:max_results]:
m = re.match(r"^([^:]+):(\d+):(.*)$", line)
if m:
hits.append({"path": m.group(1), "line": int(m.group(2)),
"text": m.group(3).strip()[:200]})
return hits
def list_dir(self, rel_path: str = ".") -> list:
target, reason = self._jail(rel_path)
if reason or not target.is_dir():
return []
return sorted(
p.name + ("/" if p.is_dir() else "")
for p in target.iterdir() if p.name != ".git"
)
def _git_status(self, rel_path: str) -> str:
result = run_checked(
["git", "status", "--porcelain", "--", rel_path],
cwd=self.repo_root, timeout=10,
)
if result.stdout.strip():
return result.stdout.strip()[:2]
return ""