Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
*.pyo
*.egg-info/
Empty file removed cli/.gitkeep
Empty file.
96 changes: 96 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# AIO Sandbox CLI

Command-line interface for [AIO Sandbox](https://github.com/agent-infra/sandbox) - the all-in-one agent sandbox environment.

## Installation

```bash
# Install dependencies
pip install agent-sandbox

# Run CLI directly
python main.py --help

# Or install as a package
pip install -e .
sandbox --help
```

## Quick Start

```bash
# Set up a sandbox instance
docker run --security-opt seccomp=unconfined --rm -it -p 8080:8080 ghcr.io/agent-infra/sandbox:latest

# Execute commands
sandbox exec "echo hello"
sandbox exec "ls -la /home/user"
```

## Usage

### Shell Commands

```bash
# Execute a shell command
sandbox exec "ls -la"

# List active sessions
sandbox sessions

# Open interactive shell (use VNC or web terminal)
sandbox shell
```

### File Operations

```bash
# Read a file
sandbox cat /home/user/.bashrc

# List directory
sandbox ls /home/user

# Upload file to sandbox
sandbox upload local.txt /home/user/remote.txt

# Download file from sandbox
sandbox download /home/user/remote.txt ./local.txt
```

### Browser

```bash
# Take screenshot (base64 output)
sandbox screenshot

# Save screenshot to file
sandbox screenshot screenshot.png

# Get browser info
sandbox browser-info
```

### Code Execution

```bash
# Run Python code
sandbox run-python "print('Hello, World!')"

# Get Jupyter info
sandbox jupyter-info

# Run Node.js code
sandbox run-node "console.log('Hello, World!')"
```

## Options

| Option | Description | Default |
|--------|-------------|---------|
| `--base-url` | Sandbox API base URL | `http://localhost:8080` |
| `-o, --output` | Output format (`table`, `json`, `yaml`) | `table` |

## License

Apache License 2.0
1 change: 1 addition & 0 deletions cli/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# sandbox-cli - CLI tool for AIO Sandbox
31 changes: 31 additions & 0 deletions cli/commands/browser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import click
import base64
from .root import root, get_client


@root.command()
@click.argument('output_path', required=False)
@click.pass_context
def screenshot(ctx, output_path):
"""Take a screenshot"""
client = get_client(ctx)
img_data = b''.join(client.browser.screenshot())

if output_path:
with open(output_path, 'wb') as f:
f.write(img_data)
click.echo(f"Screenshot saved to {output_path}")
else:
# Output as base64 to stdout
click.echo(base64.b64encode(img_data).decode())


@root.command()
@click.pass_context
def browser_info(ctx):
"""Get browser information"""
client = get_client(ctx)
info = client.browser.get_info()
click.echo(f"CDP URL: {info.cdp_url}")
click.echo(f"Display: {info.display}")
click.echo(f"Viewport: {info.viewport.width}x{info.viewport.height}")
64 changes: 64 additions & 0 deletions cli/commands/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import click
import json
from .root import root, get_client


@root.command()
@click.argument('path')
@click.pass_context
def cat(ctx, path):
"""Read a file"""
client = get_client(ctx)
result = client.file.read_file(file=path)
click.echo(result.data.content)


@root.command()
@click.argument('remote_path')
@click.argument('local_path')
@click.pass_context
def download(ctx, remote_path, local_path):
"""Download a file from sandbox"""
client = get_client(ctx)
result = client.file.read_file(file=remote_path)
with open(local_path, 'w') as f:
f.write(result.data.content)
click.echo(f"Downloaded to {local_path}")


@root.command()
@click.argument('local_path')
@click.argument('remote_path')
@click.pass_context
def upload(ctx, local_path, remote_path):
"""Upload a file to sandbox"""
client = get_client(ctx)
with open(local_path, 'r') as f:
content = f.read()
client.file.write_file(file=remote_path, content=content)
click.echo(f"Uploaded to {remote_path}")


@root.command()
@click.argument('path')
@click.option('--pattern', '-p', help='Search pattern')
@click.pass_context
def ls(ctx, path, pattern):
"""List files in a directory"""
client = get_client(ctx)
if pattern:
result = client.file.search_in_file(path=path, pattern=pattern)
if ctx.obj['output'] == 'json':
click.echo(json.dumps([{'path': m.path, 'line': m.line, 'content': m.content} for m in result.data.matches], indent=2))
else:
for m in result.data.matches:
click.echo(f"{m.path}:{m.line}: {m.content}")
else:
result = client.file.list_path(path=path)
if ctx.obj['output'] == 'json':
click.echo(json.dumps([{'path': f.path, 'is_dir': f.is_dir, 'size': f.size} for f in result.data.files], indent=2))
else:
for f in result.data.files:
prefix = 'd' if f.is_dir else '-'
size = f.size or 0
click.echo(f"{prefix} {size:>10} {f.path}")
29 changes: 29 additions & 0 deletions cli/commands/jupyter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import click
import json
from .root import root, get_client


@root.command()
@click.argument('code')
@click.option('--session-id', '-s', help='Jupyter session ID')
@click.pass_context
def run_python(ctx, code, session_id):
"""Execute Python code in Jupyter"""
client = get_client(ctx)
result = client.jupyter.execute_code(code=code, session_id=session_id)

for output in result.outputs:
if hasattr(output, 'text') and output.text:
click.echo(output.text)
elif hasattr(output, 'error') and output.error:
click.echo(output.error, err=True)


@root.command()
@click.pass_context
def jupyter_info(ctx):
"""Get Jupyter runtime info"""
client = get_client(ctx)
info = client.jupyter.get_info()
click.echo(f"Python Version: {info.python_version}")
click.echo(f"Working Directory: {info.working_directory}")
26 changes: 26 additions & 0 deletions cli/commands/nodejs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import click
from .root import root, get_client


@root.command()
@click.argument('code')
@click.option('--session-id', '-s', help='Node.js session ID')
@click.pass_context
def run_node(ctx, code, session_id):
"""Execute Node.js code"""
client = get_client(ctx)
result = client.nodejs.execute_code(code=code, session_id=session_id)

for output in result.outputs:
if hasattr(output, 'text') and output.text:
click.echo(output.text)


@root.command()
@click.pass_context
def node_info(ctx):
"""Get Node.js runtime info"""
client = get_client(ctx)
info = client.nodejs.get_info()
click.echo(f"Node.js Version: {info.version}")
click.echo(f"V8 Version: {info.v8_version}")
18 changes: 18 additions & 0 deletions cli/commands/root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import click
from agent_sandbox import Sandbox


@click.group()
@click.option('--base-url', default='http://localhost:8080', help='Sandbox API base URL')
@click.option('--output', '-o', type=click.Choice(['table', 'json', 'yaml']), default='table', help='Output format')
@click.pass_context
def root(ctx, base_url, output):
"""AIO Sandbox CLI - Command line tool for AI agent sandbox environments"""
ctx.ensure_object(dict)
ctx.obj['base_url'] = base_url
ctx.obj['output'] = output
ctx.obj['client'] = Sandbox(base_url=base_url)


def get_client(ctx):
return ctx.obj.get('client') or Sandbox(base_url=ctx.obj['base_url'])
69 changes: 69 additions & 0 deletions cli/commands/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import click
import json
from .root import root, get_client


@root.command()
@click.argument('command')
@click.option('--session-id', '-s', help='Shell session ID')
@click.option('--exec-dir', help='Working directory (absolute path)')
@click.option('--timeout', type=float, help='Timeout in seconds')
@click.option('--async', 'async_mode', is_flag=True, help='Run in async mode')
@click.pass_context
def exec(ctx, command, session_id, exec_dir, timeout, async_mode):
"""Execute a shell command"""
client = get_client(ctx)
result = client.shell.exec_command(
command=command,
id=session_id,
exec_dir=exec_dir,
timeout=timeout,
async_mode=async_mode
)

if ctx.obj['output'] == 'json':
click.echo(json.dumps({
'id': result.id,
'status': result.status,
'stdout': result.stdout,
'stderr': result.stderr,
'exit_code': result.exit_code
}, indent=2))
else:
click.echo(result.stdout, nl=not result.stdout.endswith('\n'))
if result.stderr:
click.echo(result.stderr, err=True, nl=not result.stderr.endswith('\n'))


@root.command()
@click.pass_context
def shell(ctx):
"""Open an interactive shell session"""
client = get_client(ctx)
click.echo("Creating interactive shell session...")
click.echo("WebSocket terminal available at: " + client.shell.get_terminal_url().data)
click.echo("(Use VNC or web terminal to interact)")


@root.command()
@click.pass_context
def sessions(ctx):
"""List all active shell sessions"""
client = get_client(ctx)
result = client.shell.list_sessions()

if ctx.obj['output'] == 'json':
click.echo(json.dumps([{
'id': s.id,
'status': s.status,
'command': s.command
} for s in result.data.sessions], indent=2))
else:
if not result.data.sessions:
click.echo("No active sessions")
else:
click.echo(f"{'ID':<40} {'STATUS':<10} {'COMMAND'}")
click.echo("-" * 80)
for s in result.data.sessions:
cmd = s.command[:40] if s.command else ''
click.echo(f"{s.id:<40} {s.status:<10} {cmd}")
40 changes: 40 additions & 0 deletions cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""AIO Sandbox CLI - Command line tool for AI agent sandbox environments"""

import sys
import sys
import os

# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from commands.root import root
from commands.shell import exec, shell, sessions
from commands.file import cat, download, upload, ls
from commands.browser import screenshot, browser_info
from commands.jupyter import run_python, jupyter_info
from commands.nodejs import run_node, node_info


def main():

# Register all commands
root.add_command(exec)
root.add_command(shell)
root.add_command(sessions)
root.add_command(cat)
root.add_command(download)
root.add_command(upload)
root.add_command(ls)
root.add_command(screenshot)
root.add_command(browser_info)
root.add_command(run_python)
root.add_command(jupyter_info)
root.add_command(run_node)
root.add_command(node_info)

root()


if __name__ == '__main__':
main()
Loading