From a8cb182a83dcf72e3d879852c6c037cf4a3cd2f7 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 14:49:17 -0800 Subject: [PATCH 1/7] ci: share the slow tier's computer images through a mirror registry rather than per-shard caches The slow tier saved each runner's whole registry to the Actions cache, keyed by shard and week. The same image landed on up to three shards, so the entries (1.6 to 6.3GB each) overflowed the repository's 10GB, evicted each other, and most shards built every image from scratch. - The composer copies an image of a key the env's image store lacks from AGENT_ENV_COMPUTER_IMAGE_MIRROR, read anonymously, before it would build it; a mirror it can't read leaves the image to be built. - registry_api.copy_image copies an image between registries by its digest, sending only the blobs the target lacks and each manifest byte for byte. - After its tests pass on main, each slow shard shares the images it built or copied to ghcr.io/scaleapi/agentenv-ci-computers under their keys and -seen-. - computer-image-mirror.yml deletes, daily, the versions no run has used for 7 days, keeping a kept index's platform manifests. Co-Authored-By: Claude --- .github/scripts/prune_computer_images.py | 112 ++++++++++++++++++ .github/scripts/share_computer_images.py | 53 +++++++++ .github/workflows/computer-image-mirror.yml | 31 +++++ .github/workflows/local-backends.yml | 64 ++++------ src/agent_env/config/describe.py | 1 + src/agent_env/config/runtime.py | 8 ++ .../env/envs/computer/utils/composed.py | 6 + .../env/envs/computer/utils/composer.py | 19 ++- .../store/image_store/registry_api.py | 77 ++++++++++-- tst/unit/env/envs/test_computer_env.py | 72 +++++++++++ tst/unit/store/registry_api_test.py | 81 +++++++++++++ .../test_computer_image_mirror_scripts.py | 109 +++++++++++++++++ 12 files changed, 579 insertions(+), 54 deletions(-) create mode 100644 .github/scripts/prune_computer_images.py create mode 100644 .github/scripts/share_computer_images.py create mode 100644 .github/workflows/computer-image-mirror.yml create mode 100644 tst/unit/test_computer_image_mirror_scripts.py 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..51c2fb864 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,33 @@ 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. - 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 - 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 + # 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() && 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 }} + always() && steps.tests.outcome == 'success' + && (github.event_name == 'push' || github.head_ref == 'andrew-park/ci-shared-computer-images') + env: + GITHUB_TOKEN: ${{ github.token }} + run: .venv/bin/python .github/scripts/share_computer_images.py "$COMPUTER_IMAGE_MIRROR" - 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..14c116e33 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,25 @@ 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"]) + 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 +317,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/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..e38afa194 100644 --- a/tst/unit/store/registry_api_test.py +++ b/tst/unit/store/registry_api_test.py @@ -327,3 +327,84 @@ 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"] 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 From 5949e91a4d5b0fd528959827d4b351f0b2b2800c Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 15:59:49 -0800 Subject: [PATCH 2/7] ci: share computer images from main's runs only Co-Authored-By: Claude --- .github/workflows/local-backends.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/local-backends.yml b/.github/workflows/local-backends.yml index 51c2fb864..e62af4203 100644 --- a/.github/workflows/local-backends.yml +++ b/.github/workflows/local-backends.yml @@ -198,8 +198,7 @@ jobs: - name: Share the computer images continue-on-error: true if: >- - always() && steps.tests.outcome == 'success' - && (github.event_name == 'push' || github.head_ref == 'andrew-park/ci-shared-computer-images') + 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" From 2c89b45a081d304f718e54463161039b06708337 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 16:19:49 -0800 Subject: [PATCH 3/7] ci: refresh the shard durations from a run that found every computer image in the mirror The slow tier's times are from PR #580's run 42716509774, attempt 2, which built no image; the installer's from the same run; the gateway's and unit tier's from main's run 42715765775, after #575 and #576. Co-Authored-By: Claude --- tst/integration/slow_durations.json | 1122 ++++++++++++++------------- 1 file changed, 562 insertions(+), 560 deletions(-) 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 } } From 5d957be668365c4fe838119b5ba2623a319a77b6 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 16:52:54 -0800 Subject: [PATCH 4/7] fix(registry): refuse an upload a registry sends to another origin before its credentials go Co-Authored-By: Claude --- .../store/image_store/registry_api.py | 4 ++++ tst/unit/store/registry_api_test.py | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/agent_env/store/image_store/registry_api.py b/src/agent_env/store/image_store/registry_api.py index 14c116e33..07d92a8ed 100644 --- a/src/agent_env/store/image_store/registry_api.py +++ b/src/agent_env/store/image_store/registry_api.py @@ -300,6 +300,10 @@ def upload(self, digest: str, path: Path) -> None: 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: diff --git a/tst/unit/store/registry_api_test.py b/tst/unit/store/registry_api_test.py index e38afa194..120992dd8 100644 --- a/tst/unit/store/registry_api_test.py +++ b/tst/unit/store/registry_api_test.py @@ -408,3 +408,27 @@ def registries(request): 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")) From a27fa4ca28b3df06302497350d1e72c408a2657b Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 16:51:47 -0800 Subject: [PATCH 5/7] test: count and remove only the containers of sandboxes this test's process made multi_env_computers_test compared every container on the machine before and after a failed redeploy, so another pytest worker's sandboxes coming and going failed it when the slow tier ran two tests at a time (PR #582). tst/util/local_sandboxes.record notes the LocalSandboxes a process makes and containers() names theirs; the test and local_stack_at, which #584 taught the same, both use it. Co-Authored-By: Claude --- .../computer/multi_env_computers_test.py | 33 +++++++++++-------- .../store/local_parallel_deploy_test.py | 18 +++------- tst/util/local_sandboxes.py | 24 ++++++++++++++ 3 files changed, 48 insertions(+), 27 deletions(-) create mode 100644 tst/util/local_sandboxes.py 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/store/local_parallel_deploy_test.py b/tst/integration/store/local_parallel_deploy_test.py index 03c213584..4fefff067 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,9 +91,9 @@ 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) + for name in local_sandboxes.container_names_carrying(made): + if name.startswith("agent-env-"): + _docker("rm", "-f", name) reset_artifact_store() reset_config() _docker("rm", "-f", reg_name) 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)} From aaa2ef157e1920f75986f6d6aaab65e8ed133ae1 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 17:21:04 -0800 Subject: [PATCH 6/7] test: still reset the stores and remove the registry when listing containers fails Co-Authored-By: Claude --- .../store/local_parallel_deploy_test.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tst/integration/store/local_parallel_deploy_test.py b/tst/integration/store/local_parallel_deploy_test.py index 4fefff067..4acc1e9d8 100644 --- a/tst/integration/store/local_parallel_deploy_test.py +++ b/tst/integration/store/local_parallel_deploy_test.py @@ -91,12 +91,14 @@ def local_stack_at(monkeypatch: pytest.MonkeyPatch, path: Path): try: yield host finally: - for name in local_sandboxes.container_names_carrying(made): - if name.startswith("agent-env-"): - _docker("rm", "-f", name) - 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(): From 29c56c459f67e6fe768566cf0c8c026620567243 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Mon, 9 Nov 2026 16:22:42 -0800 Subject: [PATCH 7/7] experiment: run the slow tier two tests at a time and sample memory (do not merge) Co-Authored-By: Claude --- .github/workflows/local-backends.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/local-backends.yml b/.github/workflows/local-backends.yml index e62af4203..2ebe22961 100644 --- a/.github/workflows/local-backends.yml +++ b/.github/workflows/local-backends.yml @@ -185,14 +185,17 @@ jobs: 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 + .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 @@ -202,6 +205,11 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} run: .venv/bin/python .github/scripts/share_computer_images.py "$COMPUTER_IMAGE_MIRROR" + - name: Peak memory + if: always() + run: | + 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: |