diff --git a/.github/scripts/prune_computer_images.py b/.github/scripts/prune_computer_images.py new file mode 100644 index 000000000..2cf220edf --- /dev/null +++ b/.github/scripts/prune_computer_images.py @@ -0,0 +1,112 @@ +"""Delete the CI computer image mirror's images no run has used for ``--days`` days. + +``share_computer_images.py`` tags each image a passing main run used ``-seen-YYYYMMDD``. An image is kept while its +newest such tag, or its upload when it has none, is within ``--days`` of now, with every platform manifest a kept index +lists, since GHCR lists those as versions of their own. Every other version is deleted through GitHub's packages API, +read and written with ``GITHUB_TOKEN``, as GHCR is. A version GitHub won't delete warns. ``--dry-run`` prints what +would go and deletes nothing. +""" + +from __future__ import annotations + +import argparse +import base64 +import datetime +import json +import os +import re +import sys +import urllib.error +import urllib.parse +import urllib.request + +SEEN = re.compile(r"^[0-9a-f]{16}-[0-9a-f]{64}-seen-(\d{8})$") +_INDEXES = {"application/vnd.oci.image.index.v1+json", "application/vnd.docker.distribution.manifest.list.v2+json"} +_ACCEPT = ", ".join([*_INDEXES, "application/vnd.oci.image.manifest.v1+json", + "application/vnd.docker.distribution.manifest.v2+json"]) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("org") + parser.add_argument("package", help="the container package, as agentenv-ci-computers") + parser.add_argument("--days", type=int, default=7) + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + api = f"https://api.github.com/orgs/{args.org}/packages/container/{args.package}/versions" + try: + versions = _versions(api) + except urllib.error.HTTPError as e: + if e.code != 404: + raise + print(f"::warning::prune computer images: no package {args.org}/{args.package}, or this repository has no " + "access to it, so nothing is pruned") + return 0 + cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=args.days) + stale = stale_versions(versions, cutoff, lambda digest: _children(args.org, args.package, digest)) + failed = 0 + for version in stale: + print(f"{'would delete' if args.dry_run else 'deleting'} {version['name']} " + f"{version['metadata']['container']['tags'] or '(untagged)'}") + if not args.dry_run: + try: + _github("DELETE", f"{api}/{version['id']}") + except urllib.error.HTTPError as e: + failed += 1 + print(f"::warning::couldn't delete {version['name']}: HTTP {e.code} {e.read().decode()[:200]}") + print(f"prune computer images: {len(stale) - failed} of {len(versions)} versions " + f"{'would be ' if args.dry_run else ''}deleted, none used since {cutoff:%Y-%m-%d}") + return 0 + + +def stale_versions(versions: list[dict], cutoff: datetime.datetime, children) -> list[dict]: + """The ``versions`` to delete: neither used nor uploaded since ``cutoff``, nor a platform manifest of one kept. + ``children(digest)`` lists the digests of the manifests the index of that digest lists, or none for an image.""" + kept = {version["name"] for version in versions if _last_used(version) >= cutoff} + for digest in list(kept): + kept.update(children(digest)) + return [version for version in versions if version["name"] not in kept] + + +def _last_used(version: dict) -> datetime.datetime: + created = datetime.datetime.fromisoformat(version["created_at"].replace("Z", "+00:00")) + seen = [datetime.datetime.strptime(m.group(1), "%Y%m%d").replace(tzinfo=datetime.timezone.utc) + for tag in version["metadata"]["container"]["tags"] if (m := SEEN.match(tag))] + return max([created, *(day + datetime.timedelta(days=1) for day in seen)]) + + +def _versions(api: str) -> list[dict]: + versions, page = [], 1 + while batch := _github("GET", f"{api}?per_page=100&page={page}"): + versions += batch + page += 1 + return versions + + +def _children(org: str, package: str, digest: str) -> list[str]: + """The platform manifests the index ``digest`` lists, read from GHCR with ``GITHUB_TOKEN``; none for an image.""" + repository = f"{org}/{package}" + signed_in = base64.b64encode(f"x:{os.environ['GITHUB_TOKEN']}".encode()).decode() + with urllib.request.urlopen(urllib.request.Request(f"https://ghcr.io/token?scope=repository:{repository}:pull", + headers={"Authorization": f"Basic {signed_in}"})) as response: + token = json.load(response)["token"] + request = urllib.request.Request(f"https://ghcr.io/v2/{repository}/manifests/{digest}", + headers={"Authorization": f"Bearer {token}", "Accept": _ACCEPT}) + with urllib.request.urlopen(request) as response: + manifest = json.load(response) + media = response.headers.get("Content-Type", "").split(";")[0].strip() + return [child["digest"] for child in manifest.get("manifests", [])] if media in _INDEXES else [] + + +def _github(method: str, url: str): + request = urllib.request.Request(url, method=method, headers={ + "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}", "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28"}) + with urllib.request.urlopen(request) as response: + body = response.read() + return json.loads(body) if body else None + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/share_computer_images.py b/.github/scripts/share_computer_images.py new file mode 100644 index 000000000..17fb93078 --- /dev/null +++ b/.github/scripts/share_computer_images.py @@ -0,0 +1,53 @@ +"""Copy every composed computer image a slow-tier runner's registry holds to the CI computer image mirror, the one +``AGENT_ENV_COMPUTER_IMAGE_MIRROR`` names, so later runs copy them back rather than build them. + +Each image goes under its mirror tag (``composed.mirror_tag``: the composer's digest, then the key), and again with +today's date, ``-seen-YYYYMMDD``, which is how ``prune_computer_images.py`` tells an image a recent run used from one +none has. An image already in the mirror is only tagged again: no blob it holds is sent twice. The mirror is written as +``GITHUB_ACTOR`` with ``GITHUB_TOKEN``; one the composer can't read anonymously, as GHCR makes a new package, warns. +""" + +from __future__ import annotations + +import argparse +import datetime +import json +import os +import re +import sys +import urllib.request + +from agent_env.env.envs.computer.utils.composed import REPOSITORY, mirror_tag +from agent_env.store.image_store.oci_registry_credentials import RegistryAuth +from agent_env.store.image_store.registry_api import copy_image, pin_digest, pin_if_served + +KEY = re.compile(r"^[0-9a-f]{64}$") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("mirror", help="the mirror repository, as ghcr.io/org/agentenv-ci-computers") + parser.add_argument("--registry", default="localhost:5000", help="the runner's registry") + args = parser.parse_args() + + with urllib.request.urlopen(f"http://{args.registry}/v2/{REPOSITORY}/tags/list") as response: + keys = sorted(tag for tag in json.load(response).get("tags") or [] if KEY.match(tag)) + auth = RegistryAuth(args.mirror.partition("/")[0], os.environ["GITHUB_ACTOR"], os.environ["GITHUB_TOKEN"]) + seen = f"seen-{datetime.datetime.now(datetime.timezone.utc):%Y%m%d}" + for key in keys: + source = pin_digest(f"{args.registry}/{REPOSITORY}:{key}", None) + for tag in (mirror_tag(key), f"{mirror_tag(key)}-{seen}"): + copy_image(source, f"{args.mirror}:{tag}", None, auth) + print(f"shared {key}") + print(f"share computer images: {len(keys)} images in {args.mirror}, each tagged {seen}") + if keys: + try: + pin_if_served(f"{args.mirror}:{mirror_tag(keys[0])}", None) + except ValueError as e: + print(f"::warning::the composer reads {args.mirror} anonymously and can't, so every run builds its images; " + f"make the package public: {e}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/computer-image-mirror.yml b/.github/workflows/computer-image-mirror.yml new file mode 100644 index 000000000..7c0865cdf --- /dev/null +++ b/.github/workflows/computer-image-mirror.yml @@ -0,0 +1,31 @@ +name: Computer image mirror + +# Deletes the slow tier's shared computer images (local-backends.yml) that no passing main run has used for a week. +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + inputs: + dry_run: + description: List what would be deleted, and delete nothing + type: boolean + default: false + +permissions: + contents: read + packages: write + +jobs: + prune: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + sparse-checkout: .github/scripts + - name: Delete the images unused for a week + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + python3 .github/scripts/prune_computer_images.py scaleapi agentenv-ci-computers --days 7 \ + ${{ inputs.dry_run && '--dry-run' || '' }} diff --git a/.github/workflows/local-backends.yml b/.github/workflows/local-backends.yml index 5b58947c2..2ebe22961 100644 --- a/.github/workflows/local-backends.yml +++ b/.github/workflows/local-backends.yml @@ -22,6 +22,8 @@ concurrency: env: AGENT_ENV_ENVIRONMENT: dev + # The slow tier's composed computer images, shared between its runners (computer-image-mirror.yml prunes it). + COMPUTER_IMAGE_MIRROR: ghcr.io/scaleapi/agentenv-ci-computers PIP_DISABLE_PIP_VERSION_CHECK: "1" jobs: @@ -154,8 +156,14 @@ jobs: shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] runs-on: ubuntu-latest timeout-minutes: 40 + permissions: + contents: read + packages: write steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + # The tests run with packages: write; only the share step, on main, gets the token. + with: + persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" @@ -168,63 +176,40 @@ jobs: # starts afterwards. - name: Pull the base images from mirror.gcr.io run: python3 .github/scripts/mirror_base_images.py src tst - # A computer image or disk is tagged with a key of everything it is built from, and the composer reuses one the - # image store already holds for its key. The registry keeps its storage on the runner, restored from the computer - # images main saved latest this week, so a run reuses them rather than building them again. A shard that runs no - # computer tests misses. - - name: Week of the computer images' cache - id: week - run: echo "week=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT" - - name: Restore the computer images - id: computers - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ runner.temp }}/registry - key: computers-${{ matrix.shard }}-${{ steps.week.outputs.week }} - restore-keys: computers-${{ matrix.shard }}-${{ steps.week.outputs.week }}- - name: Start the local registry run: | - mkdir -p "$RUNNER_TEMP/registry" - docker run --detach --name registry --publish 5000:5000 --volume "$RUNNER_TEMP/registry:/var/lib/registry" \ - mirror.gcr.io/library/registry:2 + docker run --detach --name registry --publish 5000:5000 mirror.gcr.io/library/registry:2 timeout 30 sh -c 'until curl -sf http://localhost:5000/v2/ >/dev/null; do sleep 1; done' # x64 hosted runners have /dev/kvm; the disk form's tests boot under it, and skip without it, which fails below. - name: Let the local provider boot disks with KVM run: sudo chmod 666 /dev/kvm + # A computer image or disk is tagged with a key of everything it is built from. A compose that finds no image of + # its key in the local registry copies the one the mirror holds, which main's runs share, rather than build it. + # Experiment: sample the runner's used memory every 5s while the tests run two at a time. + - name: Sample memory + run: nohup sh -c 'while true; do free -m | awk "NR==2{print \$3}" >> "$RUNNER_TEMP/mem.log"; sleep 5; done' >/dev/null 2>&1 & - name: Slow integration tier on the local defaults id: tests env: AGENTENV_TEST_SHARD: ${{ matrix.shard }}/12 + AGENT_ENV_COMPUTER_IMAGE_MIRROR: ${{ env.COMPUTER_IMAGE_MIRROR }} run: | - .venv/bin/python -m pytest -m int_test_slow -rA --timeout=1200 --junitxml=junit-integration-slow.xml \ - tst/integration --ignore=tst/integration/env/gateway/gateway_test.py - # Only the computer images are kept, and only main saves them, once its tests pass, so pull requests reuse main's - # without filling the repository's cache. The entry is named by the image keys the registry holds, so a run that - # built any new image saves a new one. The garbage collection frees what only the deleted repositories - # referenced; with --delete-untagged it would also delete the untagged platform manifests of an image index, and - # their layers. - - name: Keep only the computer images - id: kept + .venv/bin/python -m pytest -m int_test_slow -n 2 --dist loadfile -rA --timeout=1200 \ + --junitxml=junit-integration-slow.xml tst/integration --ignore=tst/integration/env/gateway/gateway_test.py + # Once main's tests pass, every image this runner built or copied goes to the mirror under its key and today's + # date, which the prune of computer-image-mirror.yml keeps an image a run used within the week by. + - name: Share the computer images + continue-on-error: true + if: >- + always() && steps.tests.outcome == 'success' && github.event_name == 'push' + env: + GITHUB_TOKEN: ${{ github.token }} + run: .venv/bin/python .github/scripts/share_computer_images.py "$COMPUTER_IMAGE_MIRROR" + - name: Peak memory if: always() run: | - repositories="$RUNNER_TEMP/registry/docker/registry/v2/repositories" - if [ ! -d "$repositories/agentenv-computers" ]; then echo "computers=false" >> "$GITHUB_OUTPUT"; exit 0; fi - docker stop registry - sudo find "$repositories" -mindepth 1 -maxdepth 1 ! -name agentenv-computers -exec rm -rf {} + - docker run --rm --volume "$RUNNER_TEMP/registry:/var/lib/registry" mirror.gcr.io/library/registry:2 \ - garbage-collect /etc/docker/registry/config.yml > /dev/null - du -sh "$RUNNER_TEMP/registry" - tags=$(sudo ls "$repositories/agentenv-computers/_manifests/tags" | sort | sha256sum | cut -c1-16) - echo "key=computers-${{ matrix.shard }}-${{ steps.week.outputs.week }}-$tags" >> "$GITHUB_OUTPUT" - echo "computers=true" >> "$GITHUB_OUTPUT" - - name: Save the computer images - if: >- - always() && github.event_name == 'push' && steps.tests.outcome == 'success' - && steps.kept.outputs.computers == 'true' && steps.kept.outputs.key != steps.computers.outputs.cache-matched-key - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ runner.temp }}/registry - key: ${{ steps.kept.outputs.key }} + echo "peak used MB: $(sort -n "$RUNNER_TEMP/mem.log" | tail -1) of $(free -m | awk 'NR==2{print $2}')" + if sudo dmesg | grep -q -i -E "out of memory|oom-kill"; then echo "OOM KILL SEEN"; else echo "no OOM kills"; fi - name: Skips must be declared capability gaps if: always() run: | diff --git a/src/agent_env/config/describe.py b/src/agent_env/config/describe.py index e8e291d7f..73e289cbc 100644 --- a/src/agent_env/config/describe.py +++ b/src/agent_env/config/describe.py @@ -673,6 +673,7 @@ def env_sources() -> tuple[EnvSource, ...]: EnvSource("AGENT_ENV_MODAL_REGION", None, "no file equivalent"), EnvSource("AGENT_ENV_MODAL_APP_NAME", None, "no file equivalent"), EnvSource("AGENT_ENV_FIXTURE_PREFIX", None, "no file equivalent"), + EnvSource("AGENT_ENV_COMPUTER_IMAGE_MIRROR", None, "no file equivalent"), EnvSource("MODAL_TOKEN_ID", None, "credential, no file equivalent"), EnvSource("MODAL_TOKEN_SECRET", None, "credential, no file equivalent"), ] diff --git a/src/agent_env/config/runtime.py b/src/agent_env/config/runtime.py index a98b17005..66326e090 100644 --- a/src/agent_env/config/runtime.py +++ b/src/agent_env/config/runtime.py @@ -24,6 +24,11 @@ objects, image builds, env and agent snapshots, changelogs, default agent and judge trajectories, verifier outputs and the A2A validator's fixtures, so a fresh control-plane backend can write without colliding with existing objects in a shared store. Empty in prod. + +AGENT_ENV_COMPUTER_IMAGE_MIRROR (default ""): a registry repository, read anonymously, holding composed computer images +under their keys and the composer's own digest, as ``ghcr.io/org/computers``. A compose that finds no image of a key in the +env's image store copies the mirror's into it instead of building it. CI sets it so its runners share the images main +built. """ from __future__ import annotations @@ -226,6 +231,9 @@ class Config: fixture_prefix: str = field( default_factory=lambda: os.getenv("AGENT_ENV_FIXTURE_PREFIX", "") ) + computer_image_mirror: str = field( + default_factory=lambda: os.getenv("AGENT_ENV_COMPUTER_IMAGE_MIRROR", "") + ) agent_sandbox_mode: str = field( default_factory=lambda: os.getenv("AGENT_SANDBOX_MODE") or _default_sandbox_mode() ) diff --git a/src/agent_env/env/envs/computer/utils/composed.py b/src/agent_env/env/envs/computer/utils/composed.py index 3ee5281ad..ec3d5d993 100644 --- a/src/agent_env/env/envs/computer/utils/composed.py +++ b/src/agent_env/env/envs/computer/utils/composed.py @@ -117,6 +117,12 @@ def composition() -> str: return "sha256:" + digest.hexdigest() +def mirror_tag(key: str) -> str: + """The tag of the image of ``key`` in a computer image mirror: the key, under the start of ``composition()``, so a + change to the code that composes an image builds it again rather than reuse one the old code built.""" + return f"{composition().removeprefix('sha256:')[:16]}-{key.removeprefix('sha256:')}" + + def _rank(result: dict) -> int: """How far a pair got: ready, then built and either unprobed or failed at its probe, then anything short of an image, and below that one another composer recorded. A result takes a stored one's place only when it got at least diff --git a/src/agent_env/env/envs/computer/utils/composer.py b/src/agent_env/env/envs/computer/utils/composer.py index 95fbc8e9a..22c755c21 100644 --- a/src/agent_env/env/envs/computer/utils/composer.py +++ b/src/agent_env/env/envs/computer/utils/composer.py @@ -49,7 +49,7 @@ thin_disk, windows_disk, ) -from agent_env.env.envs.computer.utils.composed import BUILT, REPOSITORY, Record, failed, now, stale +from agent_env.env.envs.computer.utils.composed import BUILT, REPOSITORY, Record, failed, mirror_tag, now, stale from agent_env.env.envs.computer.utils.install import Install, for_os, platform_of from agent_env.env.published_images import DISK, DISK_TOOLS_IMAGE, published_disk, published_pinned from agent_env.providers.sandbox_providers.host_images import platform_tag @@ -57,7 +57,7 @@ from agent_env.store.image_store.image_contents import image_config, labels from agent_env.store.image_store.oci_registry_credentials import names_registry from agent_env.store.image_store.image_store import ImageStore -from agent_env.store.image_store.registry_api import pin_digest, pin_if_served, write_index +from agent_env.store.image_store.registry_api import copy_image, pin_digest, pin_if_served, write_index from agent_env.store.local_state import holding_locks from agent_env.utils.docker.build import DockerBuildError @@ -691,9 +691,20 @@ def _key(**inputs: Any) -> str: def _stored(env: ComputerEnv, key: str) -> tuple[ImageStore, str, str | None]: """The env's image store, the ref there of the image of ``key``, and that ref pinned when the store serves one - already, for any env that built it.""" + already, for any env that built it, or once it's copied there from the configured computer image mirror.""" store = _image_store(env) - ref = store.image_ref(REPOSITORY, key.removeprefix("sha256:")) + tag = key.removeprefix("sha256:") + ref = store.image_ref(REPOSITORY, tag) + if (pinned := pin_if_served(ref, store.auth(ref))) is not None or not (mirror := get_config().computer_image_mirror): + return store, ref, pinned + try: + if (mirrored := pin_if_served(f"{mirror}:{mirror_tag(tag)}", None)) is None: + return store, ref, None + copy_image(mirrored, ref, None, store.auth(ref)) + except (ValueError, KeyError, OSError) as e: + logger.warning(f"the computer image mirror {mirror} couldn't give {tag}, so it's built: {e}") + return store, ref, None + logger.info(f"copied {tag} from the computer image mirror {mirror}") return store, ref, pin_if_served(ref, store.auth(ref)) diff --git a/src/agent_env/store/image_store/registry_api.py b/src/agent_env/store/image_store/registry_api.py index 4544cfed8..07d92a8ed 100644 --- a/src/agent_env/store/image_store/registry_api.py +++ b/src/agent_env/store/image_store/registry_api.py @@ -1,5 +1,6 @@ """Read an image from its registry over the OCI distribution API: its digest, to pin a tag to the image it names now, -and its manifests and blobs, to inspect what it holds without pulling it; and write an index of images it holds.""" +and its manifests and blobs, to inspect what it holds without pulling it; write an index of images it holds; and copy an +image from one registry to another.""" from __future__ import annotations @@ -7,6 +8,7 @@ import hashlib import json import re +import tempfile from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path @@ -217,6 +219,40 @@ def write_index(ref: str, images: dict[str, str], auth: RegistryAuth | None, *, {"schemaVersion": 2, "mediaType": media, "manifests": manifests}).encode(), media) +def copy_image(source: str, target: str, source_auth: RegistryAuth | None, target_auth: RegistryAuth | None, *, + timeout: float = _TIMEOUT_SECONDS) -> None: + """Copy the image ``source`` names, pinned by its digest, to ``target``'s tag: every blob ``target``'s repository + lacks, then each manifest byte for byte, so the copy keeps ``source``'s digest; an index's platform manifests + first, each by its digest.""" + digest = _parse(source).digest + if digest is None: + raise ValueError(f"{source}: a copy goes by the image's digest, and this names none") + image = _parse(target) + with (_Repository(_parse(source), source_auth, timeout, source) as reading, + _Repository(image, target_auth, timeout, target) as writing, + tempfile.TemporaryDirectory(prefix="agentenv-copy-") as work): + + def manifest(reference: str) -> tuple[bytes, str]: + response = reading.get(f"manifests/{reference}", accept=_MANIFEST_TYPES) + body = response.json() + served = response.headers.get("content-type", "").split(";")[0].strip() or body.get("mediaType", "") + if served in _INDEXES: + for child in body["manifests"]: + manifest(child["digest"]) + else: + for blob in [body["config"], *body["layers"]]: + if writing.has_blob(blob["digest"]): + continue + path = Path(work) / blob["digest"].removeprefix("sha256:") + read_blob(source, blob["digest"], source_auth, path, timeout=timeout) + writing.upload(blob["digest"], path) + path.unlink() + writing.put(f"manifests/{reference}", response.content, served) + return response.content, served + + writing.put(f"manifests/{image.tag or 'latest'}", *manifest(digest)) + + class _Repository: """One image's repository on its registry, read with the Bearer token the registry's first challenge asks for.""" @@ -250,6 +286,29 @@ def put(self, path: str, content: bytes, content_type: str) -> None: if response.status_code not in (200, 201): self._raise_for(response) + def has_blob(self, digest: str) -> bool: + response = self._send("HEAD", f"blobs/{digest}", None, stream=False) + if response.status_code == 404: + return False + self._raise_for(response) + return True + + def upload(self, digest: str, path: Path) -> None: + """Push the file ``path`` as the blob ``digest``, in one request after the registry opens an upload.""" + opened = self._send("POST", "blobs/uploads/", None, stream=False) + if opened.status_code != 202 or "location" not in opened.headers: + self._raise_for(opened) + raise ValueError(f"{self._what}: {self._host} opened no upload for {digest} (HTTP {opened.status_code})") + location = httpx.URL(f"{self._base}/").join(opened.headers["location"]) + base = httpx.URL(self._base) + if (location.scheme, location.netloc) != (base.scheme, base.netloc): + raise ValueError(f"{self._what}: {self._host} sent the upload of {digest} to {location.scheme}://" + f"{location.netloc.decode()}, which its credentials don't go to") + response = self._send("PUT", str(location.copy_merge_params({"digest": digest})), None, stream=False, + content=path, content_type="application/octet-stream") + if response.status_code != 201: + self._raise_for(response) + @contextlib.contextmanager def stream(self, path: str) -> Iterator[httpx.Response]: """``path`` as a streamed response, closed when the block ends.""" @@ -262,19 +321,27 @@ def stream(self, path: str) -> Iterator[httpx.Response]: finally: response.close() - def _send(self, method: str, path: str, accept: str | None, *, stream: bool, content: bytes | None = None, + def _send(self, method: str, path: str, accept: str | None, *, stream: bool, content: bytes | Path | None = None, content_type: str | None = None) -> httpx.Response: + """``path`` is under the repository, or a whole URL the registry gave; a file's ``content`` streams from disk.""" + url = path if path.startswith(("http://", "https://")) else f"{self._base}/{path}" try: - for _ in range(2): + for attempt in range(2): headers = {"Accept": accept} if accept else {} if content_type: headers["Content-Type"] = content_type if self._token: headers["Authorization"] = f"Bearer {self._token}" - request = self._client.build_request(method, f"{self._base}/{path}", headers=headers, content=content) - # Basic credentials go with every request until a Bearer token replaces them. - response = self._client.send(request, auth=None if self._token else self._basic, stream=stream) - if response.status_code != 401 or self._token: + with contextlib.ExitStack() as opened: + body = content + if isinstance(content, Path): + headers["Content-Length"] = str(content.stat().st_size) + body = opened.enter_context(content.open("rb")) + request = self._client.build_request(method, url, headers=headers, content=body) + # Basic credentials go with every request until a Bearer token replaces them. + response = self._client.send(request, auth=None if self._token else self._basic, stream=stream) + # A held token is asked for again once: GHCR scopes it to the first challenge, a pull's before a push. + if response.status_code != 401 or attempt: return response if stream: response.read() diff --git a/tst/integration/computer/multi_env_computers_test.py b/tst/integration/computer/multi_env_computers_test.py index 34a1adf6e..6524714d6 100644 --- a/tst/integration/computer/multi_env_computers_test.py +++ b/tst/integration/computer/multi_env_computers_test.py @@ -45,6 +45,7 @@ from agent_env.task_step.task_steps.load_artifact import LoadArtifactTaskStep from agent_env.task_step.task_steps.reset_env import ResetEnvTaskStep from agent_env.task_step.task_steps.snapshot_env import SnapshotEnvTaskStep +from tst.util import local_sandboxes from tst.integration.env.envs.server_env_local_test import _items_artifact, _put_items_image from tst.integration.store.local_parallel_deploy_test import _docker, _seed_bootstrap_envs @@ -165,13 +166,18 @@ def _containers(sandbox_id: str) -> list[str]: return _docker("ps", "-aq", "--filter", f"name=agent-{sandbox_id}").stdout.split() -def _ours() -> set[str]: - """The containers local sandboxes run: computers' and gateway stacks'.""" - return {name for name in _docker("ps", "-a", "--format", "{{.Names}}").stdout.split() if name.startswith("agent-")} +@pytest.fixture +def made(monkeypatch) -> list[str]: + return local_sandboxes.recorded_sandbox_ids(monkeypatch) -def _gateway(deployed) -> str: - return next(name for name in _ours() if name.endswith("-gateway-1") and deployed.sandbox_id.removeprefix("local-") in name) +def _containers_made(made: list[str]) -> set[str]: + return local_sandboxes.container_names_carrying(made) + + +def _gateway(deployed, made: list[str]) -> str: + return next(name for name in _containers_made(made) + if name.endswith("-gateway-1") and deployed.sandbox_id.removeprefix("local-") in name) @pytest.mark.asyncio @@ -251,13 +257,12 @@ async def test_a_computer_in_a_multi_env_is_served_through_its_gateway_under_its @pytest.mark.asyncio -async def test_two_computers_keep_serving_through_restarts_and_a_failed_redeploy_and_close_ends_them(desktop, items, +async def test_two_computers_keep_serving_through_restarts_and_a_failed_redeploy_and_close_ends_them(desktop, items, made, monkeypatch): """One deployment, to keep the tier's time: two computers side by side, the desktop's container killed and started again, the gateway restarted, then a redeploy of the same env object whose gateway fails, which leaves this deployment as it was; close() ends it all.""" every = ["list_items", "desktop_computer_screenshot", "laptop_computer_screenshot"] - before = _ours() with namespace_routing(): laptop = ComputerEnv.put(id=f"{desktop.id}-laptop", computer=desktop.computer, driver=desktop.driver, environment_name="laptop") @@ -272,10 +277,10 @@ async def test_two_computers_keep_serving_through_restarts_and_a_failed_redeploy killed = await _answers(deployed, every, until=False) _docker("start", desktop_box) started = await _answers(deployed, every) - _docker("restart", _gateway(deployed)) + _docker("restart", _gateway(deployed, made)) restarted = await _answers(deployed, every) - deployed_before = _ours() + deployed_before = _containers_made(made) real = EnvironmentGatewayProvider._compose_up async def no_gateway(self, sandbox, args=""): @@ -288,7 +293,7 @@ async def no_gateway(self, sandbox, args=""): with pytest.raises(RuntimeError, match="the gateway didn't start"): await env.deploy(sandbox_type="local", ttl_seconds=900, child_envs={"desktop": CONTAINER, "laptop": CONTAINER}) - after_failure = _ours() + after_failure = _containers_made(made) still = await _answers(deployed, every) kept = [computer._deployed.sandbox_id for computer in env.computer_envs] finally: @@ -299,15 +304,15 @@ async def no_gateway(self, sandbox, args=""): assert all(started.values()) and all(restarted.values()), (started, restarted) assert after_failure == deployed_before and all(still.values()), (after_failure - deployed_before, still) assert kept == [deployed.child_envs[name]["sandbox_id"] for name in ("desktop", "laptop")] - assert _ours() == before + assert _containers_made(made) == set() @pytest.mark.asyncio -async def test_a_computer_that_fails_mid_deploy_fails_the_deploy_and_leaves_nothing_up(desktop, items, monkeypatch): +async def test_a_computer_that_fails_mid_deploy_fails_the_deploy_and_leaves_nothing_up(desktop, items, made, + monkeypatch): async def refuse(self, deployed): raise RuntimeError("the computer's tools couldn't be applied") - before = _ours() monkeypatch.setattr(ComputerEnv, "_give_tools", refuse) with namespace_routing(): office = _multi(items, Env.get(desktop.id)) @@ -316,7 +321,7 @@ async def refuse(self, deployed): await DeployEnvTaskStep(id="office", version=None, env_id=office.id, sandbox_type="local", ttl_seconds=600, child_envs={"desktop": CONTAINER}).execute(context) - assert context.deployed_envs == [] and _ours() == before + assert context.deployed_envs == [] and _containers_made(made) == set() @pytest.mark.asyncio diff --git a/tst/integration/slow_durations.json b/tst/integration/slow_durations.json index 4955f5578..6061360bb 100644 --- a/tst/integration/slow_durations.json +++ b/tst/integration/slow_durations.json @@ -7,182 +7,183 @@ "tst/integration/env/gateway/gateway_test.py" ], "seconds": { - "packages/agentenv-protocol/tests/test_a2a_agent.py": 1.1, + "packages/agentenv-protocol/tests/test_a2a_agent.py": 1.2, "packages/agentenv-protocol/tests/test_a2a_agent_examples.py": 0.1, - "packages/agentenv-protocol/tests/test_a2a_cdp_config.py": 0.2, - "packages/agentenv-protocol/tests/test_a2a_object_transfers.py": 0.4, - "packages/agentenv-protocol/tests/test_a2a_peer_agents.py": 3.9, - "packages/agentenv-protocol/tests/test_a2a_proxy.py": 9.0, - "packages/agentenv-protocol/tests/test_a2a_staging.py": 1.4, - "packages/agentenv-protocol/tests/test_a2a_trajectory_log.py": 0.3, - "packages/agentenv-protocol/tests/test_agent_env_environment.py": 0.4, + "packages/agentenv-protocol/tests/test_a2a_cdp_config.py": 0.1, + "packages/agentenv-protocol/tests/test_a2a_object_transfers.py": 0.5, + "packages/agentenv-protocol/tests/test_a2a_peer_agents.py": 4.0, + "packages/agentenv-protocol/tests/test_a2a_proxy.py": 8.5, + "packages/agentenv-protocol/tests/test_a2a_staging.py": 1.0, + "packages/agentenv-protocol/tests/test_a2a_trajectory_log.py": 0.2, + "packages/agentenv-protocol/tests/test_agent_env_environment.py": 0.2, "packages/agentenv-protocol/tests/test_browser_cdp.py": 0.7, - "packages/agentenv-protocol/tests/test_browser_data.py": 0.3, + "packages/agentenv-protocol/tests/test_browser_data.py": 0.2, "packages/agentenv-protocol/tests/test_browser_state.py": 0.1, "packages/agentenv-protocol/tests/test_client.py": 0.1, - "packages/agentenv-protocol/tests/test_computer_app_data.py": 3.1, - "packages/agentenv-protocol/tests/test_computer_app_server.py": 8.4, - "packages/agentenv-protocol/tests/test_computer_conformance.py": 169.6, + "packages/agentenv-protocol/tests/test_computer_app_data.py": 3.0, + "packages/agentenv-protocol/tests/test_computer_app_server.py": 7.2, + "packages/agentenv-protocol/tests/test_computer_conformance.py": 114.0, "packages/agentenv-protocol/tests/test_computer_conformance_checks.py": 1.1, - "packages/agentenv-protocol/tests/test_computer_conformance_page.py": 54.1, - "packages/agentenv-protocol/tests/test_computer_conformance_recording.py": 32.7, - "packages/agentenv-protocol/tests/test_computer_dataplane.py": 3.1, - "packages/agentenv-protocol/tests/test_computer_declare.py": 0.9, - "packages/agentenv-protocol/tests/test_computer_driver.py": 3.5, - "packages/agentenv-protocol/tests/test_computer_extensions.py": 5.7, - "packages/agentenv-protocol/tests/test_computer_frames.py": 5.4, + "packages/agentenv-protocol/tests/test_computer_conformance_page.py": 32.0, + "packages/agentenv-protocol/tests/test_computer_conformance_recording.py": 28.9, + "packages/agentenv-protocol/tests/test_computer_dataplane.py": 4.5, + "packages/agentenv-protocol/tests/test_computer_declare.py": 1.1, + "packages/agentenv-protocol/tests/test_computer_driver.py": 1.6, + "packages/agentenv-protocol/tests/test_computer_extensions.py": 6.1, + "packages/agentenv-protocol/tests/test_computer_frames.py": 2.2, "packages/agentenv-protocol/tests/test_computer_macos.py": 0.0, - "packages/agentenv-protocol/tests/test_computer_page.py": 1.9, - "packages/agentenv-protocol/tests/test_computer_queue.py": 15.8, - "packages/agentenv-protocol/tests/test_computer_recording.py": 33.6, + "packages/agentenv-protocol/tests/test_computer_page.py": 1.0, + "packages/agentenv-protocol/tests/test_computer_queue.py": 16.2, + "packages/agentenv-protocol/tests/test_computer_recording.py": 33.5, "packages/agentenv-protocol/tests/test_computer_seed.py": 0.0, - "packages/agentenv-protocol/tests/test_computer_serve.py": 0.3, + "packages/agentenv-protocol/tests/test_computer_serve.py": 0.4, "packages/agentenv-protocol/tests/test_computer_served.py": 0.3, "packages/agentenv-protocol/tests/test_computer_session_windows.py": 0.4, - "packages/agentenv-protocol/tests/test_computer_spaces.py": 1.2, - "packages/agentenv-protocol/tests/test_computer_svc_forward.py": 8.5, - "packages/agentenv-protocol/tests/test_computer_tool_prefix.py": 1.1, + "packages/agentenv-protocol/tests/test_computer_spaces.py": 1.3, + "packages/agentenv-protocol/tests/test_computer_svc_forward.py": 7.6, + "packages/agentenv-protocol/tests/test_computer_tool_prefix.py": 1.6, "packages/agentenv-protocol/tests/test_control_token.py": 0.0, - "packages/agentenv-protocol/tests/test_data_objects.py": 2.9, - "packages/agentenv-protocol/tests/test_errors.py": 0.3, + "packages/agentenv-protocol/tests/test_data_objects.py": 2.6, + "packages/agentenv-protocol/tests/test_errors.py": 0.2, "packages/agentenv-protocol/tests/test_extension_methods.py": 0.1, "packages/agentenv-protocol/tests/test_intake.py": 0.0, "packages/agentenv-protocol/tests/test_manifest.py": 0.0, "packages/agentenv-protocol/tests/test_preflight.py": 0.0, "packages/agentenv-protocol/tests/test_tool_access.py": 0.2, - "packages/agentenv-protocol/tests/test_tool_annotations.py": 0.3, - "packages/agentenv-protocol/tests/test_tool_client.py": 1.1, - "packages/agentenv-protocol/tests/test_tool_context.py": 0.1, + "packages/agentenv-protocol/tests/test_tool_annotations.py": 0.2, + "packages/agentenv-protocol/tests/test_tool_client.py": 0.9, + "packages/agentenv-protocol/tests/test_tool_context.py": 0.0, "packages/agentenv-protocol/tests/test_tool_context_fastmcp.py": 0.2, - "packages/agentenv-protocol/tests/test_tool_context_injection.py": 0.3, - "packages/agentenv-protocol/tests/test_website_api.py": 0.7, - "packages/agentenv-protocol/tests/test_website_browser.py": 3.7, - "packages/agentenv-protocol/tests/test_website_conformance.py": 17.5, - "packages/agentenv-protocol/tests/test_website_environment.py": 8.0, - "packages/agentenv-protocol/tests/test_website_mcp.py": 2.0, - "packages/agentenv-protocol/tests/test_website_notes_example.py": 1.7, + "packages/agentenv-protocol/tests/test_tool_context_injection.py": 0.4, + "packages/agentenv-protocol/tests/test_website_api.py": 0.6, + "packages/agentenv-protocol/tests/test_website_browser.py": 3.8, + "packages/agentenv-protocol/tests/test_website_conformance.py": 17.8, + "packages/agentenv-protocol/tests/test_website_environment.py": 7.1, + "packages/agentenv-protocol/tests/test_website_mcp.py": 1.8, + "packages/agentenv-protocol/tests/test_website_notes_example.py": 1.6, "packages/agentenv-protocol/tests/test_website_readiness.py": 0.1, - "tst/installer/test_container_journey.py": 162.4, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[pipx]": 8.6, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-project]": 10.3, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-tool]": 17.5, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[venv]": 65.7, - "tst/installer/test_plugin_changes.py::test_a_change_made_elsewhere_while_add_waits_for_confirmation_is_not_undone": 9.0, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[pipx]": 47.6, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-project]": 43.6, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-tool]": 24.5, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[venv]": 75.5, - "tst/installer/test_plugin_changes.py::test_a_package_added_next_to_a_plugin_is_removed_the_same_way": 18.8, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[pipx]": 58.5, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-project]": 46.5, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-tool]": 28.2, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[venv]": 124.7, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[pipx]": 27.9, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-project]": 33.0, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-tool]": 35.4, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[venv]": 72.3, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[uv-tool]": 26.7, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[venv]": 26.4, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[uv-tool]": 11.6, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[venv]": 50.9, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[uv-tool]": 17.6, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[venv]": 36.7, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[uv-tool]": 37.0, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[venv]": 57.3, - "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[hatch-Hatch has no command for this]": 21.4, - "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[pdm-pdm add]": 34.1, - "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[poetry-poetry add]": 34.3, - "tst/installer/test_plugin_changes.py::test_a_second_change_waits_for_the_first_to_finish": 44.9, - "tst/installer/test_plugin_changes.py::test_a_uv_project_removes_a_plugin_from_the_group_that_declares_it": 23.4, - "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[False]": 26.4, - "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[True]": 15.3, - "tst/installer/test_plugin_changes.py::test_a_uv_tool_whose_index_comes_from_its_config_takes_one_change_after_another": 75.8, - "tst/installer/test_plugin_changes.py::test_a_uv_workspace_removes_a_plugin_from_the_member_that_declares_it": 13.5, - "tst/installer/test_plugin_changes.py::test_a_virtual_uv_workspace_adds_the_plugin_to_the_member_that_declares_agent_env": 34.1, - "tst/installer/test_plugin_changes.py::test_adding_a_plugin_by_its_bare_name_keeps_the_version_the_tool_pins": 22.2, - "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[uv-tool]": 15.5, - "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[venv]": 24.0, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-broken-toy_broken is failed]": 29.1, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-clash-deploy_env is skipped]": 32.9, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-not-a-plugin-not an agent-env plugin]": 11.1, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-broken-toy_broken is failed]": 11.5, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-clash-deploy_env is skipped]": 26.4, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-not-a-plugin-not an agent-env plugin]": 14.4, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-broken-toy_broken is failed]": 13.0, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-clash-deploy_env is skipped]": 42.5, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-not-a-plugin-not an agent-env plugin]": 9.9, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-broken-toy_broken is failed]": 51.5, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-clash-deploy_env is skipped]": 29.0, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-not-a-plugin-not an agent-env plugin]": 43.6, - "tst/installer/test_plugin_changes.py::test_ctrl_c_lets_the_installer_finish_and_then_restores": 26.4, - "tst/installer/test_plugin_changes.py::test_read_only_site_packages_are_refused": 50.2, - "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[uv-tool]": 21.5, - "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[venv]": 66.0, - "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-project]": 30.5, - "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-tool]": 34.0, - "tst/installer/test_wheel.py": 0.0, + "tst/installer/test_container_journey.py": 210.8, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[pipx]": 14.6, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-project]": 16.9, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-tool]": 15.5, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[venv]": 46.3, + "tst/installer/test_plugin_changes.py::test_a_change_made_elsewhere_while_add_waits_for_confirmation_is_not_undone": 15.9, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[pipx]": 39.1, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-project]": 39.5, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-tool]": 39.7, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[venv]": 67.9, + "tst/installer/test_plugin_changes.py::test_a_package_added_next_to_a_plugin_is_removed_the_same_way": 34.5, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[pipx]": 52.5, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-project]": 43.6, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-tool]": 46.8, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[venv]": 119.8, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[pipx]": 59.3, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-project]": 38.4, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-tool]": 37.5, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[venv]": 64.4, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[uv-tool]": 22.6, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[venv]": 63.3, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[uv-tool]": 19.7, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[venv]": 70.4, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[uv-tool]": 55.1, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[venv]": 57.0, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[uv-tool]": 33.8, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[venv]": 57.7, + "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[hatch-Hatch has no command for this]": 32.0, + "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[pdm-pdm add]": 31.4, + "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[poetry-poetry add]": 34.9, + "tst/installer/test_plugin_changes.py::test_a_second_change_waits_for_the_first_to_finish": 25.3, + "tst/installer/test_plugin_changes.py::test_a_uv_project_removes_a_plugin_from_the_group_that_declares_it": 21.9, + "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[False]": 24.0, + "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[True]": 25.5, + "tst/installer/test_plugin_changes.py::test_a_uv_tool_whose_index_comes_from_its_config_takes_one_change_after_another": 67.5, + "tst/installer/test_plugin_changes.py::test_a_uv_workspace_removes_a_plugin_from_the_member_that_declares_it": 22.1, + "tst/installer/test_plugin_changes.py::test_a_virtual_uv_workspace_adds_the_plugin_to_the_member_that_declares_agent_env": 32.1, + "tst/installer/test_plugin_changes.py::test_adding_a_plugin_by_its_bare_name_keeps_the_version_the_tool_pins": 37.6, + "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[uv-tool]": 13.3, + "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[venv]": 35.6, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-broken-toy_broken is failed]": 26.8, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-clash-deploy_env is skipped]": 55.9, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-not-a-plugin-not an agent-env plugin]": 14.5, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-broken-toy_broken is failed]": 16.9, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-clash-deploy_env is skipped]": 38.0, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-not-a-plugin-not an agent-env plugin]": 16.6, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-broken-toy_broken is failed]": 21.5, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-clash-deploy_env is skipped]": 23.7, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-not-a-plugin-not an agent-env plugin]": 17.0, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-broken-toy_broken is failed]": 49.5, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-clash-deploy_env is skipped]": 52.0, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-not-a-plugin-not an agent-env plugin]": 40.6, + "tst/installer/test_plugin_changes.py::test_ctrl_c_lets_the_installer_finish_and_then_restores": 17.8, + "tst/installer/test_plugin_changes.py::test_read_only_site_packages_are_refused": 34.6, + "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[uv-tool]": 33.9, + "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[venv]": 61.0, + "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-project]": 48.7, + "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-tool]": 52.9, + "tst/installer/test_wheel.py": 0.4, "tst/integration/agent/a2a_agent/a2a_agent_test.py": 0.0, "tst/integration/agent/a2a_agent/agent_triggers_e2e_test.py": 0.0, "tst/integration/agent/proxy_modal_vm_test.py": 0.0, - "tst/integration/artifact/artifacts/docker_image_artifact_test.py": 18.4, - "tst/integration/cli/bundle_init_images_test.py": 24.8, - "tst/integration/cli/bundle_register_test.py": 43.2, - "tst/integration/cli/cli_test.py": 56.5, - "tst/integration/cli/run_bundle_browser_use_test.py": 196.6, + "tst/integration/artifact/artifacts/docker_image_artifact_test.py": 22.2, + "tst/integration/cli/bundle_init_images_test.py": 25.9, + "tst/integration/cli/bundle_register_test.py": 40.6, + "tst/integration/cli/cli_test.py": 63.3, + "tst/integration/cli/run_bundle_browser_use_test.py": 220.0, "tst/integration/cli/run_bundle_built_agents_test.py": 45.7, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_children_name_one_container_is_refused_before_anything_is_built_or_written": 0.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_servers_share_a_tool_name_is_refused_at_deploy_naming_the_tool_and_both_servers": 42.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[gateway]": 46.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[server]": 11.5, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_server_that_crashes_on_start_fails_its_deploy_naming_it_and_ending_with_what_it_printed": 30.1, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_stored_mcp_task_imports_as_a_bundle_that_runs_through_the_gateway": 64.5, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_universe_composed_every_way_seeds_each_server_of_a_multi_and_a_shared_one_seeds_its_server": 48.4, - "tst/integration/cli/run_bundle_built_envs_test.py::test_an_env_folder_is_built_named_by_its_card_deployed_and_rebuilt_only_when_it_changes": 18.7, - "tst/integration/cli/run_bundle_built_envs_test.py::test_an_output_a_bundles_task_wrote_is_written_under_the_folder_that_imports_it": 43.3, - "tst/integration/cli/run_bundle_built_envs_test.py::test_each_kind_of_bundle_env_deploys_through_the_gateway_and_a_child_edit_rewrites_its_multi": 175.7, - "tst/integration/cli/run_bundle_built_envs_test.py::test_environments_and_a_universe_written_from_a_bundle_load_into_an_env_through_the_gateway": 69.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_echo_mcp_example_runs_by_name_and_a_rerun_writes_nothing": 67.9, - "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_mail_mcp_example_runs_and_a_task_outside_a_bundle_deploys_its_env_by_its_alias": 51.5, - "tst/integration/cli/run_bundle_claude_code_install_test.py": 123.6, - "tst/integration/cli/run_bundle_claude_code_test.py": 323.1, - "tst/integration/cli/run_bundle_cli_folder_test.py": 75.1, - "tst/integration/cli/run_bundle_image_folder_test.py": 43.8, - "tst/integration/cli/run_bundle_image_ref_test.py": 30.7, - "tst/integration/cli/run_bundle_installed_agent_test.py": 22.0, - "tst/integration/cli/run_bundle_openhands_test.py": 237.4, - "tst/integration/cli/run_bundle_pydantic_ai_test.py": 107.9, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_children_name_one_container_is_refused_before_anything_is_built_or_written": 0.3, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_servers_share_a_tool_name_is_refused_at_deploy_naming_the_tool_and_both_servers": 42.6, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[gateway]": 34.2, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[server]": 16.2, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_server_that_crashes_on_start_fails_its_deploy_naming_it_and_ending_with_what_it_printed": 31.0, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_stored_mcp_task_imports_as_a_bundle_that_runs_through_the_gateway": 60.6, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_universe_composed_every_way_seeds_each_server_of_a_multi_and_a_shared_one_seeds_its_server": 42.5, + "tst/integration/cli/run_bundle_built_envs_test.py::test_an_env_folder_is_built_named_by_its_card_deployed_and_rebuilt_only_when_it_changes": 19.4, + "tst/integration/cli/run_bundle_built_envs_test.py::test_an_output_a_bundles_task_wrote_is_written_under_the_folder_that_imports_it": 49.3, + "tst/integration/cli/run_bundle_built_envs_test.py::test_each_kind_of_bundle_env_deploys_through_the_gateway_and_a_child_edit_rewrites_its_multi": 168.9, + "tst/integration/cli/run_bundle_built_envs_test.py::test_environments_and_a_universe_written_from_a_bundle_load_into_an_env_through_the_gateway": 53.1, + "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_echo_mcp_example_runs_by_name_and_a_rerun_writes_nothing": 60.2, + "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_mail_mcp_example_runs_and_a_task_outside_a_bundle_deploys_its_env_by_its_alias": 58.5, + "tst/integration/cli/run_bundle_claude_code_install_test.py": 138.8, + "tst/integration/cli/run_bundle_claude_code_test.py": 314.6, + "tst/integration/cli/run_bundle_cli_folder_test.py": 86.0, + "tst/integration/cli/run_bundle_image_folder_test.py": 46.6, + "tst/integration/cli/run_bundle_image_ref_test.py": 27.2, + "tst/integration/cli/run_bundle_installed_agent_test.py": 26.2, + "tst/integration/cli/run_bundle_openhands_test.py": 226.1, + "tst/integration/cli/run_bundle_pydantic_ai_test.py": 118.1, "tst/integration/cli/run_bundle_skills_test.py": 25.1, - "tst/integration/cli/run_role_models_test.py": 36.5, - "tst/integration/computer/base_computer_test.py": 83.2, - "tst/integration/computer/browser_computer_test.py": 303.7, - "tst/integration/computer/browser_gateway_test.py": 191.7, + "tst/integration/cli/run_bundle_task_image_test.py": 4.7, + "tst/integration/cli/run_role_models_test.py": 48.3, + "tst/integration/computer/base_computer_test.py": 86.8, + "tst/integration/computer/browser_computer_test.py": 306.6, + "tst/integration/computer/browser_gateway_test.py": 153.1, "tst/integration/computer/builder_modal_vm_test.py": 0.0, - "tst/integration/computer/ca_trust_test.py": 270.4, - "tst/integration/computer/chromium_app_test.py": 552.2, - "tst/integration/computer/computer_app_test.py::test_a_computers_apps_load_export_and_reset_each_through_its_own_data_plane": 111.4, - "tst/integration/computer/computer_app_test.py::test_an_app_deployed_alone_runs_its_service_on_its_settings_on_a_computer_named_after_it_as_its_driver": 66.8, - "tst/integration/computer/computer_app_test.py::test_the_locks_debs_and_wheels_install_from_mounts_and_stay_in_no_layer_of_the_image": 148.4, - "tst/integration/computer/computer_app_test.py::test_through_the_drivers_route_a_websocket_reaches_an_apps_server_and_a_20_mb_answer_streams_whole": 182.4, - "tst/integration/computer/computer_app_test.py::test_two_apps_composed_into_one_computer_env_each_run_on_their_own_settings": 150.5, - "tst/integration/computer/computer_env_test.py": 257.9, - "tst/integration/computer/covered_test.py": 51.0, - "tst/integration/computer/cua_driver_test.py": 73.3, - "tst/integration/computer/desktop_example_test.py": 130.8, - "tst/integration/computer/disk_boot_test.py": 132.1, + "tst/integration/computer/ca_trust_test.py": 239.3, + "tst/integration/computer/chromium_app_test.py": 609.4, + "tst/integration/computer/computer_app_test.py::test_a_computers_apps_load_export_and_reset_each_through_its_own_data_plane": 116.0, + "tst/integration/computer/computer_app_test.py::test_an_app_deployed_alone_runs_its_service_on_its_settings_on_a_computer_named_after_it_as_its_driver": 70.0, + "tst/integration/computer/computer_app_test.py::test_the_locks_debs_and_wheels_install_from_mounts_and_stay_in_no_layer_of_the_image": 220.5, + "tst/integration/computer/computer_app_test.py::test_through_the_drivers_route_a_websocket_reaches_an_apps_server_and_a_20_mb_answer_streams_whole": 243.8, + "tst/integration/computer/computer_app_test.py::test_two_apps_composed_into_one_computer_env_each_run_on_their_own_settings": 147.6, + "tst/integration/computer/computer_env_test.py": 198.4, + "tst/integration/computer/covered_test.py": 62.7, + "tst/integration/computer/cua_driver_test.py": 122.9, + "tst/integration/computer/desktop_example_test.py": 141.9, + "tst/integration/computer/disk_boot_test.py": 94.8, "tst/integration/computer/disk_modal_vm_test.py": 0.0, - "tst/integration/computer/launcher_test.py": 88.0, - "tst/integration/computer/multi_env_computers_test.py": 314.0, - "tst/integration/computer/recording_chaos_test.py": 136.5, - "tst/integration/computer/set_test.py": 151.9, - "tst/integration/computer/tool_prefix_test.py": 91.3, - "tst/integration/computer/website_browser_test.py": 201.7, + "tst/integration/computer/launcher_test.py": 119.5, + "tst/integration/computer/multi_env_computers_test.py": 274.4, + "tst/integration/computer/recording_chaos_test.py": 214.8, + "tst/integration/computer/set_test.py": 170.1, + "tst/integration/computer/tool_prefix_test.py": 93.6, + "tst/integration/computer/website_browser_test.py": 348.9, "tst/integration/computer/windows_test.py": 0.0, - "tst/integration/env/env_test.py": 15.7, - "tst/integration/env/envs/build_context_local_test.py": 165.7, - "tst/integration/env/envs/multi_env_child_envs_local_test.py": 106.0, - "tst/integration/env/envs/multi_env_local_test.py": 115.9, - "tst/integration/env/envs/playwright_mcp_local_test.py": 88.9, - "tst/integration/env/envs/server_env_local_test.py": 75.4, + "tst/integration/env/env_test.py": 13.6, + "tst/integration/env/envs/build_context_local_test.py": 199.0, + "tst/integration/env/envs/multi_env_child_envs_local_test.py": 95.7, + "tst/integration/env/envs/multi_env_local_test.py": 59.0, + "tst/integration/env/envs/playwright_mcp_local_test.py": 101.6, + "tst/integration/env/envs/server_env_local_test.py": 87.9, "tst/integration/env/envs/server_env_modal_test.py": 0.0, "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[boat]": 0.0, @@ -272,7 +273,7 @@ "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_cardless_environment_resolves_name_from_environment_name": 37.6, + "tst/integration/env/gateway/gateway_test.py::test_cardless_environment_resolves_name_from_environment_name": 37.7, "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[boxd]": 0.0, @@ -304,7 +305,7 @@ "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[sail_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[scale]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tenki]": 0.3, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[boat]": 0.0, @@ -403,7 +404,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[scale]": 29.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[scale]": 29.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[agent37]": 0.0, @@ -414,11 +415,11 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[scale]": 29.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[scale]": 28.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_request_behind_a_write_through_the_rest_proxy": 28.3, - "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_role_behind_each_write": 28.3, + "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_request_behind_a_write_through_the_rest_proxy": 27.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_role_behind_each_write": 46.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[boxd]": 0.0, @@ -427,7 +428,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[scale]": 36.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[scale]": 36.9, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[agent37]": 0.0, @@ -438,10 +439,10 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[scale]": 51.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[scale]": 51.7, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_consistent_mode": 50.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_consistent_mode": 47.9, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[boxd]": 0.0, @@ -450,7 +451,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[scale]": 28.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[scale]": 30.7, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[agent37]": 0.0, @@ -461,7 +462,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[scale]": 29.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[scale]": 28.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[agent37]": 0.0, @@ -472,7 +473,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[scale]": 31.6, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[scale]": 30.3, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[agent37]": 0.0, @@ -483,7 +484,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[scale]": 26.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[scale]": 26.6, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[agent37]": 0.0, @@ -494,13 +495,13 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[scale]": 37.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[scale]": 39.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_nl_executor[modal_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_website_trigger_fires_on_a_request_through_the_website_proxy": 72.2, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_agentenv_environment": 28.7, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_env_including_website": 261.8, + "tst/integration/env/gateway/gateway_test.py::test_gateway_website_trigger_fires_on_a_request_through_the_website_proxy": 64.8, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_agentenv_environment": 38.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_env_including_website": 141.3, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[boxd]": 0.0, @@ -509,12 +510,12 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[scale]": 192.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[scale]": 108.7, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_mcp_server": 209.7, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_website_env": 250.4, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_website_env_v1": 44.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_mcp_server": 98.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_website_env": 118.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_website_env_v1": 42.1, "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[boxd]": 0.0, @@ -647,7 +648,7 @@ "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_load_from_snapshot": 42.4, + "tst/integration/env/gateway/gateway_test.py::test_load_from_snapshot": 41.6, "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[boxd]": 0.0, @@ -769,7 +770,7 @@ "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_renamed_registration_serves_its_registered_name": 37.1, + "tst/integration/env/gateway/gateway_test.py::test_renamed_registration_serves_its_registered_name": 40.7, "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[boxd]": 0.0, @@ -836,8 +837,8 @@ "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_snapshot_agent_state_v1_and_fallback": 41.9, - "tst/integration/env/gateway/gateway_test.py::test_snapshot_cli": 56.9, + "tst/integration/env/gateway/gateway_test.py::test_snapshot_agent_state_v1_and_fallback": 39.1, + "tst/integration/env/gateway/gateway_test.py::test_snapshot_cli": 37.3, "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[boxd]": 0.0, @@ -901,7 +902,7 @@ "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[scale]": 38.8, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[scale]": 26.9, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[agent37]": 0.0, @@ -926,7 +927,7 @@ "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_the_browsers_card_and_recording_through_the_gateway_survive_its_recorder_and_container_restarting": 240.7, + "tst/integration/env/gateway/gateway_test.py::test_the_browsers_card_and_recording_through_the_gateway_survive_its_recorder_and_container_restarting": 131.4, "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[boxd]": 0.0, @@ -960,8 +961,8 @@ "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_validate_universe_compatibility": 28.1, - "tst/integration/env/gateway/gateway_test.py::test_verify_universe_roundtrip_export_all_v1_and_fallback": 28.1, + "tst/integration/env/gateway/gateway_test.py::test_validate_universe_compatibility": 28.5, + "tst/integration/env/gateway/gateway_test.py::test_verify_universe_roundtrip_export_all_v1_and_fallback": 27.1, "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[boxd]": 0.0, @@ -1028,9 +1029,9 @@ "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/task_step/task_steps_test.py": 187.9, - "tst/integration/eval/eval_test.py": 1.7, - "tst/integration/openenv/build_app_test.py": 89.8, + "tst/integration/env/task_step/task_steps_test.py": 121.1, + "tst/integration/eval/eval_test.py": 2.1, + "tst/integration/openenv/build_app_test.py": 91.3, "tst/integration/providers/sandbox_providers/agent37_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/boat_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/boxd_sandbox_smoke_test.py": 0.0, @@ -1041,262 +1042,262 @@ "tst/integration/providers/sandbox_providers/tenki_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/tensorlake_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/vercel_sandbox_smoke_test.py": 0.0, - "tst/integration/providers/sandbox_providers/vm_sandbox_build_test.py": 2.8, + "tst/integration/providers/sandbox_providers/vm_sandbox_build_test.py": 2.1, "tst/integration/providers/sandbox_providers/websockets_live_test.py": 0.0, - "tst/integration/store/local_parallel_deploy_test.py": 51.6, - "tst/integration/task_step/test_advance_to_next_event_hook_local.py": 112.1, - "tst/integration/task_step/test_agent_on_local_sandbox.py": 7.7, + "tst/integration/store/local_parallel_deploy_test.py": 47.6, + "tst/integration/task_step/test_advance_to_next_event_hook_local.py": 119.0, + "tst/integration/task_step/test_agent_on_local_sandbox.py": 24.5, "tst/integration/task_step/test_enable_docker_e2e.py": 0.0, "tst/integration/task_step/test_multiturn_verifier_delivery.py": 0.0, - "tst/integration/task_step/test_next_env_event_hook_local.py": 111.3, - "tst/integration/task_step/test_prompt_agent_notifications_local.py": 115.4, - "tst/integration/task_step/test_register_env_triggers_local.py": 91.2, - "tst/integration/training/harness_test.py": 26.8, - "tst/integration/training/trl_test.py": 32.9, - "tst/integration/utils/docker/prune_test.py": 6.3, - "tst/unit/a2a_agent/add_skill_test.py": 0.9, - "tst/unit/a2a_agent/alias_get_test.py": 0.4, + "tst/integration/task_step/test_next_env_event_hook_local.py": 109.6, + "tst/integration/task_step/test_prompt_agent_notifications_local.py": 121.3, + "tst/integration/task_step/test_register_env_triggers_local.py": 135.2, + "tst/integration/training/harness_test.py": 31.7, + "tst/integration/training/trl_test.py": 25.7, + "tst/integration/utils/docker/prune_test.py": 5.5, + "tst/unit/a2a_agent/add_skill_test.py": 1.0, + "tst/unit/a2a_agent/alias_get_test.py": 0.3, "tst/unit/a2a_agent/cdp_config_test.py": 0.0, - "tst/unit/a2a_agent/context_only_deploy_test.py": 0.7, + "tst/unit/a2a_agent/context_only_deploy_test.py": 0.3, "tst/unit/a2a_agent/deploy_cleanup_test.py": 0.1, "tst/unit/a2a_agent/negotiate_agent_config_test.py": 0.0, - "tst/unit/a2a_agent/object_transfer_reach_test.py": 0.2, - "tst/unit/a2a_agent/object_transfer_test.py": 0.3, + "tst/unit/a2a_agent/object_transfer_reach_test.py": 0.1, + "tst/unit/a2a_agent/object_transfer_test.py": 0.2, "tst/unit/a2a_agent/protocol_test.py": 0.3, - "tst/unit/a2a_agent/put_version_test.py": 0.2, + "tst/unit/a2a_agent/put_version_test.py": 0.1, "tst/unit/a2a_agent/readable_parts_test.py": 0.1, - "tst/unit/a2a_agent/staging_test.py": 2.2, + "tst/unit/a2a_agent/staging_test.py": 1.3, "tst/unit/a2a_agent/store_test.py": 0.0, "tst/unit/a2a_agent/test_trajectory_follower.py": 0.7, - "tst/unit/a2a_agent/test_validator_changelog_objects.py": 0.2, + "tst/unit/a2a_agent/test_validator_changelog_objects.py": 0.1, "tst/unit/a2a_agent/test_validator_skill_variants.py": 0.1, - "tst/unit/a2a_agent/validator_trajectory_dag_test.py": 0.3, + "tst/unit/a2a_agent/validator_trajectory_dag_test.py": 0.2, "tst/unit/a2a_agent/validator_uri_probes_test.py": 0.0, "tst/unit/a2a_agent/verify_a2a_agent_card_test.py": 0.0, - "tst/unit/agents/browser_use/test_browser_use_agent.py": 1.6, - "tst/unit/agents/claude_code/test_card_contract.py": 1.3, - "tst/unit/agents/claude_code/test_changelog.py": 107.5, + "tst/unit/agents/browser_use/test_browser_use_agent.py": 1.4, + "tst/unit/agents/claude_code/test_card_contract.py": 1.5, + "tst/unit/agents/claude_code/test_changelog.py": 17.3, "tst/unit/agents/claude_code/test_claude_bridge_builtin_tools.py": 0.1, - "tst/unit/agents/claude_code/test_claude_bridge_cancel.py": 9.6, - "tst/unit/agents/claude_code/test_claude_bridge_failure_message.py": 0.3, + "tst/unit/agents/claude_code/test_claude_bridge_cancel.py": 9.2, + "tst/unit/agents/claude_code/test_claude_bridge_failure_message.py": 0.1, "tst/unit/agents/claude_code/test_claude_bridge_live.py": 0.1, "tst/unit/agents/claude_code/test_claude_bridge_model.py": 0.1, "tst/unit/agents/claude_code/test_claude_bridge_retry.py": 0.4, "tst/unit/agents/claude_code/test_claude_bridge_stdin.py": 1.1, "tst/unit/agents/claude_code/test_cli_env.py": 0.2, - "tst/unit/agents/claude_code/test_ext_mcp_name.py": 0.1, - "tst/unit/agents/claude_code/test_extensions.py": 0.2, + "tst/unit/agents/claude_code/test_ext_mcp_name.py": 0.0, + "tst/unit/agents/claude_code/test_extensions.py": 0.1, "tst/unit/agents/claude_code/test_image_contract.py": 0.1, - "tst/unit/agents/claude_code/test_install_sh_host_root.py": 0.1, + "tst/unit/agents/claude_code/test_install_sh_host_root.py": 0.0, "tst/unit/agents/claude_code/test_message_contract.py": 1.6, "tst/unit/agents/claude_code/test_object_transfer.py": 0.5, "tst/unit/agents/claude_code/test_partial_trajectory.py": 0.2, - "tst/unit/agents/claude_code/test_workspace_snapshot.py": 0.2, + "tst/unit/agents/claude_code/test_workspace_snapshot.py": 0.1, "tst/unit/agents/codex/test_card_contract.py": 0.1, - "tst/unit/agents/codex/test_codex_bridge_events.py": 0.2, + "tst/unit/agents/codex/test_codex_bridge_events.py": 0.3, "tst/unit/agents/codex/test_codex_bridge_failure_message.py": 0.2, - "tst/unit/agents/codex/test_codex_bridge_retry.py": 0.5, - "tst/unit/agents/codex/test_codex_bridge_watchdog.py": 0.3, + "tst/unit/agents/codex/test_codex_bridge_retry.py": 0.4, + "tst/unit/agents/codex/test_codex_bridge_watchdog.py": 0.2, "tst/unit/agents/codex/test_codex_builtin_tools.py": 1.9, - "tst/unit/agents/codex/test_codex_executor_outcome.py": 0.2, - "tst/unit/agents/codex/test_codex_snapshot_hardening.py": 0.3, - "tst/unit/agents/codex/test_config_and_mcp.py": 0.3, + "tst/unit/agents/codex/test_codex_executor_outcome.py": 0.3, + "tst/unit/agents/codex/test_codex_snapshot_hardening.py": 0.4, + "tst/unit/agents/codex/test_config_and_mcp.py": 0.1, "tst/unit/agents/codex/test_image_contract.py": 0.1, "tst/unit/agents/codex/test_install.py": 0.1, "tst/unit/agents/codex/test_live_trajectory.py": 0.1, - "tst/unit/agents/codex/test_message_contract.py": 0.1, - "tst/unit/agents/codex/test_object_transfer.py": 0.8, - "tst/unit/agents/gemini/test_card_contract.py": 1.7, - "tst/unit/agents/gemini/test_extensions.py": 0.6, - "tst/unit/agents/gemini/test_gemini_bridge_attribution.py": 0.1, - "tst/unit/agents/gemini/test_gemini_bridge_cancel.py": 5.6, - "tst/unit/agents/gemini/test_gemini_bridge_failure_message.py": 0.3, + "tst/unit/agents/codex/test_message_contract.py": 0.2, + "tst/unit/agents/codex/test_object_transfer.py": 0.5, + "tst/unit/agents/gemini/test_card_contract.py": 1.8, + "tst/unit/agents/gemini/test_extensions.py": 0.3, + "tst/unit/agents/gemini/test_gemini_bridge_attribution.py": 0.0, + "tst/unit/agents/gemini/test_gemini_bridge_cancel.py": 5.8, + "tst/unit/agents/gemini/test_gemini_bridge_failure_message.py": 0.4, "tst/unit/agents/gemini/test_gemini_bridge_live.py": 1.3, "tst/unit/agents/gemini/test_image_contract.py": 0.1, "tst/unit/agents/gemini/test_install.py": 0.1, - "tst/unit/agents/gemini/test_litellm_proxy.py": 0.5, - "tst/unit/agents/gemini/test_message_contract.py": 0.4, - "tst/unit/agents/gemini/test_object_transfer.py": 3.5, - "tst/unit/agents/hermes/test_card_contract.py": 1.6, - "tst/unit/agents/hermes/test_held_work.py": 0.8, - "tst/unit/agents/hermes/test_hermes_bridge.py": 3.6, - "tst/unit/agents/hermes/test_message_contract.py": 0.7, - "tst/unit/agents/letta_code/test_bridge.py": 0.8, - "tst/unit/agents/opencode/test_card_contract.py": 1.8, - "tst/unit/agents/opencode/test_image_contract.py": 0.2, + "tst/unit/agents/gemini/test_litellm_proxy.py": 0.3, + "tst/unit/agents/gemini/test_message_contract.py": 0.3, + "tst/unit/agents/gemini/test_object_transfer.py": 2.6, + "tst/unit/agents/hermes/test_card_contract.py": 1.1, + "tst/unit/agents/hermes/test_held_work.py": 0.7, + "tst/unit/agents/hermes/test_hermes_bridge.py": 3.4, + "tst/unit/agents/hermes/test_message_contract.py": 0.3, + "tst/unit/agents/letta_code/test_bridge.py": 0.5, + "tst/unit/agents/opencode/test_card_contract.py": 1.5, + "tst/unit/agents/opencode/test_image_contract.py": 0.1, "tst/unit/agents/opencode/test_install.py": 0.1, - "tst/unit/agents/opencode/test_message_contract.py": 1.1, - "tst/unit/agents/opencode/test_object_transfer.py": 0.7, - "tst/unit/agents/opencode/test_opencode_bridge_builtin_tools.py": 0.3, + "tst/unit/agents/opencode/test_message_contract.py": 0.9, + "tst/unit/agents/opencode/test_object_transfer.py": 0.6, + "tst/unit/agents/opencode/test_opencode_bridge_builtin_tools.py": 0.1, "tst/unit/agents/opencode/test_opencode_bridge_cancel.py": 5.7, - "tst/unit/agents/opencode/test_opencode_bridge_config.py": 0.2, - "tst/unit/agents/opencode/test_opencode_bridge_events.py": 0.1, - "tst/unit/agents/opencode/test_opencode_bridge_live.py": 1.3, + "tst/unit/agents/opencode/test_opencode_bridge_config.py": 0.1, + "tst/unit/agents/opencode/test_opencode_bridge_events.py": 0.0, + "tst/unit/agents/opencode/test_opencode_bridge_live.py": 1.2, "tst/unit/agents/opencode/test_opencode_bridge_stdin.py": 0.1, "tst/unit/agents/openhands/test_conversation_run.py": 0.1, - "tst/unit/agents/openhands/test_openhands_agent.py": 4.8, - "tst/unit/agents/pi/test_pi_agent.py": 2.7, - "tst/unit/agents/proxy/test_proxy_agent.py": 0.6, - "tst/unit/agents/pydantic_ai/test_pydantic_ai_agent.py": 3.5, - "tst/unit/agents/test_host_workdir.py": 0.1, - "tst/unit/agents/test_installed_workspace.py": 0.4, + "tst/unit/agents/openhands/test_openhands_agent.py": 4.7, + "tst/unit/agents/pi/test_pi_agent.py": 2.9, + "tst/unit/agents/proxy/test_proxy_agent.py": 0.2, + "tst/unit/agents/pydantic_ai/test_pydantic_ai_agent.py": 3.4, + "tst/unit/agents/test_host_workdir.py": 0.0, + "tst/unit/agents/test_installed_workspace.py": 0.3, "tst/unit/artifact/cli_artifact_test.py": 0.1, - "tst/unit/artifact/environment_files_test.py": 0.4, + "tst/unit/artifact/environment_files_test.py": 0.3, "tst/unit/artifact/environment_universe_folder_test.py": 0.0, - "tst/unit/artifact/registry_test.py": 1.0, + "tst/unit/artifact/registry_test.py": 0.5, "tst/unit/artifact/skill_artifact_test.py": 0.2, "tst/unit/artifact/test_artifact_metadata.py": 0.1, "tst/unit/artifact/test_artifact_put.py": 0.0, - "tst/unit/artifact/test_computer_artifact.py": 1.1, - "tst/unit/artifact/test_docker_image_build_context.py": 0.5, + "tst/unit/artifact/test_computer_artifact.py": 0.8, + "tst/unit/artifact/test_docker_image_build_context.py": 0.4, "tst/unit/artifact/test_docker_image_github_clone.py": 0.1, - "tst/unit/artifact/test_docker_image_github_push.py": 0.2, - "tst/unit/artifact/test_docker_image_pulled.py": 0.1, - "tst/unit/artifact/test_docker_image_put_ref.py": 0.5, - "tst/unit/artifact/test_object_url_keys.py": 0.4, - "tst/unit/artifact/test_removed_service_aliases.py": 1.9, + "tst/unit/artifact/test_docker_image_github_push.py": 0.1, + "tst/unit/artifact/test_docker_image_pulled.py": 0.2, + "tst/unit/artifact/test_docker_image_put_ref.py": 0.3, + "tst/unit/artifact/test_object_url_keys.py": 0.3, + "tst/unit/artifact/test_removed_service_aliases.py": 1.4, "tst/unit/artifact/test_service_artifact_alias.py": 0.1, "tst/unit/artifact/test_service_universe_file_view.py": 0.1, - "tst/unit/artifact/test_store_put_document.py": 0.2, + "tst/unit/artifact/test_store_put_document.py": 0.1, "tst/unit/attribution_test.py": 0.0, - "tst/unit/bundle/agent_override_test.py": 2.7, - "tst/unit/bundle/aliases_test.py": 0.6, - "tst/unit/bundle/artifact_toml_test.py": 5.0, - "tst/unit/bundle/authoring_test.py": 0.7, - "tst/unit/bundle/cli_folder_test.py": 2.1, - "tst/unit/bundle/computer_app_env_test.py": 12.6, - "tst/unit/bundle/computer_driver_env_test.py": 18.3, - "tst/unit/bundle/computer_env_folder_test.py": 55.1, - "tst/unit/bundle/computer_folder_test.py": 1.5, - "tst/unit/bundle/env_toml_test.py": 6.5, - "tst/unit/bundle/file_artifacts_test.py": 0.8, - "tst/unit/bundle/hub_test.py": 2.6, - "tst/unit/bundle/image_folder_test.py": 3.8, - "tst/unit/bundle/importer_entities_test.py": 9.8, - "tst/unit/bundle/importer_image_refs_test.py": 4.2, - "tst/unit/bundle/importer_test.py": 20.7, - "tst/unit/bundle/installed_entities_test.py": 8.0, - "tst/unit/bundle/installed_test.py": 5.6, + "tst/unit/bundle/agent_override_test.py": 1.4, + "tst/unit/bundle/aliases_test.py": 0.4, + "tst/unit/bundle/artifact_toml_test.py": 3.5, + "tst/unit/bundle/authoring_test.py": 0.4, + "tst/unit/bundle/cli_folder_test.py": 1.1, + "tst/unit/bundle/computer_app_env_test.py": 11.0, + "tst/unit/bundle/computer_driver_env_test.py": 19.0, + "tst/unit/bundle/computer_env_folder_test.py": 30.0, + "tst/unit/bundle/computer_folder_test.py": 1.9, + "tst/unit/bundle/env_toml_test.py": 3.0, + "tst/unit/bundle/file_artifacts_test.py": 0.6, + "tst/unit/bundle/hub_test.py": 2.7, + "tst/unit/bundle/image_folder_test.py": 4.5, + "tst/unit/bundle/importer_entities_test.py": 5.7, + "tst/unit/bundle/importer_image_refs_test.py": 2.5, + "tst/unit/bundle/importer_test.py": 11.8, + "tst/unit/bundle/installed_entities_test.py": 11.1, + "tst/unit/bundle/installed_test.py": 7.4, "tst/unit/bundle/ledger_digest_test.py": 0.1, - "tst/unit/bundle/ledger_test.py": 21.3, - "tst/unit/bundle/materialize_test.py": 14.5, - "tst/unit/bundle/parse_test.py": 4.1, - "tst/unit/bundle/plan_test.py": 5.9, - "tst/unit/bundle/plugin_artifact_types_test.py": 1.2, - "tst/unit/bundle/preflight_test.py": 57.1, - "tst/unit/bundle/referenced_context_test.py": 1.9, - "tst/unit/bundle/register_test.py": 10.9, - "tst/unit/bundle/requires_test.py": 9.2, - "tst/unit/bundle/resolve_test.py": 6.0, - "tst/unit/bundle/run_teardown_test.py": 24.5, - "tst/unit/bundle/run_test.py": 11.4, - "tst/unit/bundle/shipped_bundles_test.py": 2.5, - "tst/unit/bundle/shipped_computers_test.py": 1.5, - "tst/unit/bundle/to_toml_round_trip_test.py": 6.4, - "tst/unit/bundle/toml_refs_test.py": 0.4, + "tst/unit/bundle/ledger_test.py": 14.5, + "tst/unit/bundle/materialize_test.py": 17.6, + "tst/unit/bundle/parse_test.py": 4.3, + "tst/unit/bundle/plan_test.py": 2.4, + "tst/unit/bundle/plugin_artifact_types_test.py": 0.6, + "tst/unit/bundle/preflight_test.py": 22.4, + "tst/unit/bundle/referenced_context_test.py": 1.1, + "tst/unit/bundle/register_test.py": 11.9, + "tst/unit/bundle/requires_test.py": 4.0, + "tst/unit/bundle/resolve_test.py": 3.9, + "tst/unit/bundle/run_teardown_test.py": 22.9, + "tst/unit/bundle/run_test.py": 13.0, + "tst/unit/bundle/shipped_bundles_test.py": 4.3, + "tst/unit/bundle/shipped_computers_test.py": 0.7, + "tst/unit/bundle/to_toml_round_trip_test.py": 3.8, + "tst/unit/bundle/toml_refs_test.py": 0.3, "tst/unit/bundle/toml_writer_test.py": 0.1, - "tst/unit/bundle/variants_test.py": 6.0, + "tst/unit/bundle/variants_test.py": 6.2, "tst/unit/bundle/website_optional_frontend_test.py": 0.0, - "tst/unit/cli/a2a_agent_list_test.py": 0.3, - "tst/unit/cli/a2a_agent_validate_test.py": 0.3, + "tst/unit/cli/a2a_agent_list_test.py": 0.2, + "tst/unit/cli/a2a_agent_validate_test.py": 0.2, "tst/unit/cli/base_metadata_version_test.py": 0.0, - "tst/unit/cli/build_platform_test.py": 0.4, - "tst/unit/cli/card_naming_relocation_test.py": 0.2, + "tst/unit/cli/build_platform_test.py": 0.3, + "tst/unit/cli/card_naming_relocation_test.py": 0.3, "tst/unit/cli/deploy_vm_sizing_test.py": 0.1, "tst/unit/cli/deployed_env_output_test.py": 0.1, "tst/unit/cli/env_provider_type_test.py": 0.2, - "tst/unit/cli/env_put_validation_test.py": 0.5, - "tst/unit/cli/env_state_type_parity_test.py": 0.1, + "tst/unit/cli/env_put_validation_test.py": 0.4, + "tst/unit/cli/env_state_type_parity_test.py": 0.0, "tst/unit/cli/environment_name_alias_test.py": 0.4, - "tst/unit/cli/eval_run_teardown_test.py": 0.2, - "tst/unit/cli/image_ref_put_test.py": 0.7, + "tst/unit/cli/eval_run_teardown_test.py": 0.3, + "tst/unit/cli/image_ref_put_test.py": 0.6, "tst/unit/cli/init_env_state_cli_test.py": 0.2, "tst/unit/cli/load_artifact_target_test.py": 0.1, - "tst/unit/cli/plugin_command_test.py": 41.2, - "tst/unit/cli/plugin_install_test.py": 9.3, - "tst/unit/cli/plugin_report_test.py": 21.3, - "tst/unit/cli/plugins_test.py": 10.8, + "tst/unit/cli/plugin_command_test.py": 24.6, + "tst/unit/cli/plugin_install_test.py": 4.2, + "tst/unit/cli/plugin_report_test.py": 17.6, + "tst/unit/cli/plugins_test.py": 12.0, "tst/unit/cli/run_role_models_test.py": 0.2, - "tst/unit/cli/sandbox_url_relocation_test.py": 3.1, + "tst/unit/cli/sandbox_url_relocation_test.py": 2.4, "tst/unit/cli/snapshot_step_output_test.py": 0.0, "tst/unit/cli/stage_free_commands_test.py": 0.0, "tst/unit/cli/submit_runs_test.py": 5.7, - "tst/unit/cli/task_run_requires_test.py": 3.5, - "tst/unit/cli/task_run_stored_test.py": 0.7, - "tst/unit/cli/task_run_teardown_test.py": 7.9, + "tst/unit/cli/task_run_requires_test.py": 1.8, + "tst/unit/cli/task_run_stored_test.py": 0.5, + "tst/unit/cli/task_run_teardown_test.py": 7.7, "tst/unit/cli/teardown_env_state_cli_test.py": 0.1, "tst/unit/cli/test_release_gate_decision.py": 0.1, - "tst/unit/cli/test_removed_cli_aliases.py": 0.3, + "tst/unit/cli/test_removed_cli_aliases.py": 0.4, "tst/unit/cli/test_spec_conformance_display.py": 0.1, "tst/unit/cli/test_task_create_preflight.py": 0.1, - "tst/unit/cli/user_facing_errors_test.py": 0.2, - "tst/unit/cli/verbose_logging_test.py": 2.7, - "tst/unit/cli/website_server_image_cli_test.py": 1.0, + "tst/unit/cli/user_facing_errors_test.py": 0.1, + "tst/unit/cli/verbose_logging_test.py": 2.8, + "tst/unit/cli/website_server_image_cli_test.py": 0.4, "tst/unit/cli/write_context_filename_test.py": 0.1, - "tst/unit/config/test_config_holds_its_document.py": 0.2, + "tst/unit/config/test_config_holds_its_document.py": 0.1, "tst/unit/config/test_config_profiles.py": 0.2, "tst/unit/config/test_default_a2a_agent.py": 0.2, - "tst/unit/config/test_default_env_ids.py": 0.3, - "tst/unit/config/test_describe.py": 2.6, - "tst/unit/config/test_explain_and_sources.py": 11.5, - "tst/unit/config/test_fixture_prefix_keys.py": 0.2, - "tst/unit/config/test_human_a2a_url.py": 0.4, - "tst/unit/config/test_loader.py": 1.0, + "tst/unit/config/test_default_env_ids.py": 0.2, + "tst/unit/config/test_describe.py": 1.1, + "tst/unit/config/test_explain_and_sources.py": 6.0, + "tst/unit/config/test_fixture_prefix_keys.py": 0.1, + "tst/unit/config/test_human_a2a_url.py": 0.5, + "tst/unit/config/test_loader.py": 1.1, "tst/unit/config/test_model.py": 0.8, "tst/unit/config/test_no_stage_concept.py": 0.1, - "tst/unit/config/test_one_document_per_process.py": 3.5, - "tst/unit/config/test_paths.py": 0.2, - "tst/unit/config/test_registry_builders_take_a_config.py": 0.4, - "tst/unit/config/test_reset_config.py": 1.3, + "tst/unit/config/test_one_document_per_process.py": 4.1, + "tst/unit/config/test_paths.py": 0.1, + "tst/unit/config/test_registry_builders_take_a_config.py": 0.1, + "tst/unit/config/test_reset_config.py": 1.1, "tst/unit/config/test_resolve_reference.py": 0.0, "tst/unit/config/test_run_env_ids.py": 0.1, - "tst/unit/config/test_unread_keys.py": 1.5, - "tst/unit/drivers/agent_desktop_driver_test.py": 15.2, - "tst/unit/drivers/codex_computer_use_driver_test.py": 15.4, - "tst/unit/drivers/cua_driver_test.py": 1.7, + "tst/unit/config/test_unread_keys.py": 1.4, + "tst/unit/drivers/agent_desktop_driver_test.py": 8.7, + "tst/unit/drivers/codex_computer_use_driver_test.py": 18.5, + "tst/unit/drivers/cua_driver_test.py": 1.0, "tst/unit/drivers/macos_harness_driver_test.py": 0.4, - "tst/unit/drivers/page_driver_test.py": 0.2, - "tst/unit/drivers/reference_driver_test.py": 4.1, + "tst/unit/drivers/page_driver_test.py": 0.1, + "tst/unit/drivers/reference_driver_test.py": 2.2, "tst/unit/entity_refs_import_test.py": 0.0, - "tst/unit/env/alias_get_test.py": 0.4, - "tst/unit/env/bootstrap_test.py": 8.2, + "tst/unit/env/alias_get_test.py": 0.3, + "tst/unit/env/bootstrap_test.py": 12.4, "tst/unit/env/envs/service_db/service_db_test.py": 0.3, "tst/unit/env/envs/test_artifact_staging.py": 0.3, "tst/unit/env/envs/test_chromium_app.py": 0.4, - "tst/unit/env/envs/test_computer_apps.py": 10.8, - "tst/unit/env/envs/test_computer_builder.py": 8.0, - "tst/unit/env/envs/test_computer_disk.py": 22.3, - "tst/unit/env/envs/test_computer_env.py": 59.4, - "tst/unit/env/envs/test_computer_macos_grants.py": 0.2, - "tst/unit/env/envs/test_computer_site_proxy.py": 4.1, - "tst/unit/env/envs/test_computer_sites.py": 3.0, - "tst/unit/env/envs/test_computer_vm_bundle.py": 8.0, - "tst/unit/env/envs/test_computer_windows.py": 3.4, - "tst/unit/env/envs/test_disk_recipe.py": 68.0, - "tst/unit/env/envs/test_env_card_loads.py": 0.3, + "tst/unit/env/envs/test_computer_apps.py": 4.9, + "tst/unit/env/envs/test_computer_builder.py": 4.0, + "tst/unit/env/envs/test_computer_disk.py": 18.3, + "tst/unit/env/envs/test_computer_env.py": 29.0, + "tst/unit/env/envs/test_computer_macos_grants.py": 0.1, + "tst/unit/env/envs/test_computer_site_proxy.py": 3.7, + "tst/unit/env/envs/test_computer_sites.py": 1.6, + "tst/unit/env/envs/test_computer_vm_bundle.py": 6.6, + "tst/unit/env/envs/test_computer_windows.py": 1.9, + "tst/unit/env/envs/test_disk_recipe.py": 7.8, + "tst/unit/env/envs/test_env_card_loads.py": 0.4, "tst/unit/env/envs/test_env_close_teardown.py": 0.1, "tst/unit/env/envs/test_environment_name_alias.py": 0.2, "tst/unit/env/envs/test_install_platform_keys.py": 0.0, - "tst/unit/env/envs/test_mcp_server_container_mode.py": 0.6, - "tst/unit/env/envs/test_mcp_server_load.py": 0.1, - "tst/unit/env/envs/test_mcp_server_plugin_provider.py": 0.5, - "tst/unit/env/envs/test_multi_env_computers.py": 1.3, + "tst/unit/env/envs/test_mcp_server_container_mode.py": 0.3, + "tst/unit/env/envs/test_mcp_server_load.py": 0.0, + "tst/unit/env/envs/test_mcp_server_plugin_provider.py": 0.3, + "tst/unit/env/envs/test_multi_env_computers.py": 0.9, "tst/unit/env/envs/test_multi_env_load_concurrency.py": 0.3, "tst/unit/env/envs/test_multi_env_load_resume.py": 0.2, - "tst/unit/env/envs/test_multi_env_name.py": 0.2, - "tst/unit/env/envs/test_multi_env_plugin_provider.py": 0.6, - "tst/unit/env/envs/test_multi_env_snapshot_bake.py": 0.2, + "tst/unit/env/envs/test_multi_env_name.py": 0.1, + "tst/unit/env/envs/test_multi_env_plugin_provider.py": 0.5, + "tst/unit/env/envs/test_multi_env_snapshot_bake.py": 0.1, "tst/unit/env/envs/test_multi_env_snapshot_guard.py": 0.1, - "tst/unit/env/envs/test_multi_env_snapshot_reload_name.py": 0.5, - "tst/unit/env/envs/test_multi_env_vm_sizing.py": 0.3, + "tst/unit/env/envs/test_multi_env_snapshot_reload_name.py": 0.6, + "tst/unit/env/envs/test_multi_env_vm_sizing.py": 0.2, "tst/unit/env/envs/test_playwright_mcp_recorder.py": 2.9, - "tst/unit/env/envs/test_put_from_github_token.py": 0.1, + "tst/unit/env/envs/test_put_from_github_token.py": 0.0, "tst/unit/env/envs/test_server_reset.py": 0.1, - "tst/unit/env/envs/test_website_env_name_alias.py": 0.1, - "tst/unit/env/envs/test_website_server_image.py": 0.7, - "tst/unit/env/envs/test_website_site_names.py": 1.9, - "tst/unit/env/gateway/browser_route_test.py": 3.6, - "tst/unit/env/gateway/caller_test.py": 0.1, + "tst/unit/env/envs/test_website_env_name_alias.py": 0.0, + "tst/unit/env/envs/test_website_server_image.py": 0.8, + "tst/unit/env/envs/test_website_site_names.py": 1.0, + "tst/unit/env/gateway/browser_route_test.py": 3.1, + "tst/unit/env/gateway/caller_test.py": 0.0, "tst/unit/env/gateway/changes_test.py": 0.4, "tst/unit/env/gateway/child_outage_socket_test.py": 9.3, "tst/unit/env/gateway/child_sessions_test.py": 0.5, @@ -1304,329 +1305,330 @@ "tst/unit/env/gateway/clock_advance_test.py": 0.3, "tst/unit/env/gateway/data_plane_timeout_test.py": 0.1, "tst/unit/env/gateway/env_trajectory_route_test.py": 0.1, - "tst/unit/env/gateway/gateway_card_test.py": 0.6, - "tst/unit/env/gateway/get_time_test.py": 0.5, - "tst/unit/env/gateway/hidden_by_default_test.py": 15.7, - "tst/unit/env/gateway/import_graph_test.py": 3.6, + "tst/unit/env/gateway/gateway_card_test.py": 0.5, + "tst/unit/env/gateway/get_time_test.py": 0.4, + "tst/unit/env/gateway/hidden_by_default_test.py": 13.5, + "tst/unit/env/gateway/import_graph_test.py": 2.1, "tst/unit/env/gateway/next_event_test.py": 0.4, "tst/unit/env/gateway/notifications_test.py": 1.6, "tst/unit/env/gateway/process_scope_socket_test.py": 1.0, - "tst/unit/env/gateway/rest_proxy_socket_test.py": 1.8, - "tst/unit/env/gateway/rest_proxy_stream_test.py": 0.6, + "tst/unit/env/gateway/rest_proxy_socket_test.py": 1.6, + "tst/unit/env/gateway/rest_proxy_stream_test.py": 0.7, "tst/unit/env/gateway/role_forwarding_test.py": 0.1, "tst/unit/env/gateway/role_rules_test.py": 0.3, - "tst/unit/env/gateway/tool_access_parity_test.py": 16.7, + "tst/unit/env/gateway/tool_access_parity_test.py": 12.9, "tst/unit/env/gateway/tool_name_clash_test.py": 0.1, "tst/unit/env/gateway/trajectory_virtual_time_test.py": 0.1, - "tst/unit/env/gateway/triggers_ctx_test.py": 2.5, - "tst/unit/env/gateway/triggers_test.py": 13.7, + "tst/unit/env/gateway/triggers_ctx_test.py": 2.4, + "tst/unit/env/gateway/triggers_test.py": 13.3, "tst/unit/env/gateway/triggers_time_test.py": 1.8, - "tst/unit/env/gateway/triggers_website_test.py": 1.2, - "tst/unit/env/gateway/website_mcp_test.py": 1.9, - "tst/unit/env/gateway/website_reserved_paths_test.py": 0.3, - "tst/unit/env/gateway/website_websocket_test.py": 4.8, - "tst/unit/env/published_images_test.py": 0.5, - "tst/unit/env/store_test.py": 0.2, + "tst/unit/env/gateway/triggers_website_test.py": 1.1, + "tst/unit/env/gateway/website_mcp_test.py": 2.1, + "tst/unit/env/gateway/website_reserved_paths_test.py": 0.2, + "tst/unit/env/gateway/website_websocket_test.py": 4.2, + "tst/unit/env/published_images_test.py": 0.4, + "tst/unit/env/store_test.py": 0.1, "tst/unit/env/task_step/deploy_agent_env_vars_test.py": 0.1, - "tst/unit/env/task_step/test_context_ops.py": 0.5, + "tst/unit/env/task_step/test_context_ops.py": 0.4, "tst/unit/env/task_step/test_score_aggregator.py": 0.2, - "tst/unit/env/task_step/verify_env_card_test.py": 0.2, + "tst/unit/env/task_step/verify_env_card_test.py": 0.1, "tst/unit/env/task_step/verify_env_core_protocol_test.py": 0.2, - "tst/unit/env/test_env.py": 0.9, + "tst/unit/env/test_env.py": 0.4, "tst/unit/env/test_legacy_protocol.py": 0.3, - "tst/unit/env/test_registry.py": 0.7, - "tst/unit/env/test_snapshot_fingerprint.py": 0.1, + "tst/unit/env/test_registry.py": 0.3, + "tst/unit/env/test_snapshot_fingerprint.py": 0.2, "tst/unit/explorer/entity_ids_test.py": 0.1, - "tst/unit/explorer/explorer_app_test.py": 20.8, - "tst/unit/openenv/app_test.py": 19.2, - "tst/unit/openenv/environment_test.py": 13.5, + "tst/unit/explorer/explorer_app_test.py": 21.0, + "tst/unit/openenv/app_test.py": 7.6, + "tst/unit/openenv/environment_test.py": 14.3, "tst/unit/plugin_inventory_test.py": 1.3, - "tst/unit/plugin_requirements_test.py": 0.3, - "tst/unit/plugin_settings_test.py": 11.8, - "tst/unit/plugins_test.py": 2.3, - "tst/unit/providers/compose_host_port_publish_test.py": 1.0, - "tst/unit/providers/env_providers/env_gateway_provider_test.py": 7.3, - "tst/unit/providers/env_providers/env_provider_test.py": 0.2, - "tst/unit/providers/env_providers/env_server_provider_test.py": 0.6, + "tst/unit/plugin_requirements_test.py": 0.1, + "tst/unit/plugin_settings_test.py": 12.3, + "tst/unit/plugins_test.py": 0.8, + "tst/unit/providers/compose_host_port_publish_test.py": 0.9, + "tst/unit/providers/env_providers/env_gateway_provider_test.py": 6.5, + "tst/unit/providers/env_providers/env_provider_test.py": 0.1, + "tst/unit/providers/env_providers/env_server_provider_test.py": 0.3, "tst/unit/providers/env_state/test_config_state_providers.py": 0.9, - "tst/unit/providers/env_state/test_env_state_provider.py": 0.5, - "tst/unit/providers/sandbox_providers/agent37_sandbox_provider_test.py": 0.6, - "tst/unit/providers/sandbox_providers/agent37_sandbox_test.py": 1.2, - "tst/unit/providers/sandbox_providers/attribution_wire_test.py": 0.4, - "tst/unit/providers/sandbox_providers/boat_sandbox_provider_test.py": 0.6, - "tst/unit/providers/sandbox_providers/boat_sandbox_test.py": 0.5, - "tst/unit/providers/sandbox_providers/boxd_sandbox_provider_test.py": 1.7, - "tst/unit/providers/sandbox_providers/boxd_sandbox_test.py": 0.5, - "tst/unit/providers/sandbox_providers/coreweave_deploy_paths_test.py": 0.2, - "tst/unit/providers/sandbox_providers/coreweave_sandbox_provider_test.py": 1.8, - "tst/unit/providers/sandbox_providers/coreweave_sandbox_test.py": 0.3, - "tst/unit/providers/sandbox_providers/daytona_network_test.py": 0.2, - "tst/unit/providers/sandbox_providers/daytona_sandbox_provider_test.py": 2.0, + "tst/unit/providers/env_state/test_env_state_provider.py": 0.2, + "tst/unit/providers/sandbox_providers/agent37_sandbox_provider_test.py": 0.4, + "tst/unit/providers/sandbox_providers/agent37_sandbox_test.py": 1.1, + "tst/unit/providers/sandbox_providers/attribution_wire_test.py": 0.1, + "tst/unit/providers/sandbox_providers/boat_sandbox_provider_test.py": 0.2, + "tst/unit/providers/sandbox_providers/boat_sandbox_test.py": 0.4, + "tst/unit/providers/sandbox_providers/boxd_sandbox_provider_test.py": 1.5, + "tst/unit/providers/sandbox_providers/boxd_sandbox_test.py": 0.2, + "tst/unit/providers/sandbox_providers/coreweave_deploy_paths_test.py": 0.4, + "tst/unit/providers/sandbox_providers/coreweave_sandbox_provider_test.py": 1.3, + "tst/unit/providers/sandbox_providers/coreweave_sandbox_test.py": 1.0, + "tst/unit/providers/sandbox_providers/daytona_network_test.py": 0.4, + "tst/unit/providers/sandbox_providers/daytona_sandbox_provider_test.py": 1.7, "tst/unit/providers/sandbox_providers/daytona_sandbox_test.py": 1.2, "tst/unit/providers/sandbox_providers/e2b_sandbox_provider_test.py": 0.2, "tst/unit/providers/sandbox_providers/e2b_sandbox_test.py": 1.2, - "tst/unit/providers/sandbox_providers/e2b_template_test.py": 0.6, - "tst/unit/providers/sandbox_providers/egress_hosts_test.py": 0.4, - "tst/unit/providers/sandbox_providers/host_images_test.py": 0.9, - "tst/unit/providers/sandbox_providers/local_sandbox_host_ports_test.py": 0.5, - "tst/unit/providers/sandbox_providers/local_sandbox_test.py": 12.6, - "tst/unit/providers/sandbox_providers/lume_sandbox_provider_test.py": 10.7, + "tst/unit/providers/sandbox_providers/e2b_template_test.py": 0.3, + "tst/unit/providers/sandbox_providers/egress_hosts_test.py": 0.2, + "tst/unit/providers/sandbox_providers/host_images_test.py": 0.7, + "tst/unit/providers/sandbox_providers/local_sandbox_host_ports_test.py": 0.3, + "tst/unit/providers/sandbox_providers/local_sandbox_test.py": 11.8, + "tst/unit/providers/sandbox_providers/lume_sandbox_provider_test.py": 10.4, "tst/unit/providers/sandbox_providers/modal_context_build_test.py": 0.8, - "tst/unit/providers/sandbox_providers/modal_create_signature_test.py": 0.2, - "tst/unit/providers/sandbox_providers/modal_gpu_sandbox_test.py": 0.0, - "tst/unit/providers/sandbox_providers/modal_network_policy_test.py": 0.2, + "tst/unit/providers/sandbox_providers/modal_create_signature_test.py": 0.1, + "tst/unit/providers/sandbox_providers/modal_gpu_sandbox_test.py": 0.1, + "tst/unit/providers/sandbox_providers/modal_network_policy_test.py": 0.0, "tst/unit/providers/sandbox_providers/modal_network_policy_wiring_test.py": 0.1, - "tst/unit/providers/sandbox_providers/modal_sandbox_started_log_test.py": 0.1, + "tst/unit/providers/sandbox_providers/modal_sandbox_started_log_test.py": 0.0, "tst/unit/providers/sandbox_providers/modal_sandbox_tags_test.py": 0.1, - "tst/unit/providers/sandbox_providers/modal_sandbox_test.py": 0.7, - "tst/unit/providers/sandbox_providers/modal_vm_sandbox_test.py": 1.2, - "tst/unit/providers/sandbox_providers/network_policy_providers_test.py": 0.4, + "tst/unit/providers/sandbox_providers/modal_sandbox_test.py": 0.4, + "tst/unit/providers/sandbox_providers/modal_vm_sandbox_test.py": 1.8, + "tst/unit/providers/sandbox_providers/network_policy_providers_test.py": 0.2, "tst/unit/providers/sandbox_providers/network_policy_stamping_test.py": 0.1, - "tst/unit/providers/sandbox_providers/network_policy_test.py": 0.2, - "tst/unit/providers/sandbox_providers/provider_capabilities_test.py": 3.2, + "tst/unit/providers/sandbox_providers/network_policy_test.py": 0.1, + "tst/unit/providers/sandbox_providers/provider_capabilities_test.py": 2.7, "tst/unit/providers/sandbox_providers/relay_test.py": 0.1, - "tst/unit/providers/sandbox_providers/sail_vm_model_key_test.py": 2.1, - "tst/unit/providers/sandbox_providers/sail_vm_sandbox_provider_test.py": 2.0, - "tst/unit/providers/sandbox_providers/sail_vm_sandbox_test.py": 0.3, - "tst/unit/providers/sandbox_providers/sandbox_provider_test.py": 0.7, + "tst/unit/providers/sandbox_providers/sail_vm_model_key_test.py": 1.2, + "tst/unit/providers/sandbox_providers/sail_vm_sandbox_provider_test.py": 1.4, + "tst/unit/providers/sandbox_providers/sail_vm_sandbox_test.py": 0.2, + "tst/unit/providers/sandbox_providers/sandbox_provider_test.py": 0.4, "tst/unit/providers/sandbox_providers/tenki_sandbox_provider_test.py": 0.9, - "tst/unit/providers/sandbox_providers/tenki_sandbox_test.py": 0.3, + "tst/unit/providers/sandbox_providers/tenki_sandbox_test.py": 0.2, "tst/unit/providers/sandbox_providers/tensorlake_sandbox_provider_test.py": 0.4, - "tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py": 5.4, - "tst/unit/providers/sandbox_providers/test_config_sandbox_providers.py": 1.5, - "tst/unit/providers/sandbox_providers/test_reachability_contributions.py": 0.5, - "tst/unit/providers/sandbox_providers/test_url_rewrites.py": 0.7, - "tst/unit/providers/sandbox_providers/vercel_sandbox_provider_test.py": 0.4, - "tst/unit/providers/sandbox_providers/vercel_sandbox_test.py": 3.8, + "tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py": 4.8, + "tst/unit/providers/sandbox_providers/test_config_sandbox_providers.py": 0.6, + "tst/unit/providers/sandbox_providers/test_reachability_contributions.py": 0.2, + "tst/unit/providers/sandbox_providers/test_url_rewrites.py": 0.8, + "tst/unit/providers/sandbox_providers/vercel_sandbox_provider_test.py": 0.2, + "tst/unit/providers/sandbox_providers/vercel_sandbox_test.py": 3.5, "tst/unit/providers/sandbox_providers/vm_file_transfer_test.py": 0.2, - "tst/unit/providers/sandbox_providers/vm_object_push_test.py": 0.6, - "tst/unit/providers/sandbox_providers/vm_sandbox_test.py": 0.7, + "tst/unit/providers/sandbox_providers/vm_object_push_test.py": 0.3, + "tst/unit/providers/sandbox_providers/vm_sandbox_test.py": 0.6, "tst/unit/runner/local_runner_test.py": 7.6, "tst/unit/runner/run_store_test.py": 0.1, "tst/unit/serialization/test_golden_wire_format.py": 0.4, "tst/unit/store/awaiting_lock_test.py": 1.0, - "tst/unit/store/aws/aws_secret_store_test.py": 0.3, - "tst/unit/store/aws/dynamodb_document_store_test.py": 14.1, - "tst/unit/store/aws/dynamodb_step_journal_test.py": 13.7, - "tst/unit/store/aws/ecr_image_store_test.py": 0.1, - "tst/unit/store/aws/s3_grant_lifetime_test.py": 0.1, - "tst/unit/store/aws/s3_object_store_test.py": 6.5, + "tst/unit/store/aws/aws_secret_store_test.py": 0.1, + "tst/unit/store/aws/dynamodb_document_store_test.py": 10.9, + "tst/unit/store/aws/dynamodb_step_journal_test.py": 11.3, + "tst/unit/store/aws/ecr_image_store_test.py": 0.0, + "tst/unit/store/aws/s3_grant_lifetime_test.py": 0.0, + "tst/unit/store/aws/s3_object_store_test.py": 5.5, "tst/unit/store/aws/s3_transfer_grants_test.py": 0.2, "tst/unit/store/aws/test_secret_logging.py": 0.1, "tst/unit/store/aws/test_shared_credentials.py": 0.2, - "tst/unit/store/derived_ids_test.py": 9.1, + "tst/unit/store/derived_ids_test.py": 9.0, "tst/unit/store/fake_object_store_test.py": 0.1, - "tst/unit/store/firestore_mongo_document_store_test.py": 0.3, - "tst/unit/store/gcp_secret_manager_secret_store_test.py": 0.1, - "tst/unit/store/gcs_object_store_test.py": 0.9, - "tst/unit/store/google_credentials_test.py": 0.8, - "tst/unit/store/google_transport_test.py": 0.2, + "tst/unit/store/firestore_mongo_document_store_test.py": 0.4, + "tst/unit/store/gcp_secret_manager_secret_store_test.py": 0.2, + "tst/unit/store/gcs_object_store_test.py": 0.4, + "tst/unit/store/google_credentials_test.py": 0.7, + "tst/unit/store/google_transport_test.py": 0.1, "tst/unit/store/grant_lifetime_test.py": 0.1, - "tst/unit/store/ids_test.py": 1.1, - "tst/unit/store/local_backends_composition_test.py": 1.2, - "tst/unit/store/local_grant_server_test.py": 10.9, - "tst/unit/store/local_id_acceptance_test.py": 2.0, - "tst/unit/store/local_object_store_test.py": 1.7, + "tst/unit/store/ids_test.py": 0.5, + "tst/unit/store/local_backends_composition_test.py": 0.8, + "tst/unit/store/local_grant_server_test.py": 9.7, + "tst/unit/store/local_id_acceptance_test.py": 1.7, + "tst/unit/store/local_object_store_test.py": 1.5, "tst/unit/store/local_registry_image_store_test.py": 0.1, "tst/unit/store/local_secret_store_test.py": 0.1, - "tst/unit/store/local_tls_test.py": 0.5, + "tst/unit/store/local_tls_test.py": 0.3, "tst/unit/store/local_tokens_test.py": 0.2, "tst/unit/store/mongo_document_store_test.py": 0.8, - "tst/unit/store/oci_registry_credentials_test.py": 0.2, - "tst/unit/store/oci_registry_image_store_test.py": 0.4, - "tst/unit/store/readable_url_test.py": 0.3, - "tst/unit/store/registry_api_test.py": 0.5, - "tst/unit/store/routing_test.py": 3.4, - "tst/unit/store/sqlite_document_store_test.py": 11.3, - "tst/unit/store/test_config_document_store.py": 0.3, - "tst/unit/store/test_config_image_store.py": 0.3, + "tst/unit/store/oci_registry_credentials_test.py": 0.1, + "tst/unit/store/oci_registry_image_store_test.py": 0.3, + "tst/unit/store/readable_url_test.py": 0.2, + "tst/unit/store/registry_api_test.py": 0.3, + "tst/unit/store/routing_test.py": 3.8, + "tst/unit/store/sqlite_document_store_test.py": 7.8, + "tst/unit/store/test_config_document_store.py": 0.2, + "tst/unit/store/test_config_image_store.py": 0.2, "tst/unit/store/test_config_model.py": 0.6, "tst/unit/store/test_config_mongo_client.py": 0.1, "tst/unit/store/test_config_object_store.py": 0.2, "tst/unit/store/test_config_secret_bundle_keys.py": 0.0, - "tst/unit/store/test_config_secret_store.py": 0.2, - "tst/unit/store/test_document_store.py": 0.7, - "tst/unit/store/test_object_transfer_grants.py": 0.5, - "tst/unit/store/test_task_instance_indexes.py": 0.1, - "tst/unit/store/version_lookup_benchmark_test.py": 1.5, + "tst/unit/store/test_config_secret_store.py": 0.1, + "tst/unit/store/test_document_store.py": 0.4, + "tst/unit/store/test_object_transfer_grants.py": 0.3, + "tst/unit/store/test_task_instance_indexes.py": 0.0, + "tst/unit/store/version_lookup_benchmark_test.py": 1.2, "tst/unit/task/batch_universe_ids_test.py": 0.0, "tst/unit/task/benchmark_test.py": 0.0, "tst/unit/task/control_grant_test.py": 0.1, "tst/unit/task/depends_on_test.py": 0.9, "tst/unit/task/episode/proxy_test.py": 0.3, - "tst/unit/task/episode/run_test.py": 20.1, + "tst/unit/task/episode/run_test.py": 19.2, "tst/unit/task/episode/spec_test.py": 2.5, - "tst/unit/task/local_run_write_fence_test.py": 2.9, + "tst/unit/task/local_run_write_fence_test.py": 1.9, "tst/unit/task/retry_sandbox_teardown_test.py": 0.1, "tst/unit/task/seed_context_fence_test.py": 0.1, - "tst/unit/task/seed_substitution_test.py": 0.1, + "tst/unit/task/seed_substitution_test.py": 0.0, "tst/unit/task/step_journal_rules_test.py": 0.2, "tst/unit/task/step_journal_test.py": 1.7, "tst/unit/task/task_document_test.py": 0.4, - "tst/unit/task/task_failure_write_retry_test.py": 0.2, + "tst/unit/task/task_failure_write_retry_test.py": 0.1, "tst/unit/task/task_instance_reads_test.py": 0.2, - "tst/unit/task/teardown_test.py": 2.7, - "tst/unit/task_step/add_skills_env_test.py": 0.2, - "tst/unit/task_step/attribution_serialization_test.py": 0.2, - "tst/unit/task_step/computer_recording_test.py": 1.1, + "tst/unit/task/teardown_test.py": 1.6, + "tst/unit/task_step/add_skills_env_test.py": 0.1, + "tst/unit/task_step/attribution_serialization_test.py": 0.3, + "tst/unit/task_step/computer_recording_test.py": 1.3, "tst/unit/task_step/context_ops_journal_test.py": 0.1, "tst/unit/task_step/deploy_agent_cleanup_test.py": 0.0, - "tst/unit/task_step/deploy_agent_linked_sandbox_test.py": 0.0, + "tst/unit/task_step/deploy_agent_linked_sandbox_test.py": 0.1, "tst/unit/task_step/deploy_agent_test.py": 0.1, - "tst/unit/task_step/deploy_env_test.py": 1.4, - "tst/unit/task_step/deploy_human_agent_test.py": 0.1, + "tst/unit/task_step/deploy_env_test.py": 0.6, + "tst/unit/task_step/deploy_human_agent_test.py": 0.2, "tst/unit/task_step/deploy_sandbox_default_cpu_test.py": 0.1, "tst/unit/task_step/deploy_sandbox_image_test.py": 0.1, "tst/unit/task_step/deploy_sandbox_network_policy_test.py": 0.1, "tst/unit/task_step/deploy_sandbox_provider_override_test.py": 0.1, - "tst/unit/task_step/install_agent_guards_test.py": 0.4, - "tst/unit/task_step/install_agent_test.py": 0.5, - "tst/unit/task_step/local_container_urls_test.py": 1.8, - "tst/unit/task_step/local_step_containers_test.py": 0.3, - "tst/unit/task_step/prompt_agent_file_parts_test.py": 0.3, - "tst/unit/task_step/retry_test.py": 1.8, - "tst/unit/task_step/run_context_object_url_test.py": 0.3, - "tst/unit/task_step/run_docker_container_test.py": 0.2, - "tst/unit/task_step/skill_object_url_test.py": 0.2, - "tst/unit/task_step/snapshot_computer_env_test.py": 0.6, - "tst/unit/task_step/snapshot_env_upload_grant_test.py": 1.0, + "tst/unit/task_step/install_agent_guards_test.py": 0.2, + "tst/unit/task_step/install_agent_test.py": 0.3, + "tst/unit/task_step/local_container_urls_test.py": 2.1, + "tst/unit/task_step/local_step_containers_test.py": 0.2, + "tst/unit/task_step/prompt_agent_file_parts_test.py": 0.5, + "tst/unit/task_step/retry_test.py": 1.3, + "tst/unit/task_step/run_context_object_url_test.py": 0.2, + "tst/unit/task_step/run_docker_container_test.py": 0.3, + "tst/unit/task_step/skill_object_url_test.py": 0.3, + "tst/unit/task_step/snapshot_computer_env_test.py": 0.4, + "tst/unit/task_step/snapshot_env_upload_grant_test.py": 1.2, "tst/unit/task_step/snapshot_env_without_gateway_test.py": 0.6, "tst/unit/task_step/stored_priority_test.py": 0.1, "tst/unit/task_step/stored_s3_form_fields_test.py": 0.1, - "tst/unit/task_step/sync_env_clock_test.py": 0.6, - "tst/unit/task_step/test_advance_to_next_event_hook.py": 1.1, + "tst/unit/task_step/sync_env_clock_test.py": 0.5, + "tst/unit/task_step/test_advance_to_next_event_hook.py": 1.2, "tst/unit/task_step/test_agent_model_precedence.py": 0.1, - "tst/unit/task_step/test_agent_state_capture.py": 1.2, - "tst/unit/task_step/test_apply_server_config.py": 1.0, + "tst/unit/task_step/test_agent_state_capture.py": 1.3, + "tst/unit/task_step/test_apply_server_config.py": 0.8, "tst/unit/task_step/test_browser_connect_cdp.py": 0.2, - "tst/unit/task_step/test_browser_steps.py": 1.0, - "tst/unit/task_step/test_collect_artifacts.py": 1.1, - "tst/unit/task_step/test_collect_artifacts_universe_suffix.py": 0.2, + "tst/unit/task_step/test_browser_sign_in.py": 0.2, + "tst/unit/task_step/test_browser_steps.py": 1.1, + "tst/unit/task_step/test_collect_artifacts.py": 1.2, + "tst/unit/task_step/test_collect_artifacts_universe_suffix.py": 0.3, "tst/unit/task_step/test_collect_artifacts_vm_host.py": 0.3, "tst/unit/task_step/test_collect_integrity.py": 0.1, "tst/unit/task_step/test_combine_universe_verdicts.py": 0.1, - "tst/unit/task_step/test_config_task_steps.py": 0.7, - "tst/unit/task_step/test_configure_scheduled_checks.py": 0.2, + "tst/unit/task_step/test_config_task_steps.py": 0.4, + "tst/unit/task_step/test_configure_scheduled_checks.py": 0.1, "tst/unit/task_step/test_container_sandbox_routing.py": 0.3, - "tst/unit/task_step/test_deploy_agent_changelog_objects.py": 1.0, + "tst/unit/task_step/test_deploy_agent_changelog_objects.py": 0.5, "tst/unit/task_step/test_deploy_agent_enable_docker.py": 0.1, - "tst/unit/task_step/test_deploy_agent_live_endpoint.py": 0.1, + "tst/unit/task_step/test_deploy_agent_live_endpoint.py": 0.0, "tst/unit/task_step/test_deploy_agent_mcp_alias.py": 0.2, - "tst/unit/task_step/test_deploy_agent_role_delivery.py": 1.2, - "tst/unit/task_step/test_deploy_agent_snapshot_objects.py": 0.5, + "tst/unit/task_step/test_deploy_agent_role_delivery.py": 0.5, + "tst/unit/task_step/test_deploy_agent_snapshot_objects.py": 0.2, "tst/unit/task_step/test_deploy_ttl_from_budget.py": 0.0, - "tst/unit/task_step/test_entity_refs.py": 1.0, - "tst/unit/task_step/test_field_scopes.py": 0.7, + "tst/unit/task_step/test_entity_refs.py": 3.2, + "tst/unit/task_step/test_field_scopes.py": 0.9, "tst/unit/task_step/test_install_agent_registers_instance.py": 0.1, - "tst/unit/task_step/test_load_artifact_agent_container_target.py": 0.1, + "tst/unit/task_step/test_load_artifact_agent_container_target.py": 0.0, "tst/unit/task_step/test_load_artifact_container_sandbox.py": 0.1, "tst/unit/task_step/test_load_artifact_env_binding.py": 0.1, "tst/unit/task_step/test_load_artifact_file.py": 0.1, "tst/unit/task_step/test_load_artifact_overrides.py": 0.1, - "tst/unit/task_step/test_load_artifact_service_payload.py": 0.5, + "tst/unit/task_step/test_load_artifact_service_payload.py": 0.6, "tst/unit/task_step/test_load_artifact_service_universe.py": 0.1, - "tst/unit/task_step/test_load_artifact_snapshot_reporting.py": 0.1, + "tst/unit/task_step/test_load_artifact_snapshot_reporting.py": 0.0, "tst/unit/task_step/test_load_artifact_url_filenames.py": 0.1, - "tst/unit/task_step/test_load_artifact_vm_target.py": 0.2, - "tst/unit/task_step/test_manifest_cli_builder.py": 15.0, - "tst/unit/task_step/test_mcp_cli_builder.py": 1.7, + "tst/unit/task_step/test_load_artifact_vm_target.py": 0.1, + "tst/unit/task_step/test_manifest_cli_builder.py": 14.7, + "tst/unit/task_step/test_mcp_cli_builder.py": 1.1, "tst/unit/task_step/test_modify_env_tool_access.py": 0.0, - "tst/unit/task_step/test_next_env_event_hook.py": 1.8, - "tst/unit/task_step/test_parallel_deploy_env_binding.py": 0.1, + "tst/unit/task_step/test_next_env_event_hook.py": 2.1, + "tst/unit/task_step/test_parallel_deploy_env_binding.py": 0.0, "tst/unit/task_step/test_preflight_pinned_versions.py": 0.1, - "tst/unit/task_step/test_prompt_agent.py": 0.2, + "tst/unit/task_step/test_prompt_agent.py": 0.1, "tst/unit/task_step/test_prompt_agent_builtin_tools.py": 0.0, "tst/unit/task_step/test_prompt_agent_conversation_tool_calls.py": 0.0, "tst/unit/task_step/test_prompt_agent_human_default.py": 0.0, - "tst/unit/task_step/test_prompt_agent_live_trajectory.py": 0.8, + "tst/unit/task_step/test_prompt_agent_live_trajectory.py": 0.7, "tst/unit/task_step/test_prompt_agent_model_params.py": 0.0, - "tst/unit/task_step/test_prompt_agent_notifications.py": 0.6, - "tst/unit/task_step/test_prompt_agent_post_turn_hooks.py": 1.9, - "tst/unit/task_step/test_prompt_agent_snapshots.py": 2.8, - "tst/unit/task_step/test_prompt_agent_triggers.py": 1.1, + "tst/unit/task_step/test_prompt_agent_notifications.py": 0.4, + "tst/unit/task_step/test_prompt_agent_post_turn_hooks.py": 1.2, + "tst/unit/task_step/test_prompt_agent_snapshots.py": 2.4, + "tst/unit/task_step/test_prompt_agent_triggers.py": 0.8, "tst/unit/task_step/test_prompt_agent_unreachable.py": 0.0, "tst/unit/task_step/test_prompt_agent_user_model.py": 0.0, - "tst/unit/task_step/test_prompt_agent_user_side.py": 0.4, + "tst/unit/task_step/test_prompt_agent_user_side.py": 0.5, "tst/unit/task_step/test_recorded_models.py": 0.1, - "tst/unit/task_step/test_register_agent_triggers.py": 0.2, - "tst/unit/task_step/test_register_env_triggers.py": 7.5, + "tst/unit/task_step/test_register_agent_triggers.py": 0.1, + "tst/unit/task_step/test_register_env_triggers.py": 7.3, "tst/unit/task_step/test_reset_env.py": 0.1, "tst/unit/task_step/test_review.py": 0.2, - "tst/unit/task_step/test_run_code.py": 18.3, + "tst/unit/task_step/test_run_code.py": 16.5, "tst/unit/task_step/test_sandbox_utils_agent_container.py": 0.0, "tst/unit/task_step/test_sandbox_utils_logs.py": 0.1, - "tst/unit/task_step/test_snapshot_agent_state.py": 0.2, + "tst/unit/task_step/test_snapshot_agent_state.py": 0.1, "tst/unit/task_step/test_snapshot_env.py": 1.1, - "tst/unit/task_step/test_snapshot_env_env_binding.py": 0.1, + "tst/unit/task_step/test_snapshot_env_env_binding.py": 0.2, "tst/unit/task_step/test_step_overrides_agent_and_model.py": 0.2, - "tst/unit/task_step/test_teardown_sandboxes.py": 0.4, + "tst/unit/task_step/test_teardown_sandboxes.py": 0.2, "tst/unit/task_step/test_universe_comparison.py": 0.1, "tst/unit/task_step/test_validation_gate_aggregator.py": 0.1, "tst/unit/task_step/test_verify_a2a_agent_mcp.py": 0.0, - "tst/unit/task_step/test_verify_a2a_builtin_tools.py": 0.1, + "tst/unit/task_step/test_verify_a2a_builtin_tools.py": 0.0, "tst/unit/task_step/test_verify_spec_conformance.py": 0.2, - "tst/unit/task_step/test_verify_universe_agent_judge.py": 0.1, + "tst/unit/task_step/test_verify_universe_agent_judge.py": 0.2, "tst/unit/task_step/test_verify_universe_roundtrip.py": 0.2, "tst/unit/task_step/thread_work_test.py": 0.6, - "tst/unit/task_step/unwritten_local_agent_test.py": 0.9, - "tst/unit/task_step/verifiers/judge_short_keys_test.py": 0.1, - "tst/unit/task_step/verifiers/judge_utils/test_frame_selection.py": 0.2, + "tst/unit/task_step/unwritten_local_agent_test.py": 0.3, + "tst/unit/task_step/verifiers/judge_short_keys_test.py": 0.0, + "tst/unit/task_step/verifiers/judge_utils/test_frame_selection.py": 0.1, "tst/unit/task_step/verifiers/judge_utils/test_frame_selection_tool_name.py": 0.0, - "tst/unit/task_step/verifiers/judge_utils/test_judge_output_format.py": 0.2, + "tst/unit/task_step/verifiers/judge_utils/test_judge_output_format.py": 0.3, "tst/unit/task_step/verifiers/judge_utils/test_trajectory_filter.py": 0.0, - "tst/unit/task_step/verifiers/run_container_unit_tests_verifier_test.py": 0.9, + "tst/unit/task_step/verifiers/run_container_unit_tests_verifier_test.py": 1.2, "tst/unit/task_step/verifiers/test_env_outcome_verifier_step.py": 0.1, - "tst/unit/task_step/verifiers/test_rubrics_verifier_criteria_override.py": 0.1, - "tst/unit/task_step/verifiers/test_rubrics_verifier_grade_up_to_turn.py": 0.2, - "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_batches.py": 0.2, + "tst/unit/task_step/verifiers/test_rubrics_verifier_criteria_override.py": 0.0, + "tst/unit/task_step/verifiers/test_rubrics_verifier_grade_up_to_turn.py": 0.1, + "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_batches.py": 0.3, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_model.py": 0.0, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_retry.py": 0.1, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_teardown.py": 0.0, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_timeout.py": 0.0, - "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_trajectory.py": 0.5, + "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_trajectory.py": 0.3, "tst/unit/task_step/verifiers/test_rubrics_verifier_mistakes_entry.py": 0.2, - "tst/unit/task_step/verifiers/test_rubrics_verifier_multiturn.py": 0.7, - "tst/unit/task_step/verifiers/test_rubrics_verifier_nonfinite_scores.py": 3.1, - "tst/unit/task_step/verifiers/test_rubrics_verifier_per_criterion.py": 0.4, - "tst/unit/task_step/verifiers/test_rubrics_verifier_screenshots.py": 4.4, - "tst/unit/task_step/verifiers/test_run_context.py": 1.6, + "tst/unit/task_step/verifiers/test_rubrics_verifier_multiturn.py": 0.9, + "tst/unit/task_step/verifiers/test_rubrics_verifier_nonfinite_scores.py": 2.9, + "tst/unit/task_step/verifiers/test_rubrics_verifier_per_criterion.py": 0.5, + "tst/unit/task_step/verifiers/test_rubrics_verifier_screenshots.py": 4.6, + "tst/unit/task_step/verifiers/test_run_context.py": 1.4, "tst/unit/task_step/verifiers/test_score_parity.py": 0.0, "tst/unit/task_step/verifiers/test_trajectory_mistakes_format.py": 0.2, - "tst/unit/task_step/verifiers/test_verify_sandbox.py": 0.2, - "tst/unit/test_capabilities.py": 0.3, - "tst/unit/test_check_plugin_api.py": 21.1, + "tst/unit/task_step/verifiers/test_verify_sandbox.py": 0.1, + "tst/unit/test_capabilities.py": 0.2, + "tst/unit/test_check_plugin_api.py": 10.5, "tst/unit/test_check_skip_policy.py": 0.3, "tst/unit/test_clean_install_wheel.py": 0.1, "tst/unit/test_client_identity.py": 0.0, - "tst/unit/test_infra_build_script.py": 1.3, + "tst/unit/test_infra_build_script.py": 0.6, "tst/unit/test_infra_images_script.py": 0.6, - "tst/unit/test_mirror_base_images_script.py": 0.1, - "tst/unit/test_optional_extras_stay_optional.py": 592.4, + "tst/unit/test_mirror_base_images_script.py": 0.2, + "tst/unit/test_optional_extras_stay_optional.py": 250.4, "tst/unit/test_pypi_release_script.py": 0.3, - "tst/unit/test_release_infra_images.py": 0.7, - "tst/unit/test_release_protocol_pins.py": 0.3, - "tst/unit/test_runtime_dependencies_are_imported.py": 33.9, - "tst/unit/test_shards.py": 13.2, - "tst/unit/test_slow_test_durations_script.py": 0.2, - "tst/unit/test_subclass_hooks_are_annotated.py": 2.2, - "tst/unit/test_tls_verification.py": 0.2, - "tst/unit/training/chat_test.py": 14.7, - "tst/unit/training/harness_test.py": 24.0, - "tst/unit/training/nemo_gym_test.py": 4.1, + "tst/unit/test_release_infra_images.py": 1.0, + "tst/unit/test_release_protocol_pins.py": 0.1, + "tst/unit/test_runtime_dependencies_are_imported.py": 15.4, + "tst/unit/test_shards.py": 10.6, + "tst/unit/test_slow_test_durations_script.py": 0.3, + "tst/unit/test_subclass_hooks_are_annotated.py": 4.3, + "tst/unit/test_tls_verification.py": 0.1, + "tst/unit/training/chat_test.py": 12.5, + "tst/unit/training/harness_test.py": 13.6, + "tst/unit/training/nemo_gym_test.py": 3.0, "tst/unit/training/sessions_test.py": 0.2, - "tst/unit/training/skyrl_test.py": 6.4, - "tst/unit/training/tools_test.py": 0.3, - "tst/unit/training/trl_test.py": 39.2, - "tst/unit/training/verl_test.py": 3.6, - "tst/unit/utils/docker/build_test.py": 0.4, - "tst/unit/utils/docker/context_test.py": 0.6, - "tst/unit/utils/docker/dockerfile_test.py": 1.0, - "tst/unit/utils/docker/installed_test.py": 0.2, - "tst/unit/utils/docker/lock_test.py": 0.2, + "tst/unit/training/skyrl_test.py": 6.2, + "tst/unit/training/tools_test.py": 0.2, + "tst/unit/training/trl_test.py": 38.1, + "tst/unit/training/verl_test.py": 3.8, + "tst/unit/utils/docker/build_test.py": 0.3, + "tst/unit/utils/docker/context_test.py": 0.3, + "tst/unit/utils/docker/dockerfile_test.py": 0.4, + "tst/unit/utils/docker/installed_test.py": 0.1, + "tst/unit/utils/docker/lock_test.py": 0.4, "tst/unit/utils/docker/metadata_test.py": 0.1, - "tst/unit/utils/docker/prune_test.py": 0.8, - "tst/unit/utils/exec_retry_test.py": 0.1, + "tst/unit/utils/docker/prune_test.py": 0.4, + "tst/unit/utils/exec_retry_test.py": 0.2, "tst/unit/utils/test_card_naming_github_token.py": 0.0, - "tst/unit/utils/test_paths.py": 0.2 + "tst/unit/utils/test_paths.py": 0.1 } } diff --git a/tst/integration/store/local_parallel_deploy_test.py b/tst/integration/store/local_parallel_deploy_test.py index 03c213584..4acc1e9d8 100644 --- a/tst/integration/store/local_parallel_deploy_test.py +++ b/tst/integration/store/local_parallel_deploy_test.py @@ -26,9 +26,9 @@ from agent_env.store.image_store import LocalRegistryImageStore from agent_env.task import Task from agent_env.task.store import get_task_instance_store -from agent_env.providers.sandbox_providers.local_docker.provider import LocalSandbox from agent_env.task_step.task_steps.deploy_env import DeployEnvTaskStep from tst.integration.utils.registry import REGISTRY_IMAGE +from tst.util import local_sandboxes from tst.task import journal_invariants as journal pytestmark = [pytest.mark.integration, pytest.mark.int_test_slow] @@ -81,15 +81,7 @@ def local_stack_at(monkeypatch: pytest.MonkeyPatch, path: Path): _docker("rm", "-f", reg_name) pytest.skip("local registry did not become ready") - # The sandboxes this process makes while the stack is up, whose containers alone it removes: another process's, - # as another pytest worker's, share this machine's Docker and its names. - made, init = [], LocalSandbox.__init__ - - def recorded(self, *args, **kwargs): - init(self, *args, **kwargs) - made.append(self.sandbox_id) - - monkeypatch.setattr(LocalSandbox, "__init__", recorded) + made = local_sandboxes.recorded_sandbox_ids(monkeypatch) monkeypatch.chdir(path) for var in ("DOCUMENT", "OBJECT", "IMAGE"): monkeypatch.setenv(f"AGENT_ENV_{var}_STORE", "local") @@ -99,12 +91,14 @@ def recorded(self, *args, **kwargs): try: yield host finally: - for sandbox_id in set(made): - for cid in _docker("ps", "-aq", "--filter", f"name=agent-env-{sandbox_id}-").stdout.split(): - _docker("rm", "-f", cid) - reset_artifact_store() - reset_config() - _docker("rm", "-f", reg_name) + try: + for name in local_sandboxes.container_names_carrying(made): + if name.startswith("agent-env-"): + _docker("rm", "-f", name) + finally: + reset_artifact_store() + reset_config() + _docker("rm", "-f", reg_name) def _seed_bootstrap_envs(): diff --git a/tst/unit/env/envs/test_computer_env.py b/tst/unit/env/envs/test_computer_env.py index 7b74f0872..52353f2de 100644 --- a/tst/unit/env/envs/test_computer_env.py +++ b/tst/unit/env/envs/test_computer_env.py @@ -46,6 +46,7 @@ probing, resolve_in_base) from agent_env.env.envs.computer.utils import sites as computer_sites from agent_env.env.envs.computer.utils.composer import Composing +from agent_env.store.image_store.registry_api import RegistryRefusedError from agent_env.env.envs.computer.utils.probing import _probe from agent_env.env.envs.computer.env import COMPUTER_TOOLS, ComputerEnv from agent_env.env.envs.computer import app_env @@ -734,6 +735,77 @@ def test_env_build_by_an_installed_alias_composes_under_the_id_of_the_env_it_nam "@local/acme-kit/kit/desktop__computer").image().image_name]) +MIRROR = "ghcr.io/org/computers" + + +def _mirrored(docker, monkeypatch, read=None): + """Move every composed image out of the env's image store into the computer image mirror, as a fresh runner of CI + finds them, and record each copy back: the mirror's refs, read with ``read`` when given.""" + stored = {ref: digest for ref, digest in docker.registry.tags.items() + if f"/{composed.REPOSITORY}:" in ref and ":index-" not in ref} + for ref in stored: + del docker.registry.tags[ref] + mirror = {f"{MIRROR}:{composed.mirror_tag(ref.rpartition(':')[2])}": digest for ref, digest in stored.items()} + copied, real = [], composer.pin_if_served + + def pin(ref, auth): + if ref.startswith(f"{MIRROR}:"): + return read(ref) if read else (f"{ref}@{mirror[ref]}" if ref in mirror else None) + return real(ref, auth) + + def copy(source, target, source_auth, target_auth): + copied.append((source.partition("@")[0], target)) + docker.registry.tags[target] = source.rpartition("@")[2] + + monkeypatch.setattr(composer, "pin_if_served", pin) + monkeypatch.setattr(composer, "copy_image", copy) + monkeypatch.setattr(get_config(), "computer_image_mirror", MIRROR) + return stored, copied + + +def test_an_image_the_store_lacks_is_copied_from_the_computer_image_mirror_and_reused_rather_than_built(base, driver, + docker, + monkeypatch): + first = _put(base, driver) + stored, copied = _mirrored(docker, monkeypatch) + + second = _put(base, driver, id="@local/~/kit/other", environment_name="other") + + reused = Env.get(second.id).metadata["composed"]["container"] + assert len(docker.builds) == 2 and all(r["reused"] for r in reused.values()) + assert sorted(copied) == sorted((f"{MIRROR}:{composed.mirror_tag(ref.rpartition(':')[2])}", ref) for ref in stored) + assert {arch: r["image"] for arch, r in reused.items()} == { + arch: r["image"] for arch, r in Env.get(first.id).metadata["composed"]["container"].items()} + + +def test_a_change_to_the_code_that_composes_an_image_misses_the_mirror_and_builds_it(base, driver, docker, + monkeypatch): + _put(base, driver) + _, copied = _mirrored(docker, monkeypatch) + monkeypatch.setattr(composed, "composition", lambda: "sha256:" + "f" * 64) + + second = _put(base, driver, id="@local/~/kit/other", environment_name="other") + + assert len(docker.builds) == 4 and copied == [] + assert not any(r["reused"] for r in Env.get(second.id).metadata["composed"]["container"].values()) + + +def test_a_computer_image_mirror_that_cant_be_read_leaves_the_image_to_be_built(base, driver, docker, monkeypatch, + caplog): + _put(base, driver) + + def refused(ref): + raise RegistryRefusedError(f"{ref}: ghcr.io refused to serve it (HTTP 403)") + + _, copied = _mirrored(docker, monkeypatch, read=refused) + with caplog.at_level(logging.WARNING, logger=composer.__name__): + second = _put(base, driver, id="@local/~/kit/other", environment_name="other") + + built = Env.get(second.id).metadata["composed"]["container"] + assert len(docker.builds) == 4 and not any(r["reused"] for r in built.values()) and copied == [] + assert f"the computer image mirror {MIRROR} couldn't give" in caplog.text + + def test_builds_of_one_env_at_once_keep_each_others_pairs_and_a_failure_doesnt_replace_a_built_one(base, driver, docker, monkeypatch): diff --git a/tst/unit/store/registry_api_test.py b/tst/unit/store/registry_api_test.py index aec0d54fe..120992dd8 100644 --- a/tst/unit/store/registry_api_test.py +++ b/tst/unit/store/registry_api_test.py @@ -327,3 +327,108 @@ def ghcr(request): assert json.loads(request.content) == {"schemaVersion": 2, "mediaType": written, "manifests": [ amd64_entry, *(children if arm64 == "index" else [{**amd64_entry, "digest": OTHER, "platform": {"architecture": "arm64", "os": "linux"}}])]} + + +def _sha(data: bytes) -> str: + return f"sha256:{hashlib.sha256(data).hexdigest()}" + + +def test_an_image_is_copied_by_its_digest_with_only_the_blobs_the_target_lacks_and_its_manifests_byte_for_byte( + fake_registry): + config, kept, missing = b'{"architecture": "amd64"}', b"layer the target has", b"layer it lacks" + image = json.dumps({"mediaType": OCI_IMAGE, "config": {"digest": _sha(config)}, + "layers": [{"digest": _sha(kept)}, {"digest": _sha(missing)}]}).encode() + index = json.dumps({"mediaType": OCI_INDEX, "manifests": [{"digest": _sha(image)}]}).encode() + source = {_sha(index): (OCI_INDEX, index), _sha(image): (OCI_IMAGE, image)} + blobs = {_sha(blob): blob for blob in (config, kept, missing)} + target_blobs, target_manifests = {_sha(kept): kept}, {} + + def registries(request): + path = request.url.path + if request.url.host == "ghcr.io": + if "/blobs/" in path: + return httpx.Response(200, content=blobs[path.rpartition("/")[2]]) + media, body = source[path.rpartition("/")[2]] + return httpx.Response(200, content=body, headers={"Content-Type": media}) + if request.method == "HEAD": + return httpx.Response(200 if path.rpartition("/")[2] in target_blobs else 404) + if request.method == "POST": + return httpx.Response(202, headers={"Location": "/v2/agentenv-computers/blobs/uploads/u1?_state=s"}) + if "/blobs/uploads/" in path: + assert request.url.params["_state"] == "s" + target_blobs[request.url.params["digest"]] = request.read() + return httpx.Response(201) + target_manifests[path.rpartition("/")[2]] = (request.headers["content-type"], request.content) + return httpx.Response(201) + + seen = fake_registry(registries) + registry_api.copy_image(f"ghcr.io/org/computers:k@{_sha(index)}", "localhost:5000/agentenv-computers:k", None, None) + + assert target_blobs == blobs + assert [r.url.params["digest"] for r in seen if r.method == "PUT" and "/blobs/" in r.url.path] == [ + _sha(config), _sha(missing)] + assert target_manifests == {_sha(image): (OCI_IMAGE, image), _sha(index): (OCI_INDEX, index), "k": (OCI_INDEX, index)} + + +def test_a_copy_needs_the_digest_it_copies(fake_registry): + seen = fake_registry(lambda request: httpx.Response(500)) + + with pytest.raises(ValueError, match="names none"): + registry_api.copy_image("ghcr.io/org/computers:k", "localhost:5000/agentenv-computers:k", None, None) + assert seen == [] + + +def test_a_copy_to_ghcr_asks_again_for_the_push_token_its_first_challenge_didnt_give(fake_registry): + config = b'{"architecture": "amd64"}' + image = json.dumps({"mediaType": OCI_IMAGE, "config": {"digest": _sha(config)}, "layers": []}).encode() + pushed = [] + + def registries(request): + if request.url.host == "localhost": + if "/blobs/" in request.url.path: + return httpx.Response(200, content=config) + return httpx.Response(200, content=image, headers={"Content-Type": OCI_IMAGE}) + if request.url.path == "/token": + assert request.headers["authorization"] == "Basic " + base64.b64encode(b"bot:t").decode() + return httpx.Response(200, json={"token": request.url.params["scope"]}) + writes = request.method in ("POST", "PUT") + scope = "repository:org/computers:pull,push" if writes else "repository:org/computers:pull" + if request.headers.get("authorization") not in (f"Bearer {scope}", "Bearer repository:org/computers:pull,push"): + return httpx.Response(401, headers={"WWW-Authenticate": f'Bearer realm="https://ghcr.io/token",scope="{scope}"'}) + if request.method == "HEAD": + return httpx.Response(404) + if request.method == "POST": + return httpx.Response(202, headers={"Location": "/v2/org/computers/blobs/uploads/u1"}) + pushed.append(request.url.path) + return httpx.Response(201) + + fake_registry(registries) + registry_api.copy_image(f"localhost:5000/agentenv-computers:k@{_sha(image)}", "ghcr.io/org/computers:k", None, + RegistryAuth("ghcr.io", "bot", "t")) + + assert pushed == ["/v2/org/computers/blobs/uploads/u1", f"/v2/org/computers/manifests/{_sha(image)}", + "/v2/org/computers/manifests/k"] + + +@pytest.mark.parametrize("location", ["https://elsewhere.example/v2/org/computers/blobs/uploads/u1", + "http://ghcr.io/v2/org/computers/blobs/uploads/u1"], + ids=["another-host", "plain-http"]) +def test_an_upload_the_registry_sends_elsewhere_is_refused_before_its_credentials_go(fake_registry, location): + config = b'{"architecture": "amd64"}' + image = json.dumps({"mediaType": OCI_IMAGE, "config": {"digest": _sha(config)}, "layers": []}).encode() + + def registries(request): + if request.url.host == "localhost": + if "/blobs/" in request.url.path: + return httpx.Response(200, content=config) + return httpx.Response(200, content=image, headers={"Content-Type": OCI_IMAGE}) + assert request.url.host == "ghcr.io" and request.url.scheme == "https", request.url + if request.method == "HEAD": + return httpx.Response(404) + return httpx.Response(202, headers={"Location": location}) + + fake_registry(registries) + + with pytest.raises(ValueError, match="which its credentials don't go to"): + registry_api.copy_image(f"localhost:5000/agentenv-computers:k@{_sha(image)}", "ghcr.io/org/computers:k", None, + RegistryAuth("ghcr.io", "bot", "t")) diff --git a/tst/unit/test_computer_image_mirror_scripts.py b/tst/unit/test_computer_image_mirror_scripts.py new file mode 100644 index 000000000..54f91da22 --- /dev/null +++ b/tst/unit/test_computer_image_mirror_scripts.py @@ -0,0 +1,109 @@ +"""The CI computer image mirror's two scripts: what a runner shares into it, and which of its versions a prune deletes.""" + +from __future__ import annotations + +import datetime +import io +import json +import sys + +import pytest + +from tst.util.scripts import load_script + +NOW = datetime.datetime(2026, 11, 20, 12, tzinfo=datetime.timezone.utc) +CUTOFF = NOW - datetime.timedelta(days=7) +KEY, OTHER = "c" * 16 + "-" + "a" * 64, "c" * 16 + "-" + "b" * 64 +RAW, RAW_OTHER = "a" * 64, "b" * 64 + + +def _version(name: str, created: str, *tags: str) -> dict: + return {"id": hash(name) % 10_000, "name": name, "created_at": created, "metadata": {"container": {"tags": list(tags)}}} + + +@pytest.fixture +def prune(): + return load_script("prune_computer_images.py") + + +def test_an_image_last_seen_within_the_window_is_kept_and_one_last_seen_before_it_is_deleted(prune): + used = _version("sha256:1", "2026-10-01T00:00:00Z", KEY, f"{KEY}-seen-20261001", f"{KEY}-seen-20261115") + unused = _version("sha256:2", "2026-10-01T00:00:00Z", OTHER, f"{OTHER}-seen-20261112") + + assert prune.stale_versions([used, unused], CUTOFF, lambda digest: []) == [unused] + + +def test_an_image_seen_on_the_cutoffs_day_is_kept_through_that_day(prune): + edge = _version("sha256:1", "2026-10-01T00:00:00Z", KEY, f"{KEY}-seen-20261113") + + assert prune.stale_versions([edge], CUTOFF, lambda digest: []) == [] + + +def test_a_version_uploaded_within_the_window_is_kept_though_no_run_has_tagged_it_seen(prune): + fresh = _version("sha256:1", "2026-11-19T00:00:00Z") + old = _version("sha256:2", "2026-11-01T00:00:00Z") + + assert prune.stale_versions([fresh, old], CUTOFF, lambda digest: []) == [old] + + +def test_the_platform_manifests_a_kept_index_lists_are_kept_and_a_deleted_ones_are_not(prune): + kept = _version("sha256:k", "2026-10-01T00:00:00Z", KEY, f"{KEY}-seen-20261119") + dropped = _version("sha256:d", "2026-10-01T00:00:00Z", OTHER, f"{OTHER}-seen-20261001") + children = {"sha256:k": ["sha256:k-amd64", "sha256:k-attestation"], "sha256:d": ["sha256:d-amd64"]} + untagged = [_version(digest, "2026-10-01T00:00:00Z") for digest in ("sha256:k-amd64", "sha256:k-attestation", + "sha256:d-amd64")] + + stale = prune.stale_versions([kept, dropped, *untagged], CUTOFF, lambda digest: children.get(digest, [])) + + assert [version["name"] for version in stale] == ["sha256:d", "sha256:d-amd64"] + + +def test_a_prune_without_the_package_deletes_nothing_passes_and_says_why(prune, monkeypatch, capsys): + def missing(method, url): + raise prune.urllib.error.HTTPError(url, 404, "Not Found", {}, io.BytesIO(b"")) + + monkeypatch.setattr(prune, "_github", missing) + monkeypatch.setattr(sys, "argv", ["prune_computer_images.py", "org", "agentenv-ci-computers"]) + + assert prune.main() == 0 + assert "no package org/agentenv-ci-computers, or this repository has no access to it" in capsys.readouterr().out + + +def test_a_runner_shares_each_image_under_its_mirror_tag_and_todays_seen_tag_and_leaves_its_indexes(monkeypatch, + capsys): + share = load_script("share_computer_images.py") + tags = {"tags": [RAW, f"index-{RAW_OTHER}", RAW_OTHER]} + copies = [] + monkeypatch.setattr(share.urllib.request, "urlopen", lambda url: io.BytesIO(json.dumps(tags).encode())) + monkeypatch.setattr(share, "pin_digest", lambda ref, auth: f"{ref}@sha256:{ref[-64:]}") + monkeypatch.setattr(share, "mirror_tag", lambda key: f"{'c' * 16}-{key}") + monkeypatch.setattr(share, "copy_image", lambda source, target, source_auth, auth: copies.append( + (source.rpartition("@")[2], target, auth.username))) + monkeypatch.setattr(share, "pin_if_served", lambda ref, auth: f"{ref}@sha256:{RAW}") + monkeypatch.setattr(share.datetime, "datetime", type("Fixed", (datetime.datetime,), {"now": staticmethod(lambda tz: NOW)})) + monkeypatch.setenv("GITHUB_ACTOR", "bot") + monkeypatch.setenv("GITHUB_TOKEN", "t") + monkeypatch.setattr(sys, "argv", ["share_computer_images.py", "ghcr.io/org/agentenv-ci-computers"]) + + assert share.main() == 0 + assert copies == [(f"sha256:{raw}", f"ghcr.io/org/agentenv-ci-computers:{tag}", "bot") + for raw, key in ((RAW, KEY), (RAW_OTHER, OTHER)) for tag in (key, f"{key}-seen-20261120")] + assert "::warning::" not in capsys.readouterr().out + + +def test_a_mirror_the_composer_cant_read_anonymously_warns_to_make_it_public(monkeypatch, capsys): + share = load_script("share_computer_images.py") + monkeypatch.setattr(share.urllib.request, "urlopen", lambda url: io.BytesIO(json.dumps({"tags": [RAW]}).encode())) + monkeypatch.setattr(share, "pin_digest", lambda ref, auth: f"{ref}@sha256:{RAW}") + monkeypatch.setattr(share, "copy_image", lambda *args: None) + + def private(ref, auth): + raise ValueError(f"{ref}: ghcr.io refused to serve it (HTTP 403)") + + monkeypatch.setattr(share, "pin_if_served", private) + monkeypatch.setenv("GITHUB_ACTOR", "bot") + monkeypatch.setenv("GITHUB_TOKEN", "t") + monkeypatch.setattr(sys, "argv", ["share_computer_images.py", "ghcr.io/org/agentenv-ci-computers"]) + + assert share.main() == 0 + assert "make the package public" in capsys.readouterr().out diff --git a/tst/util/local_sandboxes.py b/tst/util/local_sandboxes.py new file mode 100644 index 000000000..d37468976 --- /dev/null +++ b/tst/util/local_sandboxes.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import subprocess + +import pytest + +from agent_env.providers.sandbox_providers.local_docker.provider import LocalSandbox + + +def recorded_sandbox_ids(monkeypatch: pytest.MonkeyPatch) -> list[str]: + made, init = [], LocalSandbox.__init__ + + def recorded(self, *args, **kwargs): + init(self, *args, **kwargs) + made.append(self.sandbox_id) + + monkeypatch.setattr(LocalSandbox, "__init__", recorded) + return made + + +def container_names_carrying(sandbox_ids: list[str]) -> set[str]: + names = subprocess.run(["docker", "ps", "-a", "--format", "{{.Names}}"], capture_output=True, text=True, + check=True).stdout + return {name for name in names.split() if any(sandbox_id in name for sandbox_id in sandbox_ids)}