Skip to content

Commit c82944a

Browse files
authored
feat(sdk): added secrets as first class concepts and examples (#739)
1 parent ec0fa4e commit c82944a

13 files changed

Lines changed: 855 additions & 51 deletions

File tree

EXAMPLES.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Runnable examples live in [`examples/`](./examples).
1010
- [Blueprint with Build Context](#blueprint-with-build-context)
1111
- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
1212
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
13+
- [Secrets with Devbox (Create, Inject, Verify, Delete)](#secrets-with-devbox)
1314

1415
<a id="blueprint-with-build-context"></a>
1516
## Blueprint with Build Context
@@ -103,3 +104,32 @@ yarn test:examples
103104
```
104105

105106
**Source:** [`examples/mcp-github-tools.ts`](./examples/mcp-github-tools.ts)
107+
108+
<a id="secrets-with-devbox"></a>
109+
## Secrets with Devbox (Create, Inject, Verify, Delete)
110+
111+
**Use case:** Create a secret, inject it into a devbox as an environment variable, verify access, and clean up.
112+
113+
**Tags:** `secrets`, `devbox`, `environment-variables`, `cleanup`
114+
115+
### Workflow
116+
- Create a secret with a test value
117+
- Create a devbox with the secret mapped to an env var
118+
- Execute a command that reads the secret from the environment
119+
- Verify the value matches
120+
- Shutdown devbox and delete secret
121+
122+
### Prerequisites
123+
- `RUNLOOP_API_KEY`
124+
125+
### Run
126+
```sh
127+
yarn tsn -T examples/secrets-with-devbox.ts
128+
```
129+
130+
### Test
131+
```sh
132+
yarn test:examples
133+
```
134+
135+
**Source:** [`examples/secrets-with-devbox.ts`](./examples/secrets-with-devbox.ts)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ The SDK provides object-oriented interfaces for all major Runloop resources:
8383
- **[`runloop.agent`](https://runloopai.github.io/api-client-ts/stable/classes/sdk.AgentOps.html)** - Agent management (create, list agents from npm/pip/git)
8484
- **[`runloop.scenario`](https://runloopai.github.io/api-client-ts/stable/classes/sdk.ScenarioOps.html)** - Scenario management (list scenarios, start runs)
8585
- **[`runloop.scorer`](https://runloopai.github.io/api-client-ts/stable/classes/sdk.ScorerOps.html)** - Scorer management (create, list, update)
86+
- **[`runloop.secret`](https://runloopai.github.io/api-client-ts/stable/classes/sdk.SecretOps.html)** - Secret management (create, update, list, delete encrypted key-value pairs)
8687
- **[`runloop.api`](https://runloopai.github.io/api-client-ts/stable/modules/types.html)** - Direct access to the REST API client
8788

8889
## TypeScript Support

examples/mcp-github-tools.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,13 @@ export async function recipe(ctx: RecipeContext, options: McpExampleOptions): Pr
7878
cleanup.add(`mcp_config:${mcpConfig.id}`, () => sdk.mcpConfig.fromId(mcpConfig.id).delete());
7979

8080
// Store the GitHub PAT as a Runloop secret
81-
// Note: secrets are currently available via sdk.api, not sdk.secret ops
82-
const secretName = uniqueName('example-github-mcp');
83-
await sdk.api.secrets.create({
81+
const secretName = 'GITHUB_MCP_EXAMPLE';
82+
const secret = await sdk.secret.create({
8483
name: secretName,
8584
value: githubToken,
8685
});
8786
resourcesCreated.push(`secret:${secretName}`);
88-
cleanup.add(`secret:${secretName}`, () => sdk.api.secrets.delete(secretName));
87+
cleanup.add(`secret:${secretName}`, () => secret.delete());
8988

9089
// Launch a devbox with MCP Hub wiring
9190
const devbox = await sdk.devbox.create({
@@ -97,7 +96,7 @@ export async function recipe(ctx: RecipeContext, options: McpExampleOptions): Pr
9796
mcp: {
9897
MCP_SECRET: {
9998
mcp_config: mcpConfig.id,
100-
secret: secretName,
99+
secret: secret,
101100
},
102101
},
103102
});

examples/registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ExampleResult } from './types';
66
import { runBlueprintWithBuildContextExample } from './blueprint-with-build-context';
77
import { runDevboxFromBlueprintLifecycleExample } from './devbox-from-blueprint-lifecycle';
88
import { runMcpGithubToolsExample } from './mcp-github-tools';
9+
import { runSecretsWithDevboxExample } from './secrets-with-devbox';
910

1011
export interface ExampleRegistryEntry {
1112
slug: string;
@@ -37,4 +38,11 @@ export const exampleRegistry: ExampleRegistryEntry[] = [
3738
requiredEnv: ['RUNLOOP_API_KEY', 'GITHUB_TOKEN', 'ANTHROPIC_API_KEY'],
3839
run: runMcpGithubToolsExample,
3940
},
41+
{
42+
slug: 'secrets-with-devbox',
43+
title: 'Secrets with Devbox (Create, Inject, Verify, Delete)',
44+
fileName: 'secrets-with-devbox.ts',
45+
requiredEnv: ['RUNLOOP_API_KEY'],
46+
run: runSecretsWithDevboxExample,
47+
},
4048
];

examples/secrets-with-devbox.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
/**
4+
---
5+
title: Secrets with Devbox (Create, Inject, Verify, Delete)
6+
slug: secrets-with-devbox
7+
use_case: Create a secret, inject it into a devbox as an environment variable, verify access, and clean up.
8+
workflow:
9+
- Create a secret with a test value
10+
- Create a devbox with the secret mapped to an env var
11+
- Execute a command that reads the secret from the environment
12+
- Verify the value matches
13+
- Shutdown devbox and delete secret
14+
tags:
15+
- secrets
16+
- devbox
17+
- environment-variables
18+
- cleanup
19+
prerequisites:
20+
- RUNLOOP_API_KEY
21+
run: yarn tsn -T examples/secrets-with-devbox.ts
22+
test: yarn test:examples
23+
---
24+
*/
25+
26+
import { RunloopSDK } from '@runloop/api-client';
27+
import { wrapRecipe, runAsCli } from './_harness';
28+
import type { RecipeContext, RecipeOutput } from './types';
29+
30+
export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> {
31+
const { cleanup } = ctx;
32+
33+
const sdk = new RunloopSDK({
34+
bearerToken: process.env['RUNLOOP_API_KEY'],
35+
});
36+
37+
const secretName = 'RUNLOOP_SECRET_EXAMPLE';
38+
// Note: do NOT hardcode secret values in your code!
39+
// this is example code only; use environment variables instead!
40+
const secretValue = 'my-secret-value';
41+
42+
const secret = await sdk.secret.create({
43+
name: secretName,
44+
value: secretValue,
45+
});
46+
cleanup.add(`secret:${secretName}`, () => secret.delete());
47+
48+
const devbox = await sdk.devbox.create({
49+
name: 'secrets-example-devbox',
50+
secrets: {
51+
MY_SECRET_ENV: secret,
52+
},
53+
launch_parameters: {
54+
resource_size_request: 'X_SMALL',
55+
keep_alive_time_seconds: 60 * 5,
56+
},
57+
});
58+
cleanup.add(`devbox:${devbox.id}`, () => sdk.devbox.fromId(devbox.id).shutdown());
59+
60+
const result = await devbox.cmd.exec('echo $MY_SECRET_ENV');
61+
const stdout = await result.stdout();
62+
63+
const updatedSecret = await sdk.secret.update(secret, {
64+
value: 'updated-secret-value',
65+
});
66+
67+
const secrets = await sdk.secret.list();
68+
const foundSecret = secrets.find((s) => s.name === secretName);
69+
70+
const secretInfo = await secret.getInfo();
71+
const updatedInfo = await updatedSecret.getInfo();
72+
73+
return {
74+
resourcesCreated: [`secret:${secretName}`, `devbox:${devbox.id}`],
75+
checks: [
76+
{
77+
name: 'secret created successfully',
78+
passed: secret.name === secretName && secretInfo.id.startsWith('sec_'),
79+
details: `name=${secret.name}, id=${secretInfo.id}`,
80+
},
81+
{
82+
name: 'devbox can read secret as env var',
83+
passed: result.exitCode === 0 && stdout.trim() === secretValue,
84+
details: `exitCode=${result.exitCode}, stdout="${stdout.trim()}"`,
85+
},
86+
{
87+
name: 'secret updated successfully',
88+
passed: updatedSecret.name === secretName,
89+
details: `update_time_ms=${updatedInfo.update_time_ms}`,
90+
},
91+
{
92+
name: 'secret appears in list',
93+
passed: foundSecret !== undefined && foundSecret.name === secretName,
94+
details: foundSecret ? `found name=${foundSecret.name}` : 'not found',
95+
},
96+
],
97+
};
98+
}
99+
100+
export const runSecretsWithDevboxExample = wrapRecipe({ recipe });
101+
102+
if (require.main === module) {
103+
void runAsCli(runSecretsWithDevboxExample);
104+
}

llms.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
- [Devbox lifecycle example](examples/devbox-from-blueprint-lifecycle.ts): Create blueprint, launch devbox, run commands, cleanup
1313
- [MCP GitHub example](examples/mcp-github-tools.ts): MCP Hub integration with Claude Code
14+
- [Secrets with Devbox example](examples/secrets-with-devbox.ts): Create secret, inject into devbox, verify, cleanup
1415

1516
## API Reference
1617

@@ -21,7 +22,7 @@
2122

2223
- Always use `RunloopSDK` as the entry point (not legacy `Runloop` default export)
2324
- Prefer SDK methods (`sdk.devbox`, `sdk.blueprint`, etc.) for object-oriented patterns and convenience
24-
- For resources without SDK coverage (e.g., secrets, benchmarks), use `sdk.api.*` as a fallback
25+
- For resources without SDK coverage (e.g., benchmarks), use `sdk.api.*` as a fallback
2526
- Use `sdk.devbox.create()` to create devboxes; they auto-await readiness
2627
- Use `devbox.cmd.exec('command')` for commands expected to return immediately (e.g., `echo`, `pwd`, `cat`)—blocks until completion, returns `ExecutionResult` with stdout/stderr
2728
- Use `devbox.cmd.execAsync('command')` for long-running or background processes (servers, watchers, builds)—returns immediately with `Execution` handle to check status, get result, or kill

src/resources/secrets.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export class Secrets extends APIResource {
1313
return this._client.post('/v1/secrets', { body, ...options });
1414
}
1515

16+
/**
17+
* Retrieve a Secret by name.
18+
*/
19+
retrieve(name: string, options?: Core.RequestOptions): Core.APIPromise<SecretView> {
20+
return this._client.get(`/v1/secrets/${name}`, options);
21+
}
22+
1623
/**
1724
* Update the value of an existing Secret by name. The new value will be encrypted
1825
* at rest.

0 commit comments

Comments
 (0)