chalkcompute 0.0.0__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.
Files changed (108) hide show
  1. chalkcompute-0.0.0/PKG-INFO +375 -0
  2. chalkcompute-0.0.0/README.md +353 -0
  3. chalkcompute-0.0.0/chalkcompute/__init__.py +87 -0
  4. chalkcompute-0.0.0/chalkcompute/__init__.pyi +113 -0
  5. chalkcompute-0.0.0/chalkcompute/_arrow_schema.py +291 -0
  6. chalkcompute-0.0.0/chalkcompute/_cls.py +1415 -0
  7. chalkcompute-0.0.0/chalkcompute/_concurrency.py +37 -0
  8. chalkcompute-0.0.0/chalkcompute/_connect.py +460 -0
  9. chalkcompute-0.0.0/chalkcompute/_container.py +904 -0
  10. chalkcompute-0.0.0/chalkcompute/_function.py +2074 -0
  11. chalkcompute-0.0.0/chalkcompute/_gen/__init__.py +0 -0
  12. chalkcompute-0.0.0/chalkcompute/_gen/buf/__init__.py +0 -0
  13. chalkcompute-0.0.0/chalkcompute/_gen/buf/validate/__init__.py +0 -0
  14. chalkcompute-0.0.0/chalkcompute/_gen/buf/validate/validate_pb2.py +866 -0
  15. chalkcompute-0.0.0/chalkcompute/_gen/buf/validate/validate_pb2.pyi +1026 -0
  16. chalkcompute-0.0.0/chalkcompute/_gen/chalk/__init__.py +0 -0
  17. chalkcompute-0.0.0/chalkcompute/_gen/chalk/arrow/__init__.py +0 -0
  18. chalkcompute-0.0.0/chalkcompute/_gen/chalk/arrow/v1/__init__.py +0 -0
  19. chalkcompute-0.0.0/chalkcompute/_gen/chalk/arrow/v1/arrow_pb2.py +84 -0
  20. chalkcompute-0.0.0/chalkcompute/_gen/chalk/arrow/v1/arrow_pb2.pyi +549 -0
  21. chalkcompute-0.0.0/chalkcompute/_gen/chalk/arrow/v1/arrow_pb2_grpc.py +4 -0
  22. chalkcompute-0.0.0/chalkcompute/_gen/chalk/arrow/v1/arrow_pb2_grpc.pyi +4 -0
  23. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/__init__.py +0 -0
  24. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/__init__.py +0 -0
  25. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/agent_pb2.py +60 -0
  26. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/agent_pb2.pyi +195 -0
  27. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/agent_pb2_grpc.py +4 -0
  28. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/agent_pb2_grpc.pyi +4 -0
  29. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/audit_pb2.py +35 -0
  30. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/audit_pb2.pyi +27 -0
  31. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/audit_pb2_grpc.py +4 -0
  32. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/audit_pb2_grpc.pyi +4 -0
  33. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/displayagent_pb2.py +53 -0
  34. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/displayagent_pb2.pyi +228 -0
  35. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/displayagent_pb2_grpc.py +4 -0
  36. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/displayagent_pb2_grpc.pyi +4 -0
  37. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/featurepermission_pb2.py +36 -0
  38. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/featurepermission_pb2.pyi +41 -0
  39. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/featurepermission_pb2_grpc.py +4 -0
  40. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/featurepermission_pb2_grpc.pyi +4 -0
  41. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/permissions_pb2.py +176 -0
  42. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/permissions_pb2.pyi +87 -0
  43. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/permissions_pb2_grpc.py +4 -0
  44. chalkcompute-0.0.0/chalkcompute/_gen/chalk/auth/v1/permissions_pb2_grpc.pyi +4 -0
  45. chalkcompute-0.0.0/chalkcompute/_gen/chalk/container/__init__.py +0 -0
  46. chalkcompute-0.0.0/chalkcompute/_gen/chalk/container/v1/__init__.py +0 -0
  47. chalkcompute-0.0.0/chalkcompute/_gen/chalk/container/v1/service_pb2.py +145 -0
  48. chalkcompute-0.0.0/chalkcompute/_gen/chalk/container/v1/service_pb2.pyi +513 -0
  49. chalkcompute-0.0.0/chalkcompute/_gen/chalk/container/v1/service_pb2_grpc.py +534 -0
  50. chalkcompute-0.0.0/chalkcompute/_gen/chalk/container/v1/service_pb2_grpc.pyi +194 -0
  51. chalkcompute-0.0.0/chalkcompute/_gen/chalk/externalfunctioncatalog/__init__.py +0 -0
  52. chalkcompute-0.0.0/chalkcompute/_gen/chalk/externalfunctioncatalog/v1/__init__.py +0 -0
  53. chalkcompute-0.0.0/chalkcompute/_gen/chalk/externalfunctioncatalog/v1/service_pb2.py +113 -0
  54. chalkcompute-0.0.0/chalkcompute/_gen/chalk/externalfunctioncatalog/v1/service_pb2.pyi +368 -0
  55. chalkcompute-0.0.0/chalkcompute/_gen/chalk/externalfunctioncatalog/v1/service_pb2_grpc.py +307 -0
  56. chalkcompute-0.0.0/chalkcompute/_gen/chalk/externalfunctioncatalog/v1/service_pb2_grpc.pyi +98 -0
  57. chalkcompute-0.0.0/chalkcompute/_gen/chalk/sandbox/__init__.py +0 -0
  58. chalkcompute-0.0.0/chalkcompute/_gen/chalk/sandbox/v1/__init__.py +0 -0
  59. chalkcompute-0.0.0/chalkcompute/_gen/chalk/sandbox/v1/service_pb2.py +145 -0
  60. chalkcompute-0.0.0/chalkcompute/_gen/chalk/sandbox/v1/service_pb2.pyi +491 -0
  61. chalkcompute-0.0.0/chalkcompute/_gen/chalk/sandbox/v1/service_pb2_grpc.py +473 -0
  62. chalkcompute-0.0.0/chalkcompute/_gen/chalk/sandbox/v1/service_pb2_grpc.pyi +182 -0
  63. chalkcompute-0.0.0/chalkcompute/_gen/chalk/scalinggroup/__init__.py +0 -0
  64. chalkcompute-0.0.0/chalkcompute/_gen/chalk/scalinggroup/v1/__init__.py +0 -0
  65. chalkcompute-0.0.0/chalkcompute/_gen/chalk/scalinggroup/v1/service_pb2.py +104 -0
  66. chalkcompute-0.0.0/chalkcompute/_gen/chalk/scalinggroup/v1/service_pb2.pyi +314 -0
  67. chalkcompute-0.0.0/chalkcompute/_gen/chalk/scalinggroup/v1/service_pb2_grpc.py +350 -0
  68. chalkcompute-0.0.0/chalkcompute/_gen/chalk/scalinggroup/v1/service_pb2_grpc.pyi +124 -0
  69. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/__init__.py +0 -0
  70. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/__init__.py +0 -0
  71. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/encoding_pb2.py +37 -0
  72. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/encoding_pb2.pyi +23 -0
  73. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/encoding_pb2_grpc.py +4 -0
  74. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/encoding_pb2_grpc.pyi +4 -0
  75. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/field_change_pb2.py +32 -0
  76. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/field_change_pb2.pyi +42 -0
  77. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/field_change_pb2_grpc.py +4 -0
  78. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/field_change_pb2_grpc.pyi +4 -0
  79. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/sensitive_pb2.py +31 -0
  80. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/sensitive_pb2.pyi +7 -0
  81. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +4 -0
  82. chalkcompute-0.0.0/chalkcompute/_gen/chalk/utils/v1/sensitive_pb2_grpc.pyi +4 -0
  83. chalkcompute-0.0.0/chalkcompute/_gen/chalk/volume/__init__.py +0 -0
  84. chalkcompute-0.0.0/chalkcompute/_gen/chalk/volume/v1/__init__.py +0 -0
  85. chalkcompute-0.0.0/chalkcompute/_gen/chalk/volume/v1/volume_pb2.py +108 -0
  86. chalkcompute-0.0.0/chalkcompute/_gen/chalk/volume/v1/volume_pb2.pyi +228 -0
  87. chalkcompute-0.0.0/chalkcompute/_gen/chalk/volume/v1/volume_pb2_grpc.py +512 -0
  88. chalkcompute-0.0.0/chalkcompute/_gen/chalk/volume/v1/volume_pb2_grpc.pyi +161 -0
  89. chalkcompute-0.0.0/chalkcompute/_ignore.py +212 -0
  90. chalkcompute-0.0.0/chalkcompute/_image.py +822 -0
  91. chalkcompute-0.0.0/chalkcompute/_logging.py +45 -0
  92. chalkcompute-0.0.0/chalkcompute/_naming.py +70 -0
  93. chalkcompute-0.0.0/chalkcompute/_progress.py +430 -0
  94. chalkcompute-0.0.0/chalkcompute/_quota.py +61 -0
  95. chalkcompute-0.0.0/chalkcompute/_rate_limit.py +38 -0
  96. chalkcompute-0.0.0/chalkcompute/_remote_call_client.py +137 -0
  97. chalkcompute-0.0.0/chalkcompute/_remote_call_native.pyi +39 -0
  98. chalkcompute-0.0.0/chalkcompute/_retry.py +194 -0
  99. chalkcompute-0.0.0/chalkcompute/_sandbox.py +818 -0
  100. chalkcompute-0.0.0/chalkcompute/_scaling_group.py +597 -0
  101. chalkcompute-0.0.0/chalkcompute/_secret.py +331 -0
  102. chalkcompute-0.0.0/chalkcompute/_tracing.py +90 -0
  103. chalkcompute-0.0.0/chalkcompute/_volume.py +1111 -0
  104. chalkcompute-0.0.0/chalkcompute/py.typed +1 -0
  105. chalkcompute-0.0.0/pyproject.toml +50 -0
  106. chalkcompute-0.0.0/rust/chalkcompute_remote_client/Cargo.lock +1813 -0
  107. chalkcompute-0.0.0/rust/chalkcompute_remote_client/Cargo.toml +15 -0
  108. chalkcompute-0.0.0/rust/chalkcompute_remote_client/src/lib.rs +199 -0
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.4
2
+ Name: chalkcompute
3
+ Version: 0.0.0
4
+ Requires-Dist: grpcio>=1.60.0
5
+ Requires-Dist: protobuf>=4.25.0
6
+ Requires-Dist: httpx>=0.25.0
7
+ Requires-Dist: chalk-remote-call-python>=1.4.0
8
+ Requires-Dist: opentelemetry-api>=1.20.0
9
+ Requires-Dist: opentelemetry-sdk>=1.20.0
10
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0
11
+ Requires-Dist: rich>=13.0.0
12
+ Requires-Dist: attrs>=23.0.0 ; extra == 'dev'
13
+ Requires-Dist: chalkdf>=3.31.87 ; extra == 'dev'
14
+ Requires-Dist: chalkpy[runtime]>=2.114.0 ; extra == 'dev'
15
+ Requires-Dist: python-dotenv>=1.1.0 ; extra == 'dev'
16
+ Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
17
+ Provides-Extra: dev
18
+ Summary: SDK for Chalk sandboxes, containers, and volumes
19
+ Requires-Python: >=3.11, <3.15
20
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
+
22
+ # Chalk Sandbox SDK
23
+
24
+ Python SDK for the Chalk Sandbox gRPC service. Create sandboxes, execute commands, and stream output over bidirectional gRPC streams.
25
+
26
+ ## Install
27
+
28
+ ```
29
+ pip install grpcio protobuf
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from sandbox import SandboxClient
36
+
37
+ client = SandboxClient("localhost:50051")
38
+
39
+ # Create a sandbox from a pre-built image
40
+ sandbox = client.create(image="ubuntu:latest")
41
+
42
+ # Run a command
43
+ result = sandbox.exec("echo", "hello world")
44
+ print(result.stdout_text) # "hello world"
45
+ print(result.exit_code) # 0
46
+
47
+ # Clean up
48
+ sandbox.terminate()
49
+ ```
50
+
51
+ ## Declarative images
52
+
53
+ Build custom container images with a fluent API instead of writing Dockerfiles.
54
+ The image spec is serialized as protobuf and transmitted to the sandbox service,
55
+ which builds and caches the image before starting the container.
56
+
57
+ ```python
58
+ from sandbox import SandboxClient
59
+ from image import Image
60
+
61
+ client = SandboxClient("localhost:50051")
62
+
63
+ # Build a data-science image declaratively
64
+ img = (
65
+ Image.debian_slim("3.12")
66
+ .pip_install(["pandas", "numpy", "scikit-learn"])
67
+ .run_commands(
68
+ "apt-get update && apt-get install -y git curl",
69
+ )
70
+ .workdir("/home/user/app")
71
+ .env({"PYTHONDONTWRITEBYTECODE": "1"})
72
+ )
73
+
74
+ sandbox = client.create(image=img)
75
+ result = sandbox.exec("python", "-c", "import pandas; print(pandas.__version__)")
76
+ print(result.stdout_text)
77
+ ```
78
+
79
+ ### Base images
80
+
81
+ ```python
82
+ # Arbitrary base image
83
+ img = Image.base("node:22-slim")
84
+
85
+ # Convenience: python + debian slim
86
+ img = Image.debian_slim("3.12") # python:3.12-slim-bookworm
87
+
88
+ # From an existing Dockerfile (contents are inlined, so you can chain more steps)
89
+ img = Image.from_dockerfile("Dockerfile").pip_install(["extra-dep"])
90
+ ```
91
+
92
+ ### Build steps
93
+
94
+ ```python
95
+ img = (
96
+ Image.debian_slim("3.12")
97
+ # Install Python packages
98
+ .pip_install(["requests>=2.28", "flask"])
99
+
100
+ # Install from a requirements.txt (read locally, inlined into the spec)
101
+ .pip_install_from_requirements("requirements.txt")
102
+
103
+ # Run shell commands (each becomes a Docker RUN layer)
104
+ .run_commands(
105
+ "apt-get update && apt-get install -y git",
106
+ "mkdir -p /app/data",
107
+ )
108
+
109
+ # Add local files into the image
110
+ .add_local_file("config.yaml", "/app/config.yaml")
111
+ .add_local_file("entrypoint.sh", "/app/entrypoint.sh", mode=0o755)
112
+ .add_local_dir("src", "/app/src")
113
+
114
+ # Raw Dockerfile instructions
115
+ .dockerfile_commands(["EXPOSE 8080", "HEALTHCHECK CMD curl -f http://localhost:8080/"])
116
+
117
+ # Image-level configuration
118
+ .workdir("/app")
119
+ .env({"FLASK_APP": "app:create_app"})
120
+ .entrypoint(["/app/entrypoint.sh"])
121
+ .cmd(["serve"])
122
+ )
123
+ ```
124
+
125
+ ### Immutable composition
126
+
127
+ Each builder method returns a new `Image`, so intermediate images can be shared:
128
+
129
+ ```python
130
+ base = Image.debian_slim("3.12").pip_install(["requests"])
131
+
132
+ # Two different images that share the same base
133
+ api_image = base.pip_install(["flask"]).workdir("/api")
134
+ worker_image = base.pip_install(["celery"]).workdir("/worker")
135
+
136
+ api_sandbox = client.create(image=api_image)
137
+ worker_sandbox = client.create(image=worker_image)
138
+ ```
139
+
140
+ ## Connecting
141
+
142
+ ```python
143
+ from sandbox import SandboxClient
144
+ import grpc
145
+
146
+ # Insecure (local dev)
147
+ client = SandboxClient("localhost:50051")
148
+
149
+ # With TLS
150
+ creds = grpc.ssl_channel_credentials()
151
+ client = SandboxClient("sandbox.example.com:443", credentials=creds)
152
+
153
+ # As a context manager
154
+ with SandboxClient("localhost:50051") as client:
155
+ ...
156
+ ```
157
+
158
+ ## Sandbox lifecycle
159
+
160
+ ```python
161
+ # Create with resource limits
162
+ sandbox = client.create(
163
+ image="ubuntu:latest",
164
+ cpu="2",
165
+ memory="4Gi",
166
+ env={"DEBIAN_FRONTEND": "noninteractive"},
167
+ )
168
+
169
+ # List all sandboxes
170
+ for info in client.list():
171
+ print(f"{info.id} {info.state} {info.name}")
172
+
173
+ # Get a handle to an existing sandbox by ID
174
+ sandbox = client.get(id="550e8400-e29b-41d4-a716-446655440000")
175
+
176
+ # Fetch info from server
177
+ print(sandbox.info.state)
178
+ sandbox.refresh() # force re-fetch
179
+
180
+ # Terminate
181
+ sandbox.terminate()
182
+ sandbox.terminate(grace_period_seconds=30)
183
+ ```
184
+
185
+ ## Executing commands
186
+
187
+ ### Run and wait
188
+
189
+ ```python
190
+ result = sandbox.exec("ls", "-la", "/tmp")
191
+ for line in result.stdout:
192
+ print(line)
193
+ for line in result.stderr:
194
+ print(f"ERR: {line}")
195
+ print(f"exit code: {result.exit_code}")
196
+
197
+ # Or get the full text at once
198
+ print(result.stdout_text)
199
+ print(result.stderr_text)
200
+ ```
201
+
202
+ ### Stream output in real time
203
+
204
+ ```python
205
+ for event in sandbox.exec_stream("make", "build", workdir="/app"):
206
+ if event.stdout:
207
+ print(event.stdout, end="")
208
+ if event.stderr:
209
+ print(event.stderr, end="", file=sys.stderr)
210
+ if event.is_exited:
211
+ print(f"\nDone: exit code {event.exit_code}")
212
+ ```
213
+
214
+ ### Interactive processes (stdin + signals)
215
+
216
+ ```python
217
+ process = sandbox.exec_start("bash")
218
+
219
+ process.write_stdin("echo hello\n")
220
+ process.write_stdin("exit\n")
221
+ process.close_stdin()
222
+
223
+ for event in process.output():
224
+ if event.stdout:
225
+ print(event.stdout, end="")
226
+ ```
227
+
228
+ Send signals to running processes:
229
+
230
+ ```python
231
+ import signal
232
+
233
+ process = sandbox.exec_start("sleep", "300")
234
+ process.send_signal(signal.SIGTERM)
235
+ result = process.wait()
236
+ ```
237
+
238
+ ### Options
239
+
240
+ All exec methods accept the same keyword arguments:
241
+
242
+ ```python
243
+ result = sandbox.exec(
244
+ "python", "train.py",
245
+ workdir="/app", # working directory
246
+ timeout_secs=3600, # kill after 1 hour
247
+ env={"CUDA_VISIBLE_DEVICES": "0"}, # environment variables
248
+ )
249
+ ```
250
+
251
+ ## Examples
252
+
253
+ ### Clone a GitHub repo into a sandbox
254
+
255
+ ```python
256
+ from sandbox import SandboxClient
257
+
258
+ client = SandboxClient("localhost:50051")
259
+ sandbox = client.create(image="ubuntu:latest")
260
+
261
+ # Install git
262
+ sandbox.exec("apt-get", "update")
263
+ sandbox.exec("apt-get", "install", "-y", "git")
264
+
265
+ # Clone
266
+ result = sandbox.exec(
267
+ "git", "clone", "https://github.com/chalk-ai/chalk.git", "/workspace/chalk"
268
+ )
269
+ if result.exit_code != 0:
270
+ print(f"Clone failed: {result.stderr_text}")
271
+ else:
272
+ # List what we got
273
+ result = sandbox.exec("ls", "-la", "/workspace/chalk")
274
+ for line in result.stdout:
275
+ print(line)
276
+ ```
277
+
278
+ ### Spawn an OpenCode agent in a sandbox
279
+
280
+ [OpenCode](https://github.com/opencode-ai/opencode) is a terminal-based AI coding agent. You can run it inside a sandbox to give it an isolated environment to work in.
281
+
282
+ ```python
283
+ from sandbox import SandboxClient
284
+
285
+ client = SandboxClient("localhost:50051")
286
+ sandbox = client.create(
287
+ image="ubuntu:latest",
288
+ cpu="2",
289
+ memory="4Gi",
290
+ env={
291
+ "ANTHROPIC_API_KEY": "sk-ant-...",
292
+ },
293
+ )
294
+
295
+ # Install dependencies
296
+ sandbox.exec("apt-get", "update")
297
+ sandbox.exec("apt-get", "install", "-y", "git", "curl", "build-essential")
298
+
299
+ # Install Go (opencode is a Go binary)
300
+ sandbox.exec("bash", "-c", "curl -fsSL https://go.dev/dl/go1.24.1.linux-amd64.tar.gz | tar -C /usr/local -xz")
301
+ sandbox.exec("bash", "-c", "echo 'export PATH=$PATH:/usr/local/go/bin:/root/go/bin' >> /root/.bashrc")
302
+
303
+ # Install opencode
304
+ sandbox.exec("bash", "-c", "export PATH=$PATH:/usr/local/go/bin:/root/go/bin && go install github.com/opencode-ai/opencode@latest")
305
+
306
+ # Clone a repo to work on
307
+ sandbox.exec("git", "clone", "https://github.com/your-org/your-repo.git", "/workspace/repo")
308
+
309
+ # Run opencode non-interactively with a prompt
310
+ result = sandbox.exec(
311
+ "bash", "-c",
312
+ "export PATH=$PATH:/usr/local/go/bin:/root/go/bin && cd /workspace/repo && opencode -p 'fix the failing tests in pkg/auth'",
313
+ timeout_secs=600,
314
+ )
315
+ print(result.stdout_text)
316
+
317
+ # Or run it interactively and feed it commands
318
+ process = sandbox.exec_start(
319
+ "bash", "-c",
320
+ "export PATH=$PATH:/usr/local/go/bin:/root/go/bin && cd /workspace/repo && opencode",
321
+ )
322
+
323
+ # Stream its output
324
+ for event in process.output():
325
+ if event.stdout:
326
+ print(event.stdout, end="")
327
+ if event.stderr:
328
+ print(event.stderr, end="", file=sys.stderr)
329
+ if event.is_exited:
330
+ break
331
+ ```
332
+
333
+ ### Long-running build with real-time output
334
+
335
+ ```python
336
+ sandbox = client.create(image="node:22")
337
+
338
+ sandbox.exec("git", "clone", "https://github.com/your-org/frontend.git", "/app")
339
+ sandbox.exec("npm", "install", workdir="/app")
340
+
341
+ # Stream the build output as it happens
342
+ for event in sandbox.exec_stream("npm", "run", "build", workdir="/app"):
343
+ if event.stdout:
344
+ print(event.stdout, end="")
345
+ if event.stderr:
346
+ print(event.stderr, end="", file=sys.stderr)
347
+ if event.is_exited and event.exit_code != 0:
348
+ print(f"Build failed with exit code {event.exit_code}")
349
+
350
+ sandbox.terminate()
351
+ ```
352
+
353
+ ## CLI tools
354
+
355
+ ### `sandbox_exec.py` - Run a command
356
+
357
+ ```bash
358
+ python sandbox_exec.py --target localhost:50051 --sandbox-id <id> --exec "ls -la"
359
+ ```
360
+
361
+ ### `sandbox_stdout.py` - Interactive shell
362
+
363
+ ```bash
364
+ echo "echo hello" | python sandbox_stdout.py --target localhost:50051 --sandbox-id <id> --exec "bash"
365
+ ```
366
+
367
+ ## Regenerating proto stubs
368
+
369
+ If the proto definition changes, regenerate the Python stubs:
370
+
371
+ ```bash
372
+ pip install grpcio-tools
373
+ ./generate.sh
374
+ ```
375
+