Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,70 @@

## Unreleased

## 1.9.17

- **An artifact's size is read from the registry** (`environments/attest.py`;
PLAN_ENVS.md E1-25). `attest_artifact` took `size_bytes` from its caller and
nobody ever passed one — the builder answers a reference, not a weight — so
every artefact was recorded with `sizeBytes: null` and
`environments.artifact.bytes`, the series section 14 tracks the artifact
size in, had no point in it although artifacts had been recorded (seen on r1,
2026-09-16, through the OTEL query API). `Attestor.size_of()` asks the
registry, with the client the scan is already read from, and a size that
cannot be read is logged rather than raised: a missing number on a dashboard
is not a reason to refuse an artifact that is otherwise signed. 3 new tests.

## 1.9.15

- **A restart restarts the kernel, not just this client's socket**
(`jupyter_server_sandbox`, `client`; PLAN_ENV.md E0-09, Appendix B check
7). `CodeSandboxClient.restart()` was `stop()` then `start()`, which is
right for a sandbox this process owns — it is destroyed and recreated, and
nothing survives — and wrong for one *attached* to a Jupyter server
somebody else runs, which is every Datalayer runtime pod: stopping drops
the websocket while the kernel process keeps running, so the reconnect
lands in the same interpreter with every global still set. Check 7 is
"nothing is assumed to persist across restarts", and it read `state survived the restart ('True')` for exactly this reason — found live on r1,
2026-09-16, the first drill whose smoke test reached the check.
`JupyterServerSandbox.restart_kernel()` now asks the server's own
`POST /api/kernels/{id}/restart` (the way `_do_interrupt` already uses the
API rather than the client's lifecycle) and reconnects onto the new
kernel; `restart()` prefers it and falls back to the lifecycle for every
variant that draws no such distinction. 7 new tests.
- **`datalayer/python-cpu:2026.09` repinned** to
`sha256:122d3e31f5e2507251457cbf47871c39ac1753adb1d83777ab0743fa11cd6148`:
the contract layer now sets `MappingKernelManager.root_dir`, so kernels
start in `/home/datalayer/content`. The image already declared `WORKDIR`
there and `sandbox-contract/v1`'s User row already required it, but a
kernel's cwd is the Jupyter server's to choose and jupyter-python's config
roots it at `$HOME` — so every environment's kernel ran in
`/home/datalayer` and Appendix B check 2 read `cwd is '/home/datalayer', not '/home/datalayer/content'`. The file browser stays rooted at `$HOME`,
where a person expects to see everything they have; only the kernel moves.
- Two assertions that had rotted through three base releases are pinned in
one place again: the channel's digest and its apt snapshot were duplicated
across `test_environment_bases.py` and `test_environment_resolve.py`, and
2026-09-15's and 2026-09-16's releases left both red rather than catching
anything.

## 1.9.14

- **`datalayer/python-cpu:2026.09` base channel repinned** to the rebuilt
`jupyter-python:0.2.2` (now carrying `jupyter-kernels==1.2.23`) plus the
contract layer, digest
`sha256:aa5413000bb5b6ecd0a0cf03959b107f0d572f65bf230c08bbdf9a4569775545`,
released 2026-09-16 to `environments/base/python-cpu`. Every variant pins the
same digest.

## 1.9.13

- **`jupyter-kernels==1.2.23` forced into `sandbox-contract/v1`**: it carries
the pooled kernel manager the runtime's Jupyter config selects
(`kernel_manager_class = jupyter_kernels.pool.mapping.PooledMappingKernelManager`),
replacing the deprecated private `datalayer-kernels`. PyPI serves it, so a
resolve satisfies it from the index and the wheelhouse carries no wheel for
it; the pin keeps `uv pip sync --require-hashes` from stripping it out of a
user environment's image.

## 1.9.12

- **`owner_repository`, `owner_cache_repository` and `ECR_ENVIRONMENT_PREFIX`
Expand Down
2 changes: 1 addition & 1 deletion code_sandboxes/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

"""Code Sandboxes."""

__version__ = "1.9.12"
__version__ = "1.9.17"
16 changes: 15 additions & 1 deletion code_sandboxes/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,21 @@ def is_alive(self) -> bool:
return self.is_started

def restart(self) -> None:
"""Restart the wrapped sandbox through its public lifecycle."""
"""Restart the wrapped sandbox, clearing what it was holding.

A sandbox this process owns is restarted by its own lifecycle: stop
and start destroy and recreate it, and nothing survives. One that is
merely *attached* to a server somebody else runs — a Jupyter server
in a Datalayer runtime pod — is not: stopping drops this client's
websocket while the kernel process goes on running, and starting
reconnects to the same interpreter with every global still set. A
sandbox that knows how to restart what it is attached to says so with
`restart_kernel`, and that is used in preference; the lifecycle is
the fallback for every variant that has no such distinction.
"""
restart_kernel = getattr(self._sandbox, "restart_kernel", None)
if callable(restart_kernel) and restart_kernel():
return
self._sandbox.stop()
self._sandbox.start()

Expand Down
8 changes: 8 additions & 0 deletions code_sandboxes/environments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@
CONTRACT_V1,
SANDBOX_CONTRACT_V1,
SUPPORTED_CONTRACTS,
BuildContextEntry,
BuildContextFinding,
SandboxContract,
check_build_context,
check_dockerfile,
validate_build_context,
validate_dockerfile,
)
from .errors import ERROR_CODES, EnvironmentsError, ErrorCode, map_provider_error
Expand Down Expand Up @@ -91,6 +95,8 @@
"ApprovedBase",
"ArtifactReference",
"Attestor",
"BuildContextEntry",
"BuildContextFinding",
"BuildRequest",
"BuildkitResolveRunner",
"CapabilityReport",
Expand All @@ -114,6 +120,7 @@
"can_transition",
"canonical_digest",
"canonical_json",
"check_build_context",
"check_dockerfile",
"decide",
"fingerprint_matches",
Expand All @@ -131,6 +138,7 @@
"spec_digest",
"spec_findings",
"transition",
"validate_build_context",
"validate_dockerfile",
"validate_environment",
]
64 changes: 49 additions & 15 deletions code_sandboxes/environments/adapters/datalayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
apt_snapshot_in,
locked_versions,
)
from ..resolve_conda import (
MICROMAMBA_BINARY,
conda_lock_pip_requirements,
is_conda_lock,
micromamba_bootstrap_dockerfile_line,
)
from ..spec import BuildSecret, Environment, command_names_secret

__all__ = [
Expand Down Expand Up @@ -229,8 +235,8 @@ def validate(self, environment: Environment, lock_text: str | None = None) -> Ca
findings.append(
CapabilityFinding(
code=CAPABILITY_UNSUPPORTED.code,
message="conda environments are resolved by their own solver, "
"which is not built yet",
message="a conda environment is brought as a `dependencyFile` "
"whose `sourceFormat` is `conda`, not through `packages`",
field="spec.packages.python.manager",
)
)
Expand All @@ -253,7 +259,8 @@ def validate(self, environment: Environment, lock_text: str | None = None) -> Ca
field="spec.platform.architecture",
)
)
if lock_text is not None and not locked_versions(lock_text):
pins_or_lock = lock_text is not None and not is_conda_lock(lock_text)
if pins_or_lock and not locked_versions(lock_text):
findings.append(
CapabilityFinding(
code=SPEC_INVALID.code,
Expand Down Expand Up @@ -323,18 +330,45 @@ def dockerfile(self, request: BuildRequest) -> str:
f"COPY wheelhouse/ {imported_wheelhouse}/",
'RUN pip install --no-cache-dir "uv==0.12.11"',
]
lines.extend(
[
"COPY lock.txt /opt/datalayer/lock.txt",
# `sync` and not `install`: the artifact holds the lock's set,
# and `--require-hashes` means every byte was the resolved one.
# `--find-links` for what no index has — a protected pin's
# own wheel, the fork's local version above all (E1-04).
"RUN --mount=type=cache,target=/root/.cache/uv "
f"uv pip sync --system --require-hashes --find-links {find_links} "
"/opt/datalayer/lock.txt",
]
)
if is_conda_lock(request.lock_text):
# A conda source (E3-02): the lock is an `@EXPLICIT` file
# `micromamba install --file` installs without re-solving, and the
# pip layer the solve resolved — the user's own pip requirements and
# the protected pins forced over them — is in the lock's own
# `# datalayer-pip:` header. The conda layer goes into the base's
# own environment; the pip layer follows, so the kernel stack
# (E1-04) and everything the solve installed is present the same as
# every source. micromamba is copied in from its pinned image
# first: the approved base bakes uv and the wheelhouse but not it.
pip_requirements = conda_lock_pip_requirements(request.lock_text)
lines.extend(
[
micromamba_bootstrap_dockerfile_line(),
"COPY lock.txt /opt/datalayer/lock.txt",
"RUN --mount=type=cache,target=/opt/conda/pkgs "
f"{MICROMAMBA_BINARY} install --yes --name base "
"--file /opt/datalayer/lock.txt",
]
)
if pip_requirements:
requirements = " ".join(shlex.quote(req) for req in pip_requirements)
lines.append(
"RUN --mount=type=cache,target=/root/.cache/uv "
f"uv pip install --system --find-links {find_links} {requirements}"
)
else:
lines.extend(
[
"COPY lock.txt /opt/datalayer/lock.txt",
# `sync` and not `install`: the artifact holds the lock's set,
# and `--require-hashes` means every byte was the resolved one.
# `--find-links` for what no index has — a protected pin's
# own wheel, the fork's local version above all (E1-04).
"RUN --mount=type=cache,target=/root/.cache/uv "
f"uv pip sync --system --require-hashes --find-links {find_links} "
"/opt/datalayer/lock.txt",
]
)
# A build secret is mounted on the postInstall commands that name it
# and nowhere else (§4.1, D-11): never an `ARG` or `ENV`, which bakes a
# value into the image's history, never the package-install or files
Expand Down
47 changes: 36 additions & 11 deletions code_sandboxes/environments/adapters/daytona.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@

from __future__ import annotations

import shlex
import tempfile
import uuid
from collections.abc import Callable
Expand All @@ -110,6 +111,11 @@
)
from ..files import files_step
from ..resolve import WHEELHOUSE_IMAGE_PATH, apt_pins_in
from ..resolve_conda import (
conda_lock_pip_requirements,
is_conda_lock,
micromamba_bootstrap_command,
)
from ..spec import GPU_SIZE_CLASSES, Environment
from .managed import ManagedBuilder

Expand Down Expand Up @@ -171,6 +177,10 @@ class Builder(ManagedBuilder):
variant = "daytona"
item = "E2-04"
title = "Daytona"
#: A `packages` list and, for conda (E3-02), an `environment.yml`
#: dependency file installed with `micromamba`.
build_sources = ("packages", "dependencyFile")
dependency_formats = ("conda",)
#: Daytona runs GPUs, on its own hardware and the owner's account (E2-17).
#: This builder does not build one yet: see `_own_findings`.
gpu = True
Expand Down Expand Up @@ -343,28 +353,43 @@ def build(self, request: BuildRequest) -> ArtifactReference:
# wheelhouse again would only duplicate what `uv pip sync`
# can already reach at `WHEELHOUSE_IMAGE_PATH`. Only the
# lock is genuinely per-build.
image = (
image.add_local_file(str(lock_file), _LOCK_PATH)
image = image.add_local_file(str(lock_file), _LOCK_PATH)
if is_conda_lock(request.lock_text):
# A conda source (E3-02): `micromamba install --file`
# reads the `@EXPLICIT` lock without re-solving, and the
# pip layer the solve resolved — the user's pip
# requirements and the protected pins over them — comes
# from the lock's own `# datalayer-pip:` header, so the
# kernel stack (E1-04) and everything the solve installed is
# present the same as for a pip source. micromamba is
# installed first: the approved base bakes uv but not it.
image = image.run_commands(micromamba_bootstrap_command())
image = image.run_commands(
f"micromamba install --yes --name base --file {_LOCK_PATH}"
)
pip_requirements = conda_lock_pip_requirements(request.lock_text)
if pip_requirements:
requirements = " ".join(shlex.quote(req) for req in pip_requirements)
image = image.run_commands(
"pip install --no-cache-dir "
f"--find-links {WHEELHOUSE_IMAGE_PATH} {requirements}"
)
else:
# `uv` is not installed here: the approved base already
# bakes it (E1-05, `resolve.py`'s own `bootstrap_uv`
# docstring — "an approved Datalayer base already has it
# baked in"), and this phase's `build_sources` is
# `("packages",)` only, so every build starts from that
# base. Reinstalling it added an extra un-hashed network
# fetch outside the resolved lock for no reason (found in
# review) — matching the Datalayer builder's own
# `dockerfile()`, which installs `uv` only for the
# `image` source, not implemented for this variant yet.
# baked in"). Reinstalling it added an extra un-hashed
# network fetch outside the resolved lock for no reason
# (found in review).
#
# Packages install as root, the same reason the
# Datalayer and E2B builders give: a user install lands
# under the content directory's own home, which the
# runtime mounts over.
.run_commands(
image = image.run_commands(
"uv pip sync --system --require-hashes "
f"--find-links {WHEELHOUSE_IMAGE_PATH} {_LOCK_PATH}"
)
)
image = image.dockerfile_commands([f"USER 1000:100\nWORKDIR {_CONTENT_DIR}"])
for command in files_step(request.environment, variant=self.variant):
image = image.run_commands(command)
Expand Down
43 changes: 38 additions & 5 deletions code_sandboxes/environments/adapters/e2b.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@

from __future__ import annotations

import shlex
import tempfile
from collections.abc import Callable
from pathlib import Path
Expand All @@ -140,6 +141,11 @@
)
from ..files import files_step
from ..resolve import WHEELHOUSE_PATH, apt_pins_in
from ..resolve_conda import (
conda_lock_pip_requirements,
is_conda_lock,
micromamba_bootstrap_command,
)
from ..spec import Environment
from .managed import ManagedBuilder

Expand Down Expand Up @@ -191,6 +197,10 @@ class Builder(ManagedBuilder):
variant = "e2b"
item = "E2-03"
title = "E2B"
#: A `packages` list and, for conda (E3-02), an `environment.yml`
#: dependency file installed with `micromamba`.
build_sources = ("packages", "dependencyFile")
dependency_formats = ("conda",)
#: Firecracker microVMs: no GPU passthrough.
gpu = False
#: E0-04's spike found only a registry login for the private base, never
Expand Down Expand Up @@ -390,15 +400,38 @@ def build(self, request: BuildRequest) -> ArtifactReference:
chain.copy("datalayer-sandbox", _DOCTOR_PATH, mode=0o755, user="root")
.copy("wheelhouse", _WHEELHOUSE_PATH, user="root")
.copy("lock.txt", _LOCK_PATH, user="root")
.run_cmd(f'pip install --no-cache-dir "uv=={_UV_VERSION}"', user="root")
# Packages install as root (E0-04): a user install lands
# under /home/user, which the runtime mounts over.
.run_cmd(
)
if is_conda_lock(request.lock_text):
# A conda source (E3-02): `micromamba install --file` reads the
# `@EXPLICIT` lock without re-solving, and the pip layer the
# solve resolved — the user's pip requirements and the protected
# pins over them — comes from the lock's own `# datalayer-pip:`
# header, so the kernel stack (E1-04) and everything the solve
# installed is present the same as for a pip source. micromamba
# is installed first: the approved base bakes uv but not it.
chain = chain.run_cmd(micromamba_bootstrap_command(), user="root")
chain = chain.run_cmd(
f"micromamba install --yes --name base --file {_LOCK_PATH}",
user="root",
)
pip_requirements = conda_lock_pip_requirements(request.lock_text)
if pip_requirements:
requirements = " ".join(shlex.quote(req) for req in pip_requirements)
chain = chain.run_cmd(
f"pip install --no-cache-dir --find-links {_WHEELHOUSE_PATH} "
f"{requirements}",
user="root",
)
else:
chain = chain.run_cmd(
f'pip install --no-cache-dir "uv=={_UV_VERSION}"', user="root"
).run_cmd(
# Packages install as root (E0-04): a user install lands
# under /home/user, which the runtime mounts over.
"uv pip sync --system --require-hashes "
f"--find-links {_WHEELHOUSE_PATH} {_LOCK_PATH}",
user="root",
)
)
for command in files_step(request.environment, variant=self.variant):
chain = chain.run_cmd(command)
for command in spec.commands.post_install:
Expand Down
Loading
Loading