""" Minimal Modal-backed sandbox example for manual validation. This example mirrors the local and Docker sandbox demos, but it sends the workspace to a Modal sandbox. """ import argparse import asyncio import io import os import sys import tempfile from pathlib import Path from typing import Literal, cast from openai.types.responses import ResponseTextDeltaEvent from agents import ModelSettings, Runner from agents.run import RunConfig from agents.sandbox import LocalSnapshotSpec, Manifest, SandboxAgent, SandboxRunConfig from agents.sandbox.entries import GCSMount, Mount, S3Mount from agents.sandbox.session import BaseSandboxSession if __package__ is None or __package__ == "": sys.path.insert(0, str(Path(__file__).resolve().parents[3])) from examples.sandbox.misc.example_support import text_manifest from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability try: from agents.extensions.sandbox import ( ModalCloudBucketMountStrategy, ModalSandboxClient, ModalSandboxClientOptions, ) except Exception as exc: # pragma: no cover - import path depends on optional extras raise SystemExit( "Modal sandbox examples require the optional repo extra.\n" "Install it with: uv sync --extra modal" ) from exc DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences." SNAPSHOT_CHECK_PATH = Path("snapshot-check.txt") SNAPSHOT_CHECK_CONTENT = "modal snapshot round-trip ok\n" MOUNT_CHECK_FILENAME = "native-cloud-bucket-check.txt" MOUNT_CHECK_CONTENT = "modal native cloud bucket read/write ok\n" MOUNT_CHECK_UPDATED_CONTENT = "modal native cloud bucket read/write ok after resume\n" def _build_manifest( *, native_cloud_bucket_name: str | None = None, native_cloud_bucket_provider: Literal["s3", "gcs-hmac"] = "s3", native_cloud_bucket_mount_path: str | None = None, native_cloud_bucket_endpoint_url: str | None = None, native_cloud_bucket_key_prefix: str | None = None, native_cloud_bucket_secret_name: str | None = None, ) -> Manifest: manifest = text_manifest( { "README.md": ( "# Modal Demo Workspace\n\n" "This workspace exists to validate the Modal sandbox backend manually.\n" ), "incident.md": ( "# Incident\n\n" "- Customer: Fabrikam Retail.\n" "- Issue: delayed reporting rollout.\n" "- Primary blocker: incomplete security questionnaire.\n" ), "plan.md": ( "# Plan\n\n" "1. Close the questionnaire.\n" "2. Reconfirm the rollout date with the customer.\n" ), } ) if native_cloud_bucket_name is None: return manifest mount_path = ( Path(native_cloud_bucket_mount_path) if native_cloud_bucket_mount_path is not None else None ) mount_strategy = ModalCloudBucketMountStrategy( secret_name=native_cloud_bucket_secret_name, ) if native_cloud_bucket_provider == "gcs-hmac": manifest.entries["cloud-bucket"] = GCSMount( bucket=native_cloud_bucket_name, access_id=( None if native_cloud_bucket_secret_name is not None else ( os.environ.get("GCS_HMAC_ACCESS_KEY_ID") or os.environ.get("GOOGLE_ACCESS_KEY_ID") ) ), secret_access_key=( None if native_cloud_bucket_secret_name is not None else ( os.environ.get("GCS_HMAC_SECRET_ACCESS_KEY") or os.environ.get("GOOGLE_ACCESS_KEY_SECRET") ) ), endpoint_url=native_cloud_bucket_endpoint_url, prefix=native_cloud_bucket_key_prefix, mount_path=mount_path, read_only=False, mount_strategy=mount_strategy, ) else: manifest.entries["cloud-bucket"] = S3Mount( bucket=native_cloud_bucket_name, access_key_id=( None if native_cloud_bucket_secret_name is not None else os.environ.get("AWS_ACCESS_KEY_ID") ), secret_access_key=( None if native_cloud_bucket_secret_name is not None else os.environ.get("AWS_SECRET_ACCESS_KEY") ), session_token=( None if native_cloud_bucket_secret_name is not None else os.environ.get("AWS_SESSION_TOKEN") ), endpoint_url=native_cloud_bucket_endpoint_url, prefix=native_cloud_bucket_key_prefix, mount_path=mount_path, read_only=False, mount_strategy=mount_strategy, ) return manifest def _native_cloud_bucket_mount_path(manifest: Manifest) -> Path | None: entry = manifest.entries.get("cloud-bucket") if not isinstance(entry, Mount): return None if entry.mount_path is None: return Path(manifest.root) / "cloud-bucket" if entry.mount_path.is_absolute(): return entry.mount_path return Path(manifest.root) / entry.mount_path async def _read_text(session: BaseSandboxSession, path: Path) -> str: data = await session.read(path) text = cast(str | bytes, data.read()) if isinstance(text, bytes): return text.decode("utf-8") return text def _require_env(name: str) -> None: if os.environ.get(name): return raise SystemExit(f"{name} must be set before running this example.") async def _verify_stop_resume( *, manifest: Manifest, app_name: str, workspace_persistence: Literal["tar", "snapshot_filesystem", "snapshot_directory"], sandbox_create_timeout_s: float | None, ) -> None: client = ModalSandboxClient() mount_path = _native_cloud_bucket_mount_path(manifest) mount_check_path = mount_path / MOUNT_CHECK_FILENAME if mount_path is not None else None options = ModalSandboxClientOptions( app_name=app_name, workspace_persistence=workspace_persistence, sandbox_create_timeout_s=sandbox_create_timeout_s, ) with tempfile.TemporaryDirectory(prefix="modal-snapshot-example-") as snapshot_dir: sandbox = await client.create( manifest=manifest, snapshot=LocalSnapshotSpec(base_path=Path(snapshot_dir)), options=options, ) try: await sandbox.start() await sandbox.write( SNAPSHOT_CHECK_PATH, io.BytesIO(SNAPSHOT_CHECK_CONTENT.encode("utf-8")), ) await sandbox.stop() finally: await sandbox.shutdown() resumed_sandbox = await client.resume(sandbox.state) try: await resumed_sandbox.start() restored_text = await _read_text(resumed_sandbox, SNAPSHOT_CHECK_PATH) if restored_text != SNAPSHOT_CHECK_CONTENT: raise RuntimeError( f"Snapshot resume verification failed for {workspace_persistence!r}: " f"expected {SNAPSHOT_CHECK_CONTENT!r}, got {restored_text!r}" ) finally: await resumed_sandbox.aclose() print(f"native cloud bucket read/write ok ({mount_check_path})") print(f"snapshot round-trip ok ({workspace_persistence})") async def main( *, model: str, question: str, app_name: str, workspace_persistence: Literal["tar", "snapshot_filesystem", "snapshot_directory"], sandbox_create_timeout_s: float | None, native_cloud_bucket_name: str | None, native_cloud_bucket_provider: Literal["s3", "gcs-hmac"], native_cloud_bucket_mount_path: str, native_cloud_bucket_endpoint_url: str | None, native_cloud_bucket_key_prefix: str | None, native_cloud_bucket_secret_name: str | None, stream: bool, ) -> None: _require_env("OPENAI_API_KEY") manifest = _build_manifest( native_cloud_bucket_name=native_cloud_bucket_name, native_cloud_bucket_provider=native_cloud_bucket_provider, native_cloud_bucket_mount_path=native_cloud_bucket_mount_path, native_cloud_bucket_endpoint_url=native_cloud_bucket_endpoint_url, native_cloud_bucket_key_prefix=native_cloud_bucket_key_prefix, native_cloud_bucket_secret_name=native_cloud_bucket_secret_name, ) await _verify_stop_resume( manifest=manifest, app_name=app_name, workspace_persistence=workspace_persistence, sandbox_create_timeout_s=sandbox_create_timeout_s, ) agent = SandboxAgent( name="Modal Sandbox Assistant", model=model, instructions=( "Answer questions about the sandbox workspace. Inspect the files before answering " "and keep the response concise. " "Do not invent files or statuses that are not present in the workspace. Cite the " "file names you inspected." ), default_manifest=manifest, capabilities=[WorkspaceShellCapability()], model_settings=ModelSettings(tool_choice="required"), ) run_config = RunConfig( sandbox=SandboxRunConfig( client=ModalSandboxClient(), options=ModalSandboxClientOptions( app_name=app_name, workspace_persistence=workspace_persistence, sandbox_create_timeout_s=sandbox_create_timeout_s, ), ), workflow_name="Modal sandbox example", ) if not stream: result = await Runner.run(agent, question, run_config=run_config) print(result.final_output) return stream_result = Runner.run_streamed(agent, question, run_config=run_config) saw_text_delta = False async for event in stream_result.stream_events(): if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): if not saw_text_delta: print("assistant> ", end="", flush=True) saw_text_delta = True print(event.data.delta, end="", flush=True) if saw_text_delta: print() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.") parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.") parser.add_argument( "--app-name", default="openai-agents-python-sandbox-example", help="Modal app name to create or reuse for the sandbox.", ) parser.add_argument( "--workspace-persistence", default="tar", choices=["tar", "snapshot_filesystem", "snapshot_directory"], help="Workspace persistence mode for the Modal sandbox.", ) parser.add_argument( "--sandbox-create-timeout-s", type=float, default=None, help="Optional timeout for creating the Modal sandbox.", ) parser.add_argument( "--native-cloud-bucket-name", default=None, help="Optional cloud bucket name to mount with ModalCloudBucketMountStrategy.", ) parser.add_argument( "--native-cloud-bucket-provider", default="s3", choices=["s3", "gcs-hmac"], help="Provider type for --native-cloud-bucket-name.", ) parser.add_argument( "--native-cloud-bucket-mount-path", default="cloud-bucket", help=( "Mount path for --native-cloud-bucket-name. Relative paths are resolved under the " "workspace root." ), ) parser.add_argument( "--native-cloud-bucket-endpoint-url", default=None, help="Optional endpoint URL for --native-cloud-bucket-name.", ) parser.add_argument( "--native-cloud-bucket-key-prefix", default=None, help="Optional key prefix for --native-cloud-bucket-name.", ) parser.add_argument( "--native-cloud-bucket-secret-name", default=None, help=( "Optional named Modal Secret to use for --native-cloud-bucket-name instead of " "reading raw credentials from environment variables." ), ) parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.") args = parser.parse_args() asyncio.run( main( model=args.model, question=args.question, app_name=args.app_name, workspace_persistence=args.workspace_persistence, sandbox_create_timeout_s=args.sandbox_create_timeout_s, native_cloud_bucket_name=args.native_cloud_bucket_name, native_cloud_bucket_provider=args.native_cloud_bucket_provider, native_cloud_bucket_mount_path=args.native_cloud_bucket_mount_path, native_cloud_bucket_endpoint_url=args.native_cloud_bucket_endpoint_url, native_cloud_bucket_key_prefix=args.native_cloud_bucket_key_prefix, native_cloud_bucket_secret_name=args.native_cloud_bucket_secret_name, stream=args.stream, ) )