diff --git a/CLOUDFLARE.md b/CLOUDFLARE.md new file mode 100644 index 0000000..fe4f468 --- /dev/null +++ b/CLOUDFLARE.md @@ -0,0 +1,251 @@ +# Cloudflare Workers Sandbox for RLM + +This guide explains how to set up and use the `CloudflareREPL` environment, which runs Python code in Cloudflare Workers Sandbox containers. + +## Prerequisites + +1. **Cloudflare Account** with Workers Paid plan ($5/month) +2. **Wrangler CLI** installed and authenticated: + ```bash + npm install -g wrangler + wrangler login + ``` +3. **Docker** running locally (for development) + +## Architecture + +``` +┌─────────────────┐ HTTP ┌──────────────────┐ +│ CloudflareREPL │ ────────────► │ rlm-sandbox │ +│ (Python SDK) │ │ (CF Worker) │ +└─────────────────┘ └────────┬─────────┘ + │ + ▼ + ┌──────────────────┐ + │ Sandbox Container│ + │ (Python 3.10) │ + └──────────────────┘ +``` + +## Deploying the Worker + +### 1. Navigate to Worker Directory + +```bash +cd workers/rlm-sandbox +``` + +### 2. Install Dependencies + +```bash +npm install +``` + +### 3. Deploy to Cloudflare + +```bash +npm run deploy +# or: npx wrangler deploy +``` + +This will: +- Build the Docker image with Python 3.10, numpy, sympy, requests +- Upload the image to Cloudflare's container registry +- Deploy the Worker + +Output will show your Worker URL: +``` +Deployed rlm-sandbox triggers + https://rlm-sandbox..workers.dev +``` + +### 4. (Optional) Configure Authentication + +For production, set an auth token: +```bash +npx wrangler secret put AUTH_TOKEN +# Enter your secret token when prompted +``` + +## Local Development + +Run the Worker locally with Docker: +```bash +npm run dev +# Worker available at http://localhost:8787 +``` + +First run takes 2-3 minutes to build the container; subsequent runs are faster. + +## Using CloudflareREPL + +### Environment Variables + +CloudflareREPL supports configuration via environment variables: + +| Variable | Description | +|----------|-------------| +| `RLM_CF_WORKER_URL` | Default worker URL (if not passed to constructor) | +| `RLM_CF_AUTH_TOKEN` | Default auth token (if not passed to constructor) | + +```bash +# Set env vars for easy usage +export RLM_CF_WORKER_URL=https://rlm-sandbox.example.workers.dev +export RLM_CF_AUTH_TOKEN=your-auth-token +``` + +### Basic Usage + +```python +from rlm.environments import get_environment + +# With env vars set, minimal config needed: +env = get_environment("cloudflare", {}) + +# Or explicitly pass URL: +env = get_environment("cloudflare", { + "worker_url": "https://rlm-sandbox.example.workers.dev", + "auth_token": "your-auth-token", # Optional if not configured + "sandbox_id": "my-session", # Optional, auto-generated if omitted +}) + +# Execute code +result = env.execute_code("x = 2 + 2\nprint(x)") +print(result.stdout) # "4\n" +print(result.locals) # {"x": "4"} + +# Variables persist across executions +result2 = env.execute_code("y = x * 10\nprint(y)") +print(result2.stdout) # "40\n" + +# Cleanup when done +env.cleanup() +``` + +### With Context Manager + +```python +from rlm.environments.cloudflare_repl import CloudflareREPL + +# Uses RLM_CF_WORKER_URL and RLM_CF_AUTH_TOKEN env vars +with CloudflareREPL() as repl: + result = repl.execute_code("import numpy as np\nprint(np.sum([1,2,3]))") + print(result.stdout) # "6\n" + +# Or with explicit URL +with CloudflareREPL(worker_url="https://rlm-sandbox.example.workers.dev") as repl: + result = repl.execute_code("print('hello')") +``` + +### Loading Context + +```python +# String context +env = get_environment("cloudflare", { + "worker_url": "https://...", + "context_payload": "This is my context data", +}) +# Access via: context variable in sandbox + +# Dict/List context +env = get_environment("cloudflare", { + "worker_url": "https://...", + "context_payload": {"key": "value", "items": [1, 2, 3]}, +}) +``` + +## Worker API Reference + +The Worker exposes these endpoints: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | +| POST | `/sandbox/exec` | Execute shell command | +| POST | `/sandbox/write` | Write file to sandbox | +| GET | `/sandbox/read` | Read file from sandbox | +| GET | `/sandbox/health` | Check Python availability | + +### POST /sandbox/exec + +Execute a command in the sandbox. + +```bash +curl -X POST https://your-worker.workers.dev/sandbox/exec \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{"sandbox_id": "my-session", "command": "python3 -c \"print(42)\""}' +``` + +Response: +```json +{ + "stdout": "42\n", + "stderr": "", + "exitCode": 0, + "success": true +} +``` + +### POST /sandbox/write + +Write a file to the sandbox filesystem. + +```bash +curl -X POST https://your-worker.workers.dev/sandbox/write \ + -H "Content-Type: application/json" \ + -d '{"sandbox_id": "my-session", "path": "/workspace/script.py", "content": "print(\"hello\")"}' +``` + +### GET /sandbox/read + +Read a file from the sandbox. + +```bash +curl "https://your-worker.workers.dev/sandbox/read?sandbox_id=my-session&path=/workspace/script.py" +``` + +## Pre-installed Packages + +The sandbox container includes: +- Python 3.10 +- numpy +- sympy +- requests + +To add more packages, edit `workers/rlm-sandbox/Dockerfile`: +```dockerfile +RUN pip3 install --no-cache-dir \ + numpy \ + sympy \ + requests \ + your-package-here +``` + +Then redeploy: `npm run deploy` + +## Pricing + +Cloudflare Workers Sandbox pricing (as of 2025): +- **Base**: Workers Paid plan ($5/month) +- **CPU**: $0.000020/vCPU-second (375 vCPU-min/month included) +- **Memory**: $0.0000025/GiB-second (25 GiB-hours/month included) +- **Disk**: $0.00000007/GB-second (200 GB-hours/month included) + +Instance type "basic" (default): 1/4 vCPU, 1 GiB RAM, 5 GB disk. + +## Troubleshooting + +### "Sandbox binding not configured" +Ensure your `wrangler.toml` has the container and Durable Object bindings configured correctly. + +### Container build fails +- Check Docker is running: `docker info` +- Verify the base image version matches the SDK: `npm list @cloudflare/sandbox` +- Use matching versions in Dockerfile: `FROM cloudflare/sandbox:X.Y.Z` + +### Slow cold starts +First request to a new sandbox takes 2-3 seconds to initialize the container. Subsequent requests to the same `sandbox_id` are fast. + +### State not persisting +Ensure you're using the same `sandbox_id` across requests. Each unique ID gets its own container. diff --git a/rlm/environments/__init__.py b/rlm/environments/__init__.py index 9d2719f..25c6a4f 100644 --- a/rlm/environments/__init__.py +++ b/rlm/environments/__init__.py @@ -5,12 +5,12 @@ def get_environment( - environment: Literal["local", "modal", "docker"], + environment: Literal["local", "modal", "docker", "cloudflare"], environment_kwargs: dict[str, Any], ) -> BaseEnv: """ Routes a specific environment and the args (as a dict) to the appropriate environment if supported. - Currently supported environments: ['local', 'modal', 'docker'] + Currently supported environments: ['local', 'modal', 'docker', 'cloudflare'] """ if environment == "local": return LocalREPL(**environment_kwargs) @@ -22,7 +22,11 @@ def get_environment( from rlm.environments.docker_repl import DockerREPL return DockerREPL(**environment_kwargs) + elif environment == "cloudflare": + from rlm.environments.cloudflare_repl import CloudflareREPL + + return CloudflareREPL(**environment_kwargs) else: raise ValueError( - f"Unknown environment: {environment}. Supported: ['local', 'modal', 'docker']" + f"Unknown environment: {environment}. Supported: ['local', 'modal', 'docker', 'cloudflare']" ) diff --git a/rlm/environments/cloudflare_repl.py b/rlm/environments/cloudflare_repl.py new file mode 100644 index 0000000..4d0b8a3 --- /dev/null +++ b/rlm/environments/cloudflare_repl.py @@ -0,0 +1,487 @@ +""" +Cloudflare Workers Sandbox REPL environment. + +Uses Cloudflare's Sandbox SDK to run Python code in isolated containers. +Unlike Modal, Cloudflare Sandboxes are accessed via HTTP API rather than a Python SDK. +""" + +import base64 +import json +import os +import textwrap +import threading +import time +from typing import Any + +import requests + +from rlm.core.comms_utils import LMRequest, send_lm_request, send_lm_request_batched +from rlm.core.types import REPLResult, RLMChatCompletion +from rlm.environments.base_env import IsolatedEnv + + +# ============================================================================= +# Execution Script (runs inside the Cloudflare sandbox) +# ============================================================================= + + +def _build_exec_script(code: str, broker_url: str | None = None) -> str: + """ + Build a script that executes code with state persistence. + If broker_url is provided, LLM queries go through the broker. + """ + code_b64 = base64.b64encode(code.encode()).decode() + broker_url_str = f'"{broker_url}"' if broker_url else "None" + + return textwrap.dedent( + f''' +import sys +import io +import json +import base64 +import traceback +import os + +try: + import dill +except ImportError: + try: + import pickle as dill + except ImportError: + dill = None + +# ============================================================================= +# LLM Query Functions (via external broker if configured) +# ============================================================================= + +BROKER_URL = {broker_url_str} + +def llm_query(prompt, model=None): + """Query the LM via the broker (if configured).""" + if BROKER_URL is None: + return "Error: LLM broker not configured" + try: + import requests + response = requests.post( + f"{{BROKER_URL}}/enqueue", + json={{"type": "single", "prompt": prompt, "model": model}}, + timeout=300, + ) + data = response.json() + if data.get("error"): + return f"Error: {{data['error']}}" + return data.get("response", "Error: No response") + except Exception as e: + return f"Error: LM query failed - {{e}}" + + +def llm_query_batched(prompts, model=None): + """Query the LM with multiple prompts.""" + if BROKER_URL is None: + return ["Error: LLM broker not configured"] * len(prompts) + try: + import requests + response = requests.post( + f"{{BROKER_URL}}/enqueue", + json={{"type": "batched", "prompts": prompts, "model": model}}, + timeout=300, + ) + data = response.json() + if data.get("error"): + return [f"Error: {{data['error']}}"] * len(prompts) + return data.get("responses", ["Error: No response"] * len(prompts)) + except Exception as e: + return [f"Error: LM query failed - {{e}}"] * len(prompts) + + +# ============================================================================= +# State Management +# ============================================================================= + +STATE_FILE = "/tmp/rlm_state.json" + +def load_state(): + if os.path.exists(STATE_FILE): + try: + with open(STATE_FILE, "r") as f: + return json.load(f) + except: + pass + return {{}} + +def save_state(state): + clean_state = {{}} + for k, v in state.items(): + if k.startswith("_"): + continue + try: + json.dumps(v) # Test if JSON serializable + clean_state[k] = v + except: + try: + clean_state[k] = repr(v) + except: + pass + with open(STATE_FILE, "w") as f: + json.dump(clean_state, f) + +def serialize_locals(state): + result = {{}} + for k, v in state.items(): + if k.startswith("_"): + continue + try: + result[k] = repr(v) + except: + result[k] = f"<{{type(v).__name__}}>" + return result + +# ============================================================================= +# Execution +# ============================================================================= + +_locals = load_state() + +def FINAL_VAR(variable_name): + variable_name = variable_name.strip().strip("\\"\\'") + if variable_name in _locals: + return str(_locals[variable_name]) + return f"Error: Variable '{{variable_name}}' not found" + +_globals = {{ + "__builtins__": __builtins__, + "__name__": "__main__", + "llm_query": llm_query, + "llm_query_batched": llm_query_batched, + "FINAL_VAR": FINAL_VAR, +}} + +code = base64.b64decode("{code_b64}").decode() + +stdout_buf = io.StringIO() +stderr_buf = io.StringIO() +old_stdout, old_stderr = sys.stdout, sys.stderr + +try: + sys.stdout = stdout_buf + sys.stderr = stderr_buf + combined = {{**_globals, **_locals}} + exec(code, combined, combined) + for key, value in combined.items(): + if key not in _globals and not key.startswith("_"): + _locals[key] = value +except Exception as e: + traceback.print_exc(file=stderr_buf) +finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + +save_state(_locals) + +result = {{ + "stdout": stdout_buf.getvalue(), + "stderr": stderr_buf.getvalue(), + "locals": serialize_locals(_locals), +}} +print(json.dumps(result)) +''' + ) + + +class CloudflareREPL(IsolatedEnv): + """ + Cloudflare Workers Sandbox REPL environment. + + Runs Python code in Cloudflare's container-based Sandbox SDK. + Requires a running Cloudflare Worker with Sandbox SDK configured. + + Configuration: + - worker_url: URL of the Cloudflare Worker (or set RLM_CF_WORKER_URL env var) + - auth_token: Bearer token for the Worker (or set RLM_CF_AUTH_TOKEN env var) + - sandbox_id: Optional sandbox ID for session persistence (auto-generated if not provided) + + Environment variables: + - RLM_CF_WORKER_URL: Default worker URL if not passed to constructor + - RLM_CF_AUTH_TOKEN: Default auth token if not passed to constructor + + The Worker should expose these endpoints: + - POST /sandbox/exec - Execute a command in the sandbox + - POST /sandbox/write - Write a file to the sandbox + - GET /sandbox/read - Read a file from the sandbox + """ + + def __init__( + self, + worker_url: str | None = None, + auth_token: str | None = None, + sandbox_id: str | None = None, + timeout: int = 300, + lm_handler_address: tuple[str, int] | None = None, + broker_url: str | None = None, + context_payload: dict | list | str | None = None, + setup_code: str | None = None, + **kwargs, + ): + super().__init__(**kwargs) + + # Support env vars for worker URL and auth token + self.worker_url = (worker_url or os.environ.get("RLM_CF_WORKER_URL", "")).rstrip("/") + if not self.worker_url: + raise ValueError( + "worker_url must be provided or RLM_CF_WORKER_URL environment variable must be set" + ) + self.auth_token = auth_token if auth_token is not None else os.environ.get("RLM_CF_AUTH_TOKEN", "") + self.sandbox_id = sandbox_id or f"rlm-{int(time.time())}" + self.timeout = timeout + self.lm_handler_address = lm_handler_address + self.broker_url = broker_url + + # LLM call tracking + self.pending_llm_calls: list[RLMChatCompletion] = [] + self._calls_lock = threading.Lock() + + # Polling thread for LLM requests (if broker is configured) + self.poller_thread: threading.Thread | None = None + self.poller_stop = threading.Event() + + # Session for HTTP requests + self._session = requests.Session() + self._session.headers.update({ + "Authorization": f"Bearer {self.auth_token}", + "Content-Type": "application/json", + }) + + self.setup() + + if context_payload is not None: + self.load_context(context_payload) + + if setup_code: + self.execute_code(setup_code) + + def _request( + self, + method: str, + endpoint: str, + json_data: dict | None = None, + timeout: int | None = None, + ) -> dict[str, Any]: + """Make an HTTP request to the Cloudflare Worker.""" + url = f"{self.worker_url}{endpoint}" + timeout = timeout or self.timeout + + try: + if method.upper() == "GET": + resp = self._session.get(url, timeout=timeout) + elif method.upper() == "POST": + resp = self._session.post(url, json=json_data, timeout=timeout) + else: + raise ValueError(f"Unsupported method: {method}") + + resp.raise_for_status() + return resp.json() + except requests.exceptions.RequestException as e: + return {"error": str(e), "success": False} + + def setup(self): + """Initialize the Cloudflare sandbox connection.""" + # Verify connection by running a simple command + result = self._exec_command("python3 --version") + if "error" in result and not result.get("stdout"): + raise RuntimeError( + f"Failed to connect to Cloudflare Worker: {result.get('error')}" + ) + + # Start LLM polling if broker is configured + if self.broker_url and self.lm_handler_address: + self.poller_stop.clear() + self.poller_thread = threading.Thread(target=self._poll_broker, daemon=True) + self.poller_thread.start() + + def _exec_command(self, cmd: str) -> dict[str, Any]: + """Execute a shell command in the sandbox.""" + return self._request( + "POST", + "/sandbox/exec", + json_data={ + "sandbox_id": self.sandbox_id, + "command": cmd, + }, + ) + + def _write_file(self, path: str, content: str) -> dict[str, Any]: + """Write a file to the sandbox.""" + return self._request( + "POST", + "/sandbox/write", + json_data={ + "sandbox_id": self.sandbox_id, + "path": path, + "content": content, + }, + ) + + def _read_file(self, path: str) -> dict[str, Any]: + """Read a file from the sandbox.""" + return self._request( + "GET", + f"/sandbox/read?sandbox_id={self.sandbox_id}&path={path}", + ) + + def _poll_broker(self): + """Poll the broker for pending LLM requests and handle them.""" + while not self.poller_stop.is_set(): + try: + resp = self._session.get( + f"{self.broker_url}/pending", + timeout=5, + ) + pending = resp.json().get("pending", []) + + for item in pending: + request_id = item["id"] + req_data = item["request"] + + response = self._handle_llm_request(req_data) + + self._session.post( + f"{self.broker_url}/respond", + json={"id": request_id, "response": response}, + timeout=10, + ) + + except requests.exceptions.RequestException: + pass + except Exception: + pass + + time.sleep(0.1) + + def _handle_llm_request(self, req_data: dict) -> dict: + """Handle an LLM request from the sandbox.""" + req_type = req_data.get("type") + model = req_data.get("model") + + if req_type == "single": + prompt = req_data.get("prompt") + request = LMRequest(prompt=prompt, model=model) + response = send_lm_request(self.lm_handler_address, request) + + if not response.success: + return {"error": response.error} + + with self._calls_lock: + self.pending_llm_calls.append(response.chat_completion) + + return {"response": response.chat_completion.response} + + elif req_type == "batched": + prompts = req_data.get("prompts", []) + responses = send_lm_request_batched( + self.lm_handler_address, prompts, model=model + ) + + results = [] + for resp in responses: + if not resp.success: + results.append(f"Error: {resp.error}") + else: + with self._calls_lock: + self.pending_llm_calls.append(resp.chat_completion) + results.append(resp.chat_completion.response) + + return {"responses": results} + + return {"error": "Unknown request type"} + + def load_context(self, context_payload: dict | list | str): + """Load context into the sandbox environment.""" + if isinstance(context_payload, str): + escaped = context_payload.replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + context_code = f'context = """{escaped}"""' + else: + context_json = json.dumps(context_payload) + escaped_json = context_json.replace("\\", "\\\\").replace("'", "\\'") + context_code = f"import json; context = json.loads('{escaped_json}')" + + self.execute_code(context_code) + + def execute_code(self, code: str) -> REPLResult: + """Execute Python code in the Cloudflare sandbox and return result.""" + start_time = time.perf_counter() + + # Clear pending LLM calls + with self._calls_lock: + self.pending_llm_calls.clear() + + # Build the execution script + script = _build_exec_script(code, self.broker_url) + + # Write script to temp file and execute + script_path = "/tmp/rlm_exec.py" + write_result = self._write_file(script_path, script) + if "error" in write_result and not write_result.get("success", True): + return REPLResult( + stdout="", + stderr=f"Failed to write script: {write_result.get('error')}", + locals={}, + execution_time=time.perf_counter() - start_time, + rlm_calls=[], + ) + + # Execute the script + exec_result = self._exec_command(f"python3 {script_path}") + + # Collect LLM calls made during this execution + with self._calls_lock: + pending_calls = self.pending_llm_calls.copy() + self.pending_llm_calls.clear() + + execution_time = time.perf_counter() - start_time + + stdout = exec_result.get("stdout", "") + stderr = exec_result.get("stderr", "") + + # Parse the JSON result from stdout + try: + lines = stdout.strip().split("\n") + result_json = lines[-1] if lines else "{}" + result = json.loads(result_json) + + return REPLResult( + stdout=result.get("stdout", ""), + stderr=result.get("stderr", "") + stderr, + locals=result.get("locals", {}), + execution_time=execution_time, + rlm_calls=pending_calls, + ) + except json.JSONDecodeError: + return REPLResult( + stdout=stdout, + stderr=stderr or "Failed to parse execution result", + locals={}, + execution_time=execution_time, + rlm_calls=pending_calls, + ) + + def cleanup(self): + """Stop polling and clean up resources.""" + if self.poller_thread is not None: + self.poller_stop.set() + self.poller_thread.join(timeout=2) + self.poller_thread = None + + self._session.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.cleanup() + return False + + def __del__(self): + try: + self.cleanup() + except Exception: + pass diff --git a/tests/test_cloudflare_repl.py b/tests/test_cloudflare_repl.py new file mode 100644 index 0000000..9597051 --- /dev/null +++ b/tests/test_cloudflare_repl.py @@ -0,0 +1,86 @@ +"""Tests for CloudflareREPL environment. + +Import tests run always. E2E tests require RLM_CF_WORKER_URL environment variable. +""" + +import os + +import pytest + + +class TestCloudflareREPLImports: + """Test CloudflareREPL can be imported.""" + + def test_cloudflare_repl_import(self): + """Test that CloudflareREPL can be imported.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + assert CloudflareREPL is not None + + def test_cloudflare_repl_is_class(self): + """Test that CloudflareREPL is a class.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + assert isinstance(CloudflareREPL, type) + + def test_is_isolated_env_subclass(self): + """Test that CloudflareREPL is a subclass of IsolatedEnv.""" + from rlm.environments.base_env import IsolatedEnv + from rlm.environments.cloudflare_repl import CloudflareREPL + + assert issubclass(CloudflareREPL, IsolatedEnv) + + def test_get_environment_cloudflare(self): + """Test that get_environment supports 'cloudflare' option.""" + from rlm.environments import get_environment + + # Should not raise - just verify the option is recognized + assert callable(get_environment) + + +# E2E tests - only run when RLM_CF_WORKER_URL is set +WORKER_URL = os.environ.get("RLM_CF_WORKER_URL") +SKIP_E2E = WORKER_URL is None + + +@pytest.mark.skipif(SKIP_E2E, reason="RLM_CF_WORKER_URL not set") +class TestCloudflareREPLE2E: + """End-to-end tests against a live Cloudflare Worker. + + To run these tests: + export RLM_CF_WORKER_URL=https://your-worker.workers.dev + pytest tests/test_cloudflare_repl.py -v + """ + + def test_simple_execution(self): + """Test executing simple code.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + with CloudflareREPL(worker_url=WORKER_URL, auth_token="") as repl: + result = repl.execute_code("x = 2 + 2\nprint(x)") + assert "4" in result.stdout + + def test_variable_persistence(self): + """Test that variables persist across executions.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + with CloudflareREPL(worker_url=WORKER_URL, auth_token="") as repl: + repl.execute_code("x = 42") + result = repl.execute_code("print(x * 2)") + assert "84" in result.stdout + + def test_numpy_available(self): + """Test that numpy is available in the sandbox.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + with CloudflareREPL(worker_url=WORKER_URL, auth_token="") as repl: + result = repl.execute_code("import numpy as np\nprint(np.sum([1, 2, 3]))") + assert "6" in result.stdout + + def test_error_handling(self): + """Test that errors are captured.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + with CloudflareREPL(worker_url=WORKER_URL, auth_token="") as repl: + result = repl.execute_code("1 / 0") + assert "ZeroDivisionError" in result.stderr diff --git a/tests/test_imports.py b/tests/test_imports.py index 8703c7b..65107de 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -169,6 +169,12 @@ def test_modal_repl_import(self): assert ModalREPL is not None + def test_cloudflare_repl_import(self): + """Test CloudflareREPL import.""" + from rlm.environments.cloudflare_repl import CloudflareREPL + + assert CloudflareREPL is not None + def test_docker_repl_import(self): """Test DockerREPL import.""" from rlm.environments.docker_repl import DockerREPL diff --git a/workers/rlm-sandbox/.gitignore b/workers/rlm-sandbox/.gitignore new file mode 100644 index 0000000..d833692 --- /dev/null +++ b/workers/rlm-sandbox/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.wrangler/ +.dev.vars +dist/ diff --git a/workers/rlm-sandbox/Dockerfile b/workers/rlm-sandbox/Dockerfile new file mode 100644 index 0000000..76d4649 --- /dev/null +++ b/workers/rlm-sandbox/Dockerfile @@ -0,0 +1,22 @@ +# RLM Sandbox - Python execution environment for CloudflareREPL +# Must match @cloudflare/sandbox npm package version +FROM docker.io/cloudflare/sandbox:0.6.7 + +# Install Python 3 and common dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip \ + python-is-python3 \ + && rm -rf /var/lib/apt/lists/* + +# Install common Python packages for data science / math +RUN pip3 install --no-cache-dir \ + numpy \ + sympy \ + requests + +# Create workspace directory +RUN mkdir -p /workspace /tmp + +# Required during local development to access exposed ports +EXPOSE 8080 diff --git a/workers/rlm-sandbox/package-lock.json b/workers/rlm-sandbox/package-lock.json new file mode 100644 index 0000000..7a03f6a --- /dev/null +++ b/workers/rlm-sandbox/package-lock.json @@ -0,0 +1,1592 @@ +{ + "name": "rlm-sandbox", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rlm-sandbox", + "version": "0.1.0", + "dependencies": { + "@cloudflare/sandbox": "^0.6.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250109.0", + "typescript": "^5.7.0", + "wrangler": "^4.0.0" + } + }, + "node_modules/@cloudflare/containers": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.0.30.tgz", + "integrity": "sha512-i148xBgmyn/pje82ZIyuTr/Ae0BT/YWwa1/GTJcw6DxEjUHAzZLaBCiX446U9OeuJ2rBh/L/9FIzxX5iYNt1AQ==", + "license": "ISC" + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz", + "integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/sandbox": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@cloudflare/sandbox/-/sandbox-0.6.7.tgz", + "integrity": "sha512-b6UjMHuefkWrJRxOLHlY6z+AaH5ogLQZAc16aNDSbudJkM+pwTBqjEd2mpyBvBf2JWOgrS8o6Eh9aKSvW+c/4Q==", + "license": "Apache-2.0", + "dependencies": { + "@cloudflare/containers": "^0.0.30" + }, + "peerDependencies": { + "@openai/agents": "^0.3.3", + "@opencode-ai/sdk": "^1.0.137" + }, + "peerDependenciesMeta": { + "@openai/agents": { + "optional": true + }, + "@opencode-ai/sdk": { + "optional": true + } + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.7.13", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.13.tgz", + "integrity": "sha512-NulO1H8R/DzsJguLC0ndMuk4Ufv0KSlN+E54ay9rn9ZCQo0kpAPwwh3LhgpZ96a3Dr6L9LqW57M4CqC34iLOvw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251202.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251210.0.tgz", + "integrity": "sha512-Nn9X1moUDERA9xtFdCQ2XpQXgAS9pOjiCxvOT8sVx9UJLAiBLkfSCGbpsYdarODGybXCpjRlc77Yppuolvt7oQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251210.0.tgz", + "integrity": "sha512-Mg8iYIZQFnbevq/ls9eW/eneWTk/EE13Pej1MwfkY5et0jVpdHnvOLywy/o+QtMJFef1AjsqXGULwAneYyBfHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251210.0.tgz", + "integrity": "sha512-kjC2fCZhZ2Gkm1biwk2qByAYpGguK5Gf5ic8owzSCUw0FOUfQxTZUT9Lp3gApxsfTLbbnLBrX/xzWjywH9QR4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251210.0.tgz", + "integrity": "sha512-2IB37nXi7PZVQLa1OCuO7/6pNxqisRSO8DmCQ5x/3sezI5op1vwOxAcb1osAnuVsVN9bbvpw70HJvhKruFJTuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251210.0.tgz", + "integrity": "sha512-Uaz6/9XE+D6E7pCY4OvkCuJHu7HcSDzeGcCGY1HLhojXhHd7yL52c3yfiyJdS8hPatiAa0nn5qSI/42+aTdDSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260103.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260103.0.tgz", + "integrity": "sha512-jANmoGpJcXARnwlkvrQOeWyjYD1quTfHcs+++Z544XRHOSfLc4XSlts7snIhbiIGgA5bo66zDhraF+9lKUr2hw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peer": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "4.20251210.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251210.0.tgz", + "integrity": "sha512-k6kIoXwGVqlPZb0hcn+X7BmnK+8BjIIkusQPY22kCo2RaQJ/LzAjtxHQdGXerlHSnJyQivDQsL6BJHMpQfUFyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "7.14.0", + "workerd": "1.20251210.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20251210.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251210.0.tgz", + "integrity": "sha512-9MUUneP1BnRE9XAYi94FXxHmiLGbO75EHQZsgWqSiOXjoXSqJCw8aQbIEPxCy19TclEl/kHUFYce8ST2W+Qpjw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20251210.0", + "@cloudflare/workerd-darwin-arm64": "1.20251210.0", + "@cloudflare/workerd-linux-64": "1.20251210.0", + "@cloudflare/workerd-linux-arm64": "1.20251210.0", + "@cloudflare/workerd-windows-64": "1.20251210.0" + } + }, + "node_modules/wrangler": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.54.0.tgz", + "integrity": "sha512-bANFsjDwJLbprYoBK+hUDZsVbUv2SqJd8QvArLIcZk+fPq4h/Ohtj5vkKXD3k0s2bD1DXLk08D+hYmeNH+xC6A==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.1", + "@cloudflare/unenv-preset": "2.7.13", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.0", + "miniflare": "4.20251210.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20251210.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20251210.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/workers/rlm-sandbox/package.json b/workers/rlm-sandbox/package.json new file mode 100644 index 0000000..366e9f9 --- /dev/null +++ b/workers/rlm-sandbox/package.json @@ -0,0 +1,19 @@ +{ + "name": "rlm-sandbox", + "version": "0.1.0", + "description": "Cloudflare Worker providing Python sandbox for RLM CloudflareREPL", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "tail": "wrangler tail" + }, + "dependencies": { + "@cloudflare/sandbox": "^0.6.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250109.0", + "typescript": "^5.7.0", + "wrangler": "^4.0.0" + } +} diff --git a/workers/rlm-sandbox/src/index.ts b/workers/rlm-sandbox/src/index.ts new file mode 100644 index 0000000..8363300 --- /dev/null +++ b/workers/rlm-sandbox/src/index.ts @@ -0,0 +1,213 @@ +/** + * RLM Sandbox Worker + * + * Cloudflare Worker providing sandbox execution for CloudflareREPL. + * Implements the API endpoints required by rlm/environments/cloudflare_repl.py + */ + +import { getSandbox } from "@cloudflare/sandbox"; + +// Re-export Sandbox for Durable Objects binding +export { Sandbox } from "@cloudflare/sandbox"; + +interface Env { + Sandbox: DurableObjectNamespace; + // Optional: Bearer token for authentication + AUTH_TOKEN?: string; +} + +interface ExecRequest { + sandbox_id: string; + command: string; +} + +interface WriteRequest { + sandbox_id: string; + path: string; + content: string; +} + +/** + * Validate authentication token + */ +function validateAuth(request: Request, env: Env): Response | null { + // If no AUTH_TOKEN configured, allow all requests (dev mode) + if (!env.AUTH_TOKEN) { + return null; + } + + const authHeader = request.headers.get("Authorization"); + if (!authHeader) { + return Response.json( + { error: "Authorization header required" }, + { status: 401 } + ); + } + + const token = authHeader.replace(/^Bearer\s+/i, ""); + if (token !== env.AUTH_TOKEN) { + return Response.json({ error: "Invalid token" }, { status: 401 }); + } + + return null; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + + // Health check - no auth required + if (url.pathname === "/" || url.pathname === "/health") { + return Response.json({ + status: "ok", + service: "rlm-sandbox", + timestamp: Date.now(), + }); + } + + // All sandbox endpoints require auth + const authError = validateAuth(request, env); + if (authError) { + return authError; + } + + // ==================== Sandbox Endpoints ==================== + + /** + * POST /sandbox/exec + * Execute a command in the sandbox + * + * Request body: { sandbox_id: string, command: string } + * Response: { stdout: string, stderr: string, exitCode: number, success: boolean } + */ + if (url.pathname === "/sandbox/exec" && request.method === "POST") { + try { + const body = (await request.json()) as ExecRequest; + + if (!body.sandbox_id || !body.command) { + return Response.json( + { error: "sandbox_id and command are required" }, + { status: 400 } + ); + } + + const sandbox = getSandbox(env.Sandbox, body.sandbox_id); + const result = await sandbox.exec(body.command); + + return Response.json({ + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + success: result.success, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return Response.json({ error: message }, { status: 500 }); + } + } + + /** + * POST /sandbox/write + * Write a file to the sandbox + * + * Request body: { sandbox_id: string, path: string, content: string } + * Response: { success: boolean } + */ + if (url.pathname === "/sandbox/write" && request.method === "POST") { + try { + const body = (await request.json()) as WriteRequest; + + if (!body.sandbox_id || !body.path || body.content === undefined) { + return Response.json( + { error: "sandbox_id, path, and content are required" }, + { status: 400 } + ); + } + + const sandbox = getSandbox(env.Sandbox, body.sandbox_id); + await sandbox.writeFile(body.path, body.content); + + return Response.json({ success: true }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return Response.json({ error: message, success: false }, { status: 500 }); + } + } + + /** + * GET /sandbox/read + * Read a file from the sandbox + * + * Query params: sandbox_id, path + * Response: { content: string, success: boolean } + */ + if (url.pathname === "/sandbox/read" && request.method === "GET") { + try { + const sandboxId = url.searchParams.get("sandbox_id"); + const path = url.searchParams.get("path"); + + if (!sandboxId || !path) { + return Response.json( + { error: "sandbox_id and path query params are required" }, + { status: 400 } + ); + } + + const sandbox = getSandbox(env.Sandbox, sandboxId); + const file = await sandbox.readFile(path); + + return Response.json({ + content: file.content, + success: true, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return Response.json( + { error: message, content: "", success: false }, + { status: 500 } + ); + } + } + + /** + * GET /sandbox/health + * Check if sandbox is healthy + * + * Query params: sandbox_id (optional, defaults to "health-check") + */ + if (url.pathname === "/sandbox/health" && request.method === "GET") { + try { + const sandboxId = url.searchParams.get("sandbox_id") || "health-check"; + const sandbox = getSandbox(env.Sandbox, sandboxId); + const result = await sandbox.exec("python3 --version"); + + return Response.json({ + status: "healthy", + python: result.stdout.trim(), + exitCode: result.exitCode, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + return Response.json( + { status: "unhealthy", error: message }, + { status: 500 } + ); + } + } + + // 404 for unknown routes + return Response.json( + { + error: "Not found", + endpoints: [ + "GET /health", + "POST /sandbox/exec", + "POST /sandbox/write", + "GET /sandbox/read", + "GET /sandbox/health", + ], + }, + { status: 404 } + ); + }, +}; diff --git a/workers/rlm-sandbox/tsconfig.json b/workers/rlm-sandbox/tsconfig.json new file mode 100644 index 0000000..1d8fa33 --- /dev/null +++ b/workers/rlm-sandbox/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["@cloudflare/workers-types"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/workers/rlm-sandbox/wrangler.toml b/workers/rlm-sandbox/wrangler.toml new file mode 100644 index 0000000..22cdf4c --- /dev/null +++ b/workers/rlm-sandbox/wrangler.toml @@ -0,0 +1,32 @@ +# RLM Sandbox Worker +# Provides Python execution sandbox for CloudflareREPL +name = "rlm-sandbox" +main = "src/index.ts" +compatibility_date = "2025-01-05" +compatibility_flags = ["nodejs_compat"] + +# Local development - enable containers for Docker testing +[dev] +enable_containers = true + +# Observability +[observability] +[observability.logs] +enabled = true + +# Container configuration for Sandbox SDK +[[containers]] +class_name = "Sandbox" +image = "./Dockerfile" +instance_type = "basic" # 1/4 vCPU, 1 GiB RAM +max_instances = 10 + +# Durable Objects binding for Sandbox +[[durable_objects.bindings]] +class_name = "Sandbox" +name = "Sandbox" + +# Migration for Sandbox Durable Object +[[migrations]] +tag = "v1" +new_sqlite_classes = ["Sandbox"]