feat: Introduce a /reset command to clear the chat session and token tracking, and update documentation.
This commit is contained in:
334
SANDBOX.md
Normal file
334
SANDBOX.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# Sandbox Implementation Plan for AnchorCli
|
||||
|
||||
## Overview
|
||||
|
||||
By default, all file and directory operations are restricted to the current working directory (CWD).
|
||||
Users can bypass this restriction with the `--no-sandbox` flag.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Default: sandbox enabled (operations limited to CWD)
|
||||
anchor
|
||||
|
||||
# Disable sandbox (allow operations anywhere)
|
||||
anchor --no-sandbox
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation leverages the existing `ResolvePath()` methods in `FileTools` and `DirTools`.
|
||||
Since tools are static classes without dependency injection, we use a static `SandboxContext` class.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Create `SandboxContext.cs`
|
||||
|
||||
Create a new file `Core/SandboxContext.cs`:
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
|
||||
namespace AnchorCli;
|
||||
|
||||
/// <summary>
|
||||
/// Static context holding sandbox configuration.
|
||||
/// Checked by ResolvePath() to validate paths are within working directory.
|
||||
/// </summary>
|
||||
internal static class SandboxContext
|
||||
{
|
||||
private static string? _workingDirectory;
|
||||
private static bool _enabled = true;
|
||||
|
||||
public static bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set => _enabled = value;
|
||||
}
|
||||
|
||||
public static string WorkingDirectory
|
||||
{
|
||||
get => _workingDirectory ?? Environment.CurrentDirectory;
|
||||
set => _workingDirectory = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a resolved path is within the working directory (if sandbox is enabled).
|
||||
/// Returns the resolved path if valid, or null if outside sandbox (no exception thrown).
|
||||
/// When null is returned, the calling tool should return an error message to the agent.
|
||||
/// </summary>
|
||||
public static string? ValidatePath(string resolvedPath)
|
||||
{
|
||||
if (!_enabled)
|
||||
return resolvedPath;
|
||||
|
||||
var workDir = WorkingDirectory;
|
||||
|
||||
// Normalize paths for comparison
|
||||
var normalizedPath = Path.GetFullPath(resolvedPath).TrimEnd(Path.DirectorySeparatorChar);
|
||||
var normalizedWorkDir = Path.GetFullPath(workDir).TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
// Check if path starts with working directory
|
||||
if (!normalizedPath.StartsWith(normalizedWorkDir, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Return null to signal violation - caller handles error messaging
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
public static void Initialize(bool sandboxEnabled)
|
||||
{
|
||||
_enabled = sandboxEnabled;
|
||||
_workingDirectory = Environment.CurrentDirectory;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Modify `Program.cs`
|
||||
|
||||
Add argument parsing and initialize the sandbox context:
|
||||
|
||||
**After line 15** (after the `setup` subcommand check), add:
|
||||
|
||||
```csharp
|
||||
// ── Parse sandbox flag ──────────────────────────────────────────────────
|
||||
bool sandboxEnabled = !args.Contains("--no-sandbox");
|
||||
SandboxContext.Initialize(sandboxEnabled);
|
||||
|
||||
if (!sandboxEnabled)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[dim grey]Sandbox disabled (--no-sandbox)[/]");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Update `FileTools.ResolvePath()`
|
||||
|
||||
**Replace lines 322-323** with:
|
||||
|
||||
internal static string? ResolvePath(string path, out string? errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
var resolved = Path.IsPathRooted(path)
|
||||
? path
|
||||
: Path.GetFullPath(path, Environment.CurrentDirectory);
|
||||
|
||||
var validated = SandboxContext.ValidatePath(resolved);
|
||||
if (validated == null)
|
||||
{
|
||||
errorMessage = $"Sandbox violation: Path '{path}' is outside working directory '{SandboxContext.WorkingDirectory}'. Use --no-sandbox to disable restrictions.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Update `DirTools.ResolvePath()`
|
||||
|
||||
**Replace lines 84-85** with:
|
||||
|
||||
```csharp
|
||||
internal static string? ResolvePath(string path, out string? errorMessage)
|
||||
{
|
||||
errorMessage = null;
|
||||
var resolved = Path.IsPathRooted(path)
|
||||
? path
|
||||
: Path.GetFullPath(path, Environment.CurrentDirectory);
|
||||
|
||||
var validated = SandboxContext.ValidatePath(resolved);
|
||||
if (validated == null)
|
||||
{
|
||||
errorMessage = $"Sandbox violation: Path '{path}' is outside working directory '{SandboxContext.WorkingDirectory}'. Use --no-sandbox to disable restrictions.";
|
||||
return null;
|
||||
}
|
||||
|
||||
return validated;
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Update Tool Descriptions (Optional but Recommended)
|
||||
|
||||
Update the `[Description]` attributes to mention sandbox behavior:
|
||||
|
||||
**FileTools.cs - ReadFile** (line 23):
|
||||
```csharp
|
||||
[Description("Read a file. Max 200 lines per call. Returns lines with line:hash| anchors. Sandbox: restricted to working directory unless --no-sandbox is used. IMPORTANT: Call GrepFile first...")]
|
||||
```
|
||||
|
||||
**DirTools.cs - CreateDir** (line 63):
|
||||
```csharp
|
||||
[Description("Create a new directory. Creates parent directories if they don't exist. Sandbox: restricted to working directory unless --no-sandbox is used. Returns OK on success...")]
|
||||
```
|
||||
|
||||
Repeat for other tools as needed.
|
||||
|
||||
---
|
||||
|
||||
## How Tools Handle Sandbox Violations
|
||||
|
||||
Each tool that uses `ResolvePath()` must check for `null` return and handle it gracefully:
|
||||
|
||||
### FileTools Pattern
|
||||
|
||||
```csharp
|
||||
// Before (old code):
|
||||
var resolvedPath = ResolvePath(path);
|
||||
var content = File.ReadAllText(resolvedPath);
|
||||
|
||||
// After (new code):
|
||||
var resolvedPath = ResolvePath(path, out var errorMessage);
|
||||
if (resolvedPath == null)
|
||||
return $"ERROR: {errorMessage}";
|
||||
|
||||
var content = File.ReadAllText(resolvedPath);
|
||||
```
|
||||
|
||||
### DirTools Pattern
|
||||
|
||||
```csharp
|
||||
// Before (old code):
|
||||
var resolvedPath = ResolvePath(path);
|
||||
Directory.CreateDirectory(resolvedPath);
|
||||
|
||||
// After (new code):
|
||||
var resolvedPath = ResolvePath(path, out var errorMessage);
|
||||
if (resolvedPath == null)
|
||||
return $"ERROR: {errorMessage}";
|
||||
|
||||
Directory.CreateDirectory(resolvedPath);
|
||||
return "OK";
|
||||
```
|
||||
|
||||
### EditTools
|
||||
|
||||
No changes needed - it already calls `FileTools.ResolvePath()`, so the sandbox check happens there.
|
||||
|
||||
### Tools That Don't Use ResolvePath
|
||||
|
||||
- `ListDir` with no path argument (uses current directory)
|
||||
- `GetFileInfo` - needs to be updated to use `ResolvePath()`
|
||||
- `FindFiles` - needs to be updated to validate the search path
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## Error Handling - No Crashes
|
||||
|
||||
When a sandbox violation occurs, the program **does not crash**. Instead:
|
||||
|
||||
1. `ResolvePath()` returns `null` and sets `errorMessage`
|
||||
2. The tool returns the error message to the agent
|
||||
3. The agent sees the error and can continue the conversation
|
||||
4. The user sees a clear error message in the chat
|
||||
|
||||
**Example tool implementation pattern:**
|
||||
|
||||
```csharp
|
||||
public static async Task<string> ReadFile(string path, int startLine, int endLine)
|
||||
{
|
||||
var resolvedPath = ResolvePath(path, out var errorMessage);
|
||||
if (resolvedPath == null)
|
||||
return $"ERROR: {errorMessage}"; // Return error, don't throw
|
||||
|
||||
// ... rest of the tool logic
|
||||
}
|
||||
```
|
||||
|
||||
**What the agent sees:**
|
||||
```
|
||||
Tool result: ERROR: Sandbox violation: Path '/home/tomi/.ssh' is outside working directory '/home/tomi/dev/anchor'. Use --no-sandbox to disable restrictions.
|
||||
```
|
||||
|
||||
**What the user sees in chat:**
|
||||
> The agent tried to read `/home/tomi/.ssh` but was blocked by the sandbox. The agent can now adjust its approach or ask you to run with `--no-sandbox`.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
| Case | Behavior |
|
||||
|------|----------|
|
||||
| **Symlinks inside CWD pointing outside** | Follows symlink (user-created link = intentional) |
|
||||
| **Path traversal (`../..`)** | Blocked if result is outside CWD |
|
||||
| **Absolute paths** | Validated against CWD |
|
||||
| **Network paths** | Blocked (not under CWD) |
|
||||
| **Case sensitivity** | Uses `OrdinalIgnoreCase` for cross-platform compatibility |
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
⚠️ **The sandbox is a safety feature, not a security boundary.**
|
||||
|
||||
- It prevents **accidental** modifications to system files
|
||||
- It does **not** protect against malicious intent
|
||||
- `CommandTool.ExecuteCommand()` can still run arbitrary shell commands
|
||||
- A determined user can always use `--no-sandbox`
|
||||
|
||||
For true isolation, run anchor in a container or VM.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] `ReadFile` on file inside CWD → **Success**
|
||||
- [ ] `ReadFile` on file outside CWD → **Sandbox violation error**
|
||||
- [ ] `ReadFile` with `../` traversal outside CWD → **Sandbox violation error**
|
||||
- [ ] `CreateDir` outside CWD → **Sandbox violation error**
|
||||
- [ ] `anchor --no-sandbox` then read `/etc/passwd` → **Success**
|
||||
- [ ] Symlink inside CWD pointing to `/etc/passwd` → **Success** (user-created link)
|
||||
- [ ] Case variations on Windows (`C:\Users` vs `c:\users`) → **Success**
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Existing Workflows
|
||||
|
||||
If you have scripts or workflows that rely on accessing files outside the project:
|
||||
|
||||
```bash
|
||||
# Update your scripts to use --no-sandbox
|
||||
anchor --no-sandbox
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
For CI environments where sandbox may not be needed:
|
||||
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
- name: Run anchor
|
||||
run: anchor --no-sandbox
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `Core/SandboxContext.cs` | **New file** - Static sandbox state and validation |
|
||||
| `Program.cs` | Add `--no-sandbox` parsing, call `SandboxContext.Initialize()` |
|
||||
| `Tools/FileTools.cs` | Update `ResolvePath()` signature to return `string?` with `out errorMessage`; update all tool methods to check for null |
|
||||
| `Tools/DirTools.cs` | Update `ResolvePath()` signature to return `string?` with `out errorMessage`; update all tool methods to check for null |
|
||||
| `Tools/EditTools.cs` | No changes (uses `FileTools.ResolvePath()`, sandbox check happens there) |
|
||||
| `Tools/CommandTool.cs` | **Not sandboxed** - shell commands can access any path (documented limitation) |
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Allowlist**: Let users specify additional safe directories via config
|
||||
- **Per-tool sandbox**: Some tools (e.g., `GrepRecursive`) could have different rules
|
||||
- **Audit mode**: Log all file operations for review
|
||||
- **Interactive prompt**: Ask for confirmation before violating sandbox instead of hard fail
|
||||
Reference in New Issue
Block a user