Skip to content

Commit 5c17b58

Browse files
authored
Add comprehensive documentation and restructure API (#25)
* Expose Sandbox and related objects as part of public API * Remove abstract protocols and move private interfaces * Update README.md
1 parent 0395036 commit 5c17b58

20 files changed

+334
-425
lines changed

README.md

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,138 @@
11
# readonly-fs-tools
2-
Three safe tools for agentic code analysis
2+
3+
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
4+
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5+
6+
Three safe tools for agentic code analysis: **Globber**, **Grepper**, and **Viewer**.
7+
8+
This package provides security-focused, sandboxed file operations designed specifically for AI agents and automated code analysis workflows. Each tool enforces strict boundaries to prevent unauthorized file access while enabling powerful code exploration capabilities.
9+
10+
## Installation
11+
12+
```bash
13+
# Add to your project
14+
uv add readonly-fs-tools
15+
```
16+
17+
## Quick Start
18+
19+
```python
20+
from readonly_fs_tools import Sandbox, Globber, Grepper, Viewer, OutputBudget
21+
from pathlib import Path
22+
23+
# Create a sandbox configuration
24+
sandbox = Sandbox(
25+
sandbox_dir=Path("/path/to/your/project"),
26+
blocked_files=["*.secret", "private/*"],
27+
allow_hidden=False
28+
)
29+
30+
# Set up output budget to limit response sizes
31+
budget = OutputBudget(max_chars=10000)
32+
33+
# 1. Find files with glob patterns
34+
globber = Globber.from_sandbox(sandbox)
35+
glob_result = globber.glob("**/*.py", budget)
36+
print(f"Found {len(glob_result.paths)} Python files")
37+
38+
# 2. Search file contents with regex
39+
grepper = Grepper.from_sandbox(sandbox)
40+
grep_result = grepper.grep(r"class \w+", "**/*.py", budget)
41+
for match in grep_result.matches:
42+
print(f"{match.path}:{match.line_number}: {match.line}")
43+
44+
# 3. View file contents safely
45+
viewer = Viewer.from_sandbox(sandbox)
46+
view_result = viewer.view("src/main.py", budget)
47+
print(view_result.content.text)
48+
```
49+
50+
## Core Components
51+
52+
### Security Model
53+
All tools are executed in the context of a `Sandbox` which enforces:
54+
- **Sandbox directory constraints** - Operations limited to specified directory
55+
- **File blocklist** - Pattern-based file exclusion (e.g., `*.secret`, `private/*`)
56+
- **Hidden file controls** - Optional access to dotfiles and hidden directories (off by default)
57+
58+
In addition, each tool supports:
59+
- **Output size limits** - Configurable via `OutputBudget` to constrain token usage
60+
- **Streaming operations** - Files are processed in a memory-efficient manner, allowing for large files to be handled safely
61+
62+
### Tools
63+
64+
**Globber** - Safe file pattern matching
65+
- Uses glob patterns like `**/*.py` or `src/**/*.{js,ts}`
66+
- Respects sandbox boundaries and blocklists
67+
68+
**Grepper** - Safe content search
69+
- Regex-based content search across files
70+
- Pattern matching with full regex syntax support
71+
- Returns matches with file paths, line numbers, and the full matched line for context
72+
73+
**Viewer** - Safe file viewing
74+
- Bounded file content access with configurable limits
75+
- Support for line ranges and content windows
76+
- Safe handling of binary files and large files
77+
78+
### Key Types
79+
80+
```python
81+
# Pattern validation
82+
GlobPattern # Validated glob patterns
83+
RegexPattern # Validated regex patterns
84+
85+
# File access
86+
FileWindow # Line-based file access windows
87+
FileContent # Safe file content representation
88+
FileReadResult # Results with metadata
89+
90+
# Configuration
91+
Sandbox # Security boundary configuration
92+
OutputBudget # Output size limiting
93+
```
94+
95+
## Development
96+
97+
This project uses `uv` for dependency management.
98+
99+
```
100+
uv sync --dev
101+
```
102+
103+
### Code Quality
104+
```bash
105+
uv run ruff check --fix # Lint code
106+
uv run ruff format # Format code
107+
uv run mypy --strict . # Type checking
108+
```
109+
110+
### Testing
111+
```bash
112+
uv run pytest # Run all tests
113+
uv run pytest tests/test_specific_file.py # Run specific tests
114+
```
115+
116+
## Architecture
117+
118+
The package implements a three-layer security model:
119+
1. **Sandbox** - Directory and file access controls
120+
2. **Budget** - Resource usage limits
121+
3. **Validation** - Input pattern validation and output sanitization
122+
123+
All operations are designed to be safe for automated/AI agent use while providing the file system access needed for code analysis tasks.
124+
125+
## Generative AI disclaimer
126+
127+
This package was designed by a human developer, with assistance from generative AI. The code however was generated almost entirely by generative AI.
128+
The security model and design principles were carefully crafted to ensure safety and reliability for agentic code/filesystem analysis tasks. If you find any issues or have suggestions, please open an issue on GitHub.
129+
130+
## License
131+
132+
MIT License - see [LICENSE](LICENSE) for details.
133+
134+
## Links
135+
136+
- [Homepage](https://github.com/rbharvs/readonly-fs-tools)
137+
- [Issues](https://github.com/rbharvs/readonly-fs-tools/issues)
138+
- [Changelog](https://github.com/rbharvs/readonly-fs-tools/releases)

src/readonly_fs_tools/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Glob Grep View - Three safe tools for agentic code analysis."""
22

3-
from ._budget import OutputBudget
4-
from ._sandbox import Sandbox
3+
from .budget import OutputBudget
54
from .common import (
65
FileContent,
76
FileReadResult,
@@ -11,6 +10,7 @@
1110
)
1211
from .glob import Globber, GlobOutput
1312
from .grep import GrepOutput, Grepper
13+
from .sandbox import Sandbox, SandboxViolation
1414
from .view import Viewer, ViewOutput
1515

1616
__all__ = [
@@ -21,6 +21,7 @@
2121
"FileContent",
2222
"FileReadResult",
2323
"Sandbox",
24+
"SandboxViolation",
2425
"OutputBudget",
2526
# Glob functionality
2627
"Globber",

src/readonly_fs_tools/_defaults.py

Lines changed: 0 additions & 147 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Internal implementation classes - not part of public API."""
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Streaming file reading implementation."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from pydantic import BaseModel
8+
9+
from ..budget import BudgetExceeded, OutputBudget
10+
from ..common import FileReadResult, FileWindow
11+
from ..sandbox import Sandbox
12+
13+
14+
class StreamingFileReader(BaseModel):
15+
"""Default implementation of windowed file reading."""
16+
17+
sandbox: Sandbox
18+
19+
def read_window(
20+
self,
21+
file: Path,
22+
window: FileWindow,
23+
budget: OutputBudget,
24+
) -> FileReadResult:
25+
"""Read file window with budget constraints."""
26+
# Validate file access through sandbox
27+
self.sandbox.require_allowed(file)
28+
29+
contents = ""
30+
truncated = False
31+
lines_read = 0
32+
33+
try:
34+
# Open file with UTF-8 encoding and ignore errors for binary files
35+
budget.debit(len(file.as_posix())) # Debit budget for file path length
36+
with open(file, "r", encoding="utf-8", errors="ignore") as f:
37+
# Skip to the starting line offset
38+
current_line = 0
39+
while current_line < window.line_offset:
40+
line = f.readline()
41+
if not line: # EOF reached before offset
42+
break
43+
current_line += 1
44+
45+
# Read the requested number of lines
46+
while lines_read < window.line_count:
47+
line = f.readline()
48+
if not line: # EOF reached
49+
break
50+
budget.debit(len(line))
51+
contents += line
52+
lines_read += 1
53+
54+
except (OSError, IOError) as e:
55+
# Re-raise file system errors (like file not found)
56+
raise e
57+
58+
except BudgetExceeded:
59+
truncated = True
60+
61+
# Create actual window reflecting what was actually read
62+
actual_window = FileWindow(
63+
line_offset=window.line_offset, line_count=lines_read
64+
)
65+
66+
return FileReadResult(
67+
contents=contents, truncated=truncated, actual_window=actual_window
68+
)

0 commit comments

Comments
 (0)