Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
A comprehensive collection of Agent Skills for context engineering, multi-agent architectures, and production agent systems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
skills/hosted-agents/scripts/sandbox_manager.py
1"""2Sandbox Manager for Hosted Agent Infrastructure.34Use when: building background coding agents that need sandboxed execution5environments with pre-built images, warm pools, and session snapshots.67This module provides composable building blocks for sandbox lifecycle8management. Each class handles one concern (image building, warm pools,9session coordination) and can be used independently or combined via10SandboxManager.1112Note: This is pseudocode demonstrating architectural patterns.13Adapt for your specific infrastructure (Modal, Fly.io, etc.).14"""1516from dataclasses import dataclass, field17from datetime import datetime, timedelta18from typing import Optional, Callable, Any19from enum import Enum20import asyncio2122__all__ = [23"SandboxState",24"UserIdentity",25"SandboxConfig",26"Sandbox",27"RepositoryImage",28"ImageBuilder",29"WarmSandbox",30"WarmPoolManager",31"SandboxManager",32"AgentSession",33]343536class SandboxState(Enum):37"""Sandbox lifecycle states."""38CREATING = "creating"39SYNCING = "syncing"40READY = "ready"41EXECUTING = "executing"42SNAPSHOTTING = "snapshotting"43TERMINATED = "terminated"444546@dataclass47class UserIdentity:48"""User identity for commit attribution.4950Use when: configuring sandbox git identity so commits are51attributed to the prompting user, not the app.52"""53id: str54name: str55email: str56github_token: str575859@dataclass60class SandboxConfig:61"""Configuration for sandbox creation.6263Use when: defining resource limits and timeouts for a new sandbox64to prevent cost runaway and resource exhaustion.65"""66repo_url: str67base_image: str68memory_mb: int = 409669cpu_cores: int = 270disk_gb: int = 1071timeout_hours: int = 4727374@dataclass75class Sandbox:76"""Represents a sandboxed execution environment.7778Use when: interacting with a running sandbox to execute commands,79read/write files, or take snapshots for session continuity.80"""81id: str82config: SandboxConfig83state: SandboxState84created_at: datetime85snapshot_id: Optional[str] = None86current_user: Optional[UserIdentity] = None8788# Event handlers89on_state_change: Optional[Callable[[SandboxState], None]] = None9091async def execute_command(self, command: str) -> dict[str, Any]:92"""Execute a command in the sandbox.9394Use when: running shell commands (git, build tools, tests)95inside the isolated environment.9697Returns:98dict with keys "stdout", "stderr", "exit_code".99"""100# Implementation depends on infrastructure101pass102103async def read_file(self, path: str) -> str:104"""Read a file from the sandbox filesystem.105106Use when: agent needs to inspect source code or config files.107Safe to call before git sync completes.108"""109pass110111async def write_file(self, path: str, content: str) -> None:112"""Write a file to the sandbox filesystem.113114Use when: agent needs to modify source code. Block this115until git sync completes to avoid write conflicts.116"""117pass118119async def snapshot(self) -> str:120"""Create a snapshot of current filesystem state.121122Use when: preserving session state before sandbox termination123so follow-up prompts can restore instantly.124"""125self.state = SandboxState.SNAPSHOTTING126snapshot_id = await self._create_snapshot()127self.snapshot_id = snapshot_id128self.state = SandboxState.READY129return snapshot_id130131async def _create_snapshot(self) -> str:132"""Create snapshot (infrastructure-specific)."""133pass134135async def restore(self, snapshot_id: str) -> None:136"""Restore sandbox to a previous snapshot."""137pass138139async def terminate(self) -> None:140"""Terminate the sandbox."""141self.state = SandboxState.TERMINATED142143144@dataclass145class RepositoryImage:146"""Pre-built image for a repository.147148Use when: checking whether a cached environment image exists149and whether it is recent enough to use.150"""151repo_url: str152image_id: str153commit_sha: str154built_at: datetime155156def is_stale(self, max_age: timedelta = timedelta(minutes=30)) -> bool:157"""Check if image is older than max age."""158return datetime.utcnow() - self.built_at > max_age159160161class ImageBuilder:162"""Builds and manages repository images.163164Use when: setting up the periodic image build loop that165pre-bakes development environments for fast sandbox spin-up.166"""167168def __init__(self, github_app_token_provider: Callable[[], str]) -> None:169self.token_provider = github_app_token_provider170self.images: dict[str, RepositoryImage] = {}171172async def build_image(self, repo_url: str) -> RepositoryImage:173"""Build a new image for a repository.174175Use when: the current image is stale or no image exists yet.176Runs clone, dependency install, build, and cache warming.177"""178print(f"Building image for {repo_url}...")179180# Get fresh token for clone181token = self.token_provider()182183# These operations run in build environment184build_steps: list[str] = [185# Clone repository186f"git clone https://x-access-token:{token}@github.com/{repo_url} /workspace",187188# Install dependencies189"cd /workspace && npm install",190191# Run build192"cd /workspace && npm run build",193194# Warm caches by running once195"cd /workspace && npm run dev &",196"sleep 5", # Let dev server start197"cd /workspace && npm test -- --run || true", # Run tests to warm cache198]199200# Execute build steps (infrastructure-specific)201for step in build_steps:202await self._execute_build_step(step)203204# Get current commit205commit_sha: str = await self._get_commit_sha()206207# Create and store image208image = RepositoryImage(209repo_url=repo_url,210image_id=await self._finalize_image(),211commit_sha=commit_sha,212built_at=datetime.utcnow()213)214215self.images[repo_url] = image216return image217218def get_latest_image(self, repo_url: str) -> Optional[RepositoryImage]:219"""Get the most recent image for a repository."""220return self.images.get(repo_url)221222async def _execute_build_step(self, command: str) -> None:223"""Execute a build step (infrastructure-specific)."""224pass225226async def _get_commit_sha(self) -> str:227"""Get current HEAD commit SHA."""228pass229230async def _finalize_image(self) -> str:231"""Finalize and store the image, return image ID."""232pass233234235@dataclass236class WarmSandbox:237"""A pre-warmed sandbox ready for use.238239Use when: tracking warm pool inventory and claiming a sandbox240for an incoming user session.241"""242sandbox: Sandbox243repo_url: str244created_at: datetime245image_version: str246is_claimed: bool = False247sync_complete: bool = False248249250class WarmPoolManager:251"""Manages pools of pre-warmed sandboxes.252253Use when: reducing cold start latency by maintaining ready-to-use254sandboxes that are pre-synced to the latest code.255"""256257def __init__(258self,259image_builder: ImageBuilder,260target_pool_size: int = 3,261max_age: timedelta = timedelta(minutes=25)262) -> None:263self.image_builder = image_builder264self.target_size = target_pool_size265self.max_age = max_age266self.pools: dict[str, list[WarmSandbox]] = {}267268async def get_warm_sandbox(self, repo_url: str) -> Optional[WarmSandbox]:269"""Get a pre-warmed sandbox if available.270271Use when: a user submits a prompt and needs a sandbox immediately.272Returns None if no valid warm sandbox is available.273"""274if repo_url not in self.pools:275return None276277for warm in self.pools[repo_url]:278if not warm.is_claimed and self._is_valid(warm):279warm.is_claimed = True280return warm281282return None283284def _is_valid(self, warm: WarmSandbox) -> bool:285"""Check if a warm sandbox is still valid."""286age: timedelta = datetime.utcnow() - warm.created_at287if age > self.max_age:288return False289290# Check if image is still current291current = self.image_builder.get_latest_image(warm.repo_url)292if not current or current.image_id != warm.image_version:293return False294295return True296297async def maintain_pool(self, repo_url: str) -> None:298"""Ensure pool has target number of warm sandboxes.299300Use when: called periodically or after an image rebuild to301keep the warm pool populated.302"""303if repo_url not in self.pools:304self.pools[repo_url] = []305306# Remove invalid sandboxes307valid: list[WarmSandbox] = [w for w in self.pools[repo_url] if self._is_valid(w)]308self.pools[repo_url] = valid309310# Count available (unclaimed) sandboxes311available: int = len([w for w in valid if not w.is_claimed])312needed: int = self.target_size - available313314# Create new warm sandboxes315for _ in range(max(0, needed)):316warm = await self._create_warm_sandbox(repo_url)317self.pools[repo_url].append(warm)318319async def _create_warm_sandbox(self, repo_url: str) -> WarmSandbox:320"""Create a new warm sandbox."""321image: Optional[RepositoryImage] = self.image_builder.get_latest_image(repo_url)322if not image:323raise ValueError(f"No image available for {repo_url}")324325# Create sandbox from image326sandbox: Sandbox = await self._create_sandbox_from_image(image)327328warm = WarmSandbox(329sandbox=sandbox,330repo_url=repo_url,331created_at=datetime.utcnow(),332image_version=image.image_id,333sync_complete=False334)335336# Start syncing to latest in background337asyncio.create_task(self._sync_to_latest(warm))338339return warm340341async def _sync_to_latest(self, warm: WarmSandbox) -> None:342"""Sync sandbox to latest commit on base branch."""343await warm.sandbox.execute_command("git fetch origin main")344await warm.sandbox.execute_command("git reset --hard origin/main")345warm.sync_complete = True346347async def _create_sandbox_from_image(self, image: RepositoryImage) -> Sandbox:348"""Create a sandbox from an image (infrastructure-specific)."""349pass350351352class SandboxManager:353"""Main manager for sandbox lifecycle.354355Use when: orchestrating the full sandbox lifecycle including356image building, warm pools, and session management. This is the357top-level entry point that composes ImageBuilder and WarmPoolManager.358"""359360def __init__(361self,362repositories: list[str],363github_app_token_provider: Callable[[], str],364build_interval: timedelta = timedelta(minutes=30)365) -> None:366self.repositories = repositories367self.image_builder = ImageBuilder(github_app_token_provider)368self.warm_pool = WarmPoolManager(self.image_builder)369self.build_interval = build_interval370self.active_sessions: dict[str, Sandbox] = {}371372async def start_build_loop(self) -> None:373"""Start the background image build loop.374375Use when: initializing the system. Runs indefinitely, rebuilding376images every build_interval to keep environments fresh.377"""378while True:379for repo in self.repositories:380try:381await self.image_builder.build_image(repo)382await self.warm_pool.maintain_pool(repo)383except Exception as e:384print(f"Failed to build {repo}: {e}")385386await asyncio.sleep(self.build_interval.total_seconds())387388async def start_session(389self,390repo_url: str,391user: UserIdentity,392snapshot_id: Optional[str] = None393) -> Sandbox:394"""Start a new session for a user.395396Use when: a user submits a prompt. Tries warm pool first,397then snapshot restore, then cold start as fallback.398"""399# Try to get from warm pool first400warm: Optional[WarmSandbox] = await self.warm_pool.get_warm_sandbox(repo_url)401402if warm:403sandbox = warm.sandbox404# Wait for sync if not complete405if not warm.sync_complete:406await self._wait_for_sync(warm)407elif snapshot_id:408# Restore from previous session snapshot409sandbox = await self._restore_from_snapshot(snapshot_id)410else:411# Cold start from latest image412sandbox = await self._cold_start(repo_url)413414# Configure for user415await self._configure_for_user(sandbox, user)416417# Track session418session_id: str = f"{user.id}_{datetime.utcnow().isoformat()}"419self.active_sessions[session_id] = sandbox420421return sandbox422423async def on_user_typing(self, user: UserIdentity, repo_url: str) -> None:424"""Called when user starts typing a prompt.425426Use when: implementing predictive warm-up. Starts preparing a427sandbox so it is ready by the time the user submits.428"""429warm: Optional[WarmSandbox] = await self.warm_pool.get_warm_sandbox(repo_url)430431if not warm:432# Start warming one now433asyncio.create_task(self.warm_pool.maintain_pool(repo_url))434435async def end_session(self, session_id: str) -> Optional[str]:436"""End a session and return snapshot ID for potential follow-up.437438Use when: a session completes. Always snapshots before termination439to prevent state loss.440"""441if session_id not in self.active_sessions:442return None443444sandbox: Sandbox = self.active_sessions[session_id]445446# Create snapshot before terminating447snapshot_id: str = await sandbox.snapshot()448449# Terminate sandbox450await sandbox.terminate()451452del self.active_sessions[session_id]453454return snapshot_id455456async def _configure_for_user(457self,458sandbox: Sandbox,459user: UserIdentity460) -> None:461"""Configure sandbox for a specific user."""462sandbox.current_user = user463464# Set git identity465await sandbox.execute_command(466f'git config user.name "{user.name}"'467)468await sandbox.execute_command(469f'git config user.email "{user.email}"'470)471472async def _wait_for_sync(self, warm: WarmSandbox) -> None:473"""Wait for sync to complete."""474while not warm.sync_complete:475await asyncio.sleep(0.1)476477async def _restore_from_snapshot(self, snapshot_id: str) -> Sandbox:478"""Restore a sandbox from a snapshot."""479pass480481async def _cold_start(self, repo_url: str) -> Sandbox:482"""Start a sandbox from cold (no warm pool available)."""483pass484485486class AgentSession:487"""Agent session with file read/write coordination.488489Use when: wrapping a Sandbox to enforce the pattern where reads490are allowed before sync completes but writes are blocked until491sync finishes, preventing write conflicts.492"""493494def __init__(self, sandbox: Sandbox) -> None:495self.sandbox = sandbox496self.sync_complete: bool = False497self.pending_writes: list[tuple[str, str]] = []498499async def read_file(self, path: str) -> str:500"""Read a file -- allowed even before sync completes.501502Use when: agent needs to research code immediately. Safe because503in large repos, files being worked on are unlikely to have504changed in the last 30 minutes since image build.505"""506return await self.sandbox.read_file(path)507508async def write_file(self, path: str, content: str) -> None:509"""Write a file -- blocks until sync is complete.510511Use when: agent needs to modify source code. Queues the write512and waits for git sync to finish to prevent conflicts.513"""514if not self.sync_complete:515# Queue the write516self.pending_writes.append((path, content))517await self._wait_for_sync()518519await self.sandbox.write_file(path, content)520521def mark_sync_complete(self) -> None:522"""Called when git sync is complete."""523self.sync_complete = True524525async def _wait_for_sync(self) -> None:526"""Wait for sync to complete, then flush pending writes."""527while not self.sync_complete:528await asyncio.sleep(0.1)529530# Flush pending writes531for path, content in self.pending_writes:532await self.sandbox.write_file(path, content)533self.pending_writes.clear()534535536if __name__ == "__main__":537async def _demo() -> None:538"""Demonstrate sandbox manager usage end-to-end."""539540def get_github_token() -> str:541"""Get GitHub App installation token."""542# Implementation: call GitHub API to get installation token543return "ghs_xxxx"544545# Initialize manager with target repositories546manager = SandboxManager(547repositories=[548"myorg/frontend",549"myorg/backend",550"myorg/shared-libs"551],552github_app_token_provider=get_github_token553)554555# Start background build loop556asyncio.create_task(manager.start_build_loop())557558# Simulate user session559user = UserIdentity(560id="user123",561name="Alice Developer",562email="[email protected]",563github_token="gho_user_token"564)565566# User starts typing -- predictively warm a sandbox567await manager.on_user_typing(user, "myorg/frontend")568569# User submits prompt -- get sandbox570sandbox: Sandbox = await manager.start_session("myorg/frontend", user)571572# Create session wrapper for read/write coordination573session = AgentSession(sandbox)574575# Agent can read immediately (before sync completes)576readme: str = await session.read_file("/workspace/README.md")577578# Agent work happens here...579580# End session and get snapshot for follow-up581# Find the session_id that was generated during start_session582active_ids = list(manager.active_sessions.keys())583if active_ids:584session_id = active_ids[0]585snapshot_id: Optional[str] = await manager.end_session(session_id)586print(f"Session ended, snapshot: {snapshot_id}")587else:588print("No active session found")589590asyncio.run(_demo())591