Least-privilege filesystem sandbox & context guardrails for AI agents
Run untrusted AI agent code against a real codebase while enforcing least-privilege access at the file level.
The best agent interface remains simple: bash + filesystem. With FUSE, you can mount any world and make an agent productive with plain ls, cat, grep, and find.
But there's a gap: filesystems are usually all-or-nothing. Mount a real repo, and you often expose everything—including secrets.
AgentFense fills that gap with four permission levels:
| Level | What the agent can do |
|---|---|
none |
Path is invisible (hidden from ls, behaves like it doesn't exist) |
view |
Can list names (ls), but cannot read file content |
read |
Can read file content |
write |
Can read + modify / create files |
Example policy: "You can edit /docs, see /metadata, read everything else, but /secrets does not exist."
from agentfense import Sandbox
# One-liner: create sandbox from local directory with "agent-safe" preset
with Sandbox.from_local("./my-project") as sandbox:
result = sandbox.run("python main.py")
print(result.stdout)The agent-safe preset: read all files, write to /output and /tmp, hide secrets (.env, *.key, etc.).
For custom permissions:
sandbox = client.create_sandbox(
codebase_id=codebase.id,
permissions=[
{"pattern": "**/*", "permission": "read"}, # Default: read-only
{"pattern": "/docs/**", "permission": "write"}, # Writable
{"pattern": "/metadata/**", "permission": "view"}, # List-only
{"pattern": "/secrets/**", "permission": "none"}, # Hidden
]
)Build secure AI agents that execute bash commands with permission control:
from anthropic import Anthropic
from agentfense import Sandbox
# Define what the agent can access
PERMISSIONS = [
{"pattern": "**/*", "permission": "read"}, # Read all by default
{"pattern": "output/*", "permission": "write"}, # Can write to output/
{"pattern": ".env", "permission": "none"}, # Hide secrets
]
client = Anthropic()
with Sandbox.from_local("./project", permissions=PERMISSIONS) as sandbox:
# Agent generates bash command
response = client.messages.create(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "List all Python files"}],
system="Output bash commands in ```bash``` blocks."
)
# Execute safely in sandbox - permissions enforced at filesystem level
cmd = extract_command(response) # e.g., "find . -name '*.py'"
result = sandbox.run(cmd)
print(result.stdout)The agent cannot access .env even if it tries - the file is invisible at the filesystem level.
See example/ticket-agent/ for a complete interactive demo.
- Fine-grained permissions:
none/view/read/writewith glob patterns - Lightweight isolation: bubblewrap (
bwrap) for fast startup - Docker runtime: Full isolation with custom images and resource limits
- Delta Layer (COW): Copy-On-Write isolation for multi-sandbox write safety
- Stateful sessions: Persistent shell with working directory and environment
- Async SDK: Full async/await support for high-concurrency scenarios
- Permission presets: Built-in presets (
agent-safe,read-only,full-access)
git clone https://github.com/AjaxZhan/AgentFense.git
cd AgentFense
go mod tidy
go build -o bin/agentfense-server ./cmd/agentfense-server
# Start (gRPC :9000, REST :8080)
./bin/agentfense-server -config configs/agentfense-server.yamlPrerequisites: Go 1.21+, bubblewrap (bwrap)
pip install -e sdk/python/from agentfense import Sandbox, RuntimeType, ResourceLimits
# Basic usage
with Sandbox.from_local("./my-project") as sandbox:
result = sandbox.run("python main.py")
print(result.stdout)
# With Docker and resource limits
with Sandbox.from_local(
"./my-project",
preset="agent-safe",
runtime=RuntimeType.DOCKER,
image="python:3.11-slim",
resources=ResourceLimits(memory_bytes=512 * 1024 * 1024, pids_limit=100),
) as sandbox:
with sandbox.session() as session:
session.exec("pip install -r requirements.txt")
result = session.exec("pytest")
print(result.stdout)For high-concurrency scenarios, use the async API:
import asyncio
from agentfense import AsyncSandbox
async def main():
async with await AsyncSandbox.from_local("./my-project") as sandbox:
result = await sandbox.run("python main.py")
print(result.stdout)
# Async sessions
async with sandbox.session() as session:
await session.exec("cd /workspace")
result = await session.exec("npm test")
asyncio.run(main())The async SDK provides the same API as the sync version, with await for all operations.
| Preset | Description |
|---|---|
agent-safe |
Read all, write to /output & /tmp, hide secrets |
read-only |
Read all files, no write access |
full-access |
Full read/write access |
development |
Full access except secrets |
from agentfense import list_presets, extend_preset
# Extend a preset
rules = extend_preset("agent-safe", additions=[
{"pattern": "/custom/**", "permission": "write"}
])from agentfense import Sandbox, CommandTimeoutError, CommandExecutionError
try:
with Sandbox.from_local("./project") as sandbox:
result = sandbox.run("python main.py", timeout=30, raise_on_error=True)
except CommandTimeoutError:
print("Command timed out")
except CommandExecutionError as e:
print(f"Failed (exit {e.exit_code}): {e.stderr}")For full control, use SandboxClient directly:
from agentfense import SandboxClient
client = SandboxClient(endpoint="localhost:9000")
# Create codebase → upload files → create sandbox → start → exec → cleanup
codebase = client.create_codebase(name="my-project", owner_id="user_001")
client.upload_file(codebase.id, "main.py", b"print('hello')")
sandbox = client.create_sandbox(
codebase_id=codebase.id,
permissions=[{"pattern": "**/*", "permission": "read"}],
)
client.start_sandbox(sandbox.id)
result = client.exec(sandbox.id, command="python /workspace/main.py")
print(result.stdout)
client.destroy_sandbox(sandbox.id)
client.delete_codebase(codebase.id)# Create codebase
curl -X POST http://localhost:8080/v1/codebases \
-d '{"name": "my-project", "owner_id": "user_001"}'
# Create sandbox
curl -X POST http://localhost:8080/v1/sandboxes \
-d '{"codebase_id": "cb_xxx", "permissions": [{"pattern": "**/*", "permission": "PERMISSION_READ"}]}'
# Start → exec → cleanup
curl -X POST http://localhost:8080/v1/sandboxes/sb_xxx/start
curl -X POST http://localhost:8080/v1/sandboxes/sb_xxx/exec -d '{"command": "ls /workspace"}'
curl -X DELETE http://localhost:8080/v1/sandboxes/sb_xxx┌─────────────────────────────────────────────────────────────┐
│ Client Layer │
│ Go SDK / Python SDK / REST API │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Service Layer │
│ gRPC Server + REST Gateway (grpc-gateway) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Runtime Layer │
│ Sandbox Manager │ Permission Engine │ Executor │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Isolation Layer │
│ bwrap Runtime │ Docker Runtime │ FUSE FS │ Delta Layer │
└─────────────────────────────────────────────────────────────┘
Delta Layer (COW): Multiple sandboxes can share the same codebase with isolated writes. Each sandbox writes to its own delta directory; changes sync to source on completion (Last-Writer-Wins).
The architecture is designed to be lightweight. Each sandbox consumes minimal resources:
| Component | Per-Sandbox Overhead |
|---|---|
| Memory | ~5 MB |
| Processes | ~2 |
| FUSE mount | 1 |
| Docker container | 1 (Docker runtime only) |
Stress test results on a 2-core / 4GB RAM server (Docker runtime):
| Metric | Result |
|---|---|
| Max concurrent sandboxes | 100+ (tested up to 120) |
| Memory at 100 sandboxes | ~67% usage |
| Stability | No crashes, clean resource cleanup |
Recommended capacity (conservative):
| Server Spec | Suggested Max Sandboxes |
|---|---|
| 2 cores / 4 GB | 50–80 |
| 4 cores / 8 GB | 150–200 |
| 8 cores / 16 GB | 400+ |
The bottleneck is typically memory, not CPU or FUSE. For higher concurrency, consider sandbox pooling or on-demand creation.
| Capability | AgentFense | E2B | Docker | Others |
|---|---|---|---|---|
| Path-based least privilege | ✅ (glob + priority) | ❌ | ||
Hidden paths (none) |
✅ invisible | ❌ | ❌ | |
List-only paths (view) |
✅ | ❌ | ❌ | ❌ |
| Multi-sandbox codebase sharing | ✅ |
Completed: Session support, Docker runtime, resource limits, Delta Layer (COW), one-liner API, permission presets, semantic exceptions, async SDK.
Next: CLI tool, Go SDK, configuration files, file locking, agent communication.
Out of scope: MicroVM isolation, hibernate/wake (CRIU), million-scale concurrency.
# Run tests
go test ./...
# With coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.outThe view permission level may not work correctly on macOS with Docker Desktop due to VirtioFS limitations. Files appear as "No such file" inside containers.
Workarounds: Use Linux, use read instead of view, or use bwrap runtime.
| Permission | Linux | macOS (Docker Desktop) |
|---|---|---|
none |
✅ | ✅ |
view |
✅ | ❌ |
read |
✅ | ✅ |
write |
✅ | ✅ |
| Example | Description |
|---|---|
example/ticket-agent/ |
Interactive AI agent with permission demo (write/read/view/none) |
MIT