Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Runnable examples live in [`examples/`](./examples).
- [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)
- [Secrets with Devbox and Agent Gateway](#secrets-with-devbox)

<a id="blueprint-with-build-context"></a>
## Blueprint with Build Context
Expand Down Expand Up @@ -168,20 +168,19 @@ uv run pytest -m smoketest tests/smoketests/examples/
**Source:** [`examples/mcp_github_tools.py`](./examples/mcp_github_tools.py)

<a id="secrets-with-devbox"></a>
## Secrets with Devbox (Create, Inject, Verify, Delete)
## Secrets with Devbox and Agent Gateway

**Use case:** Create a secret, inject it into a devbox as an environment variable, verify access, and clean up.
**Use case:** Use a normal secret for sensitive app data in the devbox and agent gateway for upstream API credentials that should never be exposed to the agent.

**Tags:** `secrets`, `devbox`, `environment-variables`, `cleanup`
**Tags:** `secrets`, `devbox`, `agent-gateway`, `credentials`, `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
- Verify the value matches
- Update the secret and verify
- List secrets and verify the secret appears
- Shutdown devbox and delete secret
- Create a secret for application data that should be available inside the devbox
- Create a separate secret for an upstream API credential
- Create an agent gateway config for an upstream API
- Launch a devbox with one secret injected directly and the credential wired through agent gateway
- Verify the devbox can read MAGIC_NUMBER while the upstream API credential is replaced with gateway values
- Shutdown the devbox and delete the gateway config and both secrets

### Prerequisites
- `RUNLOOP_API_KEY`
Expand Down
2 changes: 1 addition & 1 deletion examples/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
},
{
"slug": "secrets-with-devbox",
"title": "Secrets with Devbox (Create, Inject, Verify, Delete)",
"title": "Secrets with Devbox and Agent Gateway",
"file_name": "secrets_with_devbox.py",
"required_env": ["RUNLOOP_API_KEY"],
"run": run_secrets_with_devbox_example,
Expand Down
143 changes: 107 additions & 36 deletions examples/secrets_with_devbox.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
#!/usr/bin/env -S uv run python
"""
---
title: Secrets with Devbox (Create, Inject, Verify, Delete)
title: Secrets with Devbox and Agent Gateway
slug: secrets-with-devbox
use_case: Create a secret, inject it into a devbox as an environment variable, verify access, and clean up.
use_case: Use a normal secret for sensitive app data in the devbox and agent gateway for upstream API credentials that should never be exposed to the agent.
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
- Verify the value matches
- Update the secret and verify
- List secrets and verify the secret appears
- Shutdown devbox and delete secret
- Create a secret for application data that should be available inside the devbox
- Create a separate secret for an upstream API credential
- Create an agent gateway config for an upstream API
- Launch a devbox with one secret injected directly and the credential wired through agent gateway
- Verify the devbox can read MAGIC_NUMBER while the upstream API credential is replaced with gateway values
- Shutdown the devbox and delete the gateway config and both secrets
tags:
- secrets
- devbox
- agent-gateway
- credentials
- environment-variables
- cleanup
prerequisites:
Expand All @@ -33,37 +34,85 @@

# Note: do NOT hardcode secret values in your code!
# This is example code only; use environment variables instead!
_EXAMPLE_SECRET_VALUE = "my-secret-value"
_UPDATED_SECRET_VALUE = "updated-secret-value"
_EXAMPLE_GATEWAY_ENDPOINT = "https://api.example.com"
_UPSTREAM_CREDENTIAL_VALUE = "example-upstream-api-key"
_MAGIC_NUMBER_VALUE = "42"


def recipe(ctx: RecipeContext) -> RecipeOutput:
"""Create a secret, inject it into a devbox, and verify it is accessible."""
"""Demonstrate direct secret injection for app data and agent gateway protection for upstream credentials."""
cleanup = ctx.cleanup

sdk = RunloopSDK()
resources_created: list[str] = []
checks: list[ExampleCheck] = []

secret_name = unique_name("RUNLOOP_SDK_EXAMPLE").upper().replace("-", "_")
magic_number_name = unique_name("magic-number-secret")
upstream_credential_name = unique_name("agent-gateway-secret")

secret = sdk.secret.create(name=secret_name, value=_EXAMPLE_SECRET_VALUE)
resources_created.append(f"secret:{secret_name}")
cleanup.add(f"secret:{secret_name}", lambda: secret.delete())
magic_number_secret = sdk.secret.create(name=magic_number_name, value=_MAGIC_NUMBER_VALUE)
resources_created.append(f"secret:{magic_number_name}")
cleanup.add(f"secret:{magic_number_name}", magic_number_secret.delete)

secret_info = secret.get_info()
magic_number_info = magic_number_secret.get_info()
checks.append(
ExampleCheck(
name="secret created successfully",
passed=secret.name == secret_name and secret_info.id.startswith("sec_"),
details=f"name={secret.name}, id={secret_info.id}",
name="magic number secret created successfully",
passed=(magic_number_secret.name == magic_number_name and magic_number_info.id.startswith("sec_")),
details=f"name={magic_number_secret.name}, id={magic_number_info.id}",
)
)

upstream_credential_secret = sdk.secret.create(
name=upstream_credential_name,
value=_UPSTREAM_CREDENTIAL_VALUE,
)
resources_created.append(f"secret:{upstream_credential_name}")
cleanup.add(f"secret:{upstream_credential_name}", upstream_credential_secret.delete)

upstream_credential_info = upstream_credential_secret.get_info()
checks.append(
ExampleCheck(
name="upstream credential secret created successfully",
passed=(
upstream_credential_secret.name == upstream_credential_name
and upstream_credential_info.id.startswith("sec_")
),
details=(f"name={upstream_credential_secret.name}, id={upstream_credential_info.id}"),
)
)

# Use direct secret injection when code inside the devbox legitimately needs
# the secret value at runtime. Use agent gateway for upstream credentials
# that should never be exposed to the agent.
gateway_config = sdk.gateway_config.create(
name=unique_name("agent-gateway-config"),
endpoint=_EXAMPLE_GATEWAY_ENDPOINT,
auth_mechanism={"type": "bearer"},
description="Example gateway that keeps upstream credentials off the devbox",
)
resources_created.append(f"gateway_config:{gateway_config.id}")
cleanup.add(f"gateway_config:{gateway_config.id}", gateway_config.delete)

gateway_info = gateway_config.get_info()
checks.append(
ExampleCheck(
name="gateway config created successfully",
passed=(gateway_info.id.startswith("gwc_") and gateway_info.endpoint == _EXAMPLE_GATEWAY_ENDPOINT),
details=f"id={gateway_info.id}, endpoint={gateway_info.endpoint}",
)
)

devbox = sdk.devbox.create(
name=unique_name("secrets-example-devbox"),
name=unique_name("agent-gateway-devbox"),
secrets={
"MY_SECRET_ENV": secret.name,
"MAGIC_NUMBER": magic_number_secret.name,
},
gateways={
"MY_API": {
"gateway": gateway_config.id,
"secret": upstream_credential_secret.name,
}
},
launch_parameters={
"resource_size_request": "X_SMALL",
Expand All @@ -73,32 +122,54 @@ def recipe(ctx: RecipeContext) -> RecipeOutput:
resources_created.append(f"devbox:{devbox.id}")
cleanup.add(f"devbox:{devbox.id}", devbox.shutdown)

result = devbox.cmd.exec("echo $MY_SECRET_ENV")
stdout = result.stdout().strip()
devbox_info = devbox.get_info()
checks.append(
ExampleCheck(
name="devbox records gateway wiring",
passed=(
devbox_info.gateway_specs is not None
and devbox_info.gateway_specs.get("MY_API") is not None
and devbox_info.gateway_specs["MY_API"].gateway_config_id == gateway_config.id
),
details=(
f"gateway_config_id={devbox_info.gateway_specs['MY_API'].gateway_config_id}"
if devbox_info.gateway_specs is not None and devbox_info.gateway_specs.get("MY_API") is not None
else "gateway spec missing"
),
)
)

magic_number_result = devbox.cmd.exec("echo $MAGIC_NUMBER")
magic_number = magic_number_result.stdout().strip()
checks.append(
ExampleCheck(
name="devbox can read secret as env var",
passed=result.exit_code == 0 and stdout == _EXAMPLE_SECRET_VALUE,
details=f'exit_code={result.exit_code}, stdout="{stdout}"',
name="devbox receives plain secret when app needs the value",
passed=(magic_number_result.exit_code == 0 and magic_number == _MAGIC_NUMBER_VALUE),
details=(f"exit_code={magic_number_result.exit_code}, MAGIC_NUMBER={magic_number}"),
)
)

updated_info = sdk.secret.update(secret, _UPDATED_SECRET_VALUE).get_info()
url_result = devbox.cmd.exec("echo $MY_API_URL")
gateway_url = url_result.stdout().strip()
checks.append(
ExampleCheck(
name="secret updated successfully",
passed=updated_info.name == secret_name,
details=f"update_time_ms={updated_info.update_time_ms}",
name="devbox receives gateway URL",
passed=url_result.exit_code == 0 and gateway_url.startswith("http"),
details=f"exit_code={url_result.exit_code}, url={gateway_url}",
)
)

secrets = sdk.secret.list()
found = next((s for s in secrets if s.name == secret_name), None)
token_result = devbox.cmd.exec("echo $MY_API")
gateway_token = token_result.stdout().strip()
checks.append(
ExampleCheck(
name="secret appears in list",
passed=found is not None,
details=f"found name={found.name}" if found else "not found",
name="devbox receives gateway token instead of raw secret",
passed=(
token_result.exit_code == 0
and gateway_token.startswith("gws_")
and gateway_token != _UPSTREAM_CREDENTIAL_VALUE
),
details=(f"exit_code={token_result.exit_code}, token_prefix={gateway_token[:4] or 'missing'}"),
)
)

Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- [Devbox lifecycle example](examples/devbox_from_blueprint_lifecycle.py): Create blueprint, launch devbox, run commands, cleanup
- [Devbox snapshot and resume example](examples/devbox_snapshot_resume.py): Snapshot disk, resume from snapshot, verify state isolation
- [MCP GitHub example](examples/mcp_github_tools.py): MCP Hub integration with Claude Code
- [Secrets with Devbox example](examples/secrets_with_devbox.py): Create secret, inject into devbox, verify, cleanup
- [Secrets with Devbox example](examples/secrets_with_devbox.py): Inject a normal secret for app runtime use, protect upstream credentials with agent gateway, verify both behaviors, cleanup

## API Reference

Expand Down