• English
  • 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.