sandbox: kitchen-sink images, feature-aware selection (Phase 2)

Phase 1 made the provisioner able to activate allowlisted extensions
post-ready but kept the bare upstream images. Phase 2 ships the images that
actually carry the extension/module files, and selects them only when a
venture requests features — bare sandboxes stay on the light upstream image
(no heavier pull, honoring the 'existing opters stay bare' decision).

- _PostgresEngine / _RedisEngine gain kitchen_sink_image + image_for(features):
  bare (no features) -> the light image; features requested -> the kitchen-sink
  image. The provisioner runs engine.image_for(features), not engine.image, so
  the bare path is byte-for-byte unchanged. Mongo inherits the base image_for
  (returns its image regardless — no activatable features).
- docker/sandbox-pg.Dockerfile: pgvector/pgvector:pg16 (ships vector) + postgis
  apt install; contrib (pg_trgm/citext/uuid-ossp) inherited from the official
  postgres base. Built at deploy via the sandbox-pg-image compose one-shot
  (mirrors the agent-image builders); the provisioner's _ensure_image finds the
  local tag and never pulls. Published by release.yml; pulled in registry
  compose. The verify step fails loudly if an extension's files are missing.
- _RedisEngine kitchen-sink image: redis/redis-stack-server:latest (headless;
  ships search/json/bloom as loadable-but-unloaded modules — no custom build).
- Extended the sandbox image-tag ghost-tag guard (the mongo:8-alpine regression
  test) to also cover kitchen_sink_image: skips locally-built roboco-* images,
  uses the namespaced Docker Hub endpoint for redis/redis-stack-server.

Image-specific package names / module .so paths are verified at the CEO's NAS
deploy (the spec's NAS smoke); the unit tests with the fake runner remain the
CI bar, and the verify step is the fail-loud safety net for a wrong build.
This commit is contained in:
Renn F
2026-07-13 20:05:45 +02:00
committed by Renzo F
parent b015cde9ad
commit 3838d64eaa
8 changed files with 196 additions and 10 deletions
+40 -8
View File
@@ -5,8 +5,15 @@ a tag that has never existed on Docker Hub (MongoDB ships no Alpine variant).
Every unit test mocks the docker CLI, so none of them ever touch a real
registry and none caught it — the bug only surfaces the moment a real
``docker run`` pulls the image. This test queries the Docker Hub registry API
for every ``SANDBOX_ENGINES`` entry's pinned ``image:tag`` and fails if the
tag does not actually exist, which is the check that would have caught it.
for every upstream image a sandbox engine may run (the bare ``image`` AND any
``kitchen_sink_image`` used when extensions/modules are requested) and fails if
the tag does not actually exist — the check that would have caught it.
Locally-built images (the ``roboco-sandbox-pg`` kitchen-sink, built by the
``sandbox-pg-image`` compose service) are skipped: they aren't on Docker Hub.
Namespaced upstream images (``redis/redis-stack-server``) use the namespaced
registry endpoint; bare library images (``postgres``, ``redis``, ``mongo``)
use the ``library/`` endpoint.
Network-dependent by design; skips cleanly when the registry is unreachable
rather than failing (mirrors ``test_background_engines.py``'s local-Redis
@@ -19,9 +26,12 @@ import httpx
import pytest
from roboco.models.sandbox import SANDBOX_ENGINES
_REGISTRY_URL = (
# Docker Hub registry endpoints — library images live under ``library/``;
# namespaced images (e.g. ``redis/redis-stack-server``) live under their ns.
_REGISTRY_URL_LIBRARY = (
"https://registry.hub.docker.com/v2/repositories/library/{name}/tags/{tag}"
)
_REGISTRY_URL_NS = "https://registry.hub.docker.com/v2/repositories/{name}/tags/{tag}"
_TIMEOUT_SECONDS = 10.0
_HTTP_OK = 200
@@ -31,11 +41,33 @@ def _split_image(image: str) -> tuple[str, str]:
return name, tag or "latest"
@pytest.mark.parametrize("engine_name", sorted(SANDBOX_ENGINES))
def test_sandbox_engine_image_tag_exists_on_docker_hub(engine_name: str) -> None:
image = SANDBOX_ENGINES[engine_name].image
def _engine_images() -> list[tuple[str, str]]:
"""(engine_name, image) pairs for every upstream image an engine may run."""
pairs: list[tuple[str, str]] = []
for name, engine in sorted(SANDBOX_ENGINES.items()):
pairs.append((name, engine.image))
kitchen = getattr(engine, "kitchen_sink_image", None)
if kitchen:
pairs.append((name, kitchen))
return pairs
@pytest.mark.parametrize(
"engine_name,image",
_engine_images(),
ids=[f"{n}={img}" for n, img in _engine_images()],
)
def test_sandbox_engine_image_tag_exists_on_docker_hub(
engine_name: str, image: str
) -> None:
name, tag = _split_image(image)
url = _REGISTRY_URL.format(name=name, tag=tag)
# Locally-built project images (no registry namespace, roboco- prefix) are
# built by a compose service, not pulled — skip the Docker Hub check.
if "/" not in name and name.startswith("roboco-"):
pytest.skip(f"{image!r} is a locally-built image (not on Docker Hub)")
url = (_REGISTRY_URL_NS if "/" in name else _REGISTRY_URL_LIBRARY).format(
name=name, tag=tag
)
try:
resp = httpx.get(url, timeout=_TIMEOUT_SECONDS)
@@ -44,5 +76,5 @@ def test_sandbox_engine_image_tag_exists_on_docker_hub(engine_name: str) -> None
assert resp.status_code == _HTTP_OK, (
f"{engine_name}: pinned image {image!r} not found on Docker Hub "
f"(library/{name}, tag {tag!r}, status {resp.status_code}) — {url}"
f"({name}, tag {tag!r}, status {resp.status_code}) — {url}"
)