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

EndpointPurpose
POST /v1/file/readRead a text file, optionally by line range
POST /v1/file/writeWrite text or base64-encoded binary content
POST /v1/file/replaceReplace text in a file
POST /v1/file/searchSearch one file with a regular expression
POST /v1/file/findFind files by simple glob
POST /v1/file/grepSearch a file tree with include/exclude filters
POST /v1/file/globList files by advanced glob options
POST /v1/file/listList a directory
POST /v1/file/uploadUpload a file with multipart/form-data
GET /v1/file/downloadDownload a file stream
POST /v1/file/watchCreate a file watcher
POST /v1/file/watch/{watcher_id}/pollLong-poll watcher events
POST /v1/file/watch/waitWait for one file event
GET /v1/file/watch/{watcher_id}/eventsConsume 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:

  1. Check the HTTP status code.
  2. Check success.
  3. 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
Python
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:

EncodingUse forcontent value
utf-8Text filesPlain text
base64Binary filesBase64 string
rawAdvanced byte handlingLatin-1 style string

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:

  1. Put an uploaded or downloaded file under /home/gem/workspace.
  2. Read and transform it with the file API.
  3. Process data with shell or code execution.
  4. 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}")

Performance Optimization

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.