From 1535e8142f8341e8205210e81b8902c82dea0b05 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 16:09:58 -0700 Subject: [PATCH 1/6] add mounts example --- EXAMPLES.md | 33 +++++ README.md | 2 + examples/devbox-mounts.ts | 247 ++++++++++++++++++++++++++++++++++++++ examples/registry.ts | 8 ++ llms.txt | 1 + 5 files changed, 291 insertions(+) create mode 100644 examples/devbox-mounts.ts diff --git a/EXAMPLES.md b/EXAMPLES.md index 142a14c0d..4cd06dd83 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 Mounts (Agent, Code, Object)](#devbox-mounts) - [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel) - [MCP Hub + Claude Code + GitHub](#mcp-github-tools) - [Secrets with Devbox and Agent Gateway](#secrets-with-devbox) @@ -74,6 +75,38 @@ yarn test:examples **Source:** [`examples/devbox-from-blueprint-lifecycle.ts`](./examples/devbox-from-blueprint-lifecycle.ts) + +## Devbox Mounts (Agent, Code, Object) + +**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. + +**Tags:** `devbox`, `mounts`, `agent`, `code`, `object`, `claude-code`, `agent-gateway`, `ttl` + +### Workflow +- Create or reuse a Claude Code agent by name +- Create a secret for Anthropic and route it through agent gateway +- Upload a temporary directory as a storage object with a TTL +- Launch a devbox with agent, code, and object mounts together +- Run Claude Code on Opus 4.5 through the Anthropic gateway +- Verify the rl-cli repo and extracted object files are present on the devbox +- Shutdown the devbox and delete the temporary secret and object + +### Prerequisites +- `RUNLOOP_API_KEY` +- `ANTHROPIC_API_KEY` + +### Run +```sh +ANTHROPIC_API_KEY=sk-ant-xxx yarn tsn -T examples/devbox-mounts.ts +``` + +### Test +```sh +yarn test:examples +``` + +**Source:** [`examples/devbox-mounts.ts`](./examples/devbox-mounts.ts) + ## Devbox Tunnel (HTTP Server Access) diff --git a/README.md b/README.md index 767b912b5..e82d111a2 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ yarn generate:examples-md Use direct `secrets` injection when code inside the devbox legitimately needs the secret value. For third-party API credentials that should stay off the devbox, prefer wiring a Runloop secret through agent gateway instead. See [`examples/secrets-with-devbox.ts`](./examples/secrets-with-devbox.ts) for both patterns side by side using `devbox.create({ secrets: ... })` and `devbox.create({ gateways: ... })`. +For mount patterns, see [`examples/devbox-mounts.ts`](./examples/devbox-mounts.ts). It shows when to use `agent_mount` for reusable agents like Claude Code, `code_mount` for Git repositories such as [`runloopai/rl-cli`](https://github.com/runloopai/rl-cli.git), and `object_mount` for blobs that should appear on the devbox at startup. The example also demonstrates agent gateway wiring for Anthropic credentials plus object TTL, `.tgz` compression, and extraction-on-mount behavior. + ## Agent Guidance Detailed agent-specific instructions live in [`llms.txt`](./llms.txt). Consolidated recipes for frequent tasks are in [`EXAMPLES.md`](./EXAMPLES.md). diff --git a/examples/devbox-mounts.ts b/examples/devbox-mounts.ts new file mode 100644 index 000000000..93391f485 --- /dev/null +++ b/examples/devbox-mounts.ts @@ -0,0 +1,247 @@ +#!/usr/bin/env -S npm run tsn -T + +/** +--- +title: Devbox Mounts (Agent, Code, Object) +slug: devbox-mounts +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. +workflow: + - Create or reuse an agent by name + - Create a secret for an agent and route it through agent gateway + - Upload a temporary directory as a storage object with a TTL + - Launch a devbox with agent, code, and object mounts together + - Run Claude Code on Opus 4.5 through the Anthropic gateway + - Verify the rl-cli repo and extracted object files are present on the devbox + - Shutdown the devbox and delete the temporary secret and object +tags: + - devbox + - mounts + - agent + - code + - object + - claude-code + - agent-gateway + - ttl +prerequisites: + - RUNLOOP_API_KEY + - ANTHROPIC_API_KEY +run: ANTHROPIC_API_KEY=sk-ant-xxx yarn tsn -T examples/devbox-mounts.ts +test: yarn test:examples +--- +*/ + +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; +import { RunloopSDK } from '@runloop/api-client'; +import { wrapRecipe, runAsCli } from './_harness'; +import type { RecipeContext, RecipeOutput } from './types'; + +const CLAUDE_CODE_AGENT_NAME = 'example-claude-code-agent'; +const CLAUDE_CODE_AGENT_VERSION = '1.0.0'; +const CLAUDE_CODE_PACKAGE = '@anthropic-ai/claude-code'; +const CLAUDE_MODEL = 'claude-opus-4-5'; +const OBJECT_TTL_MS = 60 * 60 * 1000; +const OBJECT_MOUNT_DIR = '/home/user/bootstrap-assets'; +const COPIED_EXAMPLE_FILE_NAME = 'devbox-mounts-source.ts'; +const GATEWAY_ENV_PREFIX = 'ANTHROPIC'; + +function uniqueName(prefix: string): string { + return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; +} + +// example setup: get or create an agent so we can mount it +async function ensureClaudeCodeAgent(sdk: RunloopSDK): Promise<{ agentId: string; reused: boolean }> { + const existingAgentsList = await sdk.agent.list({ name: CLAUDE_CODE_AGENT_NAME, limit: 20 }); + const existingAgentDetails = await Promise.all(existingAgentsList.map((candidate) => candidate.getInfo())); + + const matchingAgent = existingAgentDetails + .filter( + (info) => + info.name === CLAUDE_CODE_AGENT_NAME && + info.version === CLAUDE_CODE_AGENT_VERSION && + info.source?.type === 'npm' && + info.source?.npm?.package_name === CLAUDE_CODE_PACKAGE, + ) + .sort((left, right) => right.create_time_ms - left.create_time_ms)[0]; + + if (matchingAgent) { + return { agentId: matchingAgent.id, reused: true }; + } + + const createdAgent = await sdk.agent.createFromNpm({ + name: CLAUDE_CODE_AGENT_NAME, + version: CLAUDE_CODE_AGENT_VERSION, + package_name: CLAUDE_CODE_PACKAGE, + }); + + return { agentId: createdAgent.id, reused: false }; +} + +export async function recipe(ctx: RecipeContext): Promise { + const { cleanup } = ctx; + const anthropicApiKey = process.env['ANTHROPIC_API_KEY']; + + if (!anthropicApiKey) { + throw new Error('Set ANTHROPIC_API_KEY to run the Claude Code mount example.'); + } + + const sdk = new RunloopSDK({ + bearerToken: process.env['RUNLOOP_API_KEY'], + }); + + const resourcesCreated: string[] = []; + + const { agentId, reused } = await ensureClaudeCodeAgent(sdk); + resourcesCreated.push(reused ? `agent:${agentId}:reused` : `agent:${agentId}`); + + // best practice: create a secret for the agent's credentials and use agent gateway to route it through + // so that credentials are not exposed to the agent. + const anthropicSecret = await sdk.secret.create({ + name: uniqueName('anthropic-mount-example'), + value: anthropicApiKey, + }); + resourcesCreated.push(`secret:${anthropicSecret.name}`); + cleanup.add(`secret:${anthropicSecret.name}`, () => anthropicSecret.delete()); + + // now create some example files to mount onto the devbox via object mount. + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'runloop-mounts-')); + cleanup.add(`tempDir:${tempDir}`, () => fs.rm(tempDir, { recursive: true, force: true })); + + await fs.copyFile(__filename, path.join(tempDir, COPIED_EXAMPLE_FILE_NAME)); + await fs.writeFile( + path.join(tempDir, 'README.txt'), + 'This directory was uploaded with uploadFromDir(), stored as a tgz object, and extracted onto the devbox via object_mount.\n', + ); + + // Object mounts are a good fit for blobs or archives that should simply appear on the devbox at startup. + // uploadFromDir() compresses the directory as a .tgz, and mounting that archive to a directory path extracts it. + const archive = await sdk.storageObject.uploadFromDir(tempDir, { + name: uniqueName('mount-bootstrap'), + ttl_ms: OBJECT_TTL_MS, // set a TTL so the object is deleted after a certain time. + metadata: { example: 'devbox-mounts' }, + }); + resourcesCreated.push(`storageObject:${archive.id}`); + cleanup.add(`storageObject:${archive.id}`, () => archive.delete()); + + const archiveInfo = await archive.getInfo(); + + const devbox = await sdk.devbox.create({ + name: uniqueName('mounts-example-devbox'), + // Use agent mounts for reusable tools or agents that should be installed onto the devbox. + // For npm-based agents like Claude Code, mounting by name makes it easy to reuse the latest matching agent. + mounts: [ + { + type: 'agent_mount', + agent_id: null, + agent_name: CLAUDE_CODE_AGENT_NAME, + }, + // Use code mounts for Git projects that should be cloned onto the devbox. + { + type: 'code_mount', + repo_owner: 'runloopai', + repo_name: 'rl-cli', + }, + { + type: 'object_mount', + object_id: archive.id, + object_path: OBJECT_MOUNT_DIR, + }, + ], + // Route Anthropic access through agent gateway so Claude Code sees only a gateway token and URL. + gateways: { + [GATEWAY_ENV_PREFIX]: { + gateway: 'anthropic', + secret: anthropicSecret, + }, + }, + launch_parameters: { + resource_size_request: 'SMALL', + keep_alive_time_seconds: 60 * 5, + }, + }); + resourcesCreated.push(`devbox:${devbox.id}`); + cleanup.add(`devbox:${devbox.id}`, () => sdk.devbox.fromId(devbox.id).shutdown()); + + const devboxInfo = await devbox.getInfo(); + + const gatewayUrlResult = await devbox.cmd.exec(`echo $${GATEWAY_ENV_PREFIX}_URL`); + const gatewayUrl = (await gatewayUrlResult.stdout()).trim(); + + const gatewayTokenResult = await devbox.cmd.exec(`echo $${GATEWAY_ENV_PREFIX}`); + const gatewayToken = (await gatewayTokenResult.stdout()).trim(); + + const claudeVersionResult = await devbox.cmd.exec('claude --version'); + const claudeVersion = (await claudeVersionResult.stdout()).trim(); + + const claudePromptResult = await devbox.cmd.exec( + `ANTHROPIC_BASE_URL="$${GATEWAY_ENV_PREFIX}_URL" ANTHROPIC_API_KEY="$${GATEWAY_ENV_PREFIX}" claude --model ${CLAUDE_MODEL} -p "Reply with the exact text mounted-through-agent-gateway and nothing else." --dangerously-skip-permissions`, + ); + const claudeStdout = (await claudePromptResult.stdout()).trim(); + + const repoPathResult = await devbox.cmd.exec( + 'if [ -d /home/user/rl-cli ]; then printf /home/user/rl-cli; elif [ -d /home/user/rl-clis ]; then printf /home/user/rl-clis; else exit 1; fi', + ); + const repoMountPath = (await repoPathResult.stdout()).trim(); + const repoPackageJson = + + const mountedExamplePath = path.posix.join(OBJECT_MOUNT_DIR, COPIED_EXAMPLE_FILE_NAME); + const mountedExampleContents = await devbox.file.read({ path: mountedExamplePath }); + + return { + resourcesCreated, + checks: [ + { + name: 'Claude Code agent exists and is callable on the devbox', + passed: claudeVersionResult.exitCode === 0 && claudeVersion.length > 0, + details: claudeVersion || `exitCode=${claudeVersionResult.exitCode}`, + }, + { + name: 'Anthropic access is routed through agent gateway', + passed: + devboxInfo.gateway_specs?.[GATEWAY_ENV_PREFIX] !== undefined && + gatewayUrlResult.exitCode === 0 && + gatewayUrl.startsWith('http') && + gatewayTokenResult.exitCode === 0 && + gatewayToken.startsWith('gws_') && + gatewayToken !== anthropicApiKey, + details: `gateway_url=${gatewayUrl}, token_prefix=${gatewayToken.slice(0, 4) || 'missing'}`, + }, + { + name: 'Claude Code runs on Opus 4.5 through the gateway', + passed: claudePromptResult.exitCode === 0 && claudeStdout === 'mounted-through-agent-gateway', + details: claudeStdout || `exitCode=${claudePromptResult.exitCode}`, + }, + { + name: 'rl-cli repository is available through code mount', + passed: + repoPathResult.exitCode === 0 && + repoMountPath.length > 0 && + repoPackageJson.includes('"name": "@runloop/rl-cli"'), + details: repoMountPath || `exitCode=${repoPathResult.exitCode}`, + }, + { + name: 'object mount extracted the uploaded example file onto the devbox', + passed: + mountedExampleContents.includes('title: Devbox Mounts (Agent, Code, Object)') && + mountedExampleContents.startsWith('#!/usr/bin/env -S npm run tsn -T'), + details: mountedExamplePath, + }, + { + name: 'uploaded object shows TTL and compression details', + passed: + archiveInfo.content_type === 'tgz' && + archiveInfo.delete_after_time_ms !== null && + archiveInfo.delete_after_time_ms !== undefined && + archiveInfo.delete_after_time_ms > archiveInfo.create_time_ms, + details: `content_type=${archiveInfo.content_type}, delete_after_time_ms=${archiveInfo.delete_after_time_ms ?? 'missing'}`, + }, + ], + }; +} + +export const runDevboxMountsExample = wrapRecipe({ recipe }); + +if (require.main === module) { + void runAsCli(runDevboxMountsExample); +} diff --git a/examples/registry.ts b/examples/registry.ts index 4acc0ce1c..6a3e88d1e 100644 --- a/examples/registry.ts +++ b/examples/registry.ts @@ -5,6 +5,7 @@ import type { ExampleResult } from './types'; import { runBlueprintWithBuildContextExample } from './blueprint-with-build-context'; import { runDevboxFromBlueprintLifecycleExample } from './devbox-from-blueprint-lifecycle'; +import { runDevboxMountsExample } from './devbox-mounts'; import { runDevboxTunnelExample } from './devbox-tunnel'; import { runMcpGithubToolsExample } from './mcp-github-tools'; import { runSecretsWithDevboxExample } from './secrets-with-devbox'; @@ -32,6 +33,13 @@ export const exampleRegistry: ExampleRegistryEntry[] = [ requiredEnv: ['RUNLOOP_API_KEY'], run: runDevboxFromBlueprintLifecycleExample, }, + { + slug: 'devbox-mounts', + title: 'Devbox Mounts (Agent, Code, Object)', + fileName: 'devbox-mounts.ts', + requiredEnv: ['RUNLOOP_API_KEY', 'ANTHROPIC_API_KEY'], + run: runDevboxMountsExample, + }, { slug: 'devbox-tunnel', title: 'Devbox Tunnel (HTTP Server Access)', diff --git a/llms.txt b/llms.txt index 805fc2c03..31af41ec0 100644 --- a/llms.txt +++ b/llms.txt @@ -10,6 +10,7 @@ ## Core Patterns - [Devbox lifecycle example](examples/devbox-from-blueprint-lifecycle.ts): Create blueprint, launch devbox, run commands, cleanup +- [Devbox mounts example](examples/devbox-mounts.ts): Show `agent_mount`, `code_mount`, and `object_mount` together, including Claude Code via Anthropic agent gateway, the `runloopai/rl-cli` repo, and object TTL/compression/extraction behavior - [MCP GitHub example](examples/mcp-github-tools.ts): MCP Hub integration with Claude Code - [Secrets with Devbox example](examples/secrets-with-devbox.ts): Show direct secret injection for app data alongside agent gateway for upstream credentials that should stay off the devbox From a33812d759c2e99c173fd78aa045c06479c75ac7 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 16:15:18 -0700 Subject: [PATCH 2/6] restore accidentally eaten line --- examples/devbox-mounts.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/devbox-mounts.ts b/examples/devbox-mounts.ts index 93391f485..47110be5b 100644 --- a/examples/devbox-mounts.ts +++ b/examples/devbox-mounts.ts @@ -95,7 +95,7 @@ export async function recipe(ctx: RecipeContext): Promise { const { agentId, reused } = await ensureClaudeCodeAgent(sdk); resourcesCreated.push(reused ? `agent:${agentId}:reused` : `agent:${agentId}`); - // best practice: create a secret for the agent's credentials and use agent gateway to route it through + // best practice: create a secret for the agent's credentials and use agent gateway to route it through // so that credentials are not exposed to the agent. const anthropicSecret = await sdk.secret.create({ name: uniqueName('anthropic-mount-example'), @@ -118,7 +118,7 @@ export async function recipe(ctx: RecipeContext): Promise { // uploadFromDir() compresses the directory as a .tgz, and mounting that archive to a directory path extracts it. const archive = await sdk.storageObject.uploadFromDir(tempDir, { name: uniqueName('mount-bootstrap'), - ttl_ms: OBJECT_TTL_MS, // set a TTL so the object is deleted after a certain time. + ttl_ms: OBJECT_TTL_MS, // best practice: set a TTL so the object is deleted after a certain time. metadata: { example: 'devbox-mounts' }, }); resourcesCreated.push(`storageObject:${archive.id}`); @@ -184,9 +184,10 @@ export async function recipe(ctx: RecipeContext): Promise { ); const repoMountPath = (await repoPathResult.stdout()).trim(); const repoPackageJson = - + repoMountPath ? await devbox.file.read({ file_path: `${repoMountPath}/package.json` }) : ''; + const mountedExamplePath = path.posix.join(OBJECT_MOUNT_DIR, COPIED_EXAMPLE_FILE_NAME); - const mountedExampleContents = await devbox.file.read({ path: mountedExamplePath }); + const mountedExampleContents = await devbox.file.read({ file_path: mountedExamplePath }); return { resourcesCreated, From 4efbd996a7b6eac4bd7bc284f522bb313c5639ff Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 16:22:47 -0700 Subject: [PATCH 3/6] fix chomped code --- examples/devbox-mounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/devbox-mounts.ts b/examples/devbox-mounts.ts index 47110be5b..73bfb100c 100644 --- a/examples/devbox-mounts.ts +++ b/examples/devbox-mounts.ts @@ -10,7 +10,7 @@ workflow: - Create a secret for an agent and route it through agent gateway - Upload a temporary directory as a storage object with a TTL - Launch a devbox with agent, code, and object mounts together - - Run Claude Code on Opus 4.5 through the Anthropic gateway + - Run Claude Code on Opus 4.5 through the Anthropic agent gateway - Verify the rl-cli repo and extracted object files are present on the devbox - Shutdown the devbox and delete the temporary secret and object tags: From b8c311cca201c73f1ef522bf4bbc0dbc9a6775d7 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 16:23:39 -0700 Subject: [PATCH 4/6] updated examples --- EXAMPLES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 4cd06dd83..4ff68d616 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -83,11 +83,11 @@ yarn test:examples **Tags:** `devbox`, `mounts`, `agent`, `code`, `object`, `claude-code`, `agent-gateway`, `ttl` ### Workflow -- Create or reuse a Claude Code agent by name -- Create a secret for Anthropic and route it through agent gateway +- Create or reuse an agent by name +- Create a secret for an agent and route it through agent gateway - Upload a temporary directory as a storage object with a TTL - Launch a devbox with agent, code, and object mounts together -- Run Claude Code on Opus 4.5 through the Anthropic gateway +- Run Claude Code on Opus 4.5 through the Anthropic agent gateway - Verify the rl-cli repo and extracted object files are present on the devbox - Shutdown the devbox and delete the temporary secret and object From 20088b79cf2aa45498442ff49292c2ba99401681 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 16:28:42 -0700 Subject: [PATCH 5/6] caught a mistake in a different example --- src/sdk/devbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index b09776e90..3f7142d5c 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -429,7 +429,7 @@ export class DevboxFileOps { * * @example * ```typescript - * const content = await devbox.file.read({ path: '/app/config.json' }); + * const content = await devbox.file.read({ file_path: '/app/config.json' }); * const config = JSON.parse(content); * ``` * From 08a7e8d165877f653d5a62d4875cbe2168f48ee0 Mon Sep 17 00:00:00 2001 From: James Chainey Date: Mon, 23 Mar 2026 16:30:47 -0700 Subject: [PATCH 6/6] lint --- src/sdk/devbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts index 3f7142d5c..eee19c5eb 100644 --- a/src/sdk/devbox.ts +++ b/src/sdk/devbox.ts @@ -429,7 +429,7 @@ export class DevboxFileOps { * * @example * ```typescript - * const content = await devbox.file.read({ file_path: '/app/config.json' }); + * const content = await devbox.file.read({ file_path: '/app/config.json' }); * const config = JSON.parse(content); * ``` *