File Operations
AIO Sandbox exposes file APIs under /v1/file/* for reading, writing, searching, uploading, downloading, and watching files inside the sandbox.
The examples below assume the sandbox is available at http://localhost:8080 and use paths inside the container.
Endpoint Map
| Endpoint | Purpose |
|---|
POST /v1/file/read | Read a text file, optionally by line range |
POST /v1/file/write | Write text or base64-encoded binary content |
POST /v1/file/replace | Replace text in a file |
POST /v1/file/search | Search one file with a regular expression |
POST /v1/file/find | Find files by simple glob |
POST /v1/file/grep | Search a file tree with include/exclude filters |
POST /v1/file/glob | List files by advanced glob options |
POST /v1/file/list | List a directory |
POST /v1/file/upload | Upload a file with multipart/form-data |
GET /v1/file/download | Download a file stream |
POST /v1/file/watch | Create a file watcher |
POST /v1/file/watch/{watcher_id}/poll | Long-poll watcher events |
POST /v1/file/watch/wait | Wait for one file event |
GET /v1/file/watch/{watcher_id}/events | Consume watcher events with SSE |
DELETE /v1/file/watch/{watcher_id} | Stop a watcher |
Error Handling
For most file APIs, expected filesystem failures return HTTP 200 with success=false and a structured data object. Check responses in this order:
- Check the HTTP status code.
- Check
success.
- When
success=false, inspect data.error_type and data.errno_name.
Example not-found response:
{
"success": false,
"message": "Failed to read file: [Errno 2] No such file or directory: '/tmp/missing.txt'",
"data": {
"path": "/tmp/missing.txt",
"operation": "read",
"message": "Failed to read file: [Errno 2] No such file or directory: '/tmp/missing.txt'",
"error_type": "not_found",
"retryable": false,
"errno": 2,
"errno_name": "ENOENT",
"exception_type": "FileNotFoundError"
}
}
Common error_type values include not_found, permission_denied, invalid_target, already_exists, invalid_path, read_only_filesystem, no_space_left, decode_error, and io_error.
GET /v1/file/download is the main exception: it returns a binary stream on success and uses HTTP errors for download failures. File watch endpoints also use resource-style JSON and HTTP status codes instead of the standard success wrapper.
For cross-API conventions, see Error Handling.
Read Files
Read a file, optionally with a 0-based line range. end_line is exclusive.
curl -X POST http://localhost:8080/v1/file/read \
-H "Content-Type: application/json" \
-d '{
"file": "/home/gem/.bashrc",
"start_line": 0,
"end_line": 10,
"sudo": false
}'
Successful response:
{
"success": true,
"message": "File read successfully",
"data": {
"content": "export PATH=...",
"line_count": 10,
"file": "/home/gem/.bashrc"
}
}
Write Files
Text
curl -X POST http://localhost:8080/v1/file/write \
-H "Content-Type: application/json" \
-d '{
"file": "/tmp/output.txt",
"content": "Hello, World!",
"encoding": "utf-8",
"append": false,
"leading_newline": false,
"trailing_newline": true,
"sudo": false
}'
Binary
Use encoding: "base64" for images, PDFs, archives, and other binary files. The client encodes bytes before sending them.
curl -X POST http://localhost:8080/v1/file/write \
-H "Content-Type: application/json" \
-d '{
"file": "/tmp/pixel.png",
"content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"encoding": "base64"
}'
Supported encodings:
| Encoding | Use for | content value |
|---|
utf-8 | Text files | Plain text |
base64 | Binary files | Base64 string |
raw | Advanced byte handling | Latin-1 style string |
Replace And Search
Replace text:
curl -X POST http://localhost:8080/v1/file/replace \
-H "Content-Type: application/json" \
-d '{
"file": "/tmp/output.txt",
"old_str": "World",
"new_str": "Sandbox",
"sudo": false
}'
Search one file with a regular expression:
curl -X POST http://localhost:8080/v1/file/search \
-H "Content-Type: application/json" \
-d '{
"file": "/tmp/output.txt",
"regex": "Hello,\\s+\\w+"
}'
Search a directory tree:
curl -X POST http://localhost:8080/v1/file/grep \
-H "Content-Type: application/json" \
-d '{
"path": "/home/gem/workspace",
"pattern": "TODO",
"include": ["*.py", "*.ts", "*.tsx"],
"exclude": ["node_modules", ".git"],
"case_insensitive": true,
"max_results": 100
}'
Find, Glob, And List
Use find for simple filename patterns:
curl -X POST http://localhost:8080/v1/file/find \
-H "Content-Type: application/json" \
-d '{
"path": "/home/gem/workspace/src",
"glob": "*.py"
}'
Use glob for recursive matching, metadata, sorting, and hidden-file controls:
curl -X POST http://localhost:8080/v1/file/glob \
-H "Content-Type: application/json" \
-d '{
"path": "/home/gem/workspace",
"pattern": "**/*.json",
"exclude": ["node_modules/**"],
"include_hidden": false,
"files_only": true,
"include_metadata": true,
"max_results": 200,
"sort_by": "path"
}'
List a directory:
curl -X POST http://localhost:8080/v1/file/list \
-H "Content-Type: application/json" \
-d '{
"path": "/home/gem/workspace",
"recursive": false,
"show_hidden": true,
"include_size": true
}'
Upload And Download
Upload with multipart form data:
curl -X POST http://localhost:8080/v1/file/upload \
-F "file=@./report.pdf" \
-F "path=/tmp/report.pdf"
Download as a file stream:
curl -o report.pdf \
"http://localhost:8080/v1/file/download?path=/tmp/report.pdf"
When downloading files that may still be changing, use change_policy=abort. The server returns HTTP 409 if it detects the source changed before or during the transfer.
curl -o report.pdf \
"http://localhost:8080/v1/file/download?path=/tmp/report.pdf&change_policy=abort"
File Watch
File watch helps agents wait for generated files, react to browser downloads, or refresh a file tree after background commands.
Wait For One File
Use wait when you only need to know that one file was created, written, removed, renamed, or chmod-ed.
curl -X POST http://localhost:8080/v1/file/watch/wait \
-H "Content-Type: application/json" \
-d '{
"path": "/tmp/demo/result.json",
"timeout": 30,
"event_types": ["create", "write"]
}'
Example response:
{
"event": {
"seq": 1,
"type": "write",
"path": "/tmp/demo/result.json",
"relative_path": "result.json",
"old_path": null,
"is_dir": false,
"timestamp": 1776823501.334,
"mtime": 1776823501.321,
"size": 2048,
"inode": 91827555
}
}
If the file already exists and event_types includes create, wait may return immediately with a create event. If you only care about future changes, omit create.
Long-Poll A Directory
Use create + poll + delete for CLIs, CI jobs, and agents that need reliable cleanup.
WATCHER_ID=$(curl -s -X POST http://localhost:8080/v1/file/watch \
-H "Content-Type: application/json" \
-d '{
"path": "/home/gem/workspace",
"recursive": true,
"debounce": 200,
"exclude": [".git", "node_modules", "dist", ".next"],
"include_patterns": ["*.py", "*.ts", "*.tsx", "*.json"]
}' | jq -r ".data.watcher_id")
curl -X POST "http://localhost:8080/v1/file/watch/${WATCHER_ID}/poll" \
-H "Content-Type: application/json" \
-d '{
"cursor": 0,
"limit": 100,
"timeout": 10
}'
curl -X DELETE "http://localhost:8080/v1/file/watch/${WATCHER_ID}"
cursor means "the last consumed seq". Pass the response cursor directly into the next poll; do not add one yourself. If overflow=true, refresh your local file tree and continue from the returned cursor.
SSE For UI Refresh
Browser-based UIs can use Server-Sent Events:
GET /v1/file/watch/{watcher_id}/events
Accept: text/event-stream
The stream emits watch_started, file_change, and overflow events. Each file_change event contains the same file event shape shown above and includes an SSE id in the form {watcher_id}:{seq}.
Integration Examples
Python Integration
import requests
class SandboxFileAPI:
def __init__(self, base_url="http://localhost:8080"):
self.base_url = base_url.rstrip("/")
def read_file(self, file_path, start_line=None, end_line=None):
payload = {"file": file_path}
if start_line is not None:
payload["start_line"] = start_line
if end_line is not None:
payload["end_line"] = end_line
response = requests.post(
f"{self.base_url}/v1/file/read",
json=payload,
timeout=30,
)
return response.json()
def write_file(self, file_path, content, append=False):
response = requests.post(
f"{self.base_url}/v1/file/write",
json={
"file": file_path,
"content": content,
"append": append,
},
timeout=30,
)
return response.json()
def search_files(self, pattern, directory="/home/gem/workspace"):
response = requests.post(
f"{self.base_url}/v1/file/find",
json={
"path": directory,
"glob": pattern,
},
timeout=30,
)
return response.json()
api = SandboxFileAPI()
api.write_file("/home/gem/workspace/config.json", '{"debug": false}\n')
config = api.read_file("/home/gem/workspace/config.json")
print(config["data"]["content"])
api.write_file("/home/gem/workspace/app.log", "Process started\n", append=True)
files = api.search_files("*.py", "/home/gem/workspace/src")
for file_path in files["data"]["files"]:
print(f"Found: {file_path}")
JavaScript / Node.js Integration
class SandboxFileAPI {
constructor(baseUrl = "http://localhost:8080") {
this.baseUrl = baseUrl.replace(/\/$/, "");
}
async readFile(filePath, options = {}) {
const response = await fetch(`${this.baseUrl}/v1/file/read`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file: filePath, ...options }),
});
return response.json();
}
async writeFile(filePath, content, options = {}) {
const response = await fetch(`${this.baseUrl}/v1/file/write`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file: filePath, content, ...options }),
});
return response.json();
}
async replaceInFile(filePath, oldStr, newStr) {
const response = await fetch(`${this.baseUrl}/v1/file/replace`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file: filePath,
old_str: oldStr,
new_str: newStr,
}),
});
return response.json();
}
}
const api = new SandboxFileAPI();
const result = await api.readFile("/home/gem/workspace/data.txt");
if (result.success) {
console.log("File content:", result.data.content);
await api.replaceInFile(
"/home/gem/workspace/config.json",
'"debug": false',
'"debug": true',
);
}
Filesystem Integration
Shared Access
Files are shared across shell, browser downloads, code execution, code-server, and file APIs:
# Create a file through the API
curl -X POST http://localhost:8080/v1/file/write \
-H "Content-Type: application/json" \
-d '{"file": "/home/gem/workspace/shared.txt", "content": "Shared content"}'
# Read it from a shell
# ws://localhost:8080/v1/shell/ws
# > cat /home/gem/workspace/shared.txt
# Shared content
# Edit it in code-server
# http://localhost:8080/code-server/
# Open /home/gem/workspace/shared.txt
Workflow Example
A typical file workflow is:
- Put an uploaded or downloaded file under
/home/gem/workspace.
- Read and transform it with the file API.
- Process data with shell or code execution.
- Inspect the result in code-server, or download the artifact.
import json
content = client.file.read_file(
file="/home/gem/Downloads/data.csv",
).data.content
processed = process_csv(content)
client.file.write_file(
file="/home/gem/workspace/results.json",
content=json.dumps(processed),
)
python /home/gem/workspace/analyze.py /home/gem/workspace/results.json
curl -o report.pdf \
"http://localhost:8080/v1/file/download?path=/home/gem/workspace/report.pdf"
Advanced Features
Batch Operations
Process multiple files efficiently:
def batch_process_files(api, directory, pattern):
files_result = api.search_files(pattern, directory)
for file_path in files_result["data"]["files"]:
content_result = api.read_file(file_path)
if content_result["success"]:
content = content_result["data"]["content"]
output_path = file_path.replace(".txt", "_processed.txt")
api.write_file(output_path, content.upper())
batch_process_files(api, "/home/gem/workspace/data", "*.txt")
Error Handling
Handle both HTTP errors and success=false responses:
import json
import requests
def safe_file_operation(api, operation, **kwargs):
try:
result = operation(**kwargs)
if result["success"]:
return result["data"]
error = result.get("data") or {}
if error.get("error_type") == "not_found":
print("File does not exist")
return None
print(f"Operation failed: {result['message']}")
return None
except requests.exceptions.RequestException as exc:
print(f"Network error: {exc}")
return None
except json.JSONDecodeError as exc:
print(f"JSON decode error: {exc}")
return None
content = safe_file_operation(
api,
api.read_file,
file_path="/home/gem/workspace/data.txt",
)
Permissions
File APIs use the sandbox application user by default. Enable sudo only when the workflow needs a protected path:
result = api.read_file("/home/gem/workspace/file.txt")
result = api.read_file("/etc/nginx/nginx.conf", sudo=True)
api.write_file(
"/etc/cron.d/example",
"0 2 * * * root /home/gem/workspace/backup.sh\n",
sudo=True,
)
Security Notes
File Access Control
- Use the default application user when possible.
- Enable
sudo only intentionally, and do not pass raw user input into protected paths.
- Normalize user-provided paths and enforce an allowed base directory.
- Apply application-level limits for upload and download sizes.
Secure Path Example
import os
import re
def secure_file_operation(file_path, base_directory="/home/gem/workspace"):
normalized = os.path.normpath(file_path)
if not normalized.startswith(base_directory):
raise ValueError("Path is outside the allowed directory")
if not re.match(r"^[a-zA-Z0-9._/-]+$", normalized):
raise ValueError("Invalid characters in filename")
return normalized
try:
safe_path = secure_file_operation("/home/gem/workspace/data.txt")
except ValueError as exc:
print(f"Security violation: {exc}")
Large Files
Read large text files by line range to avoid putting the entire file in one response:
def read_large_file(api, file_path, chunk_size=1000):
total_lines = 0
content_parts = []
while True:
result = api.read_file(
file_path,
start_line=total_lines,
end_line=total_lines + chunk_size,
)
if not result["success"] or not result["data"]["content"]:
break
content = result["data"]["content"]
content_parts.append(content)
total_lines += chunk_size
if len(content.splitlines()) < chunk_size:
break
return "\n".join(content_parts)
Concurrent Operations
import aiohttp
import asyncio
async def read_file_async(session, file_path):
payload = {"file": file_path}
async with session.post(
"http://localhost:8080/v1/file/read",
json=payload,
) as response:
return await response.json()
async def parallel_file_operations(files):
async with aiohttp.ClientSession() as session:
tasks = [read_file_async(session, file_path) for file_path in files]
return await asyncio.gather(*tasks)
Ready to integrate file operations? See the API reference for the complete schema.