diff --git a/EXAMPLES.md b/EXAMPLES.md index a036aa01d..b419b5eb5 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -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) ## Blueprint with Build Context @@ -168,20 +168,19 @@ 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) +## 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` diff --git a/examples/registry.py b/examples/registry.py index edc52f445..7362e2612 100644 --- a/examples/registry.py +++ b/examples/registry.py @@ -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, diff --git a/examples/secrets_with_devbox.py b/examples/secrets_with_devbox.py index c992c53e2..c0eaafcb9 100644 --- a/examples/secrets_with_devbox.py +++ b/examples/secrets_with_devbox.py @@ -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: @@ -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", @@ -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'}"), ) ) diff --git a/llms.txt b/llms.txt index 3785def20..fe07b3bac 100644 --- a/llms.txt +++ b/llms.txt @@ -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