opencomputer-sdk 0.4.7__tar.gz → 0.4.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/PKG-INFO +1 -1
- opencomputer_sdk-0.4.8/examples/test_secretstore.py +241 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/__init__.py +5 -1
- opencomputer_sdk-0.4.8/opencomputer/project.py +182 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/sandbox.py +8 -3
- opencomputer_sdk-0.4.8/opencomputer/template.py +75 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/pyproject.toml +1 -1
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/.gitignore +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/README.md +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/run_all_tests.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_commands.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_concurrent.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_declarative_images.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_default_template.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_domain_tls.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_environment.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_exec.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_file_ops.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_multi_template.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_python_sdk.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_reconnect.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/examples/test_timeout.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/agent.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/commands.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/exec.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/filesystem.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/image.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/pty.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/snapshot.py +0 -0
- {opencomputer_sdk-0.4.7 → opencomputer_sdk-0.4.8}/opencomputer/sse.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencomputer-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Python SDK for OpenComputer - cloud sandbox platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/diggerhq/opensandbox
|
|
6
6
|
Project-URL: Repository, https://github.com/diggerhq/opensandbox
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Secret Stores & Secrets Test
|
|
4
|
+
|
|
5
|
+
Tests:
|
|
6
|
+
1. Create a secret store
|
|
7
|
+
2. List secret stores
|
|
8
|
+
3. Get secret store by ID
|
|
9
|
+
4. Update secret store
|
|
10
|
+
5. Set secrets on a store
|
|
11
|
+
6. List secrets (names only, values never returned)
|
|
12
|
+
7. Create sandbox with secret store (inherits secrets)
|
|
13
|
+
8. Verify secrets are injected as sealed env vars
|
|
14
|
+
9. Delete secret
|
|
15
|
+
10. Delete secret store
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python examples/test_projects.py
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from opencomputer import Sandbox, SecretStore
|
|
26
|
+
|
|
27
|
+
GREEN = "\033[32m"
|
|
28
|
+
RED = "\033[31m"
|
|
29
|
+
BOLD = "\033[1m"
|
|
30
|
+
DIM = "\033[2m"
|
|
31
|
+
RESET = "\033[0m"
|
|
32
|
+
|
|
33
|
+
passed = 0
|
|
34
|
+
failed = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def green(msg: str) -> None:
|
|
38
|
+
print(f"{GREEN}\u2713 {msg}{RESET}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def red(msg: str) -> None:
|
|
42
|
+
print(f"{RED}\u2717 {msg}{RESET}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def bold(msg: str) -> None:
|
|
46
|
+
print(f"{BOLD}{msg}{RESET}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dim(msg: str) -> None:
|
|
50
|
+
print(f"{DIM} {msg}{RESET}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check(desc: str, condition: bool, detail: str = "") -> None:
|
|
54
|
+
global passed, failed
|
|
55
|
+
if condition:
|
|
56
|
+
green(desc)
|
|
57
|
+
passed += 1
|
|
58
|
+
else:
|
|
59
|
+
red(f"{desc} ({detail})" if detail else desc)
|
|
60
|
+
failed += 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def main() -> None:
|
|
64
|
+
global passed, failed
|
|
65
|
+
|
|
66
|
+
bold("\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")
|
|
67
|
+
bold("\u2551 Secret Stores & Secrets Test \u2551")
|
|
68
|
+
bold("\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\n")
|
|
69
|
+
|
|
70
|
+
store_id = None
|
|
71
|
+
sandbox = None
|
|
72
|
+
store_name = f"test-store-{int(time.time())}"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# -- Test 1: Create secret store --
|
|
76
|
+
bold("--- Test 1: Create secret store ---\n")
|
|
77
|
+
|
|
78
|
+
store = await SecretStore.create(
|
|
79
|
+
name=store_name,
|
|
80
|
+
egress_allowlist=["api.anthropic.com"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
store_id = store["id"]
|
|
84
|
+
check("Store created", bool(store["id"]))
|
|
85
|
+
check("Name matches", store["name"] == store_name)
|
|
86
|
+
check("Egress allowlist set", len(store.get("egressAllowlist", [])) == 1)
|
|
87
|
+
dim(f"Store ID: {store_id}")
|
|
88
|
+
print()
|
|
89
|
+
|
|
90
|
+
# -- Test 2: List secret stores --
|
|
91
|
+
bold("--- Test 2: List secret stores ---\n")
|
|
92
|
+
|
|
93
|
+
stores = await SecretStore.list()
|
|
94
|
+
check("List returns list", isinstance(stores, list))
|
|
95
|
+
found = any(s["id"] == store_id for s in stores)
|
|
96
|
+
check("Created store in list", found)
|
|
97
|
+
dim(f"Total stores: {len(stores)}")
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
# -- Test 3: Get secret store --
|
|
101
|
+
bold("--- Test 3: Get secret store by ID ---\n")
|
|
102
|
+
|
|
103
|
+
fetched = await SecretStore.get(store_id)
|
|
104
|
+
check("Get returns correct store", fetched["id"] == store_id)
|
|
105
|
+
check("Get has correct name", fetched["name"] == store_name)
|
|
106
|
+
print()
|
|
107
|
+
|
|
108
|
+
# -- Test 4: Update secret store --
|
|
109
|
+
bold("--- Test 4: Update secret store ---\n")
|
|
110
|
+
|
|
111
|
+
updated_name = f"{store_name}-updated"
|
|
112
|
+
updated = await SecretStore.update(
|
|
113
|
+
store_id,
|
|
114
|
+
name=updated_name,
|
|
115
|
+
egress_allowlist=["api.anthropic.com", "*.openai.com"],
|
|
116
|
+
)
|
|
117
|
+
check("Name updated", updated["name"] == updated_name)
|
|
118
|
+
check("Egress allowlist updated", len(updated.get("egressAllowlist", [])) == 2)
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
# -- Test 5: Set secrets --
|
|
122
|
+
bold("--- Test 5: Set secrets ---\n")
|
|
123
|
+
|
|
124
|
+
await SecretStore.set_secret(store_id, "TEST_API_KEY", "sk-test-12345")
|
|
125
|
+
green("Set TEST_API_KEY")
|
|
126
|
+
|
|
127
|
+
await SecretStore.set_secret(store_id, "DATABASE_URL", "postgres://localhost/test")
|
|
128
|
+
green("Set DATABASE_URL")
|
|
129
|
+
|
|
130
|
+
await SecretStore.set_secret(store_id, "TEMP_SECRET", "will-be-deleted")
|
|
131
|
+
green("Set TEMP_SECRET")
|
|
132
|
+
print()
|
|
133
|
+
|
|
134
|
+
# -- Test 6: List secrets --
|
|
135
|
+
bold("--- Test 6: List secret entries ---\n")
|
|
136
|
+
|
|
137
|
+
entries = await SecretStore.list_secrets(store_id)
|
|
138
|
+
check("Returns list", isinstance(entries, list))
|
|
139
|
+
names = [e["name"] for e in entries]
|
|
140
|
+
check("Has TEST_API_KEY", "TEST_API_KEY" in names)
|
|
141
|
+
check("Has DATABASE_URL", "DATABASE_URL" in names)
|
|
142
|
+
check("Has TEMP_SECRET", "TEMP_SECRET" in names)
|
|
143
|
+
check("3 secrets total", len(entries) == 3, f"got {len(entries)}")
|
|
144
|
+
dim(f"Secret names: {', '.join(names)}")
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
# -- Test 7: Create sandbox with secret store --
|
|
148
|
+
bold("--- Test 7: Create sandbox with secret store ---\n")
|
|
149
|
+
|
|
150
|
+
sandbox = await Sandbox.create(
|
|
151
|
+
secret_store=updated_name,
|
|
152
|
+
timeout=120,
|
|
153
|
+
)
|
|
154
|
+
check("Sandbox created", bool(sandbox.sandbox_id))
|
|
155
|
+
dim(f"Sandbox ID: {sandbox.sandbox_id}")
|
|
156
|
+
print()
|
|
157
|
+
|
|
158
|
+
# -- Test 8: Verify secrets are sealed in sandbox --
|
|
159
|
+
bold("--- Test 8: Verify secrets sealed in sandbox ---\n")
|
|
160
|
+
|
|
161
|
+
# Secrets should be sealed tokens (osb_sealed_*) inside the VM.
|
|
162
|
+
# The MITM proxy replaces sealed tokens with real values on outbound HTTPS requests,
|
|
163
|
+
# so the real secret never exists in VM memory.
|
|
164
|
+
api_key_result = await sandbox.commands.run("echo $TEST_API_KEY")
|
|
165
|
+
api_key_val = api_key_result.stdout.strip()
|
|
166
|
+
check("TEST_API_KEY is sealed", api_key_val.startswith("osb_sealed_"),
|
|
167
|
+
f'got "{api_key_val}"')
|
|
168
|
+
|
|
169
|
+
db_url_result = await sandbox.commands.run("echo $DATABASE_URL")
|
|
170
|
+
db_url_val = db_url_result.stdout.strip()
|
|
171
|
+
check("DATABASE_URL is sealed", db_url_val.startswith("osb_sealed_"),
|
|
172
|
+
f'got "{db_url_val}"')
|
|
173
|
+
|
|
174
|
+
temp_result = await sandbox.commands.run("echo $TEMP_SECRET")
|
|
175
|
+
temp_val = temp_result.stdout.strip()
|
|
176
|
+
check("TEMP_SECRET is sealed", temp_val.startswith("osb_sealed_"),
|
|
177
|
+
f'got "{temp_val}"')
|
|
178
|
+
print()
|
|
179
|
+
|
|
180
|
+
# -- Test 9: Delete secret --
|
|
181
|
+
bold("--- Test 9: Delete secret ---\n")
|
|
182
|
+
|
|
183
|
+
await SecretStore.delete_secret(store_id, "TEMP_SECRET")
|
|
184
|
+
green("Deleted TEMP_SECRET")
|
|
185
|
+
|
|
186
|
+
after_delete = await SecretStore.list_secrets(store_id)
|
|
187
|
+
after_names = [e["name"] for e in after_delete]
|
|
188
|
+
check("TEMP_SECRET removed", "TEMP_SECRET" not in after_names)
|
|
189
|
+
check("2 secrets remaining", len(after_delete) == 2, f"got {len(after_delete)}")
|
|
190
|
+
print()
|
|
191
|
+
|
|
192
|
+
# -- Test 10: Delete secret store --
|
|
193
|
+
bold("--- Test 10: Delete secret store ---\n")
|
|
194
|
+
|
|
195
|
+
# Kill sandbox first
|
|
196
|
+
await sandbox.kill()
|
|
197
|
+
green("Sandbox killed")
|
|
198
|
+
sandbox = None
|
|
199
|
+
|
|
200
|
+
await SecretStore.delete(store_id)
|
|
201
|
+
green("Secret store deleted")
|
|
202
|
+
|
|
203
|
+
# Verify it's gone
|
|
204
|
+
try:
|
|
205
|
+
await SecretStore.get(store_id)
|
|
206
|
+
red("Store should not exist after delete")
|
|
207
|
+
failed += 1
|
|
208
|
+
except Exception:
|
|
209
|
+
green("Store not found after delete (expected)")
|
|
210
|
+
passed += 1
|
|
211
|
+
store_id = None
|
|
212
|
+
print()
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
red(f"Fatal error: {e}")
|
|
216
|
+
import traceback
|
|
217
|
+
traceback.print_exc()
|
|
218
|
+
failed += 1
|
|
219
|
+
finally:
|
|
220
|
+
# Cleanup
|
|
221
|
+
if sandbox:
|
|
222
|
+
try:
|
|
223
|
+
await sandbox.kill()
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
if store_id:
|
|
227
|
+
try:
|
|
228
|
+
await SecretStore.delete(store_id)
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
# --- Summary ---
|
|
233
|
+
bold("========================================")
|
|
234
|
+
bold(f" Results: {passed} passed, {failed} failed")
|
|
235
|
+
bold("========================================\n")
|
|
236
|
+
if failed > 0:
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
asyncio.run(main())
|
|
@@ -6,6 +6,8 @@ from opencomputer.filesystem import Filesystem
|
|
|
6
6
|
from opencomputer.exec import Exec, ProcessResult, ExecSessionInfo
|
|
7
7
|
from opencomputer.image import Image
|
|
8
8
|
from opencomputer.pty import Pty, PtySession
|
|
9
|
+
from opencomputer.template import Template
|
|
10
|
+
from opencomputer.project import SecretStore
|
|
9
11
|
from opencomputer.snapshot import Snapshots
|
|
10
12
|
|
|
11
13
|
__all__ = [
|
|
@@ -21,7 +23,9 @@ __all__ = [
|
|
|
21
23
|
"Image",
|
|
22
24
|
"Pty",
|
|
23
25
|
"PtySession",
|
|
26
|
+
"Template",
|
|
27
|
+
"SecretStore",
|
|
24
28
|
"Snapshots",
|
|
25
29
|
]
|
|
26
30
|
|
|
27
|
-
__version__ = "0.5.
|
|
31
|
+
__version__ = "0.5.1"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Secret store management — CRUD for secret stores and encrypted secrets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_client(
|
|
12
|
+
api_key: str | None = None,
|
|
13
|
+
api_url: str | None = None,
|
|
14
|
+
) -> httpx.AsyncClient:
|
|
15
|
+
url = api_url or os.environ.get("OPENCOMPUTER_API_URL", "https://app.opencomputer.dev")
|
|
16
|
+
url = url.rstrip("/")
|
|
17
|
+
key = api_key or os.environ.get("OPENCOMPUTER_API_KEY", "")
|
|
18
|
+
|
|
19
|
+
api_base = url if url.endswith("/api") else f"{url}/api"
|
|
20
|
+
|
|
21
|
+
headers: dict[str, str] = {}
|
|
22
|
+
if key:
|
|
23
|
+
headers["X-API-Key"] = key
|
|
24
|
+
|
|
25
|
+
return httpx.AsyncClient(base_url=api_base, headers=headers, timeout=30.0)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SecretStore:
|
|
29
|
+
"""Static methods for managing secret stores and their secrets."""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
async def create(
|
|
33
|
+
name: str,
|
|
34
|
+
egress_allowlist: list[str] | None = None,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
api_url: str | None = None,
|
|
37
|
+
) -> dict:
|
|
38
|
+
"""Create a new secret store.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Store name (unique per org).
|
|
42
|
+
egress_allowlist: Allowed egress hosts (e.g. ["api.anthropic.com"]).
|
|
43
|
+
api_key: API key (or OPENCOMPUTER_API_KEY env var).
|
|
44
|
+
api_url: API URL (or OPENCOMPUTER_API_URL env var).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Secret store info dict with id, name, egressAllowlist, etc.
|
|
48
|
+
"""
|
|
49
|
+
body: dict[str, Any] = {"name": name}
|
|
50
|
+
if egress_allowlist:
|
|
51
|
+
body["egressAllowlist"] = egress_allowlist
|
|
52
|
+
|
|
53
|
+
async with _get_client(api_key, api_url) as client:
|
|
54
|
+
resp = await client.post("/secret-stores", json=body)
|
|
55
|
+
resp.raise_for_status()
|
|
56
|
+
return resp.json()
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
async def list(
|
|
60
|
+
api_key: str | None = None,
|
|
61
|
+
api_url: str | None = None,
|
|
62
|
+
) -> list[dict]:
|
|
63
|
+
"""List all secret stores for the authenticated org."""
|
|
64
|
+
async with _get_client(api_key, api_url) as client:
|
|
65
|
+
resp = await client.get("/secret-stores")
|
|
66
|
+
resp.raise_for_status()
|
|
67
|
+
return resp.json()
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
async def get(
|
|
71
|
+
store_id: str,
|
|
72
|
+
api_key: str | None = None,
|
|
73
|
+
api_url: str | None = None,
|
|
74
|
+
) -> dict:
|
|
75
|
+
"""Get secret store details by ID."""
|
|
76
|
+
async with _get_client(api_key, api_url) as client:
|
|
77
|
+
resp = await client.get(f"/secret-stores/{store_id}")
|
|
78
|
+
resp.raise_for_status()
|
|
79
|
+
return resp.json()
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
async def update(
|
|
83
|
+
store_id: str,
|
|
84
|
+
name: str = "",
|
|
85
|
+
egress_allowlist: list[str] | None = None,
|
|
86
|
+
api_key: str | None = None,
|
|
87
|
+
api_url: str | None = None,
|
|
88
|
+
) -> dict:
|
|
89
|
+
"""Update a secret store's configuration.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
store_id: UUID of the store to update.
|
|
93
|
+
name: New store name (empty = no change).
|
|
94
|
+
egress_allowlist: New allowed egress hosts (None = no change).
|
|
95
|
+
api_key: API key (or OPENCOMPUTER_API_KEY env var).
|
|
96
|
+
api_url: API URL (or OPENCOMPUTER_API_URL env var).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Updated secret store info dict.
|
|
100
|
+
"""
|
|
101
|
+
body: dict[str, Any] = {}
|
|
102
|
+
if name:
|
|
103
|
+
body["name"] = name
|
|
104
|
+
if egress_allowlist is not None:
|
|
105
|
+
body["egressAllowlist"] = egress_allowlist
|
|
106
|
+
|
|
107
|
+
async with _get_client(api_key, api_url) as client:
|
|
108
|
+
resp = await client.put(f"/secret-stores/{store_id}", json=body)
|
|
109
|
+
resp.raise_for_status()
|
|
110
|
+
return resp.json()
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
async def delete(
|
|
114
|
+
store_id: str,
|
|
115
|
+
api_key: str | None = None,
|
|
116
|
+
api_url: str | None = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Delete a secret store and all its secrets."""
|
|
119
|
+
async with _get_client(api_key, api_url) as client:
|
|
120
|
+
resp = await client.delete(f"/secret-stores/{store_id}")
|
|
121
|
+
resp.raise_for_status()
|
|
122
|
+
|
|
123
|
+
# ── Secret Entries ──────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
async def set_secret(
|
|
127
|
+
store_id: str,
|
|
128
|
+
name: str,
|
|
129
|
+
value: str,
|
|
130
|
+
allowed_hosts: list[str] | None = None,
|
|
131
|
+
api_key: str | None = None,
|
|
132
|
+
api_url: str | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Set (create or update) an encrypted secret in a store.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
store_id: UUID of the secret store.
|
|
138
|
+
name: Secret name (used as env var name in sandboxes).
|
|
139
|
+
value: Secret value (encrypted at rest, never returned by API).
|
|
140
|
+
allowed_hosts: Restrict this secret to specific hosts only.
|
|
141
|
+
api_key: API key (or OPENCOMPUTER_API_KEY env var).
|
|
142
|
+
api_url: API URL (or OPENCOMPUTER_API_URL env var).
|
|
143
|
+
"""
|
|
144
|
+
body: dict[str, Any] = {"value": value}
|
|
145
|
+
if allowed_hosts:
|
|
146
|
+
body["allowedHosts"] = allowed_hosts
|
|
147
|
+
|
|
148
|
+
async with _get_client(api_key, api_url) as client:
|
|
149
|
+
resp = await client.put(
|
|
150
|
+
f"/secret-stores/{store_id}/secrets/{name}",
|
|
151
|
+
json=body,
|
|
152
|
+
)
|
|
153
|
+
resp.raise_for_status()
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
async def delete_secret(
|
|
157
|
+
store_id: str,
|
|
158
|
+
name: str,
|
|
159
|
+
api_key: str | None = None,
|
|
160
|
+
api_url: str | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Delete a secret from a store."""
|
|
163
|
+
async with _get_client(api_key, api_url) as client:
|
|
164
|
+
resp = await client.delete(f"/secret-stores/{store_id}/secrets/{name}")
|
|
165
|
+
if resp.status_code != 404:
|
|
166
|
+
resp.raise_for_status()
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
async def list_secrets(
|
|
170
|
+
store_id: str,
|
|
171
|
+
api_key: str | None = None,
|
|
172
|
+
api_url: str | None = None,
|
|
173
|
+
) -> list[dict]:
|
|
174
|
+
"""List secret entries in a store (names and allowed hosts, no values).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of secret entry dicts with name, allowedHosts, etc.
|
|
178
|
+
"""
|
|
179
|
+
async with _get_client(api_key, api_url) as client:
|
|
180
|
+
resp = await client.get(f"/secret-stores/{store_id}/secrets")
|
|
181
|
+
resp.raise_for_status()
|
|
182
|
+
return resp.json()
|
|
@@ -39,6 +39,7 @@ class Sandbox:
|
|
|
39
39
|
api_url: str | None = None,
|
|
40
40
|
envs: dict[str, str] | None = None,
|
|
41
41
|
metadata: dict[str, str] | None = None,
|
|
42
|
+
secret_store: str | None = None,
|
|
42
43
|
image: Image | None = None,
|
|
43
44
|
snapshot: str | None = None,
|
|
44
45
|
on_build_log: Callable[[str], None] | None = None,
|
|
@@ -46,12 +47,14 @@ class Sandbox:
|
|
|
46
47
|
"""Create a new sandbox instance.
|
|
47
48
|
|
|
48
49
|
Args:
|
|
49
|
-
template:
|
|
50
|
+
template: Template to use (default "base").
|
|
50
51
|
timeout: Sandbox timeout in seconds (default 300).
|
|
51
52
|
api_key: API key (or OPENCOMPUTER_API_KEY env var).
|
|
52
53
|
api_url: API URL (or OPENCOMPUTER_API_URL env var).
|
|
53
|
-
envs: Environment variables to inject.
|
|
54
|
-
metadata:
|
|
54
|
+
envs: Environment variables to inject. Overrides store secrets.
|
|
55
|
+
metadata: Custom metadata key-value pairs.
|
|
56
|
+
secret_store: Secret store name — resolves encrypted secrets
|
|
57
|
+
and egress allowlist.
|
|
55
58
|
image: Declarative Image definition. The server builds and caches it as a checkpoint.
|
|
56
59
|
snapshot: Name of a pre-built snapshot to create the sandbox from.
|
|
57
60
|
on_build_log: Callback for build log streaming when using image/snapshot.
|
|
@@ -83,6 +86,8 @@ class Sandbox:
|
|
|
83
86
|
body["envs"] = envs
|
|
84
87
|
if metadata:
|
|
85
88
|
body["metadata"] = metadata
|
|
89
|
+
if secret_store:
|
|
90
|
+
body["secretStore"] = secret_store
|
|
86
91
|
if image is not None:
|
|
87
92
|
body["image"] = image.to_dict()
|
|
88
93
|
if snapshot is not None:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Template management for custom sandbox environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TemplateInfo:
|
|
12
|
+
"""Template metadata."""
|
|
13
|
+
|
|
14
|
+
template_id: str
|
|
15
|
+
name: str
|
|
16
|
+
tag: str
|
|
17
|
+
status: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Template:
|
|
22
|
+
"""Template management operations."""
|
|
23
|
+
|
|
24
|
+
_client: httpx.AsyncClient
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _from_client(cls, client: httpx.AsyncClient) -> Template:
|
|
28
|
+
return cls(_client=client)
|
|
29
|
+
|
|
30
|
+
async def build(self, name: str, dockerfile: str) -> TemplateInfo:
|
|
31
|
+
"""Build a new template from a Dockerfile."""
|
|
32
|
+
resp = await self._client.post(
|
|
33
|
+
"/templates",
|
|
34
|
+
json={"name": name, "dockerfile": dockerfile},
|
|
35
|
+
timeout=300.0,
|
|
36
|
+
)
|
|
37
|
+
resp.raise_for_status()
|
|
38
|
+
data = resp.json()
|
|
39
|
+
return TemplateInfo(
|
|
40
|
+
template_id=data["templateID"],
|
|
41
|
+
name=data["name"],
|
|
42
|
+
tag=data.get("tag", "latest"),
|
|
43
|
+
status=data.get("status", "ready"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def list(self) -> list[TemplateInfo]:
|
|
47
|
+
"""List all available templates."""
|
|
48
|
+
resp = await self._client.get("/templates")
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
return [
|
|
51
|
+
TemplateInfo(
|
|
52
|
+
template_id=t["templateID"],
|
|
53
|
+
name=t["name"],
|
|
54
|
+
tag=t.get("tag", "latest"),
|
|
55
|
+
status=t.get("status", "ready"),
|
|
56
|
+
)
|
|
57
|
+
for t in resp.json()
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
async def get(self, name: str) -> TemplateInfo:
|
|
61
|
+
"""Get template details by name."""
|
|
62
|
+
resp = await self._client.get(f"/templates/{name}")
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
data = resp.json()
|
|
65
|
+
return TemplateInfo(
|
|
66
|
+
template_id=data["templateID"],
|
|
67
|
+
name=data["name"],
|
|
68
|
+
tag=data.get("tag", "latest"),
|
|
69
|
+
status=data.get("status", "ready"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def delete(self, name: str) -> None:
|
|
73
|
+
"""Delete a template."""
|
|
74
|
+
resp = await self._client.delete(f"/templates/{name}")
|
|
75
|
+
resp.raise_for_status()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|