diff --git a/AGENTS.md b/AGENTS.md index de5dc4e..af3adec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -This repository contains 12 agent skills for the Together AI platform. Each skill is a self-contained directory following the [Agent Skills specification](https://agentskills.io/specification). +This repository contains 13 agent skills for the Together AI platform. Each skill is a self-contained directory following the [Agent Skills specification](https://agentskills.io/specification). ## Skill registry @@ -17,6 +17,7 @@ This repository contains 12 agent skills for the Together AI platform. Each skil - **together-dedicated-endpoints**: Single-tenant GPU endpoints on Together AI with autoscaling and no rate limits. Deploy fine-tuned or uploaded models, size hardware, and manage endpoint lifecycle. Reach for it whenever the user needs predictable always-on hosting rather than serverless inference, custom containers, or raw clusters. - **together-dedicated-containers**: Custom Dockerized inference workers on Together AI's managed GPU infrastructure. Build with Sprocket SDK, configure with Jig CLI, submit async queue jobs, and poll results. Reach for it whenever the user needs container-level control rather than a standard model endpoint or raw cluster. - **together-gpu-clusters**: On-demand and reserved GPU clusters (H100, H200, B200) on Together AI with Kubernetes or Slurm orchestration, shared storage, credential management, and cluster scaling for ML and HPC jobs. Reach for it when the user needs multi-node compute or infrastructure control rather than a managed model endpoint. +- **together-sandbox**: Isolated gVisor sandboxes for RL training, SFT data generation, and coding agent rollouts on Together AI. Create snapshots from Docker images, run multi-turn bash commands, read and write files, and manage sandbox lifecycle via the together-sandbox Python SDK. Reach for it whenever the user needs isolated container execution for agent environments, reward computation, or parallel code evaluation rather than managed Python notebooks or raw GPU clusters. diff --git a/README.md b/README.md index 562fa59..4378058 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Each skill contains: | **together-dedicated-endpoints** | Single-tenant GPU endpoints on Together AI with autoscaling and no rate limits. | `deploy_finetuned.py`, `manage_endpoint.py`, `upload_custom_model.py` | | **together-dedicated-containers** | Custom Dockerized inference workers on Together AI's managed GPU infrastructure. | `queue_client.py`, `sprocket_hello_world.py` | | **together-gpu-clusters** | On-demand and reserved GPU clusters (H100, H200, B200) on Together AI with Kubernetes or Slurm orchestration, shared ... | `manage_cluster.py`, `manage_storage.py` | +| **together-sandbox** | Isolated gVisor sandboxes for RL training, SFT data generation, and coding agent rollouts on Together AI. | `parallel_fanout.py`, `sandbox_lifecycle.py` | ## Installation diff --git a/quality/trigger-evals/together-sandbox.json b/quality/trigger-evals/together-sandbox.json new file mode 100644 index 0000000..3842419 --- /dev/null +++ b/quality/trigger-evals/together-sandbox.json @@ -0,0 +1,26 @@ +[ + { + "query": "Create isolated sandbox environments from a Docker image and run multi-turn bash commands for an RL training loop on Together AI.", + "should_trigger": true + }, + { + "query": "I need to fan out 32 parallel sandboxes for GRPO reward computation using the together-sandbox SDK.", + "should_trigger": true + }, + { + "query": "Set up a golden image snapshot with dependencies installed, then create ephemeral sandboxes from it for coding agent rollouts.", + "should_trigger": true + }, + { + "query": "Run Python remotely on Together AI with session reuse and generate a matplotlib chart.", + "should_trigger": false + }, + { + "query": "Fine-tune a Llama model on my custom dataset with LoRA on Together AI.", + "should_trigger": false + }, + { + "query": "Provision a multi-node H100 cluster on Together AI and run a distributed training job.", + "should_trigger": false + } +] diff --git a/skills/together-sandbox/SKILL.md b/skills/together-sandbox/SKILL.md new file mode 100644 index 0000000..318d1e2 --- /dev/null +++ b/skills/together-sandbox/SKILL.md @@ -0,0 +1,93 @@ +--- +name: together-sandbox +description: "Isolated gVisor sandboxes for RL training, SFT data generation, and coding agent rollouts on Together AI. Create snapshots from Docker images, run multi-turn bash commands, read and write files, and manage sandbox lifecycle via the together-sandbox Python SDK. Reach for it whenever the user needs isolated container execution for agent environments, reward computation, or parallel code evaluation rather than managed Python notebooks or raw GPU clusters." +--- + +# Together Sandbox + +## Overview + +Use Together Sandbox when the user needs isolated container environments for executing untrusted code, running RL training loops, or orchestrating coding agent rollouts. + +Typical fits: + +- GRPO/RL training with parallel sandbox-based reward computation +- SFT data generation with verified trajectory collection +- Coding agent rollouts (multi-turn bash execution in isolated environments) +- Batch evaluation of code against test suites (SWE-bench, Terminal-Bench) +- Any workflow requiring Docker-image-based environments with file I/O and command execution + +## When This Skill Wins + +- The user needs isolated container execution from a Docker image, not just a Python notebook +- Sandboxes must survive multi-turn command sequences (1-10 sequential bash commands) +- The workflow requires parallel sandbox fan-out (8-256+ concurrent environments) +- Files need to be uploaded to or downloaded from the sandbox filesystem +- The user references RL training, GRPO, reward computation, verifier environments, or coding agents + +## Hand Off To Another Skill + +- Use `together-sandboxes` (plural) for managed Python notebook execution via the Code Interpreter API +- Use `together-gpu-clusters` for multi-node GPU compute or distributed training jobs +- Use `together-dedicated-containers` for custom containerized inference workers +- Use `together-fine-tuning` for model training jobs (LoRA, DPO, full fine-tuning) +- Use `together-chat-completions` if the user only needs inference, not code execution + +## Quick Routing + +- **Create a sandbox and run commands** + - Start with [scripts/sandbox_lifecycle.py](scripts/sandbox_lifecycle.py) +- **Full SDK API (snapshots, sandboxes, execs, files, retries)** + - Read the SDK's own docs — see [SDK Reference](#sdk-reference) below +- **RL training patterns (GRPO batch, reward collection, golden images)** + - Read [references/rl-patterns.md](references/rl-patterns.md) +- **Parallel fan-out for batch evaluation** + - Start with [scripts/parallel_fanout.py](scripts/parallel_fanout.py) + +## Workflow + +1. Install the SDK: `pip install together-sandbox` (Python). See [SDK Reference](#sdk-reference) for TypeScript. +2. Create a snapshot from a Docker image or Dockerfile using `sdk.snapshots.create()`. +3. Create a sandbox from the snapshot using `sdk.sandboxes.create()` with CPU, memory, and disk specs. +4. Start the sandbox using `sdk.sandboxes.start()`, which returns a connected `Sandbox` object. +5. Configure DNS inside the sandbox as the first exec (sandboxes have no DNS by default). +6. Run commands with `sandbox.execs.exec("bash", ["-c", cmd])` and read/write files with `sandbox.files`. +7. For RL: collect reward files from the sandbox filesystem after test execution. +8. Shut down or hibernate via the instance (`await sandbox.shutdown()`) or by ID (`await sdk.sandboxes.shutdown(sandbox.id)`). + +## High-Signal Rules + +- The SDK is async-native. All methods are `async`. Use `asyncio.run()` or an async context. +- Run a command to completion with `sandbox.execs.exec("bash", ["-c", cmd])`, which returns a dict `{"exit_code": int, "output": str}`. For long-running or interactive commands, create the exec with `sandbox.execs.create(...)` and stream with `sandbox.execs.stream_output(id)`. Always wrap shell commands in `bash -c`. +- Sandboxes have no DNS by default. Run `echo "nameserver 1.1.1.1" > /etc/resolv.conf` as your first exec or all network calls will fail. +- Tools installed via pip land in `/root/.local/bin`, which is not on PATH. Run `export PATH="/root/.local/bin:$PATH"` before using them. +- `sdk.sandboxes.create()` returns a `SandboxModel` (metadata). `sdk.sandboxes.start()` returns a connected `Sandbox` with exec and file access. These are two separate steps. +- The SDK handles authentication automatically. Set `TOGETHER_API_KEY` and the two-auth system (management API + in-sandbox agent API) is abstracted away. +- Ephemeral sandboxes (`ephemeral=True`) auto-delete on stop and cannot hibernate. Use for disposable training runs. +- For parallel fan-out, use `asyncio.gather()` to create and start multiple sandboxes concurrently. +- Shut down or hibernate either on the connected instance (`await sandbox.shutdown()` / `await sandbox.hibernate()`) or by ID via the namespace (`await sdk.sandboxes.shutdown(sandbox.id)`). Both forms are valid. + +## Resource Map + +- **Full SDK API**: see [SDK Reference](#sdk-reference) +- **RL workflow patterns**: [references/rl-patterns.md](references/rl-patterns.md) +- **Sandbox lifecycle script**: [scripts/sandbox_lifecycle.py](scripts/sandbox_lifecycle.py) +- **Parallel fan-out script**: [scripts/parallel_fanout.py](scripts/parallel_fanout.py) + +## SDK Reference + +The SDK ships its own authoritative, version-matched API docs. **Do not restate the API surface in this skill** — read the SDK docs directly so guidance never drifts from the installed version: + +- **Python (primary):** [docs/python-sdk.md](https://github.com/togethercomputer/together-sandbox/blob/main/docs/python-sdk.md) +- **TypeScript:** [docs/typescript-sdk.md](https://github.com/togethercomputer/together-sandbox/blob/main/docs/typescript-sdk.md) +- **CLI:** [docs/cli.md](https://github.com/togethercomputer/together-sandbox/blob/main/docs/cli.md) + +### Install + +Python (published on PyPI): + +```bash +pip install together-sandbox +``` + +The TypeScript SDK (`@together-sandbox/sdk`) is not yet on npm — until it is published, install from source per the repo instructions: [github.com/togethercomputer/together-sandbox](https://github.com/togethercomputer/together-sandbox). diff --git a/skills/together-sandbox/agents/openai.yaml b/skills/together-sandbox/agents/openai.yaml new file mode 100644 index 0000000..97dd563 --- /dev/null +++ b/skills/together-sandbox/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Together Sandbox" + short_description: "Together AI isolated container execution for RL and coding agents" + default_prompt: "Use $together-sandbox to create isolated gVisor sandboxes from Docker images, execute multi-turn bash commands, and manage files for RL training, agent rollouts, and code evaluation on Together AI." diff --git a/skills/together-sandbox/references/rl-patterns.md b/skills/together-sandbox/references/rl-patterns.md new file mode 100644 index 0000000..8eb0d15 --- /dev/null +++ b/skills/together-sandbox/references/rl-patterns.md @@ -0,0 +1,273 @@ +# RL Training Patterns with Together Sandbox + +## Contents + +- [Overview](#overview) +- [The GRPO Training Loop](#the-grpo-training-loop) +- [Pattern 1: Golden Image Setup](#pattern-1-golden-image-setup) +- [Pattern 2: Batch Sandbox Fan-out](#pattern-2-batch-sandbox-fan-out) +- [Pattern 3: Multi-turn Agent Rollout](#pattern-3-multi-turn-agent-rollout) +- [Pattern 4: Reward Collection](#pattern-4-reward-collection) +- [Pattern 5: Resource Cleanup](#pattern-5-resource-cleanup) +- [Scale Reference](#scale-reference) + +## Overview + +Together Sandbox is the execution layer for RL training loops that interleave model inference with sandbox-based code execution. The primary workload is GRPO (Group Relative Policy Optimization), where each training step creates a batch of sandboxes, runs coding agent rollouts, computes rewards via test suites, and feeds trajectories back to the training service. + +The customer orchestrates the RL loop. Together controls the capacity behind the APIs. + +## The GRPO Training Loop + +``` +for step in training_steps: + create N sandboxes (groups_per_batch x group_size; default 8x4=32) + in parallel xN: + multi-turn rollout: inference -> sandbox exec -> observe (1-10 turns) + compute reward inside sandbox (run test suite, read reward file) + close sandbox + center rewards within each group (GRPO advantage) + assemble training batch from trajectories + train step (forward/backward + optimizer on GPU) + save weights, get new inference client + loop +``` + +Five integration points where sandbox behavior affects training: + +| Integration point | What happens | Why it matters | +|-------------------|-------------|----------------| +| Image onboarding | Task Dockerfile to snapshot, once per task | Blocks training start | +| Batch creation | All sandboxes must reach "running" before rollouts | GPU sits idle waiting for slowest sandbox | +| Multi-turn exec | 1-10 bash commands per rollout, sandbox must survive all | Dead sandbox wastes all prior inference cost | +| Reward collection | Read reward file from sandbox after test suite | Missing reward invalidates entire GRPO group | +| Cleanup | Destroy sandboxes after reward collection | Orphans accumulate over multi-day runs | + +## Pattern 1: Golden Image Setup + +Build a reusable snapshot with dependencies and agent tools installed. Run once before training begins. + +```python +import asyncio +from together_sandbox import ( + TogetherSandbox, + CreateImageSnapshotParams, +) + +async def setup_golden_image(): + async with TogetherSandbox() as sdk: + # Create base snapshot from Docker image + result = await sdk.snapshots.create(CreateImageSnapshotParams( + image="python:3.11-slim", + alias="python-base", + )) + + # Create a non-ephemeral sandbox to customize + model = await sdk.sandboxes.create( + snapshot_id=result.snapshot_id, + millicpu=2000, + memory_bytes=4 * 1024**3, + disk_bytes=10 * 1024**3, + ephemeral=False, # Must be non-ephemeral to hibernate + ) + sandbox = await sdk.sandboxes.start(model.id) + + # Configure DNS (required for pip install) + await sandbox.execs.exec("bash", ["-c", + 'echo "nameserver 1.1.1.1" > /etc/resolv.conf && ' + 'echo "nameserver 8.8.8.8" >> /etc/resolv.conf' + ]) + + # Install dependencies + result = await sandbox.execs.exec("bash", ["-c", + 'pip install numpy pytest torch transformers' + ]) + print(f"pip install: exit {result['exit_code']}") + + # Upload reward function + await sandbox.files.create("/app/reward_fn.py", """ +import subprocess +def compute_reward(test_file: str) -> float: + result = subprocess.run(["python3", "-m", "pytest", test_file, "-q"], + capture_output=True, text=True) + return 1.0 if result.returncode == 0 else 0.0 +""") + + # Hibernate to capture state as new snapshot + await sdk.sandboxes.hibernate(sandbox.id) + print("Golden image created via hibernate.") + +asyncio.run(setup_golden_image()) +``` + +## Pattern 2: Batch Sandbox Fan-out + +Create N sandboxes in parallel for one GRPO training step. GPU sits idle until all are running. + +```python +import asyncio +import time +from together_sandbox import TogetherSandbox, CreateImageSnapshotParams + +async def batch_fanout(snapshot_alias: str, count: int = 32): + async with TogetherSandbox() as sdk: + # Create all sandboxes concurrently + models = await asyncio.gather(*[ + sdk.sandboxes.create( + snapshot_alias=snapshot_alias, + millicpu=2000, + memory_bytes=4 * 1024**3, + disk_bytes=10 * 1024**3, + ephemeral=True, + ) + for _ in range(count) + ]) + + # Start all concurrently and measure timing + start_time = time.time() + + async def start_and_time(model): + t0 = time.time() + sandbox = await sdk.sandboxes.start(model.id) + return sandbox, time.time() - t0 + + results = await asyncio.gather(*[start_and_time(m) for m in models]) + sandboxes = [r[0] for r in results] + times = [r[1] for r in results] + + total = time.time() - start_time + times.sort() + print(f"Batch of {count}: {total:.1f}s total") + print(f" p50: {times[len(times)//2]:.1f}s") + print(f" max: {times[-1]:.1f}s (this is how long GPU waits)") + + return sandboxes + +asyncio.run(batch_fanout("rl-env-v1", count=8)) +``` + +## Pattern 3: Multi-turn Agent Rollout + +Simulate a coding agent executing sequential commands inside a sandbox. Each turn depends on the previous one. + +```python +async def execute_rollout(sandbox, task_prompt: str) -> list[dict]: + """Execute a multi-turn rollout. Returns list of (action, observation) pairs.""" + trajectory = [] + + # Configure environment + await sandbox.execs.exec("bash", ["-c", + 'echo "nameserver 1.1.1.1" > /etc/resolv.conf && ' + 'echo "nameserver 8.8.8.8" >> /etc/resolv.conf' + ]) + + for turn in range(10): # max 10 turns + # In a real RL loop, the command comes from the inference API + command = get_next_command(task_prompt, trajectory) + + if command is None: + break # Agent decided to stop + + result = await sandbox.execs.exec("bash", ["-c", command]) + observation = { + "turn": turn, + "command": command, + "stdout": result["output"], + "exit_code": result["exit_code"], + } + trajectory.append(observation) + + return trajectory + + +def get_next_command(task: str, history: list[dict]) -> str | None: + """Placeholder: in production, this calls the Together inference API.""" + # Replace with: response = client.chat.completions.create(...) + if len(history) >= 5: + return None + commands = [ + "echo 'def add(a, b): return a + b' > /tmp/solution.py", + "cd /tmp && python3 -c 'from solution import add; print(add(2,3))'", + "python3 -m pytest /tmp/test_solution.py -q 2>&1 || true", + "cat /tmp/test_solution.py 2>&1 || echo 'no test file'", + "echo 'done'", + ] + return commands[len(history)] if len(history) < len(commands) else None +``` + +## Pattern 4: Reward Collection + +Collect rewards from all sandboxes in a GRPO group. All rewards must be present for the group to be valid. + +```python +async def collect_group_rewards( + sdk, + sandboxes: list, + test_command: str = "python3 -m pytest /tmp/tests/ -q", + reward_path: str = "/tmp/reward.txt", +) -> dict: + """Collect rewards from a GRPO group. Returns rewards and group validity.""" + + async def collect_one(sandbox) -> float | None: + try: + # Run test suite + result = await sandbox.execs.exec("bash", ["-c", test_command]) + + # Write reward based on test result + reward = 1.0 if result["exit_code"] == 0 else 0.0 + await sandbox.files.create(reward_path, str(reward)) + + # Read it back (validates file API round-trip) + content = await sandbox.files.read(reward_path) + return float(content.strip()) + except Exception as e: + print(f"Sandbox {sandbox.id} failed: {e}") + return None + + rewards = await asyncio.gather(*[collect_one(sbx) for sbx in sandboxes]) + + valid_rewards = [r for r in rewards if r is not None] + group_complete = len(valid_rewards) == len(sandboxes) + + return { + "rewards": rewards, + "group_complete": group_complete, + "group_mean": sum(valid_rewards) / len(valid_rewards) if valid_rewards else 0.0, + "collected": len(valid_rewards), + "expected": len(sandboxes), + } +``` + +## Pattern 5: Resource Cleanup + +Clean up orphaned resources after training. Critical for multi-day runs with 400,000+ sandbox instantiations. + +```python +async def cleanup_orphans(): + async with TogetherSandbox() as sdk: + snapshots = await sdk.snapshots.list() + + # Snapshot objects do not carry alias information. + # To check if a snapshot is aliased, you must try get_by_alias() + # for known aliases, or track aliases separately in your application. + print(f"Total snapshots: {len(snapshots)}") + + for s in snapshots: + print(f" {s.id}: {s.byte_size} bytes, created {s.created_at}") + +asyncio.run(cleanup_orphans()) +``` + +Note: The `Snapshot` model does not include an `alias` field. Aliases are separate entities managed via `sdk.snapshots.alias()`, `get_by_alias()`, and `delete_by_alias()`. To determine if a snapshot is aliased, maintain a mapping in your application or attempt `get_by_alias()` for known names. + +## Scale Reference + +| Dimension | Eval | Training (GRPO) | Production | +|-----------|------|-----------------|------------| +| Concurrent sandboxes | 50-200 | 32-256 per batch | 500-1,000 sustained | +| Sandbox lifetime | Up to 1 hour | ~10 minutes | ~15 minutes average | +| Unique task images | 50-500 | 89 (Terminal-Bench) | 25,000+ | +| Run duration | Hours | 8+ hours | 5+ days | +| Total instantiations | 200-500 | Hundreds per run | 400,000+ | + +Default GRPO batch: `groups_per_batch=8`, `group_size=4` = 32 sandboxes per step. Both parameters are configurable with no upper bound. Async mode doubles effective concurrency. diff --git a/skills/together-sandbox/scripts/parallel_fanout.py b/skills/together-sandbox/scripts/parallel_fanout.py new file mode 100644 index 0000000..621f322 --- /dev/null +++ b/skills/together-sandbox/scripts/parallel_fanout.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Together Sandbox — Parallel Fan-out for RL Batch Evaluation + +Creates N sandboxes in parallel, executes a command in each, collects results, +and cleans up. Demonstrates the GRPO batch creation pattern where GPU sits idle +until all sandboxes are running. + +Usage: + python parallel_fanout.py + +Requires: + pip install together-sandbox + export TOGETHER_API_KEY=your_key + +Full SDK reference: + https://github.com/togethercomputer/together-sandbox/blob/main/docs/python-sdk.md +""" + +import asyncio +import time + +from together_sandbox import ( + TogetherSandbox, + CreateImageSnapshotParams, +) + +BATCH_SIZE = 4 # Number of parallel sandboxes (set low for demo; production uses 32-256) + + +async def main(): + async with TogetherSandbox() as sdk: + # --- Step 1: Create a shared snapshot --- + print(f"=== Creating snapshot (shared across {BATCH_SIZE} sandboxes) ===") + snap = await sdk.snapshots.create(CreateImageSnapshotParams( + image="python:3.11-slim", + )) + print(f"Snapshot: {snap.snapshot_id}") + + # --- Step 2: Create all sandboxes concurrently --- + print(f"\n=== Creating {BATCH_SIZE} ephemeral sandboxes ===") + models = await asyncio.gather(*[ + sdk.sandboxes.create( + snapshot_id=snap.snapshot_id, + millicpu=1000, + memory_bytes=2 * 1024**3, + disk_bytes=5 * 1024**3, + ephemeral=True, + ) + for _ in range(BATCH_SIZE) + ]) + print(f"Created: {[m.id for m in models]}") + + # --- Step 3: Start all concurrently and measure timing --- + print(f"\n=== Starting all {BATCH_SIZE} sandboxes (GPU waits for slowest) ===") + batch_start = time.time() + + async def start_timed(model): + t0 = time.time() + sandbox = await sdk.sandboxes.start(model.id) + elapsed = time.time() - t0 + return sandbox, elapsed + + results = await asyncio.gather(*[start_timed(m) for m in models]) + sandboxes = [r[0] for r in results] + startup_times = sorted([r[1] for r in results]) + + batch_elapsed = time.time() - batch_start + print(f"All running in {batch_elapsed:.1f}s") + print(f" Fastest: {startup_times[0]:.1f}s") + print(f" Median: {startup_times[len(startup_times)//2]:.1f}s") + print(f" Slowest: {startup_times[-1]:.1f}s (GPU idle time)") + + # --- Step 4: Execute in all sandboxes concurrently --- + print(f"\n=== Executing in all {BATCH_SIZE} sandboxes ===") + + async def execute_task(sandbox, index: int) -> dict: + result = await sandbox.execs.exec("python3", ["-c", + f"import random; random.seed({index}); " + f"reward = random.uniform(0.0, 1.0); " + f"print(f'Sandbox {index}: reward = {{reward:.4f}}')" + ]) + return { + "sandbox_id": sandbox.id, + "index": index, + "output": result["output"].strip(), + "exit_code": result["exit_code"], + } + + exec_results = await asyncio.gather(*[ + execute_task(sbx, i) for i, sbx in enumerate(sandboxes) + ]) + + for r in exec_results: + print(f" [{r['sandbox_id']}] {r['output']} (exit: {r['exit_code']})") + + # --- Step 5: Cleanup --- + print(f"\n=== Cleaning up {BATCH_SIZE} sandboxes ===") + await asyncio.gather(*[sdk.sandboxes.shutdown(sbx.id) for sbx in sandboxes]) + await sdk.snapshots.delete_by_id(snap.snapshot_id) + print("All resources cleaned up") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/skills/together-sandbox/scripts/sandbox_lifecycle.py b/skills/together-sandbox/scripts/sandbox_lifecycle.py new file mode 100644 index 0000000..de21375 --- /dev/null +++ b/skills/together-sandbox/scripts/sandbox_lifecycle.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Together Sandbox — Create, Execute, and Manage Sandbox Lifecycle + +Demonstrates the full sandbox lifecycle: create a snapshot from a Docker image, +start a sandbox, execute commands, read/write files, and shut down. + +Usage: + python sandbox_lifecycle.py + +Requires: + pip install together-sandbox + export TOGETHER_API_KEY=your_key + +Full SDK reference: + https://github.com/togethercomputer/together-sandbox/blob/main/docs/python-sdk.md +""" + +import asyncio +import time + +from together_sandbox import ( + TogetherSandbox, + CreateImageSnapshotParams, +) + + +async def main(): + async with TogetherSandbox() as sdk: + # --- Step 1: Create a snapshot from a Docker image --- + print("=== Creating snapshot from python:3.11-slim ===") + t0 = time.time() + snap_result = await sdk.snapshots.create(CreateImageSnapshotParams( + image="python:3.11-slim", + )) + print(f"Snapshot created: {snap_result.snapshot_id} ({time.time() - t0:.1f}s)") + + # --- Step 2: Create and start a sandbox --- + print("\n=== Creating and starting sandbox ===") + t0 = time.time() + model = await sdk.sandboxes.create( + snapshot_id=snap_result.snapshot_id, + millicpu=2000, + memory_bytes=4 * 1024**3, + disk_bytes=10 * 1024**3, + ephemeral=True, + ) + sandbox = await sdk.sandboxes.start(model.id) + print(f"Sandbox running: {sandbox.id} ({time.time() - t0:.1f}s)") + + # --- Step 3: Configure DNS (required for network access) --- + print("\n=== Configuring DNS ===") + result = await sandbox.execs.exec("bash", ["-c", + 'echo "nameserver 1.1.1.1" > /etc/resolv.conf && ' + 'echo "nameserver 8.8.8.8" >> /etc/resolv.conf' + ]) + print(f"DNS configured (exit: {result['exit_code']})") + + # --- Step 4: Execute commands --- + print("\n=== Executing commands ===") + result = await sandbox.execs.exec("bash", ["-c", "python3 --version"]) + print(f"Python version: {result['output'].strip()}") + + result = await sandbox.execs.exec("bash", ["-c", "echo hello from sandbox"]) + print(f"Output: {result['output'].strip()}") + print(f"Exit code: {result['exit_code']}") + + # --- Step 5: File operations --- + print("\n=== File operations ===") + + # Write a Python script + script = """ +import sys +print(f"Python {sys.version}") +print(f"Platform: {sys.platform}") +total = sum(range(1, 101)) +print(f"Sum 1-100: {total}") +""" + await sandbox.files.create("/tmp/info.py", script) + print("Wrote /tmp/info.py") + + # Read it back + content = await sandbox.files.read("/tmp/info.py") + print(f"Read back: {len(content)} chars") + + # Execute it + result = await sandbox.execs.exec("python3", ["/tmp/info.py"]) + print(f"Script output:\n{result['output']}") + + # List directory + files = await sandbox.directories.list("/tmp") + print(f"Files in /tmp: {len(files)} items") + + # --- Step 6: Shutdown --- + print("\n=== Shutting down ===") + await sdk.sandboxes.shutdown(sandbox.id) + print("Sandbox shut down") + + # --- Step 7: Clean up snapshot --- + await sdk.snapshots.delete_by_id(snap_result.snapshot_id) + print("Snapshot deleted") + + +if __name__ == "__main__": + asyncio.run(main())