E2-11: nightly live matrix for the managed Environment builders - #32
Conversation
tests/test_environment_live_matrix.py builds a real artifact on each real provider (daytona, e2b, modal) from a real, hash-verified lock (not the trivial ones every earlier live drill in this plan used, which is exactly what hid E2-04's build-time doctor bug until this test used a real one), launches a sandbox from it, and runs the formal core tier (conformance.run_core_tier) against it. Everything it creates is deleted, pass or fail. Opened from origin/main on purpose: E2-03/E2-04/E2-05 are each still their own open PR, so this needs no change of its own as each merges — it starts working per-variant the moment get_builder actually builds for it, and fails outright (not xfail) against an unmerged variant today, correctly, since that is the true state of main. E2B and Modal are xfail(strict=False) for their own already-documented, unticked gaps (E2-03's uid/gid mismatch; modal_sandbox.py's missing setpriv, so it execs as root). Daytona carries no such marker: E2-04's own live drill already proved it passes in full, so a failure there is a real regression. .github/workflows/environments-live.yml runs this nightly (04:17 UTC) plus on workflow_dispatch, and opens (or comments on) an issue naming the provider and the check when a genuine, non-xfail failure occurs — parsed straight out of the JUnit report's failure/error nodes, which an xfail never populates. Needs six repository secrets the owner still has to add: E2B_API_KEY, DAYTONA_API_KEY, MODAL_TOKEN_ID, MODAL_TOKEN_SECRET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY. Makefile gets an `environments-live` target mirroring the existing `live-matrix`/`kill-relaunch` pattern. Co-Authored-By: Claude Sonnet 5 <[email protected]>
There was a problem hiding this comment.
🟡 Changes recommended
Moderate unresolved issues affect workflow reliability and resource cleanup.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a nightly live matrix that builds, launches, and validates managed Environment artifacts across Daytona, E2B, and Modal.
Changes:
- Adds provider live tests and core-tier validation.
- Adds
make environments-live. - Adds scheduled/manual workflow with failure issue reporting.
Review findings: eight moderate issues remain, covering workflow permissions and timeout, warning filters, build ID collisions, partial-start cleanup, exception preservation, provider-specific base resolution, and Daytona build-context cleanup.
File summaries
| File | Description |
|---|---|
tests/test_environment_live_matrix.py |
Builds, launches, validates, and cleans up provider artifacts. |
Makefile |
Adds the live matrix command. |
.github/workflows/environments-live.yml |
Runs the matrix nightly and reports failures. |
Review details
Suppressed comments (13)
.github/workflows/environments-live.yml:34
- The three tests run sequentially, but the provider builders allow up to 30 minutes for Daytona, 30 minutes for Modal, and 20 minutes for E2B, before resolution and launch time. A valid slow run can hit this 45-minute hard limit while a provider is still active, skipping cleanup and preventing later providers from running. Increase the timeout or split this into per-provider jobs.
timeout-minutes: 45
tests/test_environment_live_matrix.py:55
- Pytest is configured with
filterwarnings = ["error", ...](pyproject.toml:95-106), and the other live suites explicitly ignore provider SDK deprecations. This module imports Modal/Daytona/E2B SDKs without those marks, so a client warning can fail a row before any sandbox check; add the live warning filters here.
pytestmark = [pytest.mark.live]
tests/test_environment_live_matrix.py:181
- Because this module-scoped fixture is set up before the test body,
_resolved_lock()runs before_skip_unless_available(). When live mode or provider/AWS credentials are missing, the workflow still performs uv/PyPI resolution (and can fail) instead of skipping immediately as documented. Gate the fixture before resolving, or resolve the lock only after the per-provider availability check.
lock_text, resolved_bases = _resolved_lock()
tests/test_environment_live_matrix.py:155
BuildRequest.lock_digestis documented as the digest oflock_text, but this passes an all-zero placeholder rather thanresolve_environment()'s returned digest. Any builder metadata or attestation that uses this field will advertise a hash that cannot verify the lock installed by this test; carry the resolver's digest through the fixture.
lock_digest="sha256:" + "00" * 32,
tests/test_environment_live_matrix.py:226
- This
xfailcovers the entire test body, so provider outages, bad credentials, SDK/import failures, and build/launch regressions are all reported as expected failures—not just the documented root-identity gap. The nightly workflow will therefore miss genuine Modal failures; verify the expected check IDs first and only xfail that specific outcome.
@pytest.mark.xfail(
reason="modal_sandbox.py has no setpriv wrapper yet: execs run as root, not "
"the contract's 1000:100 — checks 1, 2, 5 and 6 all fail on it",
strict=False,
tests/test_environment_live_matrix.py:264
- This blanket marker also turns
get_builder("e2b")/build()failures such as the currentDL_ENV_CAPABILITY_UNSUPPORTED"has not landed" state into xfails, contrary to the module's stated expectation that an unmerged builder fails outright. It likewise hides unrelated provider regressions; restrict xfail to the verified uid/gid failure and let other exceptions fail the job.
@pytest.mark.xfail(
reason="E2-03's own documented gap: code-interpreter-v1 lands datalayer "
"on uid 1001, gid 1001, not the contract's 1000:100",
strict=False,
tests/test_environment_live_matrix.py:283
- This
finallyis a no-op, so every successful E2B run leaves its remote template behind; ifbuild()fails beforeartifactis assigned, this block is not reached at all. The nightly job will accumulate provider artifacts and costs, contradicting the module's cleanup guarantee. Add provider-side deletion (for example through the provider API) or do not schedule this row until cleanup is available.
finally:
pass # E2B's SDK exposes no template delete call (found live, E2-03).
tests/test_environment_live_matrix.py:244
ModalSandbox.start()creates the remote sandbox before_startedis set, andstop()is a no-op when startup fails before that flag is set. Thus an exception during driver/context setup can leave a Modal sandbox running because this cleanup is only entered afterstart()returns. Make partial-start cleanup explicit in the provider lifecycle.
sandbox.start()
tests/test_environment_live_matrix.py:277
E2BSandbox.start()creates the remote sandbox before setting_started, whilestop()returns when_startedis false. If context setup fails after creation, this cleanup is skipped and the live matrix leaks the sandbox. Make partial-start cleanup explicit in the provider lifecycle rather than relying onstart()to complete.
sandbox.start()
tests/test_environment_live_matrix.py:252
- Deleting only
artifact.provider_artifact_idis not sufficient for Modal: its chained image build leaves intermediate layer images that the final image deletion does not remove. Each nightly build will therefore accumulate Modal artifacts despite the module's cleanup guarantee; enumerate the intermediate ids through the provider cleanup/reconciliation path as well.
modal.experimental.image_delete(
artifact.provider_artifact_id, client=modal.Client.from_env()
)
tests/test_environment_live_matrix.py:209
- The artifact delete in
finallyis unguarded, so a provider cleanup error replaces the original build/core-tier exception and the nightly issue loses the actual check failure. Preserve the primary exception while recording cleanup failure separately.
import daytona
daytona.Daytona().snapshot.delete(artifact.provider_artifact_id)
tests/test_environment_live_matrix.py:125
- The lock is resolved with
variants=['daytona'], then the Daytona base is reused for E2B and Modal.resolve_environmentreturns a per-variant base map, so this matrix will build the wrong base or fail to exercise provider-specific base pins as soon as those variants diverge. Resolve all three variants and selectresolved_bases[variant]per test.
outcome = resolve_environment(
spec=_ENVIRONMENT_SPEC, variants=["daytona"], runner=LocalResolveRunner()
)
return outcome["content"], outcome["resolved_bases"]
tests/test_environment_live_matrix.py:209
snapshot.delete(...)removes the snapshot id, but Daytona keeps the build context in provider object storage and this path has no cleanup for it. The nightly test therefore accumulates remote build data even when snapshot deletion succeeds, contradicting the claim that everything it creates is deleted. Add provider-specific context cleanup or document and bound the retained resource.
import daytona
daytona.Daytona().snapshot.delete(artifact.provider_artifact_id)
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # 04:17 UTC: off-peak for every provider this reaches, and away from the | ||
| # hour boundary every other scheduled workflow in this org tends to pick. | ||
| - cron: '17 4 * * *' | ||
| workflow_dispatch: {} |
There was a problem hiding this comment.
Fixed in dc8c811: added permissions: contents: read, issues: write at the workflow level.
| return BuildRequest( | ||
| environment_uid="01k0env0000000000000000000", | ||
| version=1, | ||
| build_uid=f"live-{variant}", |
There was a problem hiding this comment.
Fixed in dc8c811: build_uid is now f"live-{variant}-{uuid.uuid4().hex[:8]}" — a per-run nonce, provider prefix kept.
| try: | ||
| assert builder.exists(artifact) | ||
| sandbox = Sandbox.create(variant="daytona", artifact=artifact, timeout=600) | ||
| sandbox.start() |
There was a problem hiding this comment.
Real bug, confirmed, and not unique to Daytona — the same shape exists in modal_sandbox.py and e2b_sandbox.py too: start() creates the remote resource well before it marks itself started (create_context/_start_driver, building SandboxInfo all come after), and stop() was guarded on _started rather than on the resource itself.
Fixed in dc8c811 in all three, identically: stop() now returns early only when self._sandbox is None — the null-check each already had internally — so a failure between creation and _started = True still gets cleaned up. _started's meaning is unchanged everywhere else it's checked (still gates whether the sandbox is ready to use, e.g. run_code/interrupt). A regression test per file (test_daytona.py, test_e2b.py, test_modal_google_colab_sandbox.py) confirms cleanup now runs even when _started was never set, and that stop() stays a true no-op before anything was ever created.
…real orphan-on-partial-start bug
Three findings, all fixed:
- environments-live.yml declared no `permissions`, so on the default
read-only token a genuine matrix failure would 403 trying to label and
open the very issue meant to report it. Added `contents: read`,
`issues: write`.
- `_build_request`'s `build_uid` was constant across invocations
(`f"live-{variant}"`). E2B and Daytona both fold `build_uid` into the
artifact/template name, so a second nightly run would collide with the
first's own artifact instead of building a fresh one. Now a per-run
nonce, provider prefix kept.
- A real, pre-existing bug in all three sandbox launchers, not introduced
by this PR but exposed by it: `start()` creates the remote sandbox well
before it marks itself started (`create_context`/`_start_driver`,
building `SandboxInfo` all come after), while `stop()` was guarded on
`_started` rather than on the resource itself — a failure in between
left a real, running sandbox that `stop()` skipped entirely, orphaned
for good. Fixed identically in `daytona_sandbox.py`, `modal_sandbox.py`
and `e2b_sandbox.py`: `stop()` now checks `self._sandbox is None`, which
was already the inner null-check every one of them had; nothing about
`_started`'s meaning anywhere else changes. A regression test per file
confirms cleanup now runs even when `_started` was never set, and that
`stop()` is still a true no-op before anything was ever created.
ruff/mypy clean; the four affected test files all pass (line counts:
daytona 56, e2b 31, modal/colab 16, plus the live-matrix file's own
unit-checkable parts).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
…hat identity is fixed feat/modal-builder (PR #31) closed the identity gap this file's own xfail reason named: a contract-built artifact's ModalSandbox now drops its driver to 1000:100, gated on image_id so a plain ModalSandbox is unaffected. Checks 1/2 (doctor, identity) pass live now; checks 5/6 (imports, filesystem) still fail, confirmed a separate, pre-existing issue unrelated to identity -- a plain, non-Environments ModalSandbox has no trouble over the same span. Updates the xfail reason and class docstring to describe the current, accurate picture rather than the superseded one. Text-only change: this branch's own adapters/modal.py and modal_sandbox.py are still the pre-E2-05 stubs (this branch is cut from main, not from feat/modal-builder), so nothing here is runnable proof -- it just keeps this file honest about what to expect once #31 merges. Co-Authored-By: Claude Sonnet 5 <[email protected]>
…ule docstring E2-03's own fixes (feat/e2b-builder) mean the build no longer refuses at build time -- it succeeds in full. The gap moved to launch: E2B's own jupyter.service/code-interpreter.service run as root regardless of the build's own set_user, and E2B's private server hardcodes /home/user, independent of anything the build sets. Updates TestE2B's docstring and xfail reason to describe that, instead of the superseded "the build itself refuses" story -- and the module docstring's "everything is deleted" claim, which was never quite true for E2B (no template-delete call exists) and matters more now that a live run actually reaches a real template every time. Text-only, like the previous Modal xfail correction: this branch's own adapters are still the pre-E2-03/E2-05 stubs (cut from main), so nothing here is runnable proof -- it keeps this file honest about what to expect once #29 and #31 merge. Co-Authored-By: Claude Sonnet 5 <[email protected]>
…e-matrix # Conflicts: # code_sandboxes/modal_sandbox.py # tests/test_modal_google_colab_sandbox.py
What
E2-11: a nightly matrix that builds a real artifact on each real managed
Environment provider (
daytona,e2b,modal) from a real,hash-verified lock — not the trivial ones every earlier live drill in
this plan used, which is exactly what hid E2-04's build-time doctor bug
until this test built one for real — launches a sandbox from it, and runs
the formal core tier (
conformance.run_core_tier) against it. Everythingit creates is deleted, whether the run passes or not.
tests/test_environment_live_matrix.py— the test itself,pytest.mark.live, gated onCODE_SANDBOXES_LIVE=1like the existing live suites.Makefile—make environments-live, mirroring the existinglive-matrix/kill-relaunchtargets..github/workflows/environments-live.yml— runs it nightly (04:17 UTC) and onworkflow_dispatch, and opens (or comments on) an issue naming the provider and the check when a genuine, non-xfailfailure occurs.Cross-branch dependency (read this before merging)
E2-03 (#29, e2b), E2-04 (#30, daytona) and E2-05 (#31, modal) are each
still their own open PR. This branch is deliberately cut from
main, notfrom any of them, so it needs no change of its own as each merges — it
starts working per-variant the moment
get_builderactually builds forthat variant, and today, against unmerged
main, it correctly failsoutright (
DL_ENV_CAPABILITY_UNSUPPORTED, "has not landed") rather thanxfail, since that's the true state ofmain, not a defect this test isexpecting. Once #29/#30/#31 are all merged this will need no further
change to start exercising all three for real.
e2bandmodalare@pytest.mark.xfail(strict=False)for their ownalready-documented, unticked gaps:
code-interpreter-v1landsdatalayeron uid 1001/gid 1001, not the contract's 1000:100.modal_sandbox.pyhas nosetprivwrapper yet, so it execs as root today.strict=Falsemeans an unexpected pass (XPASS) doesn't fail the runeither — it's the positive signal that one of those gaps has closed, for
its own plan box to tick.
daytonacarries no such marker: E2-04's ownlive drill already proved it passes the core tier in full, so a failure
there is a real regression.
Two real bugs this test found (already fixed, elsewhere)
Building this test against a real, non-trivial resolved lock (rather
than the toy ones prior live drills used) surfaced two genuine defects,
both already fixed on their own PRs:
RUNstep's PID1 during a Daytona build is the build agent's, not the final snapshot's, sodatalayer-sandbox doctor'sinitrow fails deterministically. Fixed in environments: the Daytona builder — build, inspect, exists (E2-04) #30 by removing the doctor check frombuild()(doctor still runs fine post-launch).modal_sandbox.py'sModalSandbox.start()callsSandbox.create(**create_kwargs)with zero positional command args, so an entrypoint relying onexec "$@"alone was a no-op and the container exited immediately. Fixed in environments: the Modal builder — build, inspect, exists (E2-05) #31 by adding asleep infinityfallback when no command is given.Still needed before the nightly run does anything for real
The workflow needs six repository secrets, which I can't add myself:
E2B_API_KEY,DAYTONA_API_KEY,MODAL_TOKEN_ID,MODAL_TOKEN_SECRET,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY(the AWS pair scoped to readthe approved base from ECR only, per D-18). Until they're added, the
scheduled run will execute and every provider's test will
skipby namerather than fail — this is not itself something the workflow flags as
broken.
Verification
ruff check/ruff format --check/mypyall pass on the new test file.actionlintpasses clean on the new workflow file.feat/modal-builderbefore either fix (Daytona correctly refused as unbuilt there; Modal/E2B xfailed, but for the wrong immediate reason — themodal.experimentalimport and the entrypoint bug), and twice after, confirming the entrypoint fix keeps a real launched Modal sandbox alive through a full core-tier run (checks 3/4/7/8/9 pass; 1/2/5/6 fail on the still-open identity gap, as expected).