Skip to content

Commit 3125c49

Browse files
committed
add mount examples
1 parent 97bcd70 commit 3125c49

5 files changed

Lines changed: 328 additions & 0 deletions

File tree

EXAMPLES.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Runnable examples live in [`examples/`](./examples).
99

1010
- [Blueprint with Build Context](#blueprint-with-build-context)
1111
- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
12+
- [Devbox Mounts (Agent, Code, Object)](#devbox-mounts)
1213
- [Devbox Snapshot and Resume](#devbox-snapshot-resume)
1314
- [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel)
1415
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
@@ -75,6 +76,39 @@ uv run pytest -m smoketest tests/smoketests/examples/
7576

7677
**Source:** [`examples/devbox_from_blueprint_lifecycle.py`](./examples/devbox_from_blueprint_lifecycle.py)
7778

79+
<a id="devbox-mounts"></a>
80+
## Devbox Mounts (Agent, Code, Object)
81+
82+
**Use case:** Launch a devbox that combines an agent mount for Claude Code, a code mount for the Runloop CLI repo, and an object mount for startup files while routing Anthropic credentials through agent gateway.
83+
84+
**Tags:** `devbox`, `mounts`, `agent`, `code`, `object`, `claude-code`, `agent-gateway`, `ttl`, `async`
85+
86+
### Workflow
87+
- Create or reuse a Claude Code agent by name
88+
- Store ANTHROPIC_API_KEY as a Runloop secret for gateway-backed access
89+
- Upload a temporary bootstrap directory as a storage object with a TTL
90+
- Launch a devbox with agent, code, and object mounts together
91+
- Verify the gateway token and URL are present instead of the raw Anthropic key
92+
- Run Claude Code through the Anthropic agent gateway
93+
- Verify the code mount and extracted object mount contents are present
94+
- Shutdown the devbox and delete the temporary secret and storage object
95+
96+
### Prerequisites
97+
- `RUNLOOP_API_KEY`
98+
- `ANTHROPIC_API_KEY`
99+
100+
### Run
101+
```sh
102+
ANTHROPIC_API_KEY=sk-ant-xxx uv run python -m examples.devbox_mounts
103+
```
104+
105+
### Test
106+
```sh
107+
uv run pytest -m smoketest tests/smoketests/examples/
108+
```
109+
110+
**Source:** [`examples/devbox_mounts.py`](./examples/devbox_mounts.py)
111+
78112
<a id="devbox-snapshot-resume"></a>
79113
## Devbox Snapshot and Resume
80114

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Functionality between the synchronous and asynchronous clients is otherwise iden
9191
## Examples
9292

9393
Workflow-oriented runnable examples are documented in [`EXAMPLES.md`](./EXAMPLES.md).
94+
For a mounts-focused workflow that combines agent, code, and object mounts with agent-gateway credential protection, see [`examples/devbox_mounts.py`](./examples/devbox_mounts.py).
9495

9596
`EXAMPLES.md` is generated from metadata in `examples/*.py` and should not be edited manually.
9697
Regenerate it with:

examples/devbox_mounts.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
#!/usr/bin/env -S uv run python
2+
"""
3+
---
4+
title: Devbox Mounts (Agent, Code, Object)
5+
slug: devbox-mounts
6+
use_case: Launch a devbox that combines an agent mount for Claude Code, a code mount for the Runloop CLI repo, and an object mount for startup files while routing Anthropic credentials through agent gateway.
7+
workflow:
8+
- Create or reuse a Claude Code agent by name
9+
- Store ANTHROPIC_API_KEY as a Runloop secret for gateway-backed access
10+
- Upload a temporary bootstrap directory as a storage object with a TTL
11+
- Launch a devbox with agent, code, and object mounts together
12+
- Verify the gateway token and URL are present instead of the raw Anthropic key
13+
- Run Claude Code through the Anthropic agent gateway
14+
- Verify the code mount and extracted object mount contents are present
15+
- Shutdown the devbox and delete the temporary secret and storage object
16+
tags:
17+
- devbox
18+
- mounts
19+
- agent
20+
- code
21+
- object
22+
- claude-code
23+
- agent-gateway
24+
- ttl
25+
- async
26+
prerequisites:
27+
- RUNLOOP_API_KEY
28+
- ANTHROPIC_API_KEY
29+
run: ANTHROPIC_API_KEY=sk-ant-xxx uv run python -m examples.devbox_mounts
30+
test: uv run pytest -m smoketest tests/smoketests/examples/
31+
---
32+
"""
33+
34+
from __future__ import annotations
35+
36+
import os
37+
import shlex
38+
import shutil
39+
import asyncio
40+
import tempfile
41+
from pathlib import Path
42+
from datetime import timedelta
43+
44+
from runloop_api_client import AsyncRunloopSDK
45+
from runloop_api_client.sdk.async_storage_object import AsyncStorageObject
46+
47+
from ._harness import run_as_cli, unique_name, wrap_recipe
48+
from .example_types import ExampleCheck, RecipeOutput, RecipeContext
49+
50+
CLAUDE_CODE_AGENT_NAME = "example-claude-code-agent"
51+
CLAUDE_CODE_AGENT_VERSION = "1.0.0"
52+
CLAUDE_CODE_PACKAGE = "@anthropic-ai/claude-code"
53+
CLAUDE_MODEL = "claude-opus-4-5"
54+
GATEWAY_ENV_PREFIX = "ANTHROPIC"
55+
OBJECT_MOUNT_DIR = "/home/user/bootstrap-assets"
56+
COPIED_EXAMPLE_FILE_NAME = "devbox-mounts-source.py"
57+
OBJECT_TTL = timedelta(hours=1)
58+
CLAUDE_PROMPT = "Reply with the exact text mounted-through-agent-gateway and nothing else."
59+
60+
61+
async def ensure_claude_code_agent(sdk: AsyncRunloopSDK) -> tuple[str, bool]:
62+
"""Return a reusable Claude Code agent, creating it if needed."""
63+
existing_agents = await sdk.agent.list(name=CLAUDE_CODE_AGENT_NAME, limit=20)
64+
existing_infos = await asyncio.gather(*(agent.get_info() for agent in existing_agents))
65+
66+
matching_agents = sorted(
67+
(
68+
info
69+
for info in existing_infos
70+
if info.name == CLAUDE_CODE_AGENT_NAME
71+
and info.version == CLAUDE_CODE_AGENT_VERSION
72+
and info.source is not None
73+
and info.source.type == "npm"
74+
and info.source.npm is not None
75+
and info.source.npm.package_name == CLAUDE_CODE_PACKAGE
76+
),
77+
key=lambda info: info.create_time_ms,
78+
reverse=True,
79+
)
80+
if matching_agents:
81+
return matching_agents[0].id, True
82+
83+
agent = await sdk.agent.create_from_npm(
84+
name=CLAUDE_CODE_AGENT_NAME,
85+
version=CLAUDE_CODE_AGENT_VERSION,
86+
package_name=CLAUDE_CODE_PACKAGE,
87+
)
88+
return agent.id, False
89+
90+
91+
def create_bootstrap_dir() -> tuple[Path, Path]:
92+
"""Create local files that will be uploaded and extracted via object mount."""
93+
temp_dir = Path(tempfile.mkdtemp(prefix="runloop-devbox-mounts-"))
94+
copied_example_path = temp_dir / COPIED_EXAMPLE_FILE_NAME
95+
copied_example_path.write_text(Path(__file__).read_text())
96+
(temp_dir / "README.txt").write_text(
97+
"This directory was uploaded with upload_from_dir(), stored as a tgz object, "
98+
"and extracted onto the devbox via an object mount.\n"
99+
)
100+
return temp_dir, copied_example_path
101+
102+
103+
async def discover_code_mount_path(devbox) -> str:
104+
"""Find the repository path created by the code mount."""
105+
result = await devbox.cmd.exec(
106+
"if [ -d /home/user/rl-cli ]; then printf /home/user/rl-cli; "
107+
"elif [ -d /home/user/rl-clis ]; then printf /home/user/rl-clis; "
108+
"else exit 1; fi"
109+
)
110+
return (await result.stdout()).strip() if result.exit_code == 0 else ""
111+
112+
113+
def build_claude_gateway_command() -> str:
114+
"""Build a shell-safe Claude invocation that uses gateway-provided credentials."""
115+
return (
116+
f'ANTHROPIC_BASE_URL="${GATEWAY_ENV_PREFIX}_URL" '
117+
f'ANTHROPIC_API_KEY="${GATEWAY_ENV_PREFIX}" '
118+
f"claude --model {shlex.quote(CLAUDE_MODEL)} "
119+
f"-p {shlex.quote(CLAUDE_PROMPT)} "
120+
"--dangerously-skip-permissions"
121+
)
122+
123+
124+
async def recipe(ctx: RecipeContext) -> RecipeOutput:
125+
"""Create a devbox with agent, code, and object mounts plus agent gateway."""
126+
cleanup = ctx.cleanup
127+
anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY")
128+
if not anthropic_api_key:
129+
raise RuntimeError("Set ANTHROPIC_API_KEY to run the Claude Code mount example.")
130+
131+
sdk = AsyncRunloopSDK()
132+
resources_created: list[str] = []
133+
134+
agent_id, reused_agent = await ensure_claude_code_agent(sdk)
135+
resources_created.append(f"agent:{agent_id}:reused" if reused_agent else f"agent:{agent_id}")
136+
137+
secret = await sdk.secret.create(
138+
name=unique_name("example-anthropic-gateway").upper().replace("-", "_"),
139+
value=anthropic_api_key,
140+
)
141+
resources_created.append(f"secret:{secret.name}")
142+
cleanup.add(f"secret:{secret.name}", secret.delete)
143+
144+
bootstrap_dir, copied_example_path = create_bootstrap_dir()
145+
cleanup.add(f"temp_dir:{bootstrap_dir}", lambda: shutil.rmtree(bootstrap_dir, ignore_errors=True))
146+
147+
archive: AsyncStorageObject = await sdk.storage_object.upload_from_dir(
148+
bootstrap_dir,
149+
name=unique_name("example-devbox-mounts"),
150+
ttl=OBJECT_TTL,
151+
metadata={"example": "devbox-mounts"},
152+
)
153+
resources_created.append(f"storage_object:{archive.id}")
154+
cleanup.add(f"storage_object:{archive.id}", archive.delete)
155+
156+
devbox = await sdk.devbox.create(
157+
name=unique_name("devbox-mounts-example"),
158+
launch_parameters={
159+
"resource_size_request": "SMALL",
160+
"keep_alive_time_seconds": 60 * 5,
161+
},
162+
mounts=[
163+
{
164+
"type": "agent_mount",
165+
"agent_id": None,
166+
"agent_name": CLAUDE_CODE_AGENT_NAME,
167+
},
168+
{
169+
"type": "code_mount",
170+
"repo_owner": "runloopai",
171+
"repo_name": "rl-cli",
172+
},
173+
{
174+
"type": "object_mount",
175+
"object_id": archive.id,
176+
"object_path": OBJECT_MOUNT_DIR,
177+
},
178+
],
179+
gateways={
180+
GATEWAY_ENV_PREFIX: {
181+
"gateway": "anthropic",
182+
"secret": secret.name,
183+
}
184+
},
185+
)
186+
resources_created.append(f"devbox:{devbox.id}")
187+
cleanup.add(f"devbox:{devbox.id}", devbox.shutdown)
188+
189+
devbox_info = await devbox.get_info()
190+
archive_info = await archive.refresh()
191+
192+
gateway_url_result = await devbox.cmd.exec(f"echo ${GATEWAY_ENV_PREFIX}_URL")
193+
gateway_url = (await gateway_url_result.stdout()).strip()
194+
195+
gateway_token_result = await devbox.cmd.exec(f"echo ${GATEWAY_ENV_PREFIX}")
196+
gateway_token = (await gateway_token_result.stdout()).strip()
197+
198+
claude_version_result = await devbox.cmd.exec("claude --version")
199+
claude_version = (await claude_version_result.stdout()).strip()
200+
201+
claude_gateway_command = build_claude_gateway_command()
202+
# This is the command you would run to send Claude traffic through the agent gateway.
203+
# We intentionally do not execute it here so repeated example test runs do not incur model costs.
204+
# claude_prompt_result = await devbox.cmd.exec(claude_gateway_command)
205+
206+
repo_mount_path = await discover_code_mount_path(devbox)
207+
repo_package_json = (
208+
await devbox.file.read(file_path=f"{repo_mount_path}/package.json") if repo_mount_path else ""
209+
)
210+
211+
mounted_example_path = f"{OBJECT_MOUNT_DIR}/{COPIED_EXAMPLE_FILE_NAME}"
212+
mounted_example_contents = await devbox.file.read(file_path=mounted_example_path)
213+
mounted_readme_path = f"{OBJECT_MOUNT_DIR}/README.txt"
214+
mounted_readme_contents = await devbox.file.read(file_path=mounted_readme_path)
215+
216+
return RecipeOutput(
217+
resources_created=resources_created,
218+
checks=[
219+
ExampleCheck(
220+
name="Claude Code agent exists and is callable on the devbox",
221+
passed=claude_version_result.exit_code == 0 and bool(claude_version),
222+
details=claude_version or f"exit_code={claude_version_result.exit_code}",
223+
),
224+
ExampleCheck(
225+
name="Anthropic access is routed through agent gateway",
226+
passed=(
227+
devbox_info.gateway_specs is not None
228+
and devbox_info.gateway_specs.get(GATEWAY_ENV_PREFIX) is not None
229+
and gateway_url_result.exit_code == 0
230+
and gateway_url.startswith("http")
231+
and gateway_token_result.exit_code == 0
232+
and gateway_token.startswith("gws_")
233+
and gateway_token != anthropic_api_key
234+
),
235+
details=f"gateway_url={gateway_url}, token_prefix={gateway_token[:4] or 'missing'}",
236+
),
237+
ExampleCheck(
238+
name="Claude Code gateway invocation is documented without executing it",
239+
passed=(
240+
"ANTHROPIC_BASE_URL" in claude_gateway_command
241+
and "ANTHROPIC_API_KEY" in claude_gateway_command
242+
and "claude --model" in claude_gateway_command
243+
),
244+
details=claude_gateway_command,
245+
),
246+
ExampleCheck(
247+
name="rl-cli repository is available through the code mount",
248+
passed=bool(repo_mount_path) and '"name": "@runloop/rl-cli"' in repo_package_json,
249+
details=repo_mount_path or "repo mount not found",
250+
),
251+
ExampleCheck(
252+
name="object mount extracted the uploaded example file onto the devbox",
253+
passed=(
254+
'title: Devbox Mounts (Agent, Code, Object)' in mounted_example_contents
255+
and mounted_example_contents.startswith("#!/usr/bin/env -S uv run python")
256+
),
257+
details=mounted_example_path,
258+
),
259+
ExampleCheck(
260+
name="uploaded object shows TTL and compression details",
261+
passed=(
262+
archive_info.content_type == "tgz"
263+
and archive_info.delete_after_time_ms is not None
264+
and archive_info.delete_after_time_ms > archive_info.create_time_ms
265+
),
266+
details=(
267+
f"content_type={archive_info.content_type}, "
268+
f"delete_after_time_ms={archive_info.delete_after_time_ms}"
269+
),
270+
),
271+
ExampleCheck(
272+
name="object mount preserved the bootstrap README content",
273+
passed="uploaded with upload_from_dir()" in mounted_readme_contents,
274+
details=mounted_readme_path,
275+
),
276+
],
277+
)
278+
279+
280+
run_devbox_mounts_example = wrap_recipe(recipe)
281+
282+
283+
if __name__ == "__main__":
284+
run_as_cli(run_devbox_mounts_example)

examples/registry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from typing import Any, Callable, cast
99

10+
from .devbox_mounts import run_devbox_mounts_example
1011
from .devbox_tunnel import run_devbox_tunnel_example
1112
from .example_types import ExampleResult
1213
from .mcp_github_tools import run_mcp_github_tools_example
@@ -32,6 +33,13 @@
3233
"required_env": ["RUNLOOP_API_KEY"],
3334
"run": run_devbox_from_blueprint_lifecycle_example,
3435
},
36+
{
37+
"slug": "devbox-mounts",
38+
"title": "Devbox Mounts (Agent, Code, Object)",
39+
"file_name": "devbox_mounts.py",
40+
"required_env": ["RUNLOOP_API_KEY", "ANTHROPIC_API_KEY"],
41+
"run": run_devbox_mounts_example,
42+
},
3543
{
3644
"slug": "devbox-snapshot-resume",
3745
"title": "Devbox Snapshot and Resume",

llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
## Core Patterns
1212

1313
- [Devbox lifecycle example](examples/devbox_from_blueprint_lifecycle.py): Create blueprint, launch devbox, run commands, cleanup
14+
- [Devbox mounts example](examples/devbox_mounts.py): Combine agent, code, and object mounts while routing Anthropic access through agent gateway
1415
- [Devbox snapshot and resume example](examples/devbox_snapshot_resume.py): Snapshot disk, resume from snapshot, verify state isolation
1516
- [MCP GitHub example](examples/mcp_github_tools.py): MCP Hub integration with Claude Code
1617
- [Secrets with Devbox example](examples/secrets_with_devbox.py): Create secret, inject into devbox, verify, cleanup

0 commit comments

Comments
 (0)