quartermaster-code-runner 0.0.1__py3-none-any.whl

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 (40) hide show
  1. quartermaster_code_runner/__init__.py +38 -0
  2. quartermaster_code_runner/app.py +269 -0
  3. quartermaster_code_runner/config.py +175 -0
  4. quartermaster_code_runner/errors.py +88 -0
  5. quartermaster_code_runner/execution.py +231 -0
  6. quartermaster_code_runner/images.py +397 -0
  7. quartermaster_code_runner/runtime/bun/Dockerfile +22 -0
  8. quartermaster_code_runner/runtime/bun/completions.json +34 -0
  9. quartermaster_code_runner/runtime/bun/entrypoint.sh +32 -0
  10. quartermaster_code_runner/runtime/bun/sdk.ts +87 -0
  11. quartermaster_code_runner/runtime/deno/Dockerfile +22 -0
  12. quartermaster_code_runner/runtime/deno/completions.json +34 -0
  13. quartermaster_code_runner/runtime/deno/entrypoint.sh +32 -0
  14. quartermaster_code_runner/runtime/deno/sdk.ts +88 -0
  15. quartermaster_code_runner/runtime/go/Dockerfile +18 -0
  16. quartermaster_code_runner/runtime/go/completions.json +22 -0
  17. quartermaster_code_runner/runtime/go/entrypoint.sh +50 -0
  18. quartermaster_code_runner/runtime/go/sdk.go +101 -0
  19. quartermaster_code_runner/runtime/node/Dockerfile +31 -0
  20. quartermaster_code_runner/runtime/node/completions.json +34 -0
  21. quartermaster_code_runner/runtime/node/entrypoint.sh +33 -0
  22. quartermaster_code_runner/runtime/node/mcp-client.js +274 -0
  23. quartermaster_code_runner/runtime/node/sdk.js +109 -0
  24. quartermaster_code_runner/runtime/python/Dockerfile +42 -0
  25. quartermaster_code_runner/runtime/python/completions.json +34 -0
  26. quartermaster_code_runner/runtime/python/entrypoint.sh +30 -0
  27. quartermaster_code_runner/runtime/python/mcp-client.py +276 -0
  28. quartermaster_code_runner/runtime/python/sdk.py +103 -0
  29. quartermaster_code_runner/runtime/rust/Cargo.toml.default +9 -0
  30. quartermaster_code_runner/runtime/rust/Dockerfile +27 -0
  31. quartermaster_code_runner/runtime/rust/completions.json +34 -0
  32. quartermaster_code_runner/runtime/rust/entrypoint.sh +38 -0
  33. quartermaster_code_runner/runtime/rust/sdk/Cargo.toml +9 -0
  34. quartermaster_code_runner/runtime/rust/sdk/src/lib.rs +149 -0
  35. quartermaster_code_runner/schemas.py +154 -0
  36. quartermaster_code_runner/security.py +81 -0
  37. quartermaster_code_runner-0.0.1.dist-info/METADATA +322 -0
  38. quartermaster_code_runner-0.0.1.dist-info/RECORD +40 -0
  39. quartermaster_code_runner-0.0.1.dist-info/WHEEL +4 -0
  40. quartermaster_code_runner-0.0.1.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,231 @@
1
+ """Docker container execution logic.
2
+
3
+ Handles creating, running, and cleaning up Docker containers
4
+ for sandboxed code execution.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import io
12
+ import json
13
+ import logging
14
+ import tarfile
15
+ import time
16
+ import uuid
17
+ from typing import Any, Callable, Optional
18
+
19
+ import docker
20
+ import docker.models.containers
21
+ from docker.errors import ContainerError, ImageNotFound
22
+ from fastapi import HTTPException
23
+
24
+ from quartermaster_code_runner.config import (
25
+ METADATA_FILE_PATH,
26
+ PREBUILT_IMAGE_PREFIX,
27
+ get_image_name,
28
+ )
29
+ from quartermaster_code_runner.schemas import PrebuildSpec
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def get_docker_client(timeout: int = 600) -> docker.DockerClient:
35
+ """Create a Docker client from environment."""
36
+ return docker.from_env(timeout=timeout)
37
+
38
+
39
+ async def execute_code_in_container(
40
+ docker_client: docker.DockerClient,
41
+ image: str,
42
+ environment: dict[str, str],
43
+ timeout_seconds: int,
44
+ mem_limit: str,
45
+ cpu_shares: int,
46
+ disk_limit: str,
47
+ network_disabled: bool,
48
+ prebuild_spec: Optional[PrebuildSpec] = None,
49
+ ensure_prebuilt_fn: Optional[Callable[..., Any]] = None,
50
+ ) -> tuple[str, str, dict[str, Any], Optional[dict[str, Any]]]:
51
+ """Execute code in a Docker container.
52
+
53
+ Args:
54
+ docker_client: Docker client instance.
55
+ image: Short or full image name.
56
+ environment: Environment variables for the container.
57
+ timeout_seconds: Hard timeout in seconds.
58
+ mem_limit: Memory limit string (e.g., "256m").
59
+ cpu_shares: CPU shares allocation.
60
+ disk_limit: Disk limit for tmpfs mounts.
61
+ network_disabled: Whether to disable networking.
62
+ prebuild_spec: Optional prebuild specification.
63
+ ensure_prebuilt_fn: Function to ensure prebuilt image exists.
64
+
65
+ Returns:
66
+ Tuple of (stdout, stderr, result_dict, metadata).
67
+ """
68
+ container = None
69
+ stdout = ""
70
+ stderr = ""
71
+ result: dict[str, Any] = {}
72
+ metadata = None
73
+ full_image_name = get_image_name(image)
74
+
75
+ # Handle prebuilt image rebuilds
76
+ if image.startswith(PREBUILT_IMAGE_PREFIX) and prebuild_spec and ensure_prebuilt_fn:
77
+ try:
78
+ await asyncio.to_thread(
79
+ ensure_prebuilt_fn,
80
+ image,
81
+ prebuild_spec,
82
+ )
83
+ except Exception as e:
84
+ raise HTTPException(
85
+ status_code=503,
86
+ detail=f"Prebuilt image '{full_image_name}' could not be rebuilt: {e}",
87
+ )
88
+
89
+ # Create a temporary volume for metadata exchange
90
+ volume_name = f"quartermaster_metadata_{uuid.uuid4().hex[:12]}"
91
+ volume = None
92
+
93
+ try:
94
+ volume = await asyncio.to_thread(
95
+ docker_client.volumes.create,
96
+ name=volume_name,
97
+ )
98
+
99
+ volumes_dict = {volume_name: {"bind": "/metadata", "mode": "rw"}}
100
+
101
+ t_run = time.monotonic()
102
+ container = await asyncio.to_thread(
103
+ docker_client.containers.run,
104
+ image=full_image_name,
105
+ environment=environment,
106
+ working_dir="/tmp",
107
+ mem_limit=mem_limit,
108
+ cpu_shares=cpu_shares,
109
+ detach=True,
110
+ network_disabled=network_disabled,
111
+ read_only=True,
112
+ tmpfs={
113
+ "/tmp": f"size={disk_limit},exec",
114
+ "/workspace": f"size={disk_limit},exec",
115
+ },
116
+ volumes=volumes_dict,
117
+ )
118
+ logger.info(
119
+ "[exec] container started in %.2fs image=%s",
120
+ time.monotonic() - t_run,
121
+ full_image_name,
122
+ )
123
+
124
+ # Wait for container to finish
125
+ t_wait = time.monotonic()
126
+ wait_response = await asyncio.to_thread(container.wait, timeout=timeout_seconds)
127
+ logger.info(
128
+ "[exec] container finished in %.2fs exit=%s",
129
+ time.monotonic() - t_wait,
130
+ wait_response.get("StatusCode"),
131
+ )
132
+ result = {"StatusCode": wait_response["StatusCode"]}
133
+
134
+ # Collect output
135
+ stdout_bytes = await asyncio.to_thread(
136
+ container.logs, stdout=True, stderr=False
137
+ )
138
+ stdout = stdout_bytes.decode("utf-8")
139
+ stderr_bytes = await asyncio.to_thread(
140
+ container.logs, stdout=False, stderr=True
141
+ )
142
+ stderr = stderr_bytes.decode("utf-8")
143
+
144
+ # Read metadata from volume
145
+ metadata = await _read_metadata(container)
146
+
147
+ except ContainerError as e:
148
+ if e.container:
149
+ stderr_bytes = await asyncio.to_thread(
150
+ e.container.logs, stdout=False, stderr=True
151
+ )
152
+ stderr = stderr_bytes.decode("utf-8")
153
+ result = {"StatusCode": e.exit_status}
154
+ else:
155
+ stderr = str(e)
156
+ except ImageNotFound:
157
+ raise HTTPException(
158
+ status_code=404,
159
+ detail=f"Image '{full_image_name}' not found. "
160
+ f"Build it with: make build-runtime lang={image}",
161
+ )
162
+ except Exception as e:
163
+ if container:
164
+ with contextlib.suppress(Exception):
165
+ stdout_bytes = await asyncio.to_thread(
166
+ container.logs, stdout=True, stderr=False
167
+ )
168
+ stdout = stdout_bytes.decode("utf-8")
169
+ with contextlib.suppress(Exception):
170
+ stderr_bytes = await asyncio.to_thread(
171
+ container.logs, stdout=False, stderr=True
172
+ )
173
+ stderr = stderr_bytes.decode("utf-8")
174
+ with contextlib.suppress(Exception):
175
+ await asyncio.to_thread(container.stop, timeout=2)
176
+ result = {"StatusCode": -1}
177
+ else:
178
+ raise HTTPException(
179
+ status_code=500,
180
+ detail=f"An unexpected error occurred: {e}",
181
+ )
182
+ finally:
183
+ if container:
184
+ with contextlib.suppress(Exception):
185
+ await asyncio.to_thread(container.remove, v=True)
186
+ if volume:
187
+ with contextlib.suppress(Exception):
188
+ await asyncio.to_thread(volume.remove)
189
+
190
+ return stdout, stderr, result, metadata
191
+
192
+
193
+ async def _read_metadata(
194
+ container: docker.models.containers.Container,
195
+ ) -> Optional[dict[str, Any]]:
196
+ """Read metadata JSON from the container's metadata volume.
197
+
198
+ Returns None if no metadata file exists.
199
+ """
200
+ try:
201
+ bits, _stat = await asyncio.to_thread(container.get_archive, METADATA_FILE_PATH)
202
+ tar_stream = io.BytesIO()
203
+ for chunk in bits:
204
+ tar_stream.write(chunk)
205
+ tar_stream.seek(0)
206
+ with tarfile.open(fileobj=tar_stream, mode="r") as tar:
207
+ member = tar.getmembers()[0]
208
+ f = tar.extractfile(member)
209
+ if f:
210
+ result: dict[str, Any] = json.loads(f.read().decode("utf-8"))
211
+ return result
212
+ except docker.errors.NotFound:
213
+ pass
214
+ except Exception as e:
215
+ logger.warning(
216
+ "[exec] metadata read failed: %s: %s",
217
+ type(e).__name__,
218
+ e,
219
+ )
220
+ return None
221
+
222
+
223
+ def cleanup_orphaned_containers(docker_client: docker.DockerClient) -> None:
224
+ """Remove leftover metadata volumes from previous runs.
225
+
226
+ Called on startup to prevent stale resources.
227
+ """
228
+ for v in docker_client.volumes.list(filters={"name": "quartermaster_metadata_"}):
229
+ with contextlib.suppress(Exception):
230
+ v.remove(force=True)
231
+ logger.info("Removed orphaned metadata volume %s", v.name)
@@ -0,0 +1,397 @@
1
+ """Docker image management for runtime and prebuilt images.
2
+
3
+ Handles building, listing, and cleaning up runtime and prebuilt Docker images.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ import json
10
+ import logging
11
+ import os
12
+ import tempfile
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Optional
15
+
16
+ import docker
17
+ from docker.errors import BuildError, ImageNotFound
18
+ from fastapi import APIRouter, HTTPException, Query
19
+
20
+ from quartermaster_code_runner.config import (
21
+ DEFAULT_IMAGE,
22
+ PREBUILT_IMAGE_PREFIX,
23
+ SUPPORTED_IMAGES,
24
+ get_image_name,
25
+ get_short_image_name,
26
+ )
27
+ from quartermaster_code_runner.schemas import PrebuildRequest
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ router = APIRouter()
32
+
33
+ # Module-level docker client, set during app startup
34
+ _docker_client: Optional[docker.DockerClient] = None
35
+ _runtime_dir: str = ""
36
+ _verify_auth: Optional[object] = None
37
+
38
+
39
+ def configure_images(
40
+ docker_client: docker.DockerClient,
41
+ runtime_dir: str,
42
+ verify_auth_dep: object,
43
+ ) -> None:
44
+ """Configure image management module.
45
+
46
+ Args:
47
+ docker_client: Docker client instance.
48
+ runtime_dir: Path to runtime directory containing Dockerfiles.
49
+ verify_auth_dep: FastAPI dependency for auth verification.
50
+ """
51
+ global _docker_client, _runtime_dir, _verify_auth
52
+ _docker_client = docker_client
53
+ _runtime_dir = runtime_dir
54
+ _verify_auth = verify_auth_dep
55
+
56
+
57
+ def _get_client() -> docker.DockerClient:
58
+ if _docker_client is None:
59
+ raise RuntimeError(
60
+ "Image management not configured. Call configure_images() first."
61
+ )
62
+ return _docker_client
63
+
64
+
65
+ def _safe_list_images() -> list[Any]:
66
+ """List Docker images, handling race conditions."""
67
+ client = _get_client()
68
+ images = []
69
+ for img_summary in client.api.images():
70
+ img_id = img_summary.get("Id", "")
71
+ try:
72
+ images.append(client.images.get(img_id))
73
+ except ImageNotFound:
74
+ continue
75
+ return images
76
+
77
+
78
+ def cleanup_prebuilds(max_age_days: int = 7) -> list[str]:
79
+ """Remove prebuilt images older than max_age_days."""
80
+ client = _get_client()
81
+ now = datetime.now(timezone.utc)
82
+ removed: list[str] = []
83
+ targets: list[str] = []
84
+
85
+ for img in _safe_list_images():
86
+ for tag in img.tags or []:
87
+ tag_name = tag.split(":")[0]
88
+ if not tag_name.startswith(PREBUILT_IMAGE_PREFIX):
89
+ continue
90
+ created_str = img.attrs.get("Created", "")
91
+ if not created_str:
92
+ continue
93
+ created = datetime.fromisoformat(created_str.replace("Z", "+00:00"))
94
+ age_days = (now - created).total_seconds() / 86400
95
+ if age_days >= max_age_days:
96
+ targets.append(tag_name)
97
+
98
+ for tag_name in targets:
99
+ with contextlib.suppress(Exception):
100
+ client.images.remove(image=tag_name, force=True)
101
+ removed.append(tag_name)
102
+
103
+ return removed
104
+
105
+
106
+ def ensure_runtime_image(short_name: str) -> bool:
107
+ """Build a single runtime image if it's missing. Returns True if rebuilt."""
108
+ client = _get_client()
109
+ full_name = get_image_name(short_name)
110
+ try:
111
+ client.images.get(full_name)
112
+ return False
113
+ except ImageNotFound:
114
+ pass
115
+
116
+ build_path = os.path.join(_runtime_dir, short_name)
117
+ if not os.path.exists(build_path):
118
+ return False
119
+
120
+ logger.info("Auto-building missing runtime image '%s'...", full_name)
121
+ client.images.build(
122
+ path=build_path,
123
+ dockerfile="Dockerfile",
124
+ tag=full_name,
125
+ rm=True,
126
+ )
127
+ logger.info("Runtime image '%s' built successfully.", full_name)
128
+ return True
129
+
130
+
131
+ def ensure_prebuilt_image(image_name: str, spec: PrebuildRequest | object) -> bool:
132
+ """Rebuild a prebuilt image on the fly if it's missing."""
133
+ client = _get_client()
134
+ try:
135
+ client.images.get(image_name)
136
+ return False
137
+ except ImageNotFound:
138
+ pass
139
+
140
+ base_image = getattr(spec, "base_image", "python")
141
+ setup_script = getattr(spec, "setup_script", "")
142
+
143
+ tag = image_name.removeprefix(PREBUILT_IMAGE_PREFIX)
144
+ logger.info("Auto-rebuilding missing prebuilt image '%s'...", image_name)
145
+ ensure_runtime_image(get_short_image_name(base_image))
146
+ _build_prebuilt_image(tag, base_image, setup_script)
147
+ logger.info("Prebuilt image '%s' rebuilt successfully.", image_name)
148
+ return True
149
+
150
+
151
+ def build_runtime_images() -> None:
152
+ """Build all runtime Docker images."""
153
+ client = _get_client()
154
+ for image_name in SUPPORTED_IMAGES:
155
+ full_name = get_image_name(image_name)
156
+ build_path = os.path.join(_runtime_dir, image_name)
157
+
158
+ if not os.path.exists(build_path):
159
+ logger.warning(
160
+ "Skipping '%s' - directory not found at %s",
161
+ image_name,
162
+ build_path,
163
+ )
164
+ continue
165
+
166
+ try:
167
+ client.images.remove(image=full_name, force=True)
168
+ logger.info("Removed old runtime image '%s'.", full_name)
169
+ except ImageNotFound:
170
+ pass
171
+
172
+ logger.info("Building runtime image '%s'...", full_name)
173
+ try:
174
+ client.images.build(
175
+ path=build_path,
176
+ dockerfile="Dockerfile",
177
+ tag=full_name,
178
+ rm=True,
179
+ )
180
+ logger.info("Runtime image '%s' built successfully.", full_name)
181
+ except BuildError as e:
182
+ logger.error("Failed to build runtime image '%s': %s", full_name, e)
183
+ raise
184
+
185
+
186
+ # =============================================================================
187
+ # Prebuilt image helpers
188
+ # =============================================================================
189
+
190
+
191
+ def _get_prebuilt_image_tag(tag: str) -> str:
192
+ if tag.startswith(PREBUILT_IMAGE_PREFIX):
193
+ return tag
194
+ return f"{PREBUILT_IMAGE_PREFIX}{tag}"
195
+
196
+
197
+ def _get_image_size_bytes(image_name: str) -> int:
198
+ client = _get_client()
199
+ try:
200
+ img = client.images.get(image_name)
201
+ return img.attrs.get("Size", 0) # type: ignore[no-any-return]
202
+ except ImageNotFound:
203
+ return 0
204
+
205
+
206
+ def _build_prebuilt_image(
207
+ tag: str, base_image: str, setup_script: str
208
+ ) -> dict[str, Any]:
209
+ client = _get_client()
210
+ full_tag = _get_prebuilt_image_tag(tag)
211
+ base_full = get_image_name(base_image)
212
+
213
+ dockerfile_content = (
214
+ f"FROM {base_full}\n"
215
+ f"COPY setup.sh /tmp/setup.sh\n"
216
+ f"RUN sh /tmp/setup.sh && rm /tmp/setup.sh\n"
217
+ )
218
+
219
+ with contextlib.suppress(ImageNotFound):
220
+ client.images.remove(image=full_tag, force=True)
221
+
222
+ with tempfile.TemporaryDirectory() as tmpdir:
223
+ dockerfile_path = os.path.join(tmpdir, "Dockerfile")
224
+ with open(dockerfile_path, "w") as f:
225
+ f.write(dockerfile_content)
226
+
227
+ setup_path = os.path.join(tmpdir, "setup.sh")
228
+ with open(setup_path, "w") as f:
229
+ f.write(setup_script)
230
+
231
+ client.images.build(
232
+ path=tmpdir,
233
+ dockerfile="Dockerfile",
234
+ tag=full_tag,
235
+ rm=True,
236
+ )
237
+
238
+ size_bytes = _get_image_size_bytes(full_tag)
239
+ base_size = _get_image_size_bytes(base_full)
240
+ layer_size = max(size_bytes - base_size, 0)
241
+
242
+ return {
243
+ "tag": full_tag,
244
+ "base_image": base_full,
245
+ "size_bytes": size_bytes,
246
+ "layer_size_bytes": layer_size,
247
+ "status": "ready",
248
+ }
249
+
250
+
251
+ # =============================================================================
252
+ # API Endpoints
253
+ # =============================================================================
254
+
255
+
256
+ @router.get("/images")
257
+ def list_images() -> dict[str, Any]:
258
+ """List available runtime images with metadata from Docker labels."""
259
+ client = _get_client()
260
+ images = []
261
+
262
+ for short_name in SUPPORTED_IMAGES:
263
+ full_name = get_image_name(short_name)
264
+ image_data: dict[str, Any] = {
265
+ "id": full_name,
266
+ "name": short_name.capitalize(),
267
+ "description": f"{short_name.capitalize()} runtime",
268
+ "default_entrypoint": f"{short_name} main.py",
269
+ "file_extension": ".py",
270
+ "main_file": "main.py",
271
+ "completions": [],
272
+ }
273
+
274
+ try:
275
+ image = client.images.get(full_name)
276
+ labels = image.labels or {}
277
+ for label_key, data_key in [
278
+ ("qm.name", "name"),
279
+ ("qm.description", "description"),
280
+ ("qm.default_entrypoint", "default_entrypoint"),
281
+ ("qm.file_extension", "file_extension"),
282
+ ("qm.main_file", "main_file"),
283
+ ]:
284
+ if label_key in labels:
285
+ image_data[data_key] = labels[label_key]
286
+ except ImageNotFound:
287
+ pass
288
+
289
+ completions_path = os.path.join(_runtime_dir, short_name, "completions.json")
290
+ if os.path.exists(completions_path):
291
+ with open(completions_path) as f:
292
+ image_data["completions"] = json.load(f)
293
+
294
+ images.append(image_data)
295
+
296
+ return {
297
+ "images": images,
298
+ "default": get_image_name(DEFAULT_IMAGE),
299
+ }
300
+
301
+
302
+ @router.post("/prebuild")
303
+ async def prebuild_image(payload: PrebuildRequest) -> dict[str, Any]:
304
+ """Build a prebuilt Docker image extending a base runtime."""
305
+ import asyncio
306
+
307
+ try:
308
+ result = await asyncio.to_thread(
309
+ _build_prebuilt_image,
310
+ payload.tag,
311
+ payload.base_image,
312
+ payload.setup_script,
313
+ )
314
+ return result
315
+ except BuildError as e:
316
+ build_log = ""
317
+ if hasattr(e, "build_log"):
318
+ for chunk in e.build_log:
319
+ if "stream" in chunk:
320
+ build_log += chunk["stream"]
321
+ elif "error" in chunk:
322
+ build_log += chunk["error"]
323
+ raise HTTPException(
324
+ status_code=400,
325
+ detail=f"Build failed: {build_log or str(e)}",
326
+ )
327
+ except Exception as e:
328
+ raise HTTPException(
329
+ status_code=500,
330
+ detail=f"Prebuild failed: {e}",
331
+ )
332
+
333
+
334
+ @router.get("/prebuilds")
335
+ def list_prebuilds() -> dict[str, Any]:
336
+ """List all prebuilt images with their sizes."""
337
+ prebuilds: list[dict[str, Any]] = []
338
+ for img in _safe_list_images():
339
+ for tag in img.tags or []:
340
+ tag_name = tag.split(":")[0]
341
+ if tag_name.startswith(PREBUILT_IMAGE_PREFIX):
342
+ labels = img.labels or {}
343
+ prebuilds.append(
344
+ {
345
+ "tag": tag_name,
346
+ "size_bytes": img.attrs.get("Size", 0),
347
+ "created": img.attrs.get("Created", ""),
348
+ "labels": labels,
349
+ }
350
+ )
351
+ return {"prebuilds": prebuilds}
352
+
353
+
354
+ @router.get("/prebuilds/{tag}")
355
+ def get_prebuild(tag: str) -> dict[str, Any]:
356
+ """Get details of a specific prebuilt image."""
357
+ client = _get_client()
358
+ full_tag = _get_prebuilt_image_tag(tag)
359
+ try:
360
+ img = client.images.get(full_tag)
361
+ return {
362
+ "tag": full_tag,
363
+ "size_bytes": img.attrs.get("Size", 0),
364
+ "created": img.attrs.get("Created", ""),
365
+ "labels": img.labels or {},
366
+ }
367
+ except ImageNotFound:
368
+ raise HTTPException(
369
+ status_code=404,
370
+ detail=f"Prebuilt image '{full_tag}' not found.",
371
+ )
372
+
373
+
374
+ @router.delete("/prebuilds/{tag}")
375
+ def delete_prebuild(tag: str) -> dict[str, Any]:
376
+ """Delete a prebuilt image."""
377
+ client = _get_client()
378
+ full_tag = _get_prebuilt_image_tag(tag)
379
+ try:
380
+ client.images.remove(image=full_tag, force=True)
381
+ return {"status": "deleted", "tag": full_tag}
382
+ except ImageNotFound:
383
+ raise HTTPException(
384
+ status_code=404,
385
+ detail=f"Prebuilt image '{full_tag}' not found.",
386
+ )
387
+
388
+
389
+ @router.post("/prebuilds/cleanup")
390
+ async def cleanup_prebuilt_images(
391
+ max_age_days: int = Query(default=7, ge=0),
392
+ ) -> dict[str, Any]:
393
+ """Remove prebuilt images not used in max_age_days."""
394
+ import asyncio
395
+
396
+ removed = await asyncio.to_thread(cleanup_prebuilds, max_age_days)
397
+ return {"removed": removed, "count": len(removed)}
@@ -0,0 +1,22 @@
1
+ FROM oven/bun:1-slim
2
+
3
+ LABEL qm.name="Bun"
4
+ LABEL qm.description="Bun runtime with TypeScript support"
5
+ LABEL qm.default_entrypoint="bun main.ts"
6
+ LABEL qm.file_extension=".ts"
7
+ LABEL qm.main_file="main.ts"
8
+
9
+ WORKDIR /app
10
+
11
+ RUN apt-get update && \
12
+ apt-get install -y tar && \
13
+ rm -rf /var/lib/apt/lists/* && \
14
+ useradd --uid 1001 --no-create-home --shell /bin/sh runner && \
15
+ mkdir -p /home/runner/.bun && \
16
+ chown -R runner:runner /home/runner
17
+
18
+ COPY entrypoint.sh .
19
+ COPY sdk.ts .
20
+ RUN chmod +x entrypoint.sh
21
+
22
+ ENTRYPOINT ["/app/entrypoint.sh"]
@@ -0,0 +1,34 @@
1
+ [
2
+ {
3
+ "caption": "import { setMetadata } from './sdk'",
4
+ "value": "import { setMetadata } from './sdk';",
5
+ "meta": "import",
6
+ "score": 1000
7
+ },
8
+ {
9
+ "caption": "import { getMetadata } from './sdk'",
10
+ "value": "import { getMetadata } from './sdk';",
11
+ "meta": "import",
12
+ "score": 1000
13
+ },
14
+ {
15
+ "caption": "import { setMetadata, getMetadata } from './sdk'",
16
+ "value": "import { setMetadata, getMetadata } from './sdk';",
17
+ "meta": "import",
18
+ "score": 1000
19
+ },
20
+ {
21
+ "caption": "setMetadata",
22
+ "snippet": "setMetadata($1)",
23
+ "meta": "sdk",
24
+ "score": 1000,
25
+ "docText": "Set structured result metadata to be returned to the backend. Accepts any JSON-serializable data."
26
+ },
27
+ {
28
+ "caption": "getMetadata",
29
+ "snippet": "getMetadata()",
30
+ "meta": "sdk",
31
+ "score": 1000,
32
+ "docText": "Get previously set metadata. Returns null if not set."
33
+ }
34
+ ]