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
14 changes: 8 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.11.0"
".": "1.12.0"
}
2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 118
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-132ed160716591bdcd9231c00da8c506d9451a5486b165fc27b2a01d93202082.yml
openapi_spec_hash: c2b44d9e9cda56e32141a7ea3794bbba
config_hash: cbda3692cb48ab8582a0df1674b9e5c8
config_hash: 3bd89c812b96708c461fb98286ebf0b5
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 1.12.0 (2026-03-18)

Full Changelog: [v1.11.0...v1.12.0](https://github.com/runloopai/api-client-python/compare/v1.11.0...v1.12.0)

### Features

* add secrets to oo-sdk ([#756](https://github.com/runloopai/api-client-python/issues/756)) ([09c4997](https://github.com/runloopai/api-client-python/commit/09c4997908ca7ec2ab9a45173d4d0bcf33ad2ca2))


### Bug Fixes

* **deps:** bump minimum typing-extensions version ([a873aad](https://github.com/runloopai/api-client-python/commit/a873aada5cbf8d29600374039f4d65bd321fa6bb))
* **pydantic:** do not pass `by_alias` unless set ([baf3a9a](https://github.com/runloopai/api-client-python/commit/baf3a9a8fe64e14f92f31387781f95dc6c7753de))


### Chores

* configure new SDK language ([13d3ada](https://github.com/runloopai/api-client-python/commit/13d3adaa20a78b6d02db41369505f28aed92e1f5))
* **internal:** tweak CI branches ([b374486](https://github.com/runloopai/api-client-python/commit/b374486156c36b01c55135e4a0c1871b78360176))

## 1.11.0 (2026-03-10)

Full Changelog: [v1.10.3...v1.11.0](https://github.com/runloopai/api-client-python/compare/v1.10.3...v1.11.0)
Expand Down
32 changes: 32 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Runnable examples live in [`examples/`](./examples).
- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
- [Devbox Snapshot and Resume](#devbox-snapshot-resume)
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
- [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox)

<a id="blueprint-with-build-context"></a>
## Blueprint with Build Context
Expand Down Expand Up @@ -135,3 +136,34 @@ 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)

**Use case:** Create a secret, inject it into a devbox as an environment variable, verify access, and clean up.

**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
- Verify the value matches
- Update the secret and verify
- List secrets and verify the secret appears
- 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/
```

**Source:** [`examples/secrets_with_devbox.py`](./examples/secrets_with_devbox.py)
1 change: 1 addition & 0 deletions README-SDK.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ The SDK provides object-oriented interfaces for all major Runloop resources:
- **`runloop.blueprint`** - Blueprint management (create, list, build blueprints)
- **`runloop.snapshot`** - Snapshot management (list disk snapshots)
- **`runloop.storage_object`** - Storage object management (upload, download, list objects)
- **`runloop.secret`** - Secret management (create, update, list, delete encrypted key-value pairs)
- **`runloop.api`** - Direct access to the underlying REST API client

### Devbox
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and offers both synchronous and asynchronous clients powered by [httpx](https://

It is generated with [Stainless](https://www.stainless.com/).

## MCP Server

Use the Runloop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.

[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40runloop%2Fapi-client-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBydW5sb29wL2FwaS1jbGllbnQtbWNwIl0sImVudiI6eyJSVU5MT09QX0FQSV9LRVkiOiJNeSBCZWFyZXIgVG9rZW4ifX0)
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40runloop%2Fapi-client-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40runloop%2Fapi-client-mcp%22%5D%2C%22env%22%3A%7B%22RUNLOOP_API_KEY%22%3A%22My%20Bearer%20Token%22%7D%7D)

> Note: You may need to set environment variables in your MCP client.

## Documentation

The REST API documentation can be found on
Expand Down
4 changes: 2 additions & 2 deletions examples/mcp_github_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ def recipe(ctx: RecipeContext, options: McpExampleOptions) -> RecipeOutput: # n

# Store the GitHub PAT as a Runloop secret
secret_name = unique_name("example-github-mcp")
sdk.api.secrets.create(name=secret_name, value=github_token)
secret = sdk.secret.create(name=secret_name, value=github_token)
resources_created.append(f"secret:{secret_name}")
cleanup.add(f"secret:{secret_name}", lambda: sdk.api.secrets.delete(secret_name))
cleanup.add(f"secret:{secret_name}", secret.delete)

# Launch a devbox with MCP Hub wiring
devbox = sdk.devbox.create(
Expand Down
8 changes: 8 additions & 0 deletions examples/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from .example_types import ExampleResult
from .mcp_github_tools import run_mcp_github_tools_example
from .secrets_with_devbox import run_secrets_with_devbox_example
from .devbox_snapshot_resume import run_devbox_snapshot_resume_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
Expand Down Expand Up @@ -44,6 +45,13 @@
"required_env": ["RUNLOOP_API_KEY", "GITHUB_TOKEN", "ANTHROPIC_API_KEY"],
"run": run_mcp_github_tools_example,
},
{
"slug": "secrets-with-devbox",
"title": "Secrets with Devbox (Create, Inject, Verify, Delete)",
"file_name": "secrets_with_devbox.py",
"required_env": ["RUNLOOP_API_KEY"],
"run": run_secrets_with_devbox_example,
},
]


Expand Down
112 changes: 112 additions & 0 deletions examples/secrets_with_devbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env -S uv run python
"""
---
title: Secrets with Devbox (Create, Inject, Verify, Delete)
slug: secrets-with-devbox
use_case: Create a secret, inject it into a devbox as an environment variable, verify access, and clean up.
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
tags:
- secrets
- devbox
- environment-variables
- cleanup
prerequisites:
- RUNLOOP_API_KEY
run: uv run python -m examples.secrets_with_devbox
test: uv run pytest -m smoketest tests/smoketests/examples/
---
"""

from __future__ import annotations

from runloop_api_client import RunloopSDK

from ._harness import run_as_cli, unique_name, wrap_recipe
from .example_types import ExampleCheck, RecipeOutput, RecipeContext

# 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"


def recipe(ctx: RecipeContext) -> RecipeOutput:
"""Create a secret, inject it into a devbox, and verify it is accessible."""
cleanup = ctx.cleanup

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

secret_name = unique_name("RUNLOOP_SDK_EXAMPLE").upper().replace("-", "_")

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())

secret_info = 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}",
)
)

devbox = sdk.devbox.create(
name=unique_name("secrets-example-devbox"),
secrets={
"MY_SECRET_ENV": secret.name,
},
launch_parameters={
"resource_size_request": "X_SMALL",
"keep_alive_time_seconds": 60 * 5,
},
)
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()
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}"',
)
)

updated_info = sdk.secret.update(secret, _UPDATED_SECRET_VALUE).get_info()
checks.append(
ExampleCheck(
name="secret updated successfully",
passed=updated_info.name == secret_name,
details=f"update_time_ms={updated_info.update_time_ms}",
)
)

secrets = sdk.secret.list()
found = next((s for s in secrets if s.name == secret_name), None)
checks.append(
ExampleCheck(
name="secret appears in list",
passed=found is not None,
details=f"found name={found.name}" if found else "not found",
)
)

return RecipeOutput(resources_created=resources_created, checks=checks)


run_secrets_with_devbox_example = wrap_recipe(recipe)


if __name__ == "__main__":
run_as_cli(run_secrets_with_devbox_example)
3 changes: 2 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +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

## API Reference

Expand All @@ -23,7 +24,7 @@

- **Prefer `AsyncRunloopSDK` over `RunloopSDK`** for better concurrency and performance; all SDK methods have async equivalents
- Use `async with await runloop.devbox.create()` for automatic cleanup via context manager
- For resources without SDK coverage (e.g., secrets, benchmarks), use `runloop.api.*` as a fallback
- For resources without SDK coverage (e.g., benchmarks), use `runloop.api.*` as a fallback
- Use `await devbox.cmd.exec('command')` for commands expected to return immediately (e.g., `echo`, `pwd`, `cat`)—blocks until completion, returns `ExecutionResult` with stdout/stderr
- Use `await devbox.cmd.exec_async('command')` for long-running or background processes (servers, watchers, builds)—returns immediately with `Execution` handle to check status, get result, or kill
- Both `exec` and `exec_async` support streaming callbacks (`stdout`, `stderr`, `output`) for real-time output
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
version = "1.11.0"
version = "1.12.0"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
Expand All @@ -11,7 +11,7 @@ authors = [
dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=2.0, <3",
"typing-extensions>=4.10, <5",
"typing-extensions>=4.14, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
Expand Down
11 changes: 9 additions & 2 deletions src/runloop_api_client/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
from typing_extensions import Self, Literal
from typing_extensions import Self, Literal, TypedDict

import pydantic
from pydantic.fields import FieldInfo
Expand Down Expand Up @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
return model.model_dump_json(indent=indent)


class _ModelDumpKwargs(TypedDict, total=False):
by_alias: bool


def model_dump(
model: pydantic.BaseModel,
*,
Expand All @@ -142,14 +146,17 @@ def model_dump(
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
kwargs: _ModelDumpKwargs = {}
if by_alias is not None:
kwargs["by_alias"] = by_alias
return model.model_dump(
mode=mode,
exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
by_alias=by_alias,
**kwargs,
)
return cast(
"dict[str, Any]",
Expand Down
2 changes: 1 addition & 1 deletion src/runloop_api_client/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "runloop_api_client"
__version__ = "1.11.0" # x-release-please-version
__version__ = "1.12.0" # x-release-please-version
Loading
Loading