From cab71bca19851d9456d71ac3f9f4364a068e4f12 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 16:56:12 -0800 Subject: [PATCH 01/10] feat: added tunnels example --- EXAMPLES.md | 31 +++++++++++ examples/devbox_tunnel.py | 112 ++++++++++++++++++++++++++++++++++++++ examples/registry.py | 8 +++ 3 files changed, 151 insertions(+) create mode 100644 examples/devbox_tunnel.py diff --git a/EXAMPLES.md b/EXAMPLES.md index 0d3ff62cc..92927089b 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -9,6 +9,7 @@ Runnable examples live in [`examples/`](./examples). - [Blueprint with Build Context](#blueprint-with-build-context) - [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle) +- [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel) - [MCP Hub + Claude Code + GitHub](#mcp-github-tools) @@ -70,6 +71,36 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Source:** [`examples/devbox_from_blueprint_lifecycle.py`](./examples/devbox_from_blueprint_lifecycle.py) + +## Devbox Tunnel (HTTP Server Access) + +**Use case:** Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. + +**Tags:** `devbox`, `tunnel`, `networking`, `http`, `async` + +### Workflow +- Create a devbox +- Start an HTTP server inside the devbox +- Enable a tunnel for external access +- Make an HTTP request from the local machine through the tunnel +- Validate the response +- Shutdown the devbox + +### Prerequisites +- `RUNLOOP_API_KEY` + +### Run +```sh +uv run python -m examples.devbox_tunnel +``` + +### Test +```sh +uv run pytest -m smoketest tests/smoketests/examples/ +``` + +**Source:** [`examples/devbox_tunnel.py`](./examples/devbox_tunnel.py) + ## MCP Hub + Claude Code + GitHub diff --git a/examples/devbox_tunnel.py b/examples/devbox_tunnel.py new file mode 100644 index 000000000..652666fcd --- /dev/null +++ b/examples/devbox_tunnel.py @@ -0,0 +1,112 @@ +#!/usr/bin/env -S uv run python +""" +--- +title: Devbox Tunnel (HTTP Server Access) +slug: devbox-tunnel +use_case: Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. +workflow: + - Create a devbox + - Start an HTTP server inside the devbox + - Enable a tunnel for external access + - Make an HTTP request from the local machine through the tunnel + - Validate the response + - Shutdown the devbox +tags: + - devbox + - tunnel + - networking + - http + - async +prerequisites: + - RUNLOOP_API_KEY +run: uv run python -m examples.devbox_tunnel +test: uv run pytest -m smoketest tests/smoketests/examples/ +--- +""" + +from __future__ import annotations + +import asyncio + +import httpx + +from runloop_api_client import AsyncRunloopSDK + +from ._harness import run_as_cli, wrap_recipe +from .example_types import ExampleCheck, RecipeOutput, RecipeContext + +HTTP_SERVER_PORT = 8080 +SERVER_STARTUP_DELAY_S = 2 + + +async def recipe(ctx: RecipeContext) -> RecipeOutput: + """Create a devbox, start an HTTP server, enable a tunnel, and access it from the local machine.""" + cleanup = ctx.cleanup + + sdk = AsyncRunloopSDK() + + devbox = await sdk.devbox.create( + name="devbox-tunnel-example", + launch_parameters={ + "resource_size_request": "X_SMALL", + "keep_alive_time_seconds": 60 * 10, + }, + ) + cleanup.add(f"devbox:{devbox.id}", devbox.shutdown) + + # Start a simple HTTP server inside the devbox using Python's built-in http.server + # We use exec_async because the server runs indefinitely until stopped + server_execution = await devbox.cmd.exec_async(f"python3 -m http.server {HTTP_SERVER_PORT} --directory /tmp") + + # Give the server a moment to start + await asyncio.sleep(SERVER_STARTUP_DELAY_S) + + # Enable a tunnel to expose the HTTP server + # For authenticated tunnels, use auth_mode="authenticated" and include the auth_token + # in your requests via the Authorization header: `Authorization: Bearer {tunnel.auth_token}` + tunnel = await devbox.net.enable_tunnel(auth_mode="open") + + # Get the tunnel URL for the server port + tunnel_url = await devbox.get_tunnel_url(HTTP_SERVER_PORT) + + # Make an HTTP request from the LOCAL MACHINE through the tunnel to the devbox + # This demonstrates that the tunnel allows external access to the devbox service + async with httpx.AsyncClient() as client: + response = await client.get(tunnel_url) + response_text = response.text + + # Stop the HTTP server + await server_execution.kill() + + return RecipeOutput( + resources_created=[f"devbox:{devbox.id}"], + checks=[ + ExampleCheck( + name="tunnel was created successfully", + passed=bool(tunnel.tunnel_key), + details=f"tunnel_key={tunnel.tunnel_key}", + ), + ExampleCheck( + name="tunnel URL was constructed correctly", + passed=tunnel.tunnel_key in tunnel_url and str(HTTP_SERVER_PORT) in tunnel_url, + details=tunnel_url, + ), + ExampleCheck( + name="HTTP request through tunnel succeeded", + passed=response.is_success, + details=f"status={response.status_code}", + ), + ExampleCheck( + name="response contains directory listing", + passed="Directory listing" in response_text, + details=response_text[:200], + ), + ], + ) + + +run_devbox_tunnel_example = wrap_recipe(recipe) + + +if __name__ == "__main__": + run_as_cli(run_devbox_tunnel_example) diff --git a/examples/registry.py b/examples/registry.py index cb6b780a9..3148e6aee 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -8,6 +8,7 @@ from typing import Any, Callable, cast from .example_types import ExampleResult +from .devbox_tunnel import run_devbox_tunnel_example from .mcp_github_tools import run_mcp_github_tools_example from .blueprint_with_build_context import run_blueprint_with_build_context_example from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example @@ -29,6 +30,13 @@ "required_env": ["RUNLOOP_API_KEY"], "run": run_devbox_from_blueprint_lifecycle_example, }, + { + "slug": "devbox-tunnel", + "title": "Devbox Tunnel (HTTP Server Access)", + "file_name": "devbox_tunnel.py", + "required_env": ["RUNLOOP_API_KEY"], + "run": run_devbox_tunnel_example, + }, { "slug": "mcp-github-tools", "title": "MCP Hub + Claude Code + GitHub", From 44ef1e363da365204b4edb0cbfa79a27a1114b40 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 16:59:31 -0800 Subject: [PATCH 02/10] lint --- examples/devbox_tunnel.py | 6 +++++- examples/registry.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/devbox_tunnel.py b/examples/devbox_tunnel.py index 652666fcd..343d8aec9 100644 --- a/examples/devbox_tunnel.py +++ b/examples/devbox_tunnel.py @@ -68,6 +68,8 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: # Get the tunnel URL for the server port tunnel_url = await devbox.get_tunnel_url(HTTP_SERVER_PORT) + if tunnel_url is None: + raise RuntimeError("Failed to get tunnel URL after enabling tunnel") # Make an HTTP request from the LOCAL MACHINE through the tunnel to the devbox # This demonstrates that the tunnel allows external access to the devbox service @@ -88,7 +90,9 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: ), ExampleCheck( name="tunnel URL was constructed correctly", - passed=tunnel.tunnel_key in tunnel_url and str(HTTP_SERVER_PORT) in tunnel_url, + passed=bool( + tunnel.tunnel_key and tunnel.tunnel_key in tunnel_url and str(HTTP_SERVER_PORT) in tunnel_url + ), details=tunnel_url, ), ExampleCheck( diff --git a/examples/registry.py b/examples/registry.py index 3148e6aee..c2b7e7e48 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -7,8 +7,8 @@ from typing import Any, Callable, cast -from .example_types import ExampleResult from .devbox_tunnel import run_devbox_tunnel_example +from .example_types import ExampleResult from .mcp_github_tools import run_mcp_github_tools_example from .blueprint_with_build_context import run_blueprint_with_build_context_example from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example From 767524da08353aa62133ee732369f3cd2e9c8553 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 17:01:04 -0800 Subject: [PATCH 03/10] updated registry --- examples/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/registry.py b/examples/registry.py index c2b7e7e48..3148e6aee 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -7,8 +7,8 @@ from typing import Any, Callable, cast -from .devbox_tunnel import run_devbox_tunnel_example from .example_types import ExampleResult +from .devbox_tunnel import run_devbox_tunnel_example from .mcp_github_tools import run_mcp_github_tools_example from .blueprint_with_build_context import run_blueprint_with_build_context_example from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example From af4bc5f82b5a4c76299cdd8106c88fc9b226734d Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 17:03:41 -0800 Subject: [PATCH 04/10] lint --- examples/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/registry.py b/examples/registry.py index 3148e6aee..c2b7e7e48 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -7,8 +7,8 @@ from typing import Any, Callable, cast -from .example_types import ExampleResult from .devbox_tunnel import run_devbox_tunnel_example +from .example_types import ExampleResult from .mcp_github_tools import run_mcp_github_tools_example from .blueprint_with_build_context import run_blueprint_with_build_context_example from .devbox_from_blueprint_lifecycle import run_devbox_from_blueprint_lifecycle_example From 82c0b7c157323e3b7388de7402a390f6f14b4e7d Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 17:10:20 -0800 Subject: [PATCH 05/10] fixed linter issue --- scripts/generate_examples_md.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/generate_examples_md.py b/scripts/generate_examples_md.py index bf4dc587a..9e47e9cae 100644 --- a/scripts/generate_examples_md.py +++ b/scripts/generate_examples_md.py @@ -150,12 +150,13 @@ def generate_markdown(examples: list[dict[str, Any]]) -> str: def generate_registry(examples: list[dict[str, Any]]) -> str: """Generate the registry.py content.""" - imports: list[str] = [] + imports: list[tuple[str, str]] = [("example_types", "ExampleResult")] for example in examples: module = example["file_name"].replace(".py", "") runner = f"run_{module}_example" - imports.append(f"from .{module} import {runner}") - imports.sort(key=len) + imports.append((module, runner)) + imports.sort(key=lambda x: (len(x[0]), x[0])) + import_lines = [f"from .{mod} import {name}" for mod, name in imports] entries: list[str] = [] for example in examples: @@ -181,8 +182,7 @@ def generate_registry(examples: list[dict[str, Any]]) -> str: from typing import Any, Callable, cast -from .example_types import ExampleResult -{chr(10).join(imports)} +{chr(10).join(import_lines)} ExampleRegistryEntry = dict[str, Any] From aa1ee9b478bf5a3aaef977aa4ac2346f51370b73 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Tue, 3 Mar 2026 17:13:49 -0800 Subject: [PATCH 06/10] got rid of pointless keep alive --- examples/devbox_tunnel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/devbox_tunnel.py b/examples/devbox_tunnel.py index 343d8aec9..479fa9957 100644 --- a/examples/devbox_tunnel.py +++ b/examples/devbox_tunnel.py @@ -49,7 +49,6 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: name="devbox-tunnel-example", launch_parameters={ "resource_size_request": "X_SMALL", - "keep_alive_time_seconds": 60 * 10, }, ) cleanup.add(f"devbox:{devbox.id}", devbox.shutdown) From 05ed79f894d2706112c057c5f0d842d8e9af8e61 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 11:35:58 -0700 Subject: [PATCH 07/10] merged --- EXAMPLES.md | 76 +++++++++++++++++++++++++++++++++----------- examples/registry.py | 3 -- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 3861fafb5..76acfa51f 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -7,17 +7,38 @@ Runnable examples live in [`examples/`](./examples). ## Table of Contents -- [Blueprint with Build Context](#blueprint-with-build-context) -- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle) -<<<<<<< HEAD -- [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel) -======= -- [Devbox Snapshot and Resume](#devbox-snapshot-resume) ->>>>>>> main -- [MCP Hub + Claude Code + GitHub](#mcp-github-tools) -- [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox) +- [Examples](#examples) + - [Table of Contents](#table-of-contents) + - [Blueprint with Build Context](#blueprint-with-build-context) + - [Workflow](#workflow) + - [Prerequisites](#prerequisites) + - [Run](#run) + - [Test](#test) + - [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-run-command-shutdown) + - [Workflow](#workflow-1) + - [Prerequisites](#prerequisites-1) + - [Run](#run-1) + - [Test](#test-1) + - [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel-http-server-access) + - [Workflow](#workflow-2) + - [Devbox Snapshot and Resume](#devbox-snapshot-and-resume) + - [Workflow](#workflow-3) + - [Prerequisites](#prerequisites-2) + - [Run](#run-2) + - [Test](#test-2) + - [MCP Hub + Claude Code + GitHub](#mcp-hub--claude-code--github) + - [Workflow](#workflow-4) + - [Prerequisites](#prerequisites-3) + - [Run](#run-3) + - [Test](#test-3) + - [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox-create-inject-verify-delete) + - [Workflow](#workflow-5) + - [Prerequisites](#prerequisites-4) + - [Run](#run-4) + - [Test](#test-4) + ## Blueprint with Build Context **Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. Uses the async SDK. @@ -25,6 +46,7 @@ Runnable examples live in [`examples/`](./examples). **Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup`, `async` ### Workflow + - Create a temporary directory with sample application files - Upload the directory to object storage as build context - Create a blueprint with a Dockerfile that copies the context files @@ -33,14 +55,17 @@ Runnable examples live in [`examples/`](./examples). - Shutdown devbox and delete blueprint and storage object ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh uv run python -m examples.blueprint_with_build_context ``` ### Test + ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` @@ -48,6 +73,7 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Source:** [`examples/blueprint_with_build_context.py`](./examples/blueprint_with_build_context.py) + ## Devbox From Blueprint (Run Command, Shutdown) **Use case:** Create a devbox from a blueprint, run a command, fetch logs, validate output, and cleanly tear everything down. @@ -55,6 +81,7 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `devbox`, `blueprint`, `commands`, `logs`, `cleanup` ### Workflow + - Create a blueprint - Fetch blueprint build logs - Create a devbox from the blueprint @@ -64,22 +91,25 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown devbox and delete blueprint ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh uv run python -m examples.devbox_from_blueprint_lifecycle ``` ### Test + ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` **Source:** [`examples/devbox_from_blueprint_lifecycle.py`](./examples/devbox_from_blueprint_lifecycle.py) -<<<<<<< HEAD + ## Devbox Tunnel (HTTP Server Access) **Use case:** Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. @@ -87,14 +117,15 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `devbox`, `tunnel`, `networking`, `http`, `async` ### Workflow + - Create a devbox - Start an HTTP server inside the devbox - Enable a tunnel for external access - Make an HTTP request from the local machine through the tunnel - Validate the response - Shutdown the devbox -======= - + + ## Devbox Snapshot and Resume **Use case:** Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone. Uses the async SDK. @@ -102,6 +133,7 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `devbox`, `snapshot`, `resume`, `cleanup`, `async` ### Workflow + - Create a devbox - Write a file to the devbox - Create a disk snapshot @@ -109,32 +141,29 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Modify the file on the original devbox - Verify the clone has the original content - Shutdown both devboxes and delete the snapshot ->>>>>>> main ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh -<<<<<<< HEAD uv run python -m examples.devbox_tunnel -======= uv run python -m examples.devbox_snapshot_resume ->>>>>>> main ``` ### Test + ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` -<<<<<<< HEAD **Source:** [`examples/devbox_tunnel.py`](./examples/devbox_tunnel.py) -======= **Source:** [`examples/devbox_snapshot_resume.py`](./examples/devbox_snapshot_resume.py) ->>>>>>> main + ## MCP Hub + Claude Code + GitHub **Use case:** Connect Claude Code running in a devbox to GitHub tools through MCP Hub without exposing raw GitHub credentials to the devbox. @@ -142,6 +171,7 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `mcp`, `devbox`, `github`, `commands`, `cleanup` ### Workflow + - Create an MCP config for GitHub - Store GitHub token as a Runloop secret - Launch a devbox with MCP Hub wiring @@ -150,16 +180,19 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown devbox and clean up cloud resources ### Prerequisites + - `RUNLOOP_API_KEY` - `GITHUB_TOKEN (GitHub PAT with repo scope)` - `ANTHROPIC_API_KEY` ### Run + ```sh GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx uv run python -m examples.mcp_github_tools ``` ### Test + ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` @@ -167,6 +200,7 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Source:** [`examples/mcp_github_tools.py`](./examples/mcp_github_tools.py) + ## Secrets with Devbox (Create, Inject, Verify, Delete) **Use case:** Create a secret, inject it into a devbox as an environment variable, verify access, and clean up. @@ -174,6 +208,7 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `secrets`, `devbox`, `environment-variables`, `cleanup` ### Workflow + - Create a secret with a test value - Create a devbox with the secret mapped to an env var - Execute a command that reads the secret from the environment @@ -183,14 +218,17 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown devbox and delete secret ### Prerequisites + - `RUNLOOP_API_KEY` ### Run + ```sh uv run python -m examples.secrets_with_devbox ``` ### Test + ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` diff --git a/examples/registry.py b/examples/registry.py index f6b4064d3..75aa71e26 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -33,19 +33,16 @@ "run": run_devbox_from_blueprint_lifecycle_example, }, { -<<<<<<< HEAD "slug": "devbox-tunnel", "title": "Devbox Tunnel (HTTP Server Access)", "file_name": "devbox_tunnel.py", "required_env": ["RUNLOOP_API_KEY"], "run": run_devbox_tunnel_example, -======= "slug": "devbox-snapshot-resume", "title": "Devbox Snapshot and Resume", "file_name": "devbox_snapshot_resume.py", "required_env": ["RUNLOOP_API_KEY"], "run": run_devbox_snapshot_resume_example, ->>>>>>> main }, { "slug": "mcp-github-tools", From 39cd0c3a88787bcbd95861ddedb327c8fac35929 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 11:38:40 -0700 Subject: [PATCH 08/10] fmt --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 22893d164..79832c62a 100644 --- a/uv.lock +++ b/uv.lock @@ -835,7 +835,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2386,7 +2386,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "1.11.0" +version = "1.12.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From 65f034122acc5f7fa0ccd05254da1c49c3a7dd8a Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 11:40:31 -0700 Subject: [PATCH 09/10] update examples --- EXAMPLES.md | 108 +++++++++++++++---------------------------- examples/registry.py | 12 +++-- 2 files changed, 43 insertions(+), 77 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 76acfa51f..ddd4e60aa 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -7,38 +7,14 @@ Runnable examples live in [`examples/`](./examples). ## Table of Contents -- [Examples](#examples) - - [Table of Contents](#table-of-contents) - - [Blueprint with Build Context](#blueprint-with-build-context) - - [Workflow](#workflow) - - [Prerequisites](#prerequisites) - - [Run](#run) - - [Test](#test) - - [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-run-command-shutdown) - - [Workflow](#workflow-1) - - [Prerequisites](#prerequisites-1) - - [Run](#run-1) - - [Test](#test-1) - - [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel-http-server-access) - - [Workflow](#workflow-2) - - [Devbox Snapshot and Resume](#devbox-snapshot-and-resume) - - [Workflow](#workflow-3) - - [Prerequisites](#prerequisites-2) - - [Run](#run-2) - - [Test](#test-2) - - [MCP Hub + Claude Code + GitHub](#mcp-hub--claude-code--github) - - [Workflow](#workflow-4) - - [Prerequisites](#prerequisites-3) - - [Run](#run-3) - - [Test](#test-3) - - [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox-create-inject-verify-delete) - - [Workflow](#workflow-5) - - [Prerequisites](#prerequisites-4) - - [Run](#run-4) - - [Test](#test-4) +- [Blueprint with Build Context](#blueprint-with-build-context) +- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle) +- [Devbox Snapshot and Resume](#devbox-snapshot-resume) +- [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel) +- [MCP Hub + Claude Code + GitHub](#mcp-github-tools) +- [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox) - ## Blueprint with Build Context **Use case:** Create a blueprint using the object store to provide docker build context files, then verify files are copied into the image. Uses the async SDK. @@ -46,7 +22,6 @@ Runnable examples live in [`examples/`](./examples). **Tags:** `blueprint`, `object-store`, `build-context`, `devbox`, `cleanup`, `async` ### Workflow - - Create a temporary directory with sample application files - Upload the directory to object storage as build context - Create a blueprint with a Dockerfile that copies the context files @@ -55,17 +30,14 @@ Runnable examples live in [`examples/`](./examples). - Shutdown devbox and delete blueprint and storage object ### Prerequisites - - `RUNLOOP_API_KEY` ### Run - ```sh uv run python -m examples.blueprint_with_build_context ``` ### Test - ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` @@ -73,7 +45,6 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Source:** [`examples/blueprint_with_build_context.py`](./examples/blueprint_with_build_context.py) - ## Devbox From Blueprint (Run Command, Shutdown) **Use case:** Create a devbox from a blueprint, run a command, fetch logs, validate output, and cleanly tear everything down. @@ -81,7 +52,6 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `devbox`, `blueprint`, `commands`, `logs`, `cleanup` ### Workflow - - Create a blueprint - Fetch blueprint build logs - Create a devbox from the blueprint @@ -91,41 +61,21 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown devbox and delete blueprint ### Prerequisites - - `RUNLOOP_API_KEY` ### Run - ```sh uv run python -m examples.devbox_from_blueprint_lifecycle ``` ### Test - ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` **Source:** [`examples/devbox_from_blueprint_lifecycle.py`](./examples/devbox_from_blueprint_lifecycle.py) - - -## Devbox Tunnel (HTTP Server Access) - -**Use case:** Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. - -**Tags:** `devbox`, `tunnel`, `networking`, `http`, `async` - -### Workflow - -- Create a devbox -- Start an HTTP server inside the devbox -- Enable a tunnel for external access -- Make an HTTP request from the local machine through the tunnel -- Validate the response -- Shutdown the devbox - - + ## Devbox Snapshot and Resume **Use case:** Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone. Uses the async SDK. @@ -133,7 +83,6 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `devbox`, `snapshot`, `resume`, `cleanup`, `async` ### Workflow - - Create a devbox - Write a file to the devbox - Create a disk snapshot @@ -143,27 +92,51 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown both devboxes and delete the snapshot ### Prerequisites - - `RUNLOOP_API_KEY` ### Run - ```sh -uv run python -m examples.devbox_tunnel uv run python -m examples.devbox_snapshot_resume ``` ### Test +```sh +uv run pytest -m smoketest tests/smoketests/examples/ +``` + +**Source:** [`examples/devbox_snapshot_resume.py`](./examples/devbox_snapshot_resume.py) + +## Devbox Tunnel (HTTP Server Access) + +**Use case:** Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. + +**Tags:** `devbox`, `tunnel`, `networking`, `http`, `async` + +### Workflow +- Create a devbox +- Start an HTTP server inside the devbox +- Enable a tunnel for external access +- Make an HTTP request from the local machine through the tunnel +- Validate the response +- Shutdown the devbox + +### Prerequisites +- `RUNLOOP_API_KEY` + +### Run +```sh +uv run python -m examples.devbox_tunnel +``` + +### Test ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` **Source:** [`examples/devbox_tunnel.py`](./examples/devbox_tunnel.py) -**Source:** [`examples/devbox_snapshot_resume.py`](./examples/devbox_snapshot_resume.py) - ## MCP Hub + Claude Code + GitHub **Use case:** Connect Claude Code running in a devbox to GitHub tools through MCP Hub without exposing raw GitHub credentials to the devbox. @@ -171,7 +144,6 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `mcp`, `devbox`, `github`, `commands`, `cleanup` ### Workflow - - Create an MCP config for GitHub - Store GitHub token as a Runloop secret - Launch a devbox with MCP Hub wiring @@ -180,19 +152,16 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown devbox and clean up cloud resources ### Prerequisites - - `RUNLOOP_API_KEY` - `GITHUB_TOKEN (GitHub PAT with repo scope)` - `ANTHROPIC_API_KEY` ### Run - ```sh GITHUB_TOKEN=ghp_xxx ANTHROPIC_API_KEY=sk-ant-xxx uv run python -m examples.mcp_github_tools ``` ### Test - ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` @@ -200,7 +169,6 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Source:** [`examples/mcp_github_tools.py`](./examples/mcp_github_tools.py) - ## Secrets with Devbox (Create, Inject, Verify, Delete) **Use case:** Create a secret, inject it into a devbox as an environment variable, verify access, and clean up. @@ -208,7 +176,6 @@ uv run pytest -m smoketest tests/smoketests/examples/ **Tags:** `secrets`, `devbox`, `environment-variables`, `cleanup` ### Workflow - - Create a secret with a test value - Create a devbox with the secret mapped to an env var - Execute a command that reads the secret from the environment @@ -218,17 +185,14 @@ uv run pytest -m smoketest tests/smoketests/examples/ - Shutdown devbox and delete secret ### Prerequisites - - `RUNLOOP_API_KEY` ### Run - ```sh uv run python -m examples.secrets_with_devbox ``` ### Test - ```sh uv run pytest -m smoketest tests/smoketests/examples/ ``` diff --git a/examples/registry.py b/examples/registry.py index 75aa71e26..edc52f445 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -33,17 +33,19 @@ "run": run_devbox_from_blueprint_lifecycle_example, }, { - "slug": "devbox-tunnel", - "title": "Devbox Tunnel (HTTP Server Access)", - "file_name": "devbox_tunnel.py", - "required_env": ["RUNLOOP_API_KEY"], - "run": run_devbox_tunnel_example, "slug": "devbox-snapshot-resume", "title": "Devbox Snapshot and Resume", "file_name": "devbox_snapshot_resume.py", "required_env": ["RUNLOOP_API_KEY"], "run": run_devbox_snapshot_resume_example, }, + { + "slug": "devbox-tunnel", + "title": "Devbox Tunnel (HTTP Server Access)", + "file_name": "devbox_tunnel.py", + "required_env": ["RUNLOOP_API_KEY"], + "run": run_devbox_tunnel_example, + }, { "slug": "mcp-github-tools", "title": "MCP Hub + Claude Code + GitHub", From 33d8c331daceee1e5d73083ba53d19d30d952d84 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 13:43:25 -0700 Subject: [PATCH 10/10] updated tunnels example --- EXAMPLES.md | 5 ++--- examples/devbox_tunnel.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index ddd4e60aa..a036aa01d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -109,14 +109,13 @@ uv run pytest -m smoketest tests/smoketests/examples/ ## Devbox Tunnel (HTTP Server Access) -**Use case:** Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. +**Use case:** Create a devbox with a tunnel, start an HTTP server, and access the server from the local machine through the tunnel. Uses the async SDK. **Tags:** `devbox`, `tunnel`, `networking`, `http`, `async` ### Workflow -- Create a devbox +- Create a devbox with tunnel configuration - Start an HTTP server inside the devbox -- Enable a tunnel for external access - Make an HTTP request from the local machine through the tunnel - Validate the response - Shutdown the devbox diff --git a/examples/devbox_tunnel.py b/examples/devbox_tunnel.py index 479fa9957..3bb5d9bb0 100644 --- a/examples/devbox_tunnel.py +++ b/examples/devbox_tunnel.py @@ -3,11 +3,10 @@ --- title: Devbox Tunnel (HTTP Server Access) slug: devbox-tunnel -use_case: Create a devbox, start an HTTP server, enable a tunnel, and access the server from the local machine through the tunnel. Uses the async SDK. +use_case: Create a devbox with a tunnel, start an HTTP server, and access the server from the local machine through the tunnel. Uses the async SDK. workflow: - - Create a devbox + - Create a devbox with tunnel configuration - Start an HTTP server inside the devbox - - Enable a tunnel for external access - Make an HTTP request from the local machine through the tunnel - Validate the response - Shutdown the devbox @@ -40,7 +39,7 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: - """Create a devbox, start an HTTP server, enable a tunnel, and access it from the local machine.""" + """Create a devbox with a tunnel, start an HTTP server, and access it from the local machine.""" cleanup = ctx.cleanup sdk = AsyncRunloopSDK() @@ -50,6 +49,7 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: launch_parameters={ "resource_size_request": "X_SMALL", }, + tunnel={"auth_mode": "open"}, ) cleanup.add(f"devbox:{devbox.id}", devbox.shutdown) @@ -60,15 +60,17 @@ async def recipe(ctx: RecipeContext) -> RecipeOutput: # Give the server a moment to start await asyncio.sleep(SERVER_STARTUP_DELAY_S) - # Enable a tunnel to expose the HTTP server - # For authenticated tunnels, use auth_mode="authenticated" and include the auth_token + # The tunnel was created with the devbox. For authenticated tunnels, set + # tunnel={"auth_mode": "authenticated"} on create and include the auth_token # in your requests via the Authorization header: `Authorization: Bearer {tunnel.auth_token}` - tunnel = await devbox.net.enable_tunnel(auth_mode="open") + tunnel = await devbox.get_tunnel() + if tunnel is None: + raise RuntimeError("Failed to create tunnel at devbox launch time") # Get the tunnel URL for the server port tunnel_url = await devbox.get_tunnel_url(HTTP_SERVER_PORT) if tunnel_url is None: - raise RuntimeError("Failed to get tunnel URL after enabling tunnel") + raise RuntimeError("Failed to get tunnel URL after creating tunnel") # Make an HTTP request from the LOCAL MACHINE through the tunnel to the devbox # This demonstrates that the tunnel allows external access to the devbox service