mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -457,6 +457,87 @@ async def test_provision_mongo_features_are_a_noop() -> None:
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Feature-aware image selection: kitchen-sink image only when features are
|
||||
# requested, bare provisions stay on the light upstream image (no heavier pull).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _run_call(runner: _FakeRunner) -> list[str]:
|
||||
runs = [c for c in runner.calls if c[0] == "run"]
|
||||
assert runs, "no docker run call recorded"
|
||||
return runs[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provision_pg_features_uses_kitchen_sink_image() -> None:
|
||||
runner = _FakeRunner(run_rc=0, exec_rc=0)
|
||||
runner.exec_out = b"1\n"
|
||||
provisioner = SandboxProvisioner(network=_NETWORK, runner=runner)
|
||||
|
||||
await provisioner.provision(
|
||||
"dev-pgimg", ["postgres"], features={"postgres": ["vector"]}
|
||||
)
|
||||
|
||||
joined = " ".join(_run_call(runner))
|
||||
assert "roboco-sandbox-pg:latest" in joined
|
||||
assert "postgres:16-alpine" not in joined
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provision_pg_bare_uses_light_image() -> None:
|
||||
"""Bare pg (no extensions) stays on the light upstream image — no heavier pull."""
|
||||
runner = _FakeRunner(run_rc=0, exec_rc=0)
|
||||
provisioner = SandboxProvisioner(network=_NETWORK, runner=runner)
|
||||
|
||||
await provisioner.provision("dev-pgbare", ["postgres"])
|
||||
|
||||
joined = " ".join(_run_call(runner))
|
||||
assert "postgres:16-alpine" in joined
|
||||
# The container is named roboco-sandbox-pg-<agent> regardless of image, so
|
||||
# check the full image ref (with tag) — the name carries no :latest.
|
||||
assert "roboco-sandbox-pg:latest" not in joined
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provision_redis_features_uses_redis_stack_server() -> None:
|
||||
runner = _FakeRunner(run_rc=0, exec_rc=0)
|
||||
runner.exec_out = b"search\n99999\n"
|
||||
provisioner = SandboxProvisioner(network=_NETWORK, runner=runner)
|
||||
|
||||
await provisioner.provision(
|
||||
"dev-redisimg", ["redis"], features={"redis": ["search"]}
|
||||
)
|
||||
|
||||
joined = " ".join(_run_call(runner))
|
||||
assert "redis/redis-stack-server:latest" in joined
|
||||
assert "redis:8-alpine" not in joined
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provision_redis_bare_uses_light_image() -> None:
|
||||
runner = _FakeRunner(run_rc=0, exec_rc=0)
|
||||
provisioner = SandboxProvisioner(network=_NETWORK, runner=runner)
|
||||
|
||||
await provisioner.provision("dev-redisbare", ["redis"])
|
||||
|
||||
joined = " ".join(_run_call(runner))
|
||||
assert "redis:8-alpine" in joined
|
||||
assert "redis/redis-stack-server:latest" not in joined
|
||||
|
||||
|
||||
def test_engine_image_for_selects_kitchen_sink_iff_features() -> None:
|
||||
"""Pure unit check on the registry: bare -> light image, features -> kitchen-sink;
|
||||
mongo (no activatable features) ignores features and returns its base image."""
|
||||
pg = sandbox_module.SANDBOX_ENGINES["postgres"]
|
||||
assert pg.image_for([]) == "postgres:16-alpine"
|
||||
assert pg.image_for(["vector"]) == "roboco-sandbox-pg:latest"
|
||||
redis = sandbox_module.SANDBOX_ENGINES["redis"]
|
||||
assert redis.image_for([]) == "redis:8-alpine"
|
||||
assert redis.image_for(["search"]) == "redis/redis-stack-server:latest"
|
||||
assert sandbox_module.SANDBOX_ENGINES["mongo"].image_for(["anything"]) == "mongo:8"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_provision_rejects_unallowed_pg_extension() -> None:
|
||||
"""plpython3u is a superuser-RCE vector — the allowlist rejects it before
|
||||
|
||||
Reference in New Issue
Block a user