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/prune_registry.py b/.github/scripts/prune_registry.py deleted file mode 100644 index 0b2ace7f8..000000000 --- a/.github/scripts/prune_registry.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Keep only the images of a local registry's repository that a run used, before the registry's storage is cached. - -``prune`` drops each tag the registry's log has no manifest request for, then each manifest no remaining tag reaches, -following an image index to the manifests it lists. The registry's ``garbage-collect`` then frees the layers only those -manifests used; with ``--delete-untagged`` it would also delete a kept index's untagged platform manifests. ``links``, -run after it, drops the repository's links to layers it freed. -""" - -from __future__ import annotations - -import argparse -import json -import re -import shutil -import sys -from pathlib import Path - -_V2 = Path("docker") / "registry" / "v2" - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description=__doc__) - commands = parser.add_subparsers(dest="command", required=True) - pruning = commands.add_parser("prune", help="drop the tags and manifests the run didn't use") - linking = commands.add_parser("links", help="drop links to layers garbage-collect freed") - for command in (pruning, linking): - command.add_argument("storage", type=Path, help="the registry's storage root") - command.add_argument("repository") - pruning.add_argument("log", type=Path, help="the registry's log of the run") - args = parser.parse_args(argv) - if args.command == "prune": - kept, dropped = prune(args.storage, args.repository, used_tags(args.log.read_text(errors="replace"), - args.repository)) - print(f"kept {kept} tags of {args.repository}, dropped {dropped}") - else: - print(f"dropped {drop_freed_links(args.storage, args.repository)} links to freed layers") - return 0 - - -def used_tags(log: str, repository: str) -> set[str]: - """The tags the log names a request for a manifest of ``repository`` by.""" - return set(re.findall(rf"/v2/{re.escape(repository)}/manifests/([A-Za-z0-9_][A-Za-z0-9_.-]{{0,127}})(?=[\s\"?])", - log)) - - -def prune(storage: Path, repository: str, used: set[str]) -> tuple[int, int]: - """Drop the tags of ``repository`` not in ``used``, then the manifests no remaining tag reaches; how many tags it - kept and dropped.""" - manifests = storage / _V2 / "repositories" / repository / "_manifests" - tags = _children(manifests / "tags") - for tag in tags: - if tag.name not in used: - shutil.rmtree(tag) - kept = [tag for tag in tags if tag.name in used] - reached = _reached(storage, [(tag / "current" / "link").read_text().strip() for tag in kept]) - for revision in _children(manifests / "revisions" / "sha256"): - if f"sha256:{revision.name}" not in reached: - shutil.rmtree(revision) - return len(kept), len(tags) - len(kept) - - -def drop_freed_links(storage: Path, repository: str) -> int: - """Drop each link of ``repository`` to a layer that's no longer stored; how many.""" - dropped = 0 - for layer in _children(storage / _V2 / "repositories" / repository / "_layers" / "sha256"): - if not _blob(storage, f"sha256:{layer.name}").is_file(): - shutil.rmtree(layer) - dropped += 1 - return dropped - - -def _reached(storage: Path, digests: list[str]) -> set[str]: - """``digests`` and every manifest an image index among them lists, recursively.""" - reached, pending = set(), list(digests) - while pending: - digest = pending.pop() - if digest in reached: - continue - reached.add(digest) - try: - manifest = json.loads(_blob(storage, digest).read_bytes()) - except (FileNotFoundError, ValueError): - continue - pending += [listed["digest"] for listed in manifest.get("manifests", []) if "digest" in listed] - return reached - - -def _children(folder: Path) -> list[Path]: - return [child for child in folder.iterdir() if child.is_dir()] if folder.is_dir() else [] - - -def _blob(storage: Path, digest: str) -> Path: - hex_digest = digest.removeprefix("sha256:") - return storage / _V2 / "blobs" / "sha256" / hex_digest[:2] / hex_digest / "data" - - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) 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 3b08d5c52..4eadbcff3 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,81 +176,66 @@ 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. That key includes the lock of the packages the image installs, which the - # composer resolves from Ubuntu's archive, PyPI and download URLs unless the store holds it. The registry keeps - # its storage on the runner, and the computer tests share their locks and files in the packages folder - # (tst/util/package_cache.py); both are restored from what main saved latest this week, so a run neither resolves - # nor builds again what hasn't changed. A shard that runs no computer tests misses. - - name: Week of the computer images' cache + # The computer tests share the package locks a compose resolves, and the files they name, in the packages folder + # (tst/util/package_cache.py). An image's key includes its lock, which the composer otherwise resolves from + # Ubuntu's archive, PyPI and download URLs, so the folder is restored from what main saved latest this week. + - name: Week of the packages' cache id: week run: echo "week=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT" - - name: Restore the computer images and packages - id: computers + - name: Restore the packages + id: packages uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - path: | - ${{ runner.temp }}/registry - ${{ runner.temp }}/packages - key: computers-${{ matrix.shard }}-${{ steps.week.outputs.week }} - restore-keys: computers-${{ matrix.shard }}-${{ steps.week.outputs.week }}- + path: ${{ runner.temp }}/packages + key: packages-${{ matrix.shard }}-${{ steps.week.outputs.week }} + restore-keys: packages-${{ 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 AGENTENV_TEST_PACKAGES: ${{ runner.temp }}/packages + AGENT_ENV_COMPUTER_IMAGE_MIRROR: ${{ env.COMPUTER_IMAGE_MIRROR }} run: | .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 - # Only the computer images this run used are kept, with the packages, and only main saves them, once its tests - # pass, so pull requests reuse main's without filling the repository's cache. An image is used when the registry's - # log has a request for it by its tag (prune_registry.py), so what a week's earlier runs built and this one no - # longer asks for is dropped. The garbage collection frees what only the dropped tags and repositories referenced; - # with --delete-untagged it would also delete the untagged platform manifests of an image index, and their layers. - # The entry is named by the image keys the registry holds and the locks the packages hold, so a run that built a - # new image or resolved a new lock saves a new one. - - name: Keep only the computer images this run used - id: kept + # Only main saves the packages, once its tests pass, so pull requests reuse main's without filling the + # repository's cache. The entry is named by the locks the folder holds, so a run that resolved a new lock saves a + # new one. + - name: Name the packages' cache entry + id: lockkey 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 logs registry > "$RUNNER_TEMP/registry.log" 2>&1 - docker stop registry - sudo find "$repositories" -mindepth 1 -maxdepth 1 ! -name agentenv-computers -exec rm -rf {} + - sudo python3 .github/scripts/prune_registry.py prune "$RUNNER_TEMP/registry" agentenv-computers \ - "$RUNNER_TEMP/registry.log" - 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 - sudo python3 .github/scripts/prune_registry.py links "$RUNNER_TEMP/registry" agentenv-computers - mkdir -p "$RUNNER_TEMP/packages" - du -sh "$RUNNER_TEMP/registry" "$RUNNER_TEMP/packages" - if [ -z "$(sudo ls "$repositories/agentenv-computers/_manifests/tags")" ]; then - echo "computers=false" >> "$GITHUB_OUTPUT"; exit 0 - fi - key=$({ sudo ls "$repositories/agentenv-computers/_manifests/tags" - ls "$RUNNER_TEMP/packages/agentenv-packages/locks" 2>/dev/null; } | sort | sha256sum | cut -c1-16) - echo "key=computers-${{ matrix.shard }}-${{ steps.week.outputs.week }}-$key" >> "$GITHUB_OUTPUT" - echo "computers=true" >> "$GITHUB_OUTPUT" - - name: Save the computer images and packages + locks="$RUNNER_TEMP/packages/agentenv-packages/locks" + if [ -z "$(ls "$locks" 2>/dev/null)" ]; then echo "packages=false" >> "$GITHUB_OUTPUT"; exit 0; fi + key=$(ls "$locks" | sort | sha256sum | cut -c1-16) + echo "key=packages-${{ matrix.shard }}-${{ steps.week.outputs.week }}-$key" >> "$GITHUB_OUTPUT" + echo "packages=true" >> "$GITHUB_OUTPUT" + - name: Save the packages 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 + && steps.lockkey.outputs.packages == 'true' && steps.lockkey.outputs.key != steps.packages.outputs.cache-matched-key uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: - path: | - ${{ runner.temp }}/registry - ${{ runner.temp }}/packages - key: ${{ steps.kept.outputs.key }} + path: ${{ runner.temp }}/packages + key: ${{ steps.lockkey.outputs.key }} + # Once main's tests pass, every image this runner built or copied goes to the mirror under its key and a + # -seen- tag, which computer-image-mirror.yml's prune keeps an image by. + - name: Share the computer images + continue-on-error: true + if: >- + always() && steps.tests.outcome == 'success' && github.event_name == 'push' + env: + GITHUB_TOKEN: ${{ github.token }} + run: .venv/bin/python .github/scripts/share_computer_images.py "$COMPUTER_IMAGE_MIRROR" - name: Skips must be declared capability gaps if: always() run: | diff --git a/AGENTS.md b/AGENTS.md index 5af2559e5..e3b0cc463 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -391,9 +391,8 @@ orchestrators call it the same way. What callers rely on: `.github/scripts/slow_test_durations.py`. - The computer tests compose in stores of their own, but share the package locks and files a compose resolves (`tst/util/package_cache.py`) through `AGENTENV_TEST_PACKAGES`, a folder for the run unless - it's set. The slow tier caches that folder with the local registry's computer images, so a run - resolves from Ubuntu's archive, PyPI or a download URL only what changed since `main` last saved - them. + it's set. The slow tier caches that folder, so a run resolves from Ubuntu's archive, PyPI or a + download URL only what changed since `main` last saved it. - `tst/manual/` and `tst/benchmarks/` run only by hand, as each module's docstring says; no CI job collects them. - `tst/formal/` holds TLA+ models, run only by hand with each model's `check.sh`; the `AGENTS.md` 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 309f5521c..32aefd93a 100644 --- a/src/agent_env/env/envs/computer/utils/composer.py +++ b/src/agent_env/env/envs/computer/utils/composer.py @@ -50,7 +50,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 @@ -58,7 +58,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 @@ -771,9 +771,20 @@ def _key(**inputs: Any) -> str: def _stored(env: ComputerEnv, key: str) -> tuple[ImageStore, str, str | None]: """The env's image store, the ref there of the image of ``key``, and that ref pinned when the store serves one - already, for any env that built it.""" + already, for any env that built it, or once it's copied there from the configured computer image mirror.""" store = _image_store(env) - ref = store.image_ref(REPOSITORY, key.removeprefix("sha256:")) + tag = key.removeprefix("sha256:") + ref = store.image_ref(REPOSITORY, tag) + if (pinned := pin_if_served(ref, store.auth(ref))) is not None or not (mirror := get_config().computer_image_mirror): + return store, ref, pinned + try: + if (mirrored := pin_if_served(f"{mirror}:{mirror_tag(tag)}", None)) is None: + return store, ref, None + copy_image(mirrored, ref, None, store.auth(ref)) + except (ValueError, KeyError, OSError) as e: + logger.warning(f"the computer image mirror {mirror} couldn't give {tag}, so it's built: {e}") + return store, ref, None + logger.info(f"copied {tag} from the computer image mirror {mirror}") return store, ref, pin_if_served(ref, store.auth(ref)) diff --git a/src/agent_env/store/image_store/registry_api.py b/src/agent_env/store/image_store/registry_api.py index 4544cfed8..07d92a8ed 100644 --- a/src/agent_env/store/image_store/registry_api.py +++ b/src/agent_env/store/image_store/registry_api.py @@ -1,5 +1,6 @@ """Read an image from its registry over the OCI distribution API: its digest, to pin a tag to the image it names now, -and its manifests and blobs, to inspect what it holds without pulling it; and write an index of images it holds.""" +and its manifests and blobs, to inspect what it holds without pulling it; write an index of images it holds; and copy an +image from one registry to another.""" from __future__ import annotations @@ -7,6 +8,7 @@ import hashlib import json import re +import tempfile from collections.abc import Iterator from dataclasses import dataclass from pathlib import Path @@ -217,6 +219,40 @@ def write_index(ref: str, images: dict[str, str], auth: RegistryAuth | None, *, {"schemaVersion": 2, "mediaType": media, "manifests": manifests}).encode(), media) +def copy_image(source: str, target: str, source_auth: RegistryAuth | None, target_auth: RegistryAuth | None, *, + timeout: float = _TIMEOUT_SECONDS) -> None: + """Copy the image ``source`` names, pinned by its digest, to ``target``'s tag: every blob ``target``'s repository + lacks, then each manifest byte for byte, so the copy keeps ``source``'s digest; an index's platform manifests + first, each by its digest.""" + digest = _parse(source).digest + if digest is None: + raise ValueError(f"{source}: a copy goes by the image's digest, and this names none") + image = _parse(target) + with (_Repository(_parse(source), source_auth, timeout, source) as reading, + _Repository(image, target_auth, timeout, target) as writing, + tempfile.TemporaryDirectory(prefix="agentenv-copy-") as work): + + def manifest(reference: str) -> tuple[bytes, str]: + response = reading.get(f"manifests/{reference}", accept=_MANIFEST_TYPES) + body = response.json() + served = response.headers.get("content-type", "").split(";")[0].strip() or body.get("mediaType", "") + if served in _INDEXES: + for child in body["manifests"]: + manifest(child["digest"]) + else: + for blob in [body["config"], *body["layers"]]: + if writing.has_blob(blob["digest"]): + continue + path = Path(work) / blob["digest"].removeprefix("sha256:") + read_blob(source, blob["digest"], source_auth, path, timeout=timeout) + writing.upload(blob["digest"], path) + path.unlink() + writing.put(f"manifests/{reference}", response.content, served) + return response.content, served + + writing.put(f"manifests/{image.tag or 'latest'}", *manifest(digest)) + + class _Repository: """One image's repository on its registry, read with the Bearer token the registry's first challenge asks for.""" @@ -250,6 +286,29 @@ def put(self, path: str, content: bytes, content_type: str) -> None: if response.status_code not in (200, 201): self._raise_for(response) + def has_blob(self, digest: str) -> bool: + response = self._send("HEAD", f"blobs/{digest}", None, stream=False) + if response.status_code == 404: + return False + self._raise_for(response) + return True + + def upload(self, digest: str, path: Path) -> None: + """Push the file ``path`` as the blob ``digest``, in one request after the registry opens an upload.""" + opened = self._send("POST", "blobs/uploads/", None, stream=False) + if opened.status_code != 202 or "location" not in opened.headers: + self._raise_for(opened) + raise ValueError(f"{self._what}: {self._host} opened no upload for {digest} (HTTP {opened.status_code})") + location = httpx.URL(f"{self._base}/").join(opened.headers["location"]) + base = httpx.URL(self._base) + if (location.scheme, location.netloc) != (base.scheme, base.netloc): + raise ValueError(f"{self._what}: {self._host} sent the upload of {digest} to {location.scheme}://" + f"{location.netloc.decode()}, which its credentials don't go to") + response = self._send("PUT", str(location.copy_merge_params({"digest": digest})), None, stream=False, + content=path, content_type="application/octet-stream") + if response.status_code != 201: + self._raise_for(response) + @contextlib.contextmanager def stream(self, path: str) -> Iterator[httpx.Response]: """``path`` as a streamed response, closed when the block ends.""" @@ -262,19 +321,27 @@ def stream(self, path: str) -> Iterator[httpx.Response]: finally: response.close() - def _send(self, method: str, path: str, accept: str | None, *, stream: bool, content: bytes | None = None, + def _send(self, method: str, path: str, accept: str | None, *, stream: bool, content: bytes | Path | None = None, content_type: str | None = None) -> httpx.Response: + """``path`` is under the repository, or a whole URL the registry gave; a file's ``content`` streams from disk.""" + url = path if path.startswith(("http://", "https://")) else f"{self._base}/{path}" try: - for _ in range(2): + for attempt in range(2): headers = {"Accept": accept} if accept else {} if content_type: headers["Content-Type"] = content_type if self._token: headers["Authorization"] = f"Bearer {self._token}" - request = self._client.build_request(method, f"{self._base}/{path}", headers=headers, content=content) - # Basic credentials go with every request until a Bearer token replaces them. - response = self._client.send(request, auth=None if self._token else self._basic, stream=stream) - if response.status_code != 401 or self._token: + with contextlib.ExitStack() as opened: + body = content + if isinstance(content, Path): + headers["Content-Length"] = str(content.stat().st_size) + body = opened.enter_context(content.open("rb")) + request = self._client.build_request(method, url, headers=headers, content=body) + # Basic credentials go with every request until a Bearer token replaces them. + response = self._client.send(request, auth=None if self._token else self._basic, stream=stream) + # A held token is asked for again once: GHCR scopes it to the first challenge, a pull's before a push. + if response.status_code != 401 or attempt: return response if stream: response.read() diff --git a/tst/integration/slow_durations.json b/tst/integration/slow_durations.json index 4955f5578..6061360bb 100644 --- a/tst/integration/slow_durations.json +++ b/tst/integration/slow_durations.json @@ -7,182 +7,183 @@ "tst/integration/env/gateway/gateway_test.py" ], "seconds": { - "packages/agentenv-protocol/tests/test_a2a_agent.py": 1.1, + "packages/agentenv-protocol/tests/test_a2a_agent.py": 1.2, "packages/agentenv-protocol/tests/test_a2a_agent_examples.py": 0.1, - "packages/agentenv-protocol/tests/test_a2a_cdp_config.py": 0.2, - "packages/agentenv-protocol/tests/test_a2a_object_transfers.py": 0.4, - "packages/agentenv-protocol/tests/test_a2a_peer_agents.py": 3.9, - "packages/agentenv-protocol/tests/test_a2a_proxy.py": 9.0, - "packages/agentenv-protocol/tests/test_a2a_staging.py": 1.4, - "packages/agentenv-protocol/tests/test_a2a_trajectory_log.py": 0.3, - "packages/agentenv-protocol/tests/test_agent_env_environment.py": 0.4, + "packages/agentenv-protocol/tests/test_a2a_cdp_config.py": 0.1, + "packages/agentenv-protocol/tests/test_a2a_object_transfers.py": 0.5, + "packages/agentenv-protocol/tests/test_a2a_peer_agents.py": 4.0, + "packages/agentenv-protocol/tests/test_a2a_proxy.py": 8.5, + "packages/agentenv-protocol/tests/test_a2a_staging.py": 1.0, + "packages/agentenv-protocol/tests/test_a2a_trajectory_log.py": 0.2, + "packages/agentenv-protocol/tests/test_agent_env_environment.py": 0.2, "packages/agentenv-protocol/tests/test_browser_cdp.py": 0.7, - "packages/agentenv-protocol/tests/test_browser_data.py": 0.3, + "packages/agentenv-protocol/tests/test_browser_data.py": 0.2, "packages/agentenv-protocol/tests/test_browser_state.py": 0.1, "packages/agentenv-protocol/tests/test_client.py": 0.1, - "packages/agentenv-protocol/tests/test_computer_app_data.py": 3.1, - "packages/agentenv-protocol/tests/test_computer_app_server.py": 8.4, - "packages/agentenv-protocol/tests/test_computer_conformance.py": 169.6, + "packages/agentenv-protocol/tests/test_computer_app_data.py": 3.0, + "packages/agentenv-protocol/tests/test_computer_app_server.py": 7.2, + "packages/agentenv-protocol/tests/test_computer_conformance.py": 114.0, "packages/agentenv-protocol/tests/test_computer_conformance_checks.py": 1.1, - "packages/agentenv-protocol/tests/test_computer_conformance_page.py": 54.1, - "packages/agentenv-protocol/tests/test_computer_conformance_recording.py": 32.7, - "packages/agentenv-protocol/tests/test_computer_dataplane.py": 3.1, - "packages/agentenv-protocol/tests/test_computer_declare.py": 0.9, - "packages/agentenv-protocol/tests/test_computer_driver.py": 3.5, - "packages/agentenv-protocol/tests/test_computer_extensions.py": 5.7, - "packages/agentenv-protocol/tests/test_computer_frames.py": 5.4, + "packages/agentenv-protocol/tests/test_computer_conformance_page.py": 32.0, + "packages/agentenv-protocol/tests/test_computer_conformance_recording.py": 28.9, + "packages/agentenv-protocol/tests/test_computer_dataplane.py": 4.5, + "packages/agentenv-protocol/tests/test_computer_declare.py": 1.1, + "packages/agentenv-protocol/tests/test_computer_driver.py": 1.6, + "packages/agentenv-protocol/tests/test_computer_extensions.py": 6.1, + "packages/agentenv-protocol/tests/test_computer_frames.py": 2.2, "packages/agentenv-protocol/tests/test_computer_macos.py": 0.0, - "packages/agentenv-protocol/tests/test_computer_page.py": 1.9, - "packages/agentenv-protocol/tests/test_computer_queue.py": 15.8, - "packages/agentenv-protocol/tests/test_computer_recording.py": 33.6, + "packages/agentenv-protocol/tests/test_computer_page.py": 1.0, + "packages/agentenv-protocol/tests/test_computer_queue.py": 16.2, + "packages/agentenv-protocol/tests/test_computer_recording.py": 33.5, "packages/agentenv-protocol/tests/test_computer_seed.py": 0.0, - "packages/agentenv-protocol/tests/test_computer_serve.py": 0.3, + "packages/agentenv-protocol/tests/test_computer_serve.py": 0.4, "packages/agentenv-protocol/tests/test_computer_served.py": 0.3, "packages/agentenv-protocol/tests/test_computer_session_windows.py": 0.4, - "packages/agentenv-protocol/tests/test_computer_spaces.py": 1.2, - "packages/agentenv-protocol/tests/test_computer_svc_forward.py": 8.5, - "packages/agentenv-protocol/tests/test_computer_tool_prefix.py": 1.1, + "packages/agentenv-protocol/tests/test_computer_spaces.py": 1.3, + "packages/agentenv-protocol/tests/test_computer_svc_forward.py": 7.6, + "packages/agentenv-protocol/tests/test_computer_tool_prefix.py": 1.6, "packages/agentenv-protocol/tests/test_control_token.py": 0.0, - "packages/agentenv-protocol/tests/test_data_objects.py": 2.9, - "packages/agentenv-protocol/tests/test_errors.py": 0.3, + "packages/agentenv-protocol/tests/test_data_objects.py": 2.6, + "packages/agentenv-protocol/tests/test_errors.py": 0.2, "packages/agentenv-protocol/tests/test_extension_methods.py": 0.1, "packages/agentenv-protocol/tests/test_intake.py": 0.0, "packages/agentenv-protocol/tests/test_manifest.py": 0.0, "packages/agentenv-protocol/tests/test_preflight.py": 0.0, "packages/agentenv-protocol/tests/test_tool_access.py": 0.2, - "packages/agentenv-protocol/tests/test_tool_annotations.py": 0.3, - "packages/agentenv-protocol/tests/test_tool_client.py": 1.1, - "packages/agentenv-protocol/tests/test_tool_context.py": 0.1, + "packages/agentenv-protocol/tests/test_tool_annotations.py": 0.2, + "packages/agentenv-protocol/tests/test_tool_client.py": 0.9, + "packages/agentenv-protocol/tests/test_tool_context.py": 0.0, "packages/agentenv-protocol/tests/test_tool_context_fastmcp.py": 0.2, - "packages/agentenv-protocol/tests/test_tool_context_injection.py": 0.3, - "packages/agentenv-protocol/tests/test_website_api.py": 0.7, - "packages/agentenv-protocol/tests/test_website_browser.py": 3.7, - "packages/agentenv-protocol/tests/test_website_conformance.py": 17.5, - "packages/agentenv-protocol/tests/test_website_environment.py": 8.0, - "packages/agentenv-protocol/tests/test_website_mcp.py": 2.0, - "packages/agentenv-protocol/tests/test_website_notes_example.py": 1.7, + "packages/agentenv-protocol/tests/test_tool_context_injection.py": 0.4, + "packages/agentenv-protocol/tests/test_website_api.py": 0.6, + "packages/agentenv-protocol/tests/test_website_browser.py": 3.8, + "packages/agentenv-protocol/tests/test_website_conformance.py": 17.8, + "packages/agentenv-protocol/tests/test_website_environment.py": 7.1, + "packages/agentenv-protocol/tests/test_website_mcp.py": 1.8, + "packages/agentenv-protocol/tests/test_website_notes_example.py": 1.6, "packages/agentenv-protocol/tests/test_website_readiness.py": 0.1, - "tst/installer/test_container_journey.py": 162.4, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[pipx]": 8.6, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-project]": 10.3, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-tool]": 17.5, - "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[venv]": 65.7, - "tst/installer/test_plugin_changes.py::test_a_change_made_elsewhere_while_add_waits_for_confirmation_is_not_undone": 9.0, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[pipx]": 47.6, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-project]": 43.6, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-tool]": 24.5, - "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[venv]": 75.5, - "tst/installer/test_plugin_changes.py::test_a_package_added_next_to_a_plugin_is_removed_the_same_way": 18.8, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[pipx]": 58.5, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-project]": 46.5, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-tool]": 28.2, - "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[venv]": 124.7, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[pipx]": 27.9, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-project]": 33.0, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-tool]": 35.4, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[venv]": 72.3, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[uv-tool]": 26.7, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[venv]": 26.4, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[uv-tool]": 11.6, - "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[venv]": 50.9, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[uv-tool]": 17.6, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[venv]": 36.7, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[uv-tool]": 37.0, - "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[venv]": 57.3, - "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[hatch-Hatch has no command for this]": 21.4, - "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[pdm-pdm add]": 34.1, - "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[poetry-poetry add]": 34.3, - "tst/installer/test_plugin_changes.py::test_a_second_change_waits_for_the_first_to_finish": 44.9, - "tst/installer/test_plugin_changes.py::test_a_uv_project_removes_a_plugin_from_the_group_that_declares_it": 23.4, - "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[False]": 26.4, - "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[True]": 15.3, - "tst/installer/test_plugin_changes.py::test_a_uv_tool_whose_index_comes_from_its_config_takes_one_change_after_another": 75.8, - "tst/installer/test_plugin_changes.py::test_a_uv_workspace_removes_a_plugin_from_the_member_that_declares_it": 13.5, - "tst/installer/test_plugin_changes.py::test_a_virtual_uv_workspace_adds_the_plugin_to_the_member_that_declares_agent_env": 34.1, - "tst/installer/test_plugin_changes.py::test_adding_a_plugin_by_its_bare_name_keeps_the_version_the_tool_pins": 22.2, - "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[uv-tool]": 15.5, - "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[venv]": 24.0, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-broken-toy_broken is failed]": 29.1, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-clash-deploy_env is skipped]": 32.9, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-not-a-plugin-not an agent-env plugin]": 11.1, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-broken-toy_broken is failed]": 11.5, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-clash-deploy_env is skipped]": 26.4, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-not-a-plugin-not an agent-env plugin]": 14.4, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-broken-toy_broken is failed]": 13.0, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-clash-deploy_env is skipped]": 42.5, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-not-a-plugin-not an agent-env plugin]": 9.9, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-broken-toy_broken is failed]": 51.5, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-clash-deploy_env is skipped]": 29.0, - "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-not-a-plugin-not an agent-env plugin]": 43.6, - "tst/installer/test_plugin_changes.py::test_ctrl_c_lets_the_installer_finish_and_then_restores": 26.4, - "tst/installer/test_plugin_changes.py::test_read_only_site_packages_are_refused": 50.2, - "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[uv-tool]": 21.5, - "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[venv]": 66.0, - "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-project]": 30.5, - "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-tool]": 34.0, - "tst/installer/test_wheel.py": 0.0, + "tst/installer/test_container_journey.py": 210.8, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[pipx]": 14.6, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-project]": 16.9, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[uv-tool]": 15.5, + "tst/installer/test_plugin_changes.py::test_a_bare_directory_that_builds_as_agent_env_never_replaces_it[venv]": 46.3, + "tst/installer/test_plugin_changes.py::test_a_change_made_elsewhere_while_add_waits_for_confirmation_is_not_undone": 15.9, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[pipx]": 39.1, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-project]": 39.5, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[uv-tool]": 39.7, + "tst/installer/test_plugin_changes.py::test_a_newer_wheel_upgrades_the_installed_plugin[venv]": 67.9, + "tst/installer/test_plugin_changes.py::test_a_package_added_next_to_a_plugin_is_removed_the_same_way": 34.5, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[pipx]": 52.5, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-project]": 43.6, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[uv-tool]": 46.8, + "tst/installer/test_plugin_changes.py::test_a_plugin_survives_the_installers_own_rebuild_and_is_removed[venv]": 119.8, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[pipx]": 59.3, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-project]": 38.4, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[uv-tool]": 37.5, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_conflicts_with_an_installed_one_is_rolled_back[venv]": 64.4, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[uv-tool]": 22.6, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_selects_a_shared_config_file_is_still_blocked_by_it[venv]": 63.3, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[uv-tool]": 19.7, + "tst/installer/test_plugin_changes.py::test_a_plugin_that_sets_the_config_on_import_is_reported[venv]": 70.4, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[uv-tool]": 55.1, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_bundled_config_is_in_effect_can_be_removed[venv]": 57.0, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[uv-tool]": 33.8, + "tst/installer/test_plugin_changes.py::test_a_plugin_whose_root_option_breaks_startup_can_still_be_removed[venv]": 57.7, + "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[hatch-Hatch has no command for this]": 32.0, + "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[pdm-pdm add]": 31.4, + "tst/installer/test_plugin_changes.py::test_a_project_tool_that_owns_the_environment_is_given_the_command_and_nothing_runs[poetry-poetry add]": 34.9, + "tst/installer/test_plugin_changes.py::test_a_second_change_waits_for_the_first_to_finish": 25.3, + "tst/installer/test_plugin_changes.py::test_a_uv_project_removes_a_plugin_from_the_group_that_declares_it": 21.9, + "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[False]": 24.0, + "tst/installer/test_plugin_changes.py::test_a_uv_project_with_its_environment_elsewhere_keeps_the_plugin_through_uv_sync[True]": 25.5, + "tst/installer/test_plugin_changes.py::test_a_uv_tool_whose_index_comes_from_its_config_takes_one_change_after_another": 67.5, + "tst/installer/test_plugin_changes.py::test_a_uv_workspace_removes_a_plugin_from_the_member_that_declares_it": 22.1, + "tst/installer/test_plugin_changes.py::test_a_virtual_uv_workspace_adds_the_plugin_to_the_member_that_declares_agent_env": 32.1, + "tst/installer/test_plugin_changes.py::test_adding_a_plugin_by_its_bare_name_keeps_the_version_the_tool_pins": 37.6, + "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[uv-tool]": 13.3, + "tst/installer/test_plugin_changes.py::test_agent_env_itself_is_never_added_or_removed[venv]": 35.6, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-broken-toy_broken is failed]": 26.8, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-clash-deploy_env is skipped]": 55.9, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[pipx-not-a-plugin-not an agent-env plugin]": 14.5, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-broken-toy_broken is failed]": 16.9, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-clash-deploy_env is skipped]": 38.0, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-project-not-a-plugin-not an agent-env plugin]": 16.6, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-broken-toy_broken is failed]": 21.5, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-clash-deploy_env is skipped]": 23.7, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[uv-tool-not-a-plugin-not an agent-env plugin]": 17.0, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-broken-toy_broken is failed]": 49.5, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-clash-deploy_env is skipped]": 52.0, + "tst/installer/test_plugin_changes.py::test_an_add_that_does_not_take_effect_restores_the_environment_exactly[venv-not-a-plugin-not an agent-env plugin]": 40.6, + "tst/installer/test_plugin_changes.py::test_ctrl_c_lets_the_installer_finish_and_then_restores": 17.8, + "tst/installer/test_plugin_changes.py::test_read_only_site_packages_are_refused": 34.6, + "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[uv-tool]": 33.9, + "tst/installer/test_plugin_changes.py::test_remove_is_blocked_while_the_config_names_its_sandbox_provider[venv]": 61.0, + "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-project]": 48.7, + "tst/installer/test_plugin_changes.py::test_removing_a_plugin_names_and_checks_the_plugins_that_go_with_it[uv-tool]": 52.9, + "tst/installer/test_wheel.py": 0.4, "tst/integration/agent/a2a_agent/a2a_agent_test.py": 0.0, "tst/integration/agent/a2a_agent/agent_triggers_e2e_test.py": 0.0, "tst/integration/agent/proxy_modal_vm_test.py": 0.0, - "tst/integration/artifact/artifacts/docker_image_artifact_test.py": 18.4, - "tst/integration/cli/bundle_init_images_test.py": 24.8, - "tst/integration/cli/bundle_register_test.py": 43.2, - "tst/integration/cli/cli_test.py": 56.5, - "tst/integration/cli/run_bundle_browser_use_test.py": 196.6, + "tst/integration/artifact/artifacts/docker_image_artifact_test.py": 22.2, + "tst/integration/cli/bundle_init_images_test.py": 25.9, + "tst/integration/cli/bundle_register_test.py": 40.6, + "tst/integration/cli/cli_test.py": 63.3, + "tst/integration/cli/run_bundle_browser_use_test.py": 220.0, "tst/integration/cli/run_bundle_built_agents_test.py": 45.7, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_children_name_one_container_is_refused_before_anything_is_built_or_written": 0.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_servers_share_a_tool_name_is_refused_at_deploy_naming_the_tool_and_both_servers": 42.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[gateway]": 46.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[server]": 11.5, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_server_that_crashes_on_start_fails_its_deploy_naming_it_and_ending_with_what_it_printed": 30.1, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_stored_mcp_task_imports_as_a_bundle_that_runs_through_the_gateway": 64.5, - "tst/integration/cli/run_bundle_built_envs_test.py::test_a_universe_composed_every_way_seeds_each_server_of_a_multi_and_a_shared_one_seeds_its_server": 48.4, - "tst/integration/cli/run_bundle_built_envs_test.py::test_an_env_folder_is_built_named_by_its_card_deployed_and_rebuilt_only_when_it_changes": 18.7, - "tst/integration/cli/run_bundle_built_envs_test.py::test_an_output_a_bundles_task_wrote_is_written_under_the_folder_that_imports_it": 43.3, - "tst/integration/cli/run_bundle_built_envs_test.py::test_each_kind_of_bundle_env_deploys_through_the_gateway_and_a_child_edit_rewrites_its_multi": 175.7, - "tst/integration/cli/run_bundle_built_envs_test.py::test_environments_and_a_universe_written_from_a_bundle_load_into_an_env_through_the_gateway": 69.2, - "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_echo_mcp_example_runs_by_name_and_a_rerun_writes_nothing": 67.9, - "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_mail_mcp_example_runs_and_a_task_outside_a_bundle_deploys_its_env_by_its_alias": 51.5, - "tst/integration/cli/run_bundle_claude_code_install_test.py": 123.6, - "tst/integration/cli/run_bundle_claude_code_test.py": 323.1, - "tst/integration/cli/run_bundle_cli_folder_test.py": 75.1, - "tst/integration/cli/run_bundle_image_folder_test.py": 43.8, - "tst/integration/cli/run_bundle_image_ref_test.py": 30.7, - "tst/integration/cli/run_bundle_installed_agent_test.py": 22.0, - "tst/integration/cli/run_bundle_openhands_test.py": 237.4, - "tst/integration/cli/run_bundle_pydantic_ai_test.py": 107.9, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_children_name_one_container_is_refused_before_anything_is_built_or_written": 0.3, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_multi_whose_servers_share_a_tool_name_is_refused_at_deploy_naming_the_tool_and_both_servers": 42.6, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[gateway]": 34.2, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_script_on_an_envs_host_reaches_it_and_its_mcp_endpoint_at_the_addresses_its_input_gives[server]": 16.2, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_server_that_crashes_on_start_fails_its_deploy_naming_it_and_ending_with_what_it_printed": 31.0, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_stored_mcp_task_imports_as_a_bundle_that_runs_through_the_gateway": 60.6, + "tst/integration/cli/run_bundle_built_envs_test.py::test_a_universe_composed_every_way_seeds_each_server_of_a_multi_and_a_shared_one_seeds_its_server": 42.5, + "tst/integration/cli/run_bundle_built_envs_test.py::test_an_env_folder_is_built_named_by_its_card_deployed_and_rebuilt_only_when_it_changes": 19.4, + "tst/integration/cli/run_bundle_built_envs_test.py::test_an_output_a_bundles_task_wrote_is_written_under_the_folder_that_imports_it": 49.3, + "tst/integration/cli/run_bundle_built_envs_test.py::test_each_kind_of_bundle_env_deploys_through_the_gateway_and_a_child_edit_rewrites_its_multi": 168.9, + "tst/integration/cli/run_bundle_built_envs_test.py::test_environments_and_a_universe_written_from_a_bundle_load_into_an_env_through_the_gateway": 53.1, + "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_echo_mcp_example_runs_by_name_and_a_rerun_writes_nothing": 60.2, + "tst/integration/cli/run_bundle_built_envs_test.py::test_the_shipped_mail_mcp_example_runs_and_a_task_outside_a_bundle_deploys_its_env_by_its_alias": 58.5, + "tst/integration/cli/run_bundle_claude_code_install_test.py": 138.8, + "tst/integration/cli/run_bundle_claude_code_test.py": 314.6, + "tst/integration/cli/run_bundle_cli_folder_test.py": 86.0, + "tst/integration/cli/run_bundle_image_folder_test.py": 46.6, + "tst/integration/cli/run_bundle_image_ref_test.py": 27.2, + "tst/integration/cli/run_bundle_installed_agent_test.py": 26.2, + "tst/integration/cli/run_bundle_openhands_test.py": 226.1, + "tst/integration/cli/run_bundle_pydantic_ai_test.py": 118.1, "tst/integration/cli/run_bundle_skills_test.py": 25.1, - "tst/integration/cli/run_role_models_test.py": 36.5, - "tst/integration/computer/base_computer_test.py": 83.2, - "tst/integration/computer/browser_computer_test.py": 303.7, - "tst/integration/computer/browser_gateway_test.py": 191.7, + "tst/integration/cli/run_bundle_task_image_test.py": 4.7, + "tst/integration/cli/run_role_models_test.py": 48.3, + "tst/integration/computer/base_computer_test.py": 86.8, + "tst/integration/computer/browser_computer_test.py": 306.6, + "tst/integration/computer/browser_gateway_test.py": 153.1, "tst/integration/computer/builder_modal_vm_test.py": 0.0, - "tst/integration/computer/ca_trust_test.py": 270.4, - "tst/integration/computer/chromium_app_test.py": 552.2, - "tst/integration/computer/computer_app_test.py::test_a_computers_apps_load_export_and_reset_each_through_its_own_data_plane": 111.4, - "tst/integration/computer/computer_app_test.py::test_an_app_deployed_alone_runs_its_service_on_its_settings_on_a_computer_named_after_it_as_its_driver": 66.8, - "tst/integration/computer/computer_app_test.py::test_the_locks_debs_and_wheels_install_from_mounts_and_stay_in_no_layer_of_the_image": 148.4, - "tst/integration/computer/computer_app_test.py::test_through_the_drivers_route_a_websocket_reaches_an_apps_server_and_a_20_mb_answer_streams_whole": 182.4, - "tst/integration/computer/computer_app_test.py::test_two_apps_composed_into_one_computer_env_each_run_on_their_own_settings": 150.5, - "tst/integration/computer/computer_env_test.py": 257.9, - "tst/integration/computer/covered_test.py": 51.0, - "tst/integration/computer/cua_driver_test.py": 73.3, - "tst/integration/computer/desktop_example_test.py": 130.8, - "tst/integration/computer/disk_boot_test.py": 132.1, + "tst/integration/computer/ca_trust_test.py": 239.3, + "tst/integration/computer/chromium_app_test.py": 609.4, + "tst/integration/computer/computer_app_test.py::test_a_computers_apps_load_export_and_reset_each_through_its_own_data_plane": 116.0, + "tst/integration/computer/computer_app_test.py::test_an_app_deployed_alone_runs_its_service_on_its_settings_on_a_computer_named_after_it_as_its_driver": 70.0, + "tst/integration/computer/computer_app_test.py::test_the_locks_debs_and_wheels_install_from_mounts_and_stay_in_no_layer_of_the_image": 220.5, + "tst/integration/computer/computer_app_test.py::test_through_the_drivers_route_a_websocket_reaches_an_apps_server_and_a_20_mb_answer_streams_whole": 243.8, + "tst/integration/computer/computer_app_test.py::test_two_apps_composed_into_one_computer_env_each_run_on_their_own_settings": 147.6, + "tst/integration/computer/computer_env_test.py": 198.4, + "tst/integration/computer/covered_test.py": 62.7, + "tst/integration/computer/cua_driver_test.py": 122.9, + "tst/integration/computer/desktop_example_test.py": 141.9, + "tst/integration/computer/disk_boot_test.py": 94.8, "tst/integration/computer/disk_modal_vm_test.py": 0.0, - "tst/integration/computer/launcher_test.py": 88.0, - "tst/integration/computer/multi_env_computers_test.py": 314.0, - "tst/integration/computer/recording_chaos_test.py": 136.5, - "tst/integration/computer/set_test.py": 151.9, - "tst/integration/computer/tool_prefix_test.py": 91.3, - "tst/integration/computer/website_browser_test.py": 201.7, + "tst/integration/computer/launcher_test.py": 119.5, + "tst/integration/computer/multi_env_computers_test.py": 274.4, + "tst/integration/computer/recording_chaos_test.py": 214.8, + "tst/integration/computer/set_test.py": 170.1, + "tst/integration/computer/tool_prefix_test.py": 93.6, + "tst/integration/computer/website_browser_test.py": 348.9, "tst/integration/computer/windows_test.py": 0.0, - "tst/integration/env/env_test.py": 15.7, - "tst/integration/env/envs/build_context_local_test.py": 165.7, - "tst/integration/env/envs/multi_env_child_envs_local_test.py": 106.0, - "tst/integration/env/envs/multi_env_local_test.py": 115.9, - "tst/integration/env/envs/playwright_mcp_local_test.py": 88.9, - "tst/integration/env/envs/server_env_local_test.py": 75.4, + "tst/integration/env/env_test.py": 13.6, + "tst/integration/env/envs/build_context_local_test.py": 199.0, + "tst/integration/env/envs/multi_env_child_envs_local_test.py": 95.7, + "tst/integration/env/envs/multi_env_local_test.py": 59.0, + "tst/integration/env/envs/playwright_mcp_local_test.py": 101.6, + "tst/integration/env/envs/server_env_local_test.py": 87.9, "tst/integration/env/envs/server_env_modal_test.py": 0.0, "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_airtable_clock_consumer[boat]": 0.0, @@ -272,7 +273,7 @@ "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_calendar_mcp_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_cardless_environment_resolves_name_from_environment_name": 37.6, + "tst/integration/env/gateway/gateway_test.py::test_cardless_environment_resolves_name_from_environment_name": 37.7, "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_clickup_clock_consumer[boxd]": 0.0, @@ -304,7 +305,7 @@ "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[sail_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[scale]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tenki]": 0.3, + "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_crm_clock_consumer[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_eight_sleep_clock_consumer[boat]": 0.0, @@ -403,7 +404,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[scale]": 29.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[scale]": 29.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_autonomous_time_trigger[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[agent37]": 0.0, @@ -414,11 +415,11 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[scale]": 29.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[scale]": 28.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_barrier_orders_a_templated_mirror[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_request_behind_a_write_through_the_rest_proxy": 28.3, - "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_role_behind_each_write": 28.3, + "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_request_behind_a_write_through_the_rest_proxy": 27.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_changes_name_the_role_behind_each_write": 46.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[boxd]": 0.0, @@ -427,7 +428,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[scale]": 36.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[scale]": 36.9, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_extension[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[agent37]": 0.0, @@ -438,10 +439,10 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[scale]": 51.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[scale]": 51.7, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_clock_rate_accuracy[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_consistent_mode": 50.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_consistent_mode": 47.9, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[boxd]": 0.0, @@ -450,7 +451,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[scale]": 28.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[scale]": 30.7, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_queued_trigger_is_not_reported_settled[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[agent37]": 0.0, @@ -461,7 +462,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[scale]": 29.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[scale]": 28.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_time_trigger_extension[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[agent37]": 0.0, @@ -472,7 +473,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[scale]": 31.6, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[scale]": 30.3, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_rearms_after_a_failed_mirror[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[agent37]": 0.0, @@ -483,7 +484,7 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[scale]": 26.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[scale]": 26.6, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_trigger_registration_is_fail_loud[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[agent37]": 0.0, @@ -494,13 +495,13 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[scale]": 37.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[scale]": 39.1, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_extension[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_triggers_nl_executor[modal_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_website_trigger_fires_on_a_request_through_the_website_proxy": 72.2, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_agentenv_environment": 28.7, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_env_including_website": 261.8, + "tst/integration/env/gateway/gateway_test.py::test_gateway_website_trigger_fires_on_a_request_through_the_website_proxy": 64.8, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_agentenv_environment": 38.9, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_env_including_website": 141.3, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[boxd]": 0.0, @@ -509,12 +510,12 @@ "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[scale]": 192.0, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[scale]": 108.7, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_gateway_with_multi_mcp_server[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_mcp_server": 209.7, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_website_env": 250.4, - "tst/integration/env/gateway/gateway_test.py::test_gateway_with_website_env_v1": 44.4, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_mcp_server": 98.1, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_single_website_env": 118.2, + "tst/integration/env/gateway/gateway_test.py::test_gateway_with_website_env_v1": 42.1, "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_github_clock_consumer[boxd]": 0.0, @@ -647,7 +648,7 @@ "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_linear_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_load_from_snapshot": 42.4, + "tst/integration/env/gateway/gateway_test.py::test_load_from_snapshot": 41.6, "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_messaging_clock_consumer[boxd]": 0.0, @@ -769,7 +770,7 @@ "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_reminder_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_renamed_registration_serves_its_registered_name": 37.1, + "tst/integration/env/gateway/gateway_test.py::test_renamed_registration_serves_its_registered_name": 40.7, "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_renpho_clock_consumer[boxd]": 0.0, @@ -836,8 +837,8 @@ "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_smart_lock_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_snapshot_agent_state_v1_and_fallback": 41.9, - "tst/integration/env/gateway/gateway_test.py::test_snapshot_cli": 56.9, + "tst/integration/env/gateway/gateway_test.py::test_snapshot_agent_state_v1_and_fallback": 39.1, + "tst/integration/env/gateway/gateway_test.py::test_snapshot_cli": 37.3, "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_snowflake_async_virtual_clock[boxd]": 0.0, @@ -901,7 +902,7 @@ "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[modal]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[modal_vm]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[sail_vm]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[scale]": 38.8, + "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[scale]": 26.9, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_sync_env_clock_step[tensorlake]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_system_clock_consumer[agent37]": 0.0, @@ -926,7 +927,7 @@ "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_target_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_the_browsers_card_and_recording_through_the_gateway_survive_its_recorder_and_container_restarting": 240.7, + "tst/integration/env/gateway/gateway_test.py::test_the_browsers_card_and_recording_through_the_gateway_survive_its_recorder_and_container_restarting": 131.4, "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_ticketmaster_clock_consumer[boxd]": 0.0, @@ -960,8 +961,8 @@ "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_trello_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/gateway/gateway_test.py::test_validate_universe_compatibility": 28.1, - "tst/integration/env/gateway/gateway_test.py::test_verify_universe_roundtrip_export_all_v1_and_fallback": 28.1, + "tst/integration/env/gateway/gateway_test.py::test_validate_universe_compatibility": 28.5, + "tst/integration/env/gateway/gateway_test.py::test_verify_universe_roundtrip_export_all_v1_and_fallback": 27.1, "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[agent37]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[boat]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_walmart_clock_consumer[boxd]": 0.0, @@ -1028,9 +1029,9 @@ "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[scale]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[tenki]": 0.0, "tst/integration/env/gateway/gateway_test.py::test_zillow_clock_consumer[tensorlake]": 0.0, - "tst/integration/env/task_step/task_steps_test.py": 187.9, - "tst/integration/eval/eval_test.py": 1.7, - "tst/integration/openenv/build_app_test.py": 89.8, + "tst/integration/env/task_step/task_steps_test.py": 121.1, + "tst/integration/eval/eval_test.py": 2.1, + "tst/integration/openenv/build_app_test.py": 91.3, "tst/integration/providers/sandbox_providers/agent37_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/boat_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/boxd_sandbox_smoke_test.py": 0.0, @@ -1041,262 +1042,262 @@ "tst/integration/providers/sandbox_providers/tenki_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/tensorlake_sandbox_smoke_test.py": 0.0, "tst/integration/providers/sandbox_providers/vercel_sandbox_smoke_test.py": 0.0, - "tst/integration/providers/sandbox_providers/vm_sandbox_build_test.py": 2.8, + "tst/integration/providers/sandbox_providers/vm_sandbox_build_test.py": 2.1, "tst/integration/providers/sandbox_providers/websockets_live_test.py": 0.0, - "tst/integration/store/local_parallel_deploy_test.py": 51.6, - "tst/integration/task_step/test_advance_to_next_event_hook_local.py": 112.1, - "tst/integration/task_step/test_agent_on_local_sandbox.py": 7.7, + "tst/integration/store/local_parallel_deploy_test.py": 47.6, + "tst/integration/task_step/test_advance_to_next_event_hook_local.py": 119.0, + "tst/integration/task_step/test_agent_on_local_sandbox.py": 24.5, "tst/integration/task_step/test_enable_docker_e2e.py": 0.0, "tst/integration/task_step/test_multiturn_verifier_delivery.py": 0.0, - "tst/integration/task_step/test_next_env_event_hook_local.py": 111.3, - "tst/integration/task_step/test_prompt_agent_notifications_local.py": 115.4, - "tst/integration/task_step/test_register_env_triggers_local.py": 91.2, - "tst/integration/training/harness_test.py": 26.8, - "tst/integration/training/trl_test.py": 32.9, - "tst/integration/utils/docker/prune_test.py": 6.3, - "tst/unit/a2a_agent/add_skill_test.py": 0.9, - "tst/unit/a2a_agent/alias_get_test.py": 0.4, + "tst/integration/task_step/test_next_env_event_hook_local.py": 109.6, + "tst/integration/task_step/test_prompt_agent_notifications_local.py": 121.3, + "tst/integration/task_step/test_register_env_triggers_local.py": 135.2, + "tst/integration/training/harness_test.py": 31.7, + "tst/integration/training/trl_test.py": 25.7, + "tst/integration/utils/docker/prune_test.py": 5.5, + "tst/unit/a2a_agent/add_skill_test.py": 1.0, + "tst/unit/a2a_agent/alias_get_test.py": 0.3, "tst/unit/a2a_agent/cdp_config_test.py": 0.0, - "tst/unit/a2a_agent/context_only_deploy_test.py": 0.7, + "tst/unit/a2a_agent/context_only_deploy_test.py": 0.3, "tst/unit/a2a_agent/deploy_cleanup_test.py": 0.1, "tst/unit/a2a_agent/negotiate_agent_config_test.py": 0.0, - "tst/unit/a2a_agent/object_transfer_reach_test.py": 0.2, - "tst/unit/a2a_agent/object_transfer_test.py": 0.3, + "tst/unit/a2a_agent/object_transfer_reach_test.py": 0.1, + "tst/unit/a2a_agent/object_transfer_test.py": 0.2, "tst/unit/a2a_agent/protocol_test.py": 0.3, - "tst/unit/a2a_agent/put_version_test.py": 0.2, + "tst/unit/a2a_agent/put_version_test.py": 0.1, "tst/unit/a2a_agent/readable_parts_test.py": 0.1, - "tst/unit/a2a_agent/staging_test.py": 2.2, + "tst/unit/a2a_agent/staging_test.py": 1.3, "tst/unit/a2a_agent/store_test.py": 0.0, "tst/unit/a2a_agent/test_trajectory_follower.py": 0.7, - "tst/unit/a2a_agent/test_validator_changelog_objects.py": 0.2, + "tst/unit/a2a_agent/test_validator_changelog_objects.py": 0.1, "tst/unit/a2a_agent/test_validator_skill_variants.py": 0.1, - "tst/unit/a2a_agent/validator_trajectory_dag_test.py": 0.3, + "tst/unit/a2a_agent/validator_trajectory_dag_test.py": 0.2, "tst/unit/a2a_agent/validator_uri_probes_test.py": 0.0, "tst/unit/a2a_agent/verify_a2a_agent_card_test.py": 0.0, - "tst/unit/agents/browser_use/test_browser_use_agent.py": 1.6, - "tst/unit/agents/claude_code/test_card_contract.py": 1.3, - "tst/unit/agents/claude_code/test_changelog.py": 107.5, + "tst/unit/agents/browser_use/test_browser_use_agent.py": 1.4, + "tst/unit/agents/claude_code/test_card_contract.py": 1.5, + "tst/unit/agents/claude_code/test_changelog.py": 17.3, "tst/unit/agents/claude_code/test_claude_bridge_builtin_tools.py": 0.1, - "tst/unit/agents/claude_code/test_claude_bridge_cancel.py": 9.6, - "tst/unit/agents/claude_code/test_claude_bridge_failure_message.py": 0.3, + "tst/unit/agents/claude_code/test_claude_bridge_cancel.py": 9.2, + "tst/unit/agents/claude_code/test_claude_bridge_failure_message.py": 0.1, "tst/unit/agents/claude_code/test_claude_bridge_live.py": 0.1, "tst/unit/agents/claude_code/test_claude_bridge_model.py": 0.1, "tst/unit/agents/claude_code/test_claude_bridge_retry.py": 0.4, "tst/unit/agents/claude_code/test_claude_bridge_stdin.py": 1.1, "tst/unit/agents/claude_code/test_cli_env.py": 0.2, - "tst/unit/agents/claude_code/test_ext_mcp_name.py": 0.1, - "tst/unit/agents/claude_code/test_extensions.py": 0.2, + "tst/unit/agents/claude_code/test_ext_mcp_name.py": 0.0, + "tst/unit/agents/claude_code/test_extensions.py": 0.1, "tst/unit/agents/claude_code/test_image_contract.py": 0.1, - "tst/unit/agents/claude_code/test_install_sh_host_root.py": 0.1, + "tst/unit/agents/claude_code/test_install_sh_host_root.py": 0.0, "tst/unit/agents/claude_code/test_message_contract.py": 1.6, "tst/unit/agents/claude_code/test_object_transfer.py": 0.5, "tst/unit/agents/claude_code/test_partial_trajectory.py": 0.2, - "tst/unit/agents/claude_code/test_workspace_snapshot.py": 0.2, + "tst/unit/agents/claude_code/test_workspace_snapshot.py": 0.1, "tst/unit/agents/codex/test_card_contract.py": 0.1, - "tst/unit/agents/codex/test_codex_bridge_events.py": 0.2, + "tst/unit/agents/codex/test_codex_bridge_events.py": 0.3, "tst/unit/agents/codex/test_codex_bridge_failure_message.py": 0.2, - "tst/unit/agents/codex/test_codex_bridge_retry.py": 0.5, - "tst/unit/agents/codex/test_codex_bridge_watchdog.py": 0.3, + "tst/unit/agents/codex/test_codex_bridge_retry.py": 0.4, + "tst/unit/agents/codex/test_codex_bridge_watchdog.py": 0.2, "tst/unit/agents/codex/test_codex_builtin_tools.py": 1.9, - "tst/unit/agents/codex/test_codex_executor_outcome.py": 0.2, - "tst/unit/agents/codex/test_codex_snapshot_hardening.py": 0.3, - "tst/unit/agents/codex/test_config_and_mcp.py": 0.3, + "tst/unit/agents/codex/test_codex_executor_outcome.py": 0.3, + "tst/unit/agents/codex/test_codex_snapshot_hardening.py": 0.4, + "tst/unit/agents/codex/test_config_and_mcp.py": 0.1, "tst/unit/agents/codex/test_image_contract.py": 0.1, "tst/unit/agents/codex/test_install.py": 0.1, "tst/unit/agents/codex/test_live_trajectory.py": 0.1, - "tst/unit/agents/codex/test_message_contract.py": 0.1, - "tst/unit/agents/codex/test_object_transfer.py": 0.8, - "tst/unit/agents/gemini/test_card_contract.py": 1.7, - "tst/unit/agents/gemini/test_extensions.py": 0.6, - "tst/unit/agents/gemini/test_gemini_bridge_attribution.py": 0.1, - "tst/unit/agents/gemini/test_gemini_bridge_cancel.py": 5.6, - "tst/unit/agents/gemini/test_gemini_bridge_failure_message.py": 0.3, + "tst/unit/agents/codex/test_message_contract.py": 0.2, + "tst/unit/agents/codex/test_object_transfer.py": 0.5, + "tst/unit/agents/gemini/test_card_contract.py": 1.8, + "tst/unit/agents/gemini/test_extensions.py": 0.3, + "tst/unit/agents/gemini/test_gemini_bridge_attribution.py": 0.0, + "tst/unit/agents/gemini/test_gemini_bridge_cancel.py": 5.8, + "tst/unit/agents/gemini/test_gemini_bridge_failure_message.py": 0.4, "tst/unit/agents/gemini/test_gemini_bridge_live.py": 1.3, "tst/unit/agents/gemini/test_image_contract.py": 0.1, "tst/unit/agents/gemini/test_install.py": 0.1, - "tst/unit/agents/gemini/test_litellm_proxy.py": 0.5, - "tst/unit/agents/gemini/test_message_contract.py": 0.4, - "tst/unit/agents/gemini/test_object_transfer.py": 3.5, - "tst/unit/agents/hermes/test_card_contract.py": 1.6, - "tst/unit/agents/hermes/test_held_work.py": 0.8, - "tst/unit/agents/hermes/test_hermes_bridge.py": 3.6, - "tst/unit/agents/hermes/test_message_contract.py": 0.7, - "tst/unit/agents/letta_code/test_bridge.py": 0.8, - "tst/unit/agents/opencode/test_card_contract.py": 1.8, - "tst/unit/agents/opencode/test_image_contract.py": 0.2, + "tst/unit/agents/gemini/test_litellm_proxy.py": 0.3, + "tst/unit/agents/gemini/test_message_contract.py": 0.3, + "tst/unit/agents/gemini/test_object_transfer.py": 2.6, + "tst/unit/agents/hermes/test_card_contract.py": 1.1, + "tst/unit/agents/hermes/test_held_work.py": 0.7, + "tst/unit/agents/hermes/test_hermes_bridge.py": 3.4, + "tst/unit/agents/hermes/test_message_contract.py": 0.3, + "tst/unit/agents/letta_code/test_bridge.py": 0.5, + "tst/unit/agents/opencode/test_card_contract.py": 1.5, + "tst/unit/agents/opencode/test_image_contract.py": 0.1, "tst/unit/agents/opencode/test_install.py": 0.1, - "tst/unit/agents/opencode/test_message_contract.py": 1.1, - "tst/unit/agents/opencode/test_object_transfer.py": 0.7, - "tst/unit/agents/opencode/test_opencode_bridge_builtin_tools.py": 0.3, + "tst/unit/agents/opencode/test_message_contract.py": 0.9, + "tst/unit/agents/opencode/test_object_transfer.py": 0.6, + "tst/unit/agents/opencode/test_opencode_bridge_builtin_tools.py": 0.1, "tst/unit/agents/opencode/test_opencode_bridge_cancel.py": 5.7, - "tst/unit/agents/opencode/test_opencode_bridge_config.py": 0.2, - "tst/unit/agents/opencode/test_opencode_bridge_events.py": 0.1, - "tst/unit/agents/opencode/test_opencode_bridge_live.py": 1.3, + "tst/unit/agents/opencode/test_opencode_bridge_config.py": 0.1, + "tst/unit/agents/opencode/test_opencode_bridge_events.py": 0.0, + "tst/unit/agents/opencode/test_opencode_bridge_live.py": 1.2, "tst/unit/agents/opencode/test_opencode_bridge_stdin.py": 0.1, "tst/unit/agents/openhands/test_conversation_run.py": 0.1, - "tst/unit/agents/openhands/test_openhands_agent.py": 4.8, - "tst/unit/agents/pi/test_pi_agent.py": 2.7, - "tst/unit/agents/proxy/test_proxy_agent.py": 0.6, - "tst/unit/agents/pydantic_ai/test_pydantic_ai_agent.py": 3.5, - "tst/unit/agents/test_host_workdir.py": 0.1, - "tst/unit/agents/test_installed_workspace.py": 0.4, + "tst/unit/agents/openhands/test_openhands_agent.py": 4.7, + "tst/unit/agents/pi/test_pi_agent.py": 2.9, + "tst/unit/agents/proxy/test_proxy_agent.py": 0.2, + "tst/unit/agents/pydantic_ai/test_pydantic_ai_agent.py": 3.4, + "tst/unit/agents/test_host_workdir.py": 0.0, + "tst/unit/agents/test_installed_workspace.py": 0.3, "tst/unit/artifact/cli_artifact_test.py": 0.1, - "tst/unit/artifact/environment_files_test.py": 0.4, + "tst/unit/artifact/environment_files_test.py": 0.3, "tst/unit/artifact/environment_universe_folder_test.py": 0.0, - "tst/unit/artifact/registry_test.py": 1.0, + "tst/unit/artifact/registry_test.py": 0.5, "tst/unit/artifact/skill_artifact_test.py": 0.2, "tst/unit/artifact/test_artifact_metadata.py": 0.1, "tst/unit/artifact/test_artifact_put.py": 0.0, - "tst/unit/artifact/test_computer_artifact.py": 1.1, - "tst/unit/artifact/test_docker_image_build_context.py": 0.5, + "tst/unit/artifact/test_computer_artifact.py": 0.8, + "tst/unit/artifact/test_docker_image_build_context.py": 0.4, "tst/unit/artifact/test_docker_image_github_clone.py": 0.1, - "tst/unit/artifact/test_docker_image_github_push.py": 0.2, - "tst/unit/artifact/test_docker_image_pulled.py": 0.1, - "tst/unit/artifact/test_docker_image_put_ref.py": 0.5, - "tst/unit/artifact/test_object_url_keys.py": 0.4, - "tst/unit/artifact/test_removed_service_aliases.py": 1.9, + "tst/unit/artifact/test_docker_image_github_push.py": 0.1, + "tst/unit/artifact/test_docker_image_pulled.py": 0.2, + "tst/unit/artifact/test_docker_image_put_ref.py": 0.3, + "tst/unit/artifact/test_object_url_keys.py": 0.3, + "tst/unit/artifact/test_removed_service_aliases.py": 1.4, "tst/unit/artifact/test_service_artifact_alias.py": 0.1, "tst/unit/artifact/test_service_universe_file_view.py": 0.1, - "tst/unit/artifact/test_store_put_document.py": 0.2, + "tst/unit/artifact/test_store_put_document.py": 0.1, "tst/unit/attribution_test.py": 0.0, - "tst/unit/bundle/agent_override_test.py": 2.7, - "tst/unit/bundle/aliases_test.py": 0.6, - "tst/unit/bundle/artifact_toml_test.py": 5.0, - "tst/unit/bundle/authoring_test.py": 0.7, - "tst/unit/bundle/cli_folder_test.py": 2.1, - "tst/unit/bundle/computer_app_env_test.py": 12.6, - "tst/unit/bundle/computer_driver_env_test.py": 18.3, - "tst/unit/bundle/computer_env_folder_test.py": 55.1, - "tst/unit/bundle/computer_folder_test.py": 1.5, - "tst/unit/bundle/env_toml_test.py": 6.5, - "tst/unit/bundle/file_artifacts_test.py": 0.8, - "tst/unit/bundle/hub_test.py": 2.6, - "tst/unit/bundle/image_folder_test.py": 3.8, - "tst/unit/bundle/importer_entities_test.py": 9.8, - "tst/unit/bundle/importer_image_refs_test.py": 4.2, - "tst/unit/bundle/importer_test.py": 20.7, - "tst/unit/bundle/installed_entities_test.py": 8.0, - "tst/unit/bundle/installed_test.py": 5.6, + "tst/unit/bundle/agent_override_test.py": 1.4, + "tst/unit/bundle/aliases_test.py": 0.4, + "tst/unit/bundle/artifact_toml_test.py": 3.5, + "tst/unit/bundle/authoring_test.py": 0.4, + "tst/unit/bundle/cli_folder_test.py": 1.1, + "tst/unit/bundle/computer_app_env_test.py": 11.0, + "tst/unit/bundle/computer_driver_env_test.py": 19.0, + "tst/unit/bundle/computer_env_folder_test.py": 30.0, + "tst/unit/bundle/computer_folder_test.py": 1.9, + "tst/unit/bundle/env_toml_test.py": 3.0, + "tst/unit/bundle/file_artifacts_test.py": 0.6, + "tst/unit/bundle/hub_test.py": 2.7, + "tst/unit/bundle/image_folder_test.py": 4.5, + "tst/unit/bundle/importer_entities_test.py": 5.7, + "tst/unit/bundle/importer_image_refs_test.py": 2.5, + "tst/unit/bundle/importer_test.py": 11.8, + "tst/unit/bundle/installed_entities_test.py": 11.1, + "tst/unit/bundle/installed_test.py": 7.4, "tst/unit/bundle/ledger_digest_test.py": 0.1, - "tst/unit/bundle/ledger_test.py": 21.3, - "tst/unit/bundle/materialize_test.py": 14.5, - "tst/unit/bundle/parse_test.py": 4.1, - "tst/unit/bundle/plan_test.py": 5.9, - "tst/unit/bundle/plugin_artifact_types_test.py": 1.2, - "tst/unit/bundle/preflight_test.py": 57.1, - "tst/unit/bundle/referenced_context_test.py": 1.9, - "tst/unit/bundle/register_test.py": 10.9, - "tst/unit/bundle/requires_test.py": 9.2, - "tst/unit/bundle/resolve_test.py": 6.0, - "tst/unit/bundle/run_teardown_test.py": 24.5, - "tst/unit/bundle/run_test.py": 11.4, - "tst/unit/bundle/shipped_bundles_test.py": 2.5, - "tst/unit/bundle/shipped_computers_test.py": 1.5, - "tst/unit/bundle/to_toml_round_trip_test.py": 6.4, - "tst/unit/bundle/toml_refs_test.py": 0.4, + "tst/unit/bundle/ledger_test.py": 14.5, + "tst/unit/bundle/materialize_test.py": 17.6, + "tst/unit/bundle/parse_test.py": 4.3, + "tst/unit/bundle/plan_test.py": 2.4, + "tst/unit/bundle/plugin_artifact_types_test.py": 0.6, + "tst/unit/bundle/preflight_test.py": 22.4, + "tst/unit/bundle/referenced_context_test.py": 1.1, + "tst/unit/bundle/register_test.py": 11.9, + "tst/unit/bundle/requires_test.py": 4.0, + "tst/unit/bundle/resolve_test.py": 3.9, + "tst/unit/bundle/run_teardown_test.py": 22.9, + "tst/unit/bundle/run_test.py": 13.0, + "tst/unit/bundle/shipped_bundles_test.py": 4.3, + "tst/unit/bundle/shipped_computers_test.py": 0.7, + "tst/unit/bundle/to_toml_round_trip_test.py": 3.8, + "tst/unit/bundle/toml_refs_test.py": 0.3, "tst/unit/bundle/toml_writer_test.py": 0.1, - "tst/unit/bundle/variants_test.py": 6.0, + "tst/unit/bundle/variants_test.py": 6.2, "tst/unit/bundle/website_optional_frontend_test.py": 0.0, - "tst/unit/cli/a2a_agent_list_test.py": 0.3, - "tst/unit/cli/a2a_agent_validate_test.py": 0.3, + "tst/unit/cli/a2a_agent_list_test.py": 0.2, + "tst/unit/cli/a2a_agent_validate_test.py": 0.2, "tst/unit/cli/base_metadata_version_test.py": 0.0, - "tst/unit/cli/build_platform_test.py": 0.4, - "tst/unit/cli/card_naming_relocation_test.py": 0.2, + "tst/unit/cli/build_platform_test.py": 0.3, + "tst/unit/cli/card_naming_relocation_test.py": 0.3, "tst/unit/cli/deploy_vm_sizing_test.py": 0.1, "tst/unit/cli/deployed_env_output_test.py": 0.1, "tst/unit/cli/env_provider_type_test.py": 0.2, - "tst/unit/cli/env_put_validation_test.py": 0.5, - "tst/unit/cli/env_state_type_parity_test.py": 0.1, + "tst/unit/cli/env_put_validation_test.py": 0.4, + "tst/unit/cli/env_state_type_parity_test.py": 0.0, "tst/unit/cli/environment_name_alias_test.py": 0.4, - "tst/unit/cli/eval_run_teardown_test.py": 0.2, - "tst/unit/cli/image_ref_put_test.py": 0.7, + "tst/unit/cli/eval_run_teardown_test.py": 0.3, + "tst/unit/cli/image_ref_put_test.py": 0.6, "tst/unit/cli/init_env_state_cli_test.py": 0.2, "tst/unit/cli/load_artifact_target_test.py": 0.1, - "tst/unit/cli/plugin_command_test.py": 41.2, - "tst/unit/cli/plugin_install_test.py": 9.3, - "tst/unit/cli/plugin_report_test.py": 21.3, - "tst/unit/cli/plugins_test.py": 10.8, + "tst/unit/cli/plugin_command_test.py": 24.6, + "tst/unit/cli/plugin_install_test.py": 4.2, + "tst/unit/cli/plugin_report_test.py": 17.6, + "tst/unit/cli/plugins_test.py": 12.0, "tst/unit/cli/run_role_models_test.py": 0.2, - "tst/unit/cli/sandbox_url_relocation_test.py": 3.1, + "tst/unit/cli/sandbox_url_relocation_test.py": 2.4, "tst/unit/cli/snapshot_step_output_test.py": 0.0, "tst/unit/cli/stage_free_commands_test.py": 0.0, "tst/unit/cli/submit_runs_test.py": 5.7, - "tst/unit/cli/task_run_requires_test.py": 3.5, - "tst/unit/cli/task_run_stored_test.py": 0.7, - "tst/unit/cli/task_run_teardown_test.py": 7.9, + "tst/unit/cli/task_run_requires_test.py": 1.8, + "tst/unit/cli/task_run_stored_test.py": 0.5, + "tst/unit/cli/task_run_teardown_test.py": 7.7, "tst/unit/cli/teardown_env_state_cli_test.py": 0.1, "tst/unit/cli/test_release_gate_decision.py": 0.1, - "tst/unit/cli/test_removed_cli_aliases.py": 0.3, + "tst/unit/cli/test_removed_cli_aliases.py": 0.4, "tst/unit/cli/test_spec_conformance_display.py": 0.1, "tst/unit/cli/test_task_create_preflight.py": 0.1, - "tst/unit/cli/user_facing_errors_test.py": 0.2, - "tst/unit/cli/verbose_logging_test.py": 2.7, - "tst/unit/cli/website_server_image_cli_test.py": 1.0, + "tst/unit/cli/user_facing_errors_test.py": 0.1, + "tst/unit/cli/verbose_logging_test.py": 2.8, + "tst/unit/cli/website_server_image_cli_test.py": 0.4, "tst/unit/cli/write_context_filename_test.py": 0.1, - "tst/unit/config/test_config_holds_its_document.py": 0.2, + "tst/unit/config/test_config_holds_its_document.py": 0.1, "tst/unit/config/test_config_profiles.py": 0.2, "tst/unit/config/test_default_a2a_agent.py": 0.2, - "tst/unit/config/test_default_env_ids.py": 0.3, - "tst/unit/config/test_describe.py": 2.6, - "tst/unit/config/test_explain_and_sources.py": 11.5, - "tst/unit/config/test_fixture_prefix_keys.py": 0.2, - "tst/unit/config/test_human_a2a_url.py": 0.4, - "tst/unit/config/test_loader.py": 1.0, + "tst/unit/config/test_default_env_ids.py": 0.2, + "tst/unit/config/test_describe.py": 1.1, + "tst/unit/config/test_explain_and_sources.py": 6.0, + "tst/unit/config/test_fixture_prefix_keys.py": 0.1, + "tst/unit/config/test_human_a2a_url.py": 0.5, + "tst/unit/config/test_loader.py": 1.1, "tst/unit/config/test_model.py": 0.8, "tst/unit/config/test_no_stage_concept.py": 0.1, - "tst/unit/config/test_one_document_per_process.py": 3.5, - "tst/unit/config/test_paths.py": 0.2, - "tst/unit/config/test_registry_builders_take_a_config.py": 0.4, - "tst/unit/config/test_reset_config.py": 1.3, + "tst/unit/config/test_one_document_per_process.py": 4.1, + "tst/unit/config/test_paths.py": 0.1, + "tst/unit/config/test_registry_builders_take_a_config.py": 0.1, + "tst/unit/config/test_reset_config.py": 1.1, "tst/unit/config/test_resolve_reference.py": 0.0, "tst/unit/config/test_run_env_ids.py": 0.1, - "tst/unit/config/test_unread_keys.py": 1.5, - "tst/unit/drivers/agent_desktop_driver_test.py": 15.2, - "tst/unit/drivers/codex_computer_use_driver_test.py": 15.4, - "tst/unit/drivers/cua_driver_test.py": 1.7, + "tst/unit/config/test_unread_keys.py": 1.4, + "tst/unit/drivers/agent_desktop_driver_test.py": 8.7, + "tst/unit/drivers/codex_computer_use_driver_test.py": 18.5, + "tst/unit/drivers/cua_driver_test.py": 1.0, "tst/unit/drivers/macos_harness_driver_test.py": 0.4, - "tst/unit/drivers/page_driver_test.py": 0.2, - "tst/unit/drivers/reference_driver_test.py": 4.1, + "tst/unit/drivers/page_driver_test.py": 0.1, + "tst/unit/drivers/reference_driver_test.py": 2.2, "tst/unit/entity_refs_import_test.py": 0.0, - "tst/unit/env/alias_get_test.py": 0.4, - "tst/unit/env/bootstrap_test.py": 8.2, + "tst/unit/env/alias_get_test.py": 0.3, + "tst/unit/env/bootstrap_test.py": 12.4, "tst/unit/env/envs/service_db/service_db_test.py": 0.3, "tst/unit/env/envs/test_artifact_staging.py": 0.3, "tst/unit/env/envs/test_chromium_app.py": 0.4, - "tst/unit/env/envs/test_computer_apps.py": 10.8, - "tst/unit/env/envs/test_computer_builder.py": 8.0, - "tst/unit/env/envs/test_computer_disk.py": 22.3, - "tst/unit/env/envs/test_computer_env.py": 59.4, - "tst/unit/env/envs/test_computer_macos_grants.py": 0.2, - "tst/unit/env/envs/test_computer_site_proxy.py": 4.1, - "tst/unit/env/envs/test_computer_sites.py": 3.0, - "tst/unit/env/envs/test_computer_vm_bundle.py": 8.0, - "tst/unit/env/envs/test_computer_windows.py": 3.4, - "tst/unit/env/envs/test_disk_recipe.py": 68.0, - "tst/unit/env/envs/test_env_card_loads.py": 0.3, + "tst/unit/env/envs/test_computer_apps.py": 4.9, + "tst/unit/env/envs/test_computer_builder.py": 4.0, + "tst/unit/env/envs/test_computer_disk.py": 18.3, + "tst/unit/env/envs/test_computer_env.py": 29.0, + "tst/unit/env/envs/test_computer_macos_grants.py": 0.1, + "tst/unit/env/envs/test_computer_site_proxy.py": 3.7, + "tst/unit/env/envs/test_computer_sites.py": 1.6, + "tst/unit/env/envs/test_computer_vm_bundle.py": 6.6, + "tst/unit/env/envs/test_computer_windows.py": 1.9, + "tst/unit/env/envs/test_disk_recipe.py": 7.8, + "tst/unit/env/envs/test_env_card_loads.py": 0.4, "tst/unit/env/envs/test_env_close_teardown.py": 0.1, "tst/unit/env/envs/test_environment_name_alias.py": 0.2, "tst/unit/env/envs/test_install_platform_keys.py": 0.0, - "tst/unit/env/envs/test_mcp_server_container_mode.py": 0.6, - "tst/unit/env/envs/test_mcp_server_load.py": 0.1, - "tst/unit/env/envs/test_mcp_server_plugin_provider.py": 0.5, - "tst/unit/env/envs/test_multi_env_computers.py": 1.3, + "tst/unit/env/envs/test_mcp_server_container_mode.py": 0.3, + "tst/unit/env/envs/test_mcp_server_load.py": 0.0, + "tst/unit/env/envs/test_mcp_server_plugin_provider.py": 0.3, + "tst/unit/env/envs/test_multi_env_computers.py": 0.9, "tst/unit/env/envs/test_multi_env_load_concurrency.py": 0.3, "tst/unit/env/envs/test_multi_env_load_resume.py": 0.2, - "tst/unit/env/envs/test_multi_env_name.py": 0.2, - "tst/unit/env/envs/test_multi_env_plugin_provider.py": 0.6, - "tst/unit/env/envs/test_multi_env_snapshot_bake.py": 0.2, + "tst/unit/env/envs/test_multi_env_name.py": 0.1, + "tst/unit/env/envs/test_multi_env_plugin_provider.py": 0.5, + "tst/unit/env/envs/test_multi_env_snapshot_bake.py": 0.1, "tst/unit/env/envs/test_multi_env_snapshot_guard.py": 0.1, - "tst/unit/env/envs/test_multi_env_snapshot_reload_name.py": 0.5, - "tst/unit/env/envs/test_multi_env_vm_sizing.py": 0.3, + "tst/unit/env/envs/test_multi_env_snapshot_reload_name.py": 0.6, + "tst/unit/env/envs/test_multi_env_vm_sizing.py": 0.2, "tst/unit/env/envs/test_playwright_mcp_recorder.py": 2.9, - "tst/unit/env/envs/test_put_from_github_token.py": 0.1, + "tst/unit/env/envs/test_put_from_github_token.py": 0.0, "tst/unit/env/envs/test_server_reset.py": 0.1, - "tst/unit/env/envs/test_website_env_name_alias.py": 0.1, - "tst/unit/env/envs/test_website_server_image.py": 0.7, - "tst/unit/env/envs/test_website_site_names.py": 1.9, - "tst/unit/env/gateway/browser_route_test.py": 3.6, - "tst/unit/env/gateway/caller_test.py": 0.1, + "tst/unit/env/envs/test_website_env_name_alias.py": 0.0, + "tst/unit/env/envs/test_website_server_image.py": 0.8, + "tst/unit/env/envs/test_website_site_names.py": 1.0, + "tst/unit/env/gateway/browser_route_test.py": 3.1, + "tst/unit/env/gateway/caller_test.py": 0.0, "tst/unit/env/gateway/changes_test.py": 0.4, "tst/unit/env/gateway/child_outage_socket_test.py": 9.3, "tst/unit/env/gateway/child_sessions_test.py": 0.5, @@ -1304,329 +1305,330 @@ "tst/unit/env/gateway/clock_advance_test.py": 0.3, "tst/unit/env/gateway/data_plane_timeout_test.py": 0.1, "tst/unit/env/gateway/env_trajectory_route_test.py": 0.1, - "tst/unit/env/gateway/gateway_card_test.py": 0.6, - "tst/unit/env/gateway/get_time_test.py": 0.5, - "tst/unit/env/gateway/hidden_by_default_test.py": 15.7, - "tst/unit/env/gateway/import_graph_test.py": 3.6, + "tst/unit/env/gateway/gateway_card_test.py": 0.5, + "tst/unit/env/gateway/get_time_test.py": 0.4, + "tst/unit/env/gateway/hidden_by_default_test.py": 13.5, + "tst/unit/env/gateway/import_graph_test.py": 2.1, "tst/unit/env/gateway/next_event_test.py": 0.4, "tst/unit/env/gateway/notifications_test.py": 1.6, "tst/unit/env/gateway/process_scope_socket_test.py": 1.0, - "tst/unit/env/gateway/rest_proxy_socket_test.py": 1.8, - "tst/unit/env/gateway/rest_proxy_stream_test.py": 0.6, + "tst/unit/env/gateway/rest_proxy_socket_test.py": 1.6, + "tst/unit/env/gateway/rest_proxy_stream_test.py": 0.7, "tst/unit/env/gateway/role_forwarding_test.py": 0.1, "tst/unit/env/gateway/role_rules_test.py": 0.3, - "tst/unit/env/gateway/tool_access_parity_test.py": 16.7, + "tst/unit/env/gateway/tool_access_parity_test.py": 12.9, "tst/unit/env/gateway/tool_name_clash_test.py": 0.1, "tst/unit/env/gateway/trajectory_virtual_time_test.py": 0.1, - "tst/unit/env/gateway/triggers_ctx_test.py": 2.5, - "tst/unit/env/gateway/triggers_test.py": 13.7, + "tst/unit/env/gateway/triggers_ctx_test.py": 2.4, + "tst/unit/env/gateway/triggers_test.py": 13.3, "tst/unit/env/gateway/triggers_time_test.py": 1.8, - "tst/unit/env/gateway/triggers_website_test.py": 1.2, - "tst/unit/env/gateway/website_mcp_test.py": 1.9, - "tst/unit/env/gateway/website_reserved_paths_test.py": 0.3, - "tst/unit/env/gateway/website_websocket_test.py": 4.8, - "tst/unit/env/published_images_test.py": 0.5, - "tst/unit/env/store_test.py": 0.2, + "tst/unit/env/gateway/triggers_website_test.py": 1.1, + "tst/unit/env/gateway/website_mcp_test.py": 2.1, + "tst/unit/env/gateway/website_reserved_paths_test.py": 0.2, + "tst/unit/env/gateway/website_websocket_test.py": 4.2, + "tst/unit/env/published_images_test.py": 0.4, + "tst/unit/env/store_test.py": 0.1, "tst/unit/env/task_step/deploy_agent_env_vars_test.py": 0.1, - "tst/unit/env/task_step/test_context_ops.py": 0.5, + "tst/unit/env/task_step/test_context_ops.py": 0.4, "tst/unit/env/task_step/test_score_aggregator.py": 0.2, - "tst/unit/env/task_step/verify_env_card_test.py": 0.2, + "tst/unit/env/task_step/verify_env_card_test.py": 0.1, "tst/unit/env/task_step/verify_env_core_protocol_test.py": 0.2, - "tst/unit/env/test_env.py": 0.9, + "tst/unit/env/test_env.py": 0.4, "tst/unit/env/test_legacy_protocol.py": 0.3, - "tst/unit/env/test_registry.py": 0.7, - "tst/unit/env/test_snapshot_fingerprint.py": 0.1, + "tst/unit/env/test_registry.py": 0.3, + "tst/unit/env/test_snapshot_fingerprint.py": 0.2, "tst/unit/explorer/entity_ids_test.py": 0.1, - "tst/unit/explorer/explorer_app_test.py": 20.8, - "tst/unit/openenv/app_test.py": 19.2, - "tst/unit/openenv/environment_test.py": 13.5, + "tst/unit/explorer/explorer_app_test.py": 21.0, + "tst/unit/openenv/app_test.py": 7.6, + "tst/unit/openenv/environment_test.py": 14.3, "tst/unit/plugin_inventory_test.py": 1.3, - "tst/unit/plugin_requirements_test.py": 0.3, - "tst/unit/plugin_settings_test.py": 11.8, - "tst/unit/plugins_test.py": 2.3, - "tst/unit/providers/compose_host_port_publish_test.py": 1.0, - "tst/unit/providers/env_providers/env_gateway_provider_test.py": 7.3, - "tst/unit/providers/env_providers/env_provider_test.py": 0.2, - "tst/unit/providers/env_providers/env_server_provider_test.py": 0.6, + "tst/unit/plugin_requirements_test.py": 0.1, + "tst/unit/plugin_settings_test.py": 12.3, + "tst/unit/plugins_test.py": 0.8, + "tst/unit/providers/compose_host_port_publish_test.py": 0.9, + "tst/unit/providers/env_providers/env_gateway_provider_test.py": 6.5, + "tst/unit/providers/env_providers/env_provider_test.py": 0.1, + "tst/unit/providers/env_providers/env_server_provider_test.py": 0.3, "tst/unit/providers/env_state/test_config_state_providers.py": 0.9, - "tst/unit/providers/env_state/test_env_state_provider.py": 0.5, - "tst/unit/providers/sandbox_providers/agent37_sandbox_provider_test.py": 0.6, - "tst/unit/providers/sandbox_providers/agent37_sandbox_test.py": 1.2, - "tst/unit/providers/sandbox_providers/attribution_wire_test.py": 0.4, - "tst/unit/providers/sandbox_providers/boat_sandbox_provider_test.py": 0.6, - "tst/unit/providers/sandbox_providers/boat_sandbox_test.py": 0.5, - "tst/unit/providers/sandbox_providers/boxd_sandbox_provider_test.py": 1.7, - "tst/unit/providers/sandbox_providers/boxd_sandbox_test.py": 0.5, - "tst/unit/providers/sandbox_providers/coreweave_deploy_paths_test.py": 0.2, - "tst/unit/providers/sandbox_providers/coreweave_sandbox_provider_test.py": 1.8, - "tst/unit/providers/sandbox_providers/coreweave_sandbox_test.py": 0.3, - "tst/unit/providers/sandbox_providers/daytona_network_test.py": 0.2, - "tst/unit/providers/sandbox_providers/daytona_sandbox_provider_test.py": 2.0, + "tst/unit/providers/env_state/test_env_state_provider.py": 0.2, + "tst/unit/providers/sandbox_providers/agent37_sandbox_provider_test.py": 0.4, + "tst/unit/providers/sandbox_providers/agent37_sandbox_test.py": 1.1, + "tst/unit/providers/sandbox_providers/attribution_wire_test.py": 0.1, + "tst/unit/providers/sandbox_providers/boat_sandbox_provider_test.py": 0.2, + "tst/unit/providers/sandbox_providers/boat_sandbox_test.py": 0.4, + "tst/unit/providers/sandbox_providers/boxd_sandbox_provider_test.py": 1.5, + "tst/unit/providers/sandbox_providers/boxd_sandbox_test.py": 0.2, + "tst/unit/providers/sandbox_providers/coreweave_deploy_paths_test.py": 0.4, + "tst/unit/providers/sandbox_providers/coreweave_sandbox_provider_test.py": 1.3, + "tst/unit/providers/sandbox_providers/coreweave_sandbox_test.py": 1.0, + "tst/unit/providers/sandbox_providers/daytona_network_test.py": 0.4, + "tst/unit/providers/sandbox_providers/daytona_sandbox_provider_test.py": 1.7, "tst/unit/providers/sandbox_providers/daytona_sandbox_test.py": 1.2, "tst/unit/providers/sandbox_providers/e2b_sandbox_provider_test.py": 0.2, "tst/unit/providers/sandbox_providers/e2b_sandbox_test.py": 1.2, - "tst/unit/providers/sandbox_providers/e2b_template_test.py": 0.6, - "tst/unit/providers/sandbox_providers/egress_hosts_test.py": 0.4, - "tst/unit/providers/sandbox_providers/host_images_test.py": 0.9, - "tst/unit/providers/sandbox_providers/local_sandbox_host_ports_test.py": 0.5, - "tst/unit/providers/sandbox_providers/local_sandbox_test.py": 12.6, - "tst/unit/providers/sandbox_providers/lume_sandbox_provider_test.py": 10.7, + "tst/unit/providers/sandbox_providers/e2b_template_test.py": 0.3, + "tst/unit/providers/sandbox_providers/egress_hosts_test.py": 0.2, + "tst/unit/providers/sandbox_providers/host_images_test.py": 0.7, + "tst/unit/providers/sandbox_providers/local_sandbox_host_ports_test.py": 0.3, + "tst/unit/providers/sandbox_providers/local_sandbox_test.py": 11.8, + "tst/unit/providers/sandbox_providers/lume_sandbox_provider_test.py": 10.4, "tst/unit/providers/sandbox_providers/modal_context_build_test.py": 0.8, - "tst/unit/providers/sandbox_providers/modal_create_signature_test.py": 0.2, - "tst/unit/providers/sandbox_providers/modal_gpu_sandbox_test.py": 0.0, - "tst/unit/providers/sandbox_providers/modal_network_policy_test.py": 0.2, + "tst/unit/providers/sandbox_providers/modal_create_signature_test.py": 0.1, + "tst/unit/providers/sandbox_providers/modal_gpu_sandbox_test.py": 0.1, + "tst/unit/providers/sandbox_providers/modal_network_policy_test.py": 0.0, "tst/unit/providers/sandbox_providers/modal_network_policy_wiring_test.py": 0.1, - "tst/unit/providers/sandbox_providers/modal_sandbox_started_log_test.py": 0.1, + "tst/unit/providers/sandbox_providers/modal_sandbox_started_log_test.py": 0.0, "tst/unit/providers/sandbox_providers/modal_sandbox_tags_test.py": 0.1, - "tst/unit/providers/sandbox_providers/modal_sandbox_test.py": 0.7, - "tst/unit/providers/sandbox_providers/modal_vm_sandbox_test.py": 1.2, - "tst/unit/providers/sandbox_providers/network_policy_providers_test.py": 0.4, + "tst/unit/providers/sandbox_providers/modal_sandbox_test.py": 0.4, + "tst/unit/providers/sandbox_providers/modal_vm_sandbox_test.py": 1.8, + "tst/unit/providers/sandbox_providers/network_policy_providers_test.py": 0.2, "tst/unit/providers/sandbox_providers/network_policy_stamping_test.py": 0.1, - "tst/unit/providers/sandbox_providers/network_policy_test.py": 0.2, - "tst/unit/providers/sandbox_providers/provider_capabilities_test.py": 3.2, + "tst/unit/providers/sandbox_providers/network_policy_test.py": 0.1, + "tst/unit/providers/sandbox_providers/provider_capabilities_test.py": 2.7, "tst/unit/providers/sandbox_providers/relay_test.py": 0.1, - "tst/unit/providers/sandbox_providers/sail_vm_model_key_test.py": 2.1, - "tst/unit/providers/sandbox_providers/sail_vm_sandbox_provider_test.py": 2.0, - "tst/unit/providers/sandbox_providers/sail_vm_sandbox_test.py": 0.3, - "tst/unit/providers/sandbox_providers/sandbox_provider_test.py": 0.7, + "tst/unit/providers/sandbox_providers/sail_vm_model_key_test.py": 1.2, + "tst/unit/providers/sandbox_providers/sail_vm_sandbox_provider_test.py": 1.4, + "tst/unit/providers/sandbox_providers/sail_vm_sandbox_test.py": 0.2, + "tst/unit/providers/sandbox_providers/sandbox_provider_test.py": 0.4, "tst/unit/providers/sandbox_providers/tenki_sandbox_provider_test.py": 0.9, - "tst/unit/providers/sandbox_providers/tenki_sandbox_test.py": 0.3, + "tst/unit/providers/sandbox_providers/tenki_sandbox_test.py": 0.2, "tst/unit/providers/sandbox_providers/tensorlake_sandbox_provider_test.py": 0.4, - "tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py": 5.4, - "tst/unit/providers/sandbox_providers/test_config_sandbox_providers.py": 1.5, - "tst/unit/providers/sandbox_providers/test_reachability_contributions.py": 0.5, - "tst/unit/providers/sandbox_providers/test_url_rewrites.py": 0.7, - "tst/unit/providers/sandbox_providers/vercel_sandbox_provider_test.py": 0.4, - "tst/unit/providers/sandbox_providers/vercel_sandbox_test.py": 3.8, + "tst/unit/providers/sandbox_providers/tensorlake_sandbox_test.py": 4.8, + "tst/unit/providers/sandbox_providers/test_config_sandbox_providers.py": 0.6, + "tst/unit/providers/sandbox_providers/test_reachability_contributions.py": 0.2, + "tst/unit/providers/sandbox_providers/test_url_rewrites.py": 0.8, + "tst/unit/providers/sandbox_providers/vercel_sandbox_provider_test.py": 0.2, + "tst/unit/providers/sandbox_providers/vercel_sandbox_test.py": 3.5, "tst/unit/providers/sandbox_providers/vm_file_transfer_test.py": 0.2, - "tst/unit/providers/sandbox_providers/vm_object_push_test.py": 0.6, - "tst/unit/providers/sandbox_providers/vm_sandbox_test.py": 0.7, + "tst/unit/providers/sandbox_providers/vm_object_push_test.py": 0.3, + "tst/unit/providers/sandbox_providers/vm_sandbox_test.py": 0.6, "tst/unit/runner/local_runner_test.py": 7.6, "tst/unit/runner/run_store_test.py": 0.1, "tst/unit/serialization/test_golden_wire_format.py": 0.4, "tst/unit/store/awaiting_lock_test.py": 1.0, - "tst/unit/store/aws/aws_secret_store_test.py": 0.3, - "tst/unit/store/aws/dynamodb_document_store_test.py": 14.1, - "tst/unit/store/aws/dynamodb_step_journal_test.py": 13.7, - "tst/unit/store/aws/ecr_image_store_test.py": 0.1, - "tst/unit/store/aws/s3_grant_lifetime_test.py": 0.1, - "tst/unit/store/aws/s3_object_store_test.py": 6.5, + "tst/unit/store/aws/aws_secret_store_test.py": 0.1, + "tst/unit/store/aws/dynamodb_document_store_test.py": 10.9, + "tst/unit/store/aws/dynamodb_step_journal_test.py": 11.3, + "tst/unit/store/aws/ecr_image_store_test.py": 0.0, + "tst/unit/store/aws/s3_grant_lifetime_test.py": 0.0, + "tst/unit/store/aws/s3_object_store_test.py": 5.5, "tst/unit/store/aws/s3_transfer_grants_test.py": 0.2, "tst/unit/store/aws/test_secret_logging.py": 0.1, "tst/unit/store/aws/test_shared_credentials.py": 0.2, - "tst/unit/store/derived_ids_test.py": 9.1, + "tst/unit/store/derived_ids_test.py": 9.0, "tst/unit/store/fake_object_store_test.py": 0.1, - "tst/unit/store/firestore_mongo_document_store_test.py": 0.3, - "tst/unit/store/gcp_secret_manager_secret_store_test.py": 0.1, - "tst/unit/store/gcs_object_store_test.py": 0.9, - "tst/unit/store/google_credentials_test.py": 0.8, - "tst/unit/store/google_transport_test.py": 0.2, + "tst/unit/store/firestore_mongo_document_store_test.py": 0.4, + "tst/unit/store/gcp_secret_manager_secret_store_test.py": 0.2, + "tst/unit/store/gcs_object_store_test.py": 0.4, + "tst/unit/store/google_credentials_test.py": 0.7, + "tst/unit/store/google_transport_test.py": 0.1, "tst/unit/store/grant_lifetime_test.py": 0.1, - "tst/unit/store/ids_test.py": 1.1, - "tst/unit/store/local_backends_composition_test.py": 1.2, - "tst/unit/store/local_grant_server_test.py": 10.9, - "tst/unit/store/local_id_acceptance_test.py": 2.0, - "tst/unit/store/local_object_store_test.py": 1.7, + "tst/unit/store/ids_test.py": 0.5, + "tst/unit/store/local_backends_composition_test.py": 0.8, + "tst/unit/store/local_grant_server_test.py": 9.7, + "tst/unit/store/local_id_acceptance_test.py": 1.7, + "tst/unit/store/local_object_store_test.py": 1.5, "tst/unit/store/local_registry_image_store_test.py": 0.1, "tst/unit/store/local_secret_store_test.py": 0.1, - "tst/unit/store/local_tls_test.py": 0.5, + "tst/unit/store/local_tls_test.py": 0.3, "tst/unit/store/local_tokens_test.py": 0.2, "tst/unit/store/mongo_document_store_test.py": 0.8, - "tst/unit/store/oci_registry_credentials_test.py": 0.2, - "tst/unit/store/oci_registry_image_store_test.py": 0.4, - "tst/unit/store/readable_url_test.py": 0.3, - "tst/unit/store/registry_api_test.py": 0.5, - "tst/unit/store/routing_test.py": 3.4, - "tst/unit/store/sqlite_document_store_test.py": 11.3, - "tst/unit/store/test_config_document_store.py": 0.3, - "tst/unit/store/test_config_image_store.py": 0.3, + "tst/unit/store/oci_registry_credentials_test.py": 0.1, + "tst/unit/store/oci_registry_image_store_test.py": 0.3, + "tst/unit/store/readable_url_test.py": 0.2, + "tst/unit/store/registry_api_test.py": 0.3, + "tst/unit/store/routing_test.py": 3.8, + "tst/unit/store/sqlite_document_store_test.py": 7.8, + "tst/unit/store/test_config_document_store.py": 0.2, + "tst/unit/store/test_config_image_store.py": 0.2, "tst/unit/store/test_config_model.py": 0.6, "tst/unit/store/test_config_mongo_client.py": 0.1, "tst/unit/store/test_config_object_store.py": 0.2, "tst/unit/store/test_config_secret_bundle_keys.py": 0.0, - "tst/unit/store/test_config_secret_store.py": 0.2, - "tst/unit/store/test_document_store.py": 0.7, - "tst/unit/store/test_object_transfer_grants.py": 0.5, - "tst/unit/store/test_task_instance_indexes.py": 0.1, - "tst/unit/store/version_lookup_benchmark_test.py": 1.5, + "tst/unit/store/test_config_secret_store.py": 0.1, + "tst/unit/store/test_document_store.py": 0.4, + "tst/unit/store/test_object_transfer_grants.py": 0.3, + "tst/unit/store/test_task_instance_indexes.py": 0.0, + "tst/unit/store/version_lookup_benchmark_test.py": 1.2, "tst/unit/task/batch_universe_ids_test.py": 0.0, "tst/unit/task/benchmark_test.py": 0.0, "tst/unit/task/control_grant_test.py": 0.1, "tst/unit/task/depends_on_test.py": 0.9, "tst/unit/task/episode/proxy_test.py": 0.3, - "tst/unit/task/episode/run_test.py": 20.1, + "tst/unit/task/episode/run_test.py": 19.2, "tst/unit/task/episode/spec_test.py": 2.5, - "tst/unit/task/local_run_write_fence_test.py": 2.9, + "tst/unit/task/local_run_write_fence_test.py": 1.9, "tst/unit/task/retry_sandbox_teardown_test.py": 0.1, "tst/unit/task/seed_context_fence_test.py": 0.1, - "tst/unit/task/seed_substitution_test.py": 0.1, + "tst/unit/task/seed_substitution_test.py": 0.0, "tst/unit/task/step_journal_rules_test.py": 0.2, "tst/unit/task/step_journal_test.py": 1.7, "tst/unit/task/task_document_test.py": 0.4, - "tst/unit/task/task_failure_write_retry_test.py": 0.2, + "tst/unit/task/task_failure_write_retry_test.py": 0.1, "tst/unit/task/task_instance_reads_test.py": 0.2, - "tst/unit/task/teardown_test.py": 2.7, - "tst/unit/task_step/add_skills_env_test.py": 0.2, - "tst/unit/task_step/attribution_serialization_test.py": 0.2, - "tst/unit/task_step/computer_recording_test.py": 1.1, + "tst/unit/task/teardown_test.py": 1.6, + "tst/unit/task_step/add_skills_env_test.py": 0.1, + "tst/unit/task_step/attribution_serialization_test.py": 0.3, + "tst/unit/task_step/computer_recording_test.py": 1.3, "tst/unit/task_step/context_ops_journal_test.py": 0.1, "tst/unit/task_step/deploy_agent_cleanup_test.py": 0.0, - "tst/unit/task_step/deploy_agent_linked_sandbox_test.py": 0.0, + "tst/unit/task_step/deploy_agent_linked_sandbox_test.py": 0.1, "tst/unit/task_step/deploy_agent_test.py": 0.1, - "tst/unit/task_step/deploy_env_test.py": 1.4, - "tst/unit/task_step/deploy_human_agent_test.py": 0.1, + "tst/unit/task_step/deploy_env_test.py": 0.6, + "tst/unit/task_step/deploy_human_agent_test.py": 0.2, "tst/unit/task_step/deploy_sandbox_default_cpu_test.py": 0.1, "tst/unit/task_step/deploy_sandbox_image_test.py": 0.1, "tst/unit/task_step/deploy_sandbox_network_policy_test.py": 0.1, "tst/unit/task_step/deploy_sandbox_provider_override_test.py": 0.1, - "tst/unit/task_step/install_agent_guards_test.py": 0.4, - "tst/unit/task_step/install_agent_test.py": 0.5, - "tst/unit/task_step/local_container_urls_test.py": 1.8, - "tst/unit/task_step/local_step_containers_test.py": 0.3, - "tst/unit/task_step/prompt_agent_file_parts_test.py": 0.3, - "tst/unit/task_step/retry_test.py": 1.8, - "tst/unit/task_step/run_context_object_url_test.py": 0.3, - "tst/unit/task_step/run_docker_container_test.py": 0.2, - "tst/unit/task_step/skill_object_url_test.py": 0.2, - "tst/unit/task_step/snapshot_computer_env_test.py": 0.6, - "tst/unit/task_step/snapshot_env_upload_grant_test.py": 1.0, + "tst/unit/task_step/install_agent_guards_test.py": 0.2, + "tst/unit/task_step/install_agent_test.py": 0.3, + "tst/unit/task_step/local_container_urls_test.py": 2.1, + "tst/unit/task_step/local_step_containers_test.py": 0.2, + "tst/unit/task_step/prompt_agent_file_parts_test.py": 0.5, + "tst/unit/task_step/retry_test.py": 1.3, + "tst/unit/task_step/run_context_object_url_test.py": 0.2, + "tst/unit/task_step/run_docker_container_test.py": 0.3, + "tst/unit/task_step/skill_object_url_test.py": 0.3, + "tst/unit/task_step/snapshot_computer_env_test.py": 0.4, + "tst/unit/task_step/snapshot_env_upload_grant_test.py": 1.2, "tst/unit/task_step/snapshot_env_without_gateway_test.py": 0.6, "tst/unit/task_step/stored_priority_test.py": 0.1, "tst/unit/task_step/stored_s3_form_fields_test.py": 0.1, - "tst/unit/task_step/sync_env_clock_test.py": 0.6, - "tst/unit/task_step/test_advance_to_next_event_hook.py": 1.1, + "tst/unit/task_step/sync_env_clock_test.py": 0.5, + "tst/unit/task_step/test_advance_to_next_event_hook.py": 1.2, "tst/unit/task_step/test_agent_model_precedence.py": 0.1, - "tst/unit/task_step/test_agent_state_capture.py": 1.2, - "tst/unit/task_step/test_apply_server_config.py": 1.0, + "tst/unit/task_step/test_agent_state_capture.py": 1.3, + "tst/unit/task_step/test_apply_server_config.py": 0.8, "tst/unit/task_step/test_browser_connect_cdp.py": 0.2, - "tst/unit/task_step/test_browser_steps.py": 1.0, - "tst/unit/task_step/test_collect_artifacts.py": 1.1, - "tst/unit/task_step/test_collect_artifacts_universe_suffix.py": 0.2, + "tst/unit/task_step/test_browser_sign_in.py": 0.2, + "tst/unit/task_step/test_browser_steps.py": 1.1, + "tst/unit/task_step/test_collect_artifacts.py": 1.2, + "tst/unit/task_step/test_collect_artifacts_universe_suffix.py": 0.3, "tst/unit/task_step/test_collect_artifacts_vm_host.py": 0.3, "tst/unit/task_step/test_collect_integrity.py": 0.1, "tst/unit/task_step/test_combine_universe_verdicts.py": 0.1, - "tst/unit/task_step/test_config_task_steps.py": 0.7, - "tst/unit/task_step/test_configure_scheduled_checks.py": 0.2, + "tst/unit/task_step/test_config_task_steps.py": 0.4, + "tst/unit/task_step/test_configure_scheduled_checks.py": 0.1, "tst/unit/task_step/test_container_sandbox_routing.py": 0.3, - "tst/unit/task_step/test_deploy_agent_changelog_objects.py": 1.0, + "tst/unit/task_step/test_deploy_agent_changelog_objects.py": 0.5, "tst/unit/task_step/test_deploy_agent_enable_docker.py": 0.1, - "tst/unit/task_step/test_deploy_agent_live_endpoint.py": 0.1, + "tst/unit/task_step/test_deploy_agent_live_endpoint.py": 0.0, "tst/unit/task_step/test_deploy_agent_mcp_alias.py": 0.2, - "tst/unit/task_step/test_deploy_agent_role_delivery.py": 1.2, - "tst/unit/task_step/test_deploy_agent_snapshot_objects.py": 0.5, + "tst/unit/task_step/test_deploy_agent_role_delivery.py": 0.5, + "tst/unit/task_step/test_deploy_agent_snapshot_objects.py": 0.2, "tst/unit/task_step/test_deploy_ttl_from_budget.py": 0.0, - "tst/unit/task_step/test_entity_refs.py": 1.0, - "tst/unit/task_step/test_field_scopes.py": 0.7, + "tst/unit/task_step/test_entity_refs.py": 3.2, + "tst/unit/task_step/test_field_scopes.py": 0.9, "tst/unit/task_step/test_install_agent_registers_instance.py": 0.1, - "tst/unit/task_step/test_load_artifact_agent_container_target.py": 0.1, + "tst/unit/task_step/test_load_artifact_agent_container_target.py": 0.0, "tst/unit/task_step/test_load_artifact_container_sandbox.py": 0.1, "tst/unit/task_step/test_load_artifact_env_binding.py": 0.1, "tst/unit/task_step/test_load_artifact_file.py": 0.1, "tst/unit/task_step/test_load_artifact_overrides.py": 0.1, - "tst/unit/task_step/test_load_artifact_service_payload.py": 0.5, + "tst/unit/task_step/test_load_artifact_service_payload.py": 0.6, "tst/unit/task_step/test_load_artifact_service_universe.py": 0.1, - "tst/unit/task_step/test_load_artifact_snapshot_reporting.py": 0.1, + "tst/unit/task_step/test_load_artifact_snapshot_reporting.py": 0.0, "tst/unit/task_step/test_load_artifact_url_filenames.py": 0.1, - "tst/unit/task_step/test_load_artifact_vm_target.py": 0.2, - "tst/unit/task_step/test_manifest_cli_builder.py": 15.0, - "tst/unit/task_step/test_mcp_cli_builder.py": 1.7, + "tst/unit/task_step/test_load_artifact_vm_target.py": 0.1, + "tst/unit/task_step/test_manifest_cli_builder.py": 14.7, + "tst/unit/task_step/test_mcp_cli_builder.py": 1.1, "tst/unit/task_step/test_modify_env_tool_access.py": 0.0, - "tst/unit/task_step/test_next_env_event_hook.py": 1.8, - "tst/unit/task_step/test_parallel_deploy_env_binding.py": 0.1, + "tst/unit/task_step/test_next_env_event_hook.py": 2.1, + "tst/unit/task_step/test_parallel_deploy_env_binding.py": 0.0, "tst/unit/task_step/test_preflight_pinned_versions.py": 0.1, - "tst/unit/task_step/test_prompt_agent.py": 0.2, + "tst/unit/task_step/test_prompt_agent.py": 0.1, "tst/unit/task_step/test_prompt_agent_builtin_tools.py": 0.0, "tst/unit/task_step/test_prompt_agent_conversation_tool_calls.py": 0.0, "tst/unit/task_step/test_prompt_agent_human_default.py": 0.0, - "tst/unit/task_step/test_prompt_agent_live_trajectory.py": 0.8, + "tst/unit/task_step/test_prompt_agent_live_trajectory.py": 0.7, "tst/unit/task_step/test_prompt_agent_model_params.py": 0.0, - "tst/unit/task_step/test_prompt_agent_notifications.py": 0.6, - "tst/unit/task_step/test_prompt_agent_post_turn_hooks.py": 1.9, - "tst/unit/task_step/test_prompt_agent_snapshots.py": 2.8, - "tst/unit/task_step/test_prompt_agent_triggers.py": 1.1, + "tst/unit/task_step/test_prompt_agent_notifications.py": 0.4, + "tst/unit/task_step/test_prompt_agent_post_turn_hooks.py": 1.2, + "tst/unit/task_step/test_prompt_agent_snapshots.py": 2.4, + "tst/unit/task_step/test_prompt_agent_triggers.py": 0.8, "tst/unit/task_step/test_prompt_agent_unreachable.py": 0.0, "tst/unit/task_step/test_prompt_agent_user_model.py": 0.0, - "tst/unit/task_step/test_prompt_agent_user_side.py": 0.4, + "tst/unit/task_step/test_prompt_agent_user_side.py": 0.5, "tst/unit/task_step/test_recorded_models.py": 0.1, - "tst/unit/task_step/test_register_agent_triggers.py": 0.2, - "tst/unit/task_step/test_register_env_triggers.py": 7.5, + "tst/unit/task_step/test_register_agent_triggers.py": 0.1, + "tst/unit/task_step/test_register_env_triggers.py": 7.3, "tst/unit/task_step/test_reset_env.py": 0.1, "tst/unit/task_step/test_review.py": 0.2, - "tst/unit/task_step/test_run_code.py": 18.3, + "tst/unit/task_step/test_run_code.py": 16.5, "tst/unit/task_step/test_sandbox_utils_agent_container.py": 0.0, "tst/unit/task_step/test_sandbox_utils_logs.py": 0.1, - "tst/unit/task_step/test_snapshot_agent_state.py": 0.2, + "tst/unit/task_step/test_snapshot_agent_state.py": 0.1, "tst/unit/task_step/test_snapshot_env.py": 1.1, - "tst/unit/task_step/test_snapshot_env_env_binding.py": 0.1, + "tst/unit/task_step/test_snapshot_env_env_binding.py": 0.2, "tst/unit/task_step/test_step_overrides_agent_and_model.py": 0.2, - "tst/unit/task_step/test_teardown_sandboxes.py": 0.4, + "tst/unit/task_step/test_teardown_sandboxes.py": 0.2, "tst/unit/task_step/test_universe_comparison.py": 0.1, "tst/unit/task_step/test_validation_gate_aggregator.py": 0.1, "tst/unit/task_step/test_verify_a2a_agent_mcp.py": 0.0, - "tst/unit/task_step/test_verify_a2a_builtin_tools.py": 0.1, + "tst/unit/task_step/test_verify_a2a_builtin_tools.py": 0.0, "tst/unit/task_step/test_verify_spec_conformance.py": 0.2, - "tst/unit/task_step/test_verify_universe_agent_judge.py": 0.1, + "tst/unit/task_step/test_verify_universe_agent_judge.py": 0.2, "tst/unit/task_step/test_verify_universe_roundtrip.py": 0.2, "tst/unit/task_step/thread_work_test.py": 0.6, - "tst/unit/task_step/unwritten_local_agent_test.py": 0.9, - "tst/unit/task_step/verifiers/judge_short_keys_test.py": 0.1, - "tst/unit/task_step/verifiers/judge_utils/test_frame_selection.py": 0.2, + "tst/unit/task_step/unwritten_local_agent_test.py": 0.3, + "tst/unit/task_step/verifiers/judge_short_keys_test.py": 0.0, + "tst/unit/task_step/verifiers/judge_utils/test_frame_selection.py": 0.1, "tst/unit/task_step/verifiers/judge_utils/test_frame_selection_tool_name.py": 0.0, - "tst/unit/task_step/verifiers/judge_utils/test_judge_output_format.py": 0.2, + "tst/unit/task_step/verifiers/judge_utils/test_judge_output_format.py": 0.3, "tst/unit/task_step/verifiers/judge_utils/test_trajectory_filter.py": 0.0, - "tst/unit/task_step/verifiers/run_container_unit_tests_verifier_test.py": 0.9, + "tst/unit/task_step/verifiers/run_container_unit_tests_verifier_test.py": 1.2, "tst/unit/task_step/verifiers/test_env_outcome_verifier_step.py": 0.1, - "tst/unit/task_step/verifiers/test_rubrics_verifier_criteria_override.py": 0.1, - "tst/unit/task_step/verifiers/test_rubrics_verifier_grade_up_to_turn.py": 0.2, - "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_batches.py": 0.2, + "tst/unit/task_step/verifiers/test_rubrics_verifier_criteria_override.py": 0.0, + "tst/unit/task_step/verifiers/test_rubrics_verifier_grade_up_to_turn.py": 0.1, + "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_batches.py": 0.3, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_model.py": 0.0, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_retry.py": 0.1, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_teardown.py": 0.0, "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_timeout.py": 0.0, - "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_trajectory.py": 0.5, + "tst/unit/task_step/verifiers/test_rubrics_verifier_judge_trajectory.py": 0.3, "tst/unit/task_step/verifiers/test_rubrics_verifier_mistakes_entry.py": 0.2, - "tst/unit/task_step/verifiers/test_rubrics_verifier_multiturn.py": 0.7, - "tst/unit/task_step/verifiers/test_rubrics_verifier_nonfinite_scores.py": 3.1, - "tst/unit/task_step/verifiers/test_rubrics_verifier_per_criterion.py": 0.4, - "tst/unit/task_step/verifiers/test_rubrics_verifier_screenshots.py": 4.4, - "tst/unit/task_step/verifiers/test_run_context.py": 1.6, + "tst/unit/task_step/verifiers/test_rubrics_verifier_multiturn.py": 0.9, + "tst/unit/task_step/verifiers/test_rubrics_verifier_nonfinite_scores.py": 2.9, + "tst/unit/task_step/verifiers/test_rubrics_verifier_per_criterion.py": 0.5, + "tst/unit/task_step/verifiers/test_rubrics_verifier_screenshots.py": 4.6, + "tst/unit/task_step/verifiers/test_run_context.py": 1.4, "tst/unit/task_step/verifiers/test_score_parity.py": 0.0, "tst/unit/task_step/verifiers/test_trajectory_mistakes_format.py": 0.2, - "tst/unit/task_step/verifiers/test_verify_sandbox.py": 0.2, - "tst/unit/test_capabilities.py": 0.3, - "tst/unit/test_check_plugin_api.py": 21.1, + "tst/unit/task_step/verifiers/test_verify_sandbox.py": 0.1, + "tst/unit/test_capabilities.py": 0.2, + "tst/unit/test_check_plugin_api.py": 10.5, "tst/unit/test_check_skip_policy.py": 0.3, "tst/unit/test_clean_install_wheel.py": 0.1, "tst/unit/test_client_identity.py": 0.0, - "tst/unit/test_infra_build_script.py": 1.3, + "tst/unit/test_infra_build_script.py": 0.6, "tst/unit/test_infra_images_script.py": 0.6, - "tst/unit/test_mirror_base_images_script.py": 0.1, - "tst/unit/test_optional_extras_stay_optional.py": 592.4, + "tst/unit/test_mirror_base_images_script.py": 0.2, + "tst/unit/test_optional_extras_stay_optional.py": 250.4, "tst/unit/test_pypi_release_script.py": 0.3, - "tst/unit/test_release_infra_images.py": 0.7, - "tst/unit/test_release_protocol_pins.py": 0.3, - "tst/unit/test_runtime_dependencies_are_imported.py": 33.9, - "tst/unit/test_shards.py": 13.2, - "tst/unit/test_slow_test_durations_script.py": 0.2, - "tst/unit/test_subclass_hooks_are_annotated.py": 2.2, - "tst/unit/test_tls_verification.py": 0.2, - "tst/unit/training/chat_test.py": 14.7, - "tst/unit/training/harness_test.py": 24.0, - "tst/unit/training/nemo_gym_test.py": 4.1, + "tst/unit/test_release_infra_images.py": 1.0, + "tst/unit/test_release_protocol_pins.py": 0.1, + "tst/unit/test_runtime_dependencies_are_imported.py": 15.4, + "tst/unit/test_shards.py": 10.6, + "tst/unit/test_slow_test_durations_script.py": 0.3, + "tst/unit/test_subclass_hooks_are_annotated.py": 4.3, + "tst/unit/test_tls_verification.py": 0.1, + "tst/unit/training/chat_test.py": 12.5, + "tst/unit/training/harness_test.py": 13.6, + "tst/unit/training/nemo_gym_test.py": 3.0, "tst/unit/training/sessions_test.py": 0.2, - "tst/unit/training/skyrl_test.py": 6.4, - "tst/unit/training/tools_test.py": 0.3, - "tst/unit/training/trl_test.py": 39.2, - "tst/unit/training/verl_test.py": 3.6, - "tst/unit/utils/docker/build_test.py": 0.4, - "tst/unit/utils/docker/context_test.py": 0.6, - "tst/unit/utils/docker/dockerfile_test.py": 1.0, - "tst/unit/utils/docker/installed_test.py": 0.2, - "tst/unit/utils/docker/lock_test.py": 0.2, + "tst/unit/training/skyrl_test.py": 6.2, + "tst/unit/training/tools_test.py": 0.2, + "tst/unit/training/trl_test.py": 38.1, + "tst/unit/training/verl_test.py": 3.8, + "tst/unit/utils/docker/build_test.py": 0.3, + "tst/unit/utils/docker/context_test.py": 0.3, + "tst/unit/utils/docker/dockerfile_test.py": 0.4, + "tst/unit/utils/docker/installed_test.py": 0.1, + "tst/unit/utils/docker/lock_test.py": 0.4, "tst/unit/utils/docker/metadata_test.py": 0.1, - "tst/unit/utils/docker/prune_test.py": 0.8, - "tst/unit/utils/exec_retry_test.py": 0.1, + "tst/unit/utils/docker/prune_test.py": 0.4, + "tst/unit/utils/exec_retry_test.py": 0.2, "tst/unit/utils/test_card_naming_github_token.py": 0.0, - "tst/unit/utils/test_paths.py": 0.2 + "tst/unit/utils/test_paths.py": 0.1 } } diff --git a/tst/unit/env/envs/test_computer_env.py b/tst/unit/env/envs/test_computer_env.py index 3679af1f1..5bf92da16 100644 --- a/tst/unit/env/envs/test_computer_env.py +++ b/tst/unit/env/envs/test_computer_env.py @@ -46,6 +46,7 @@ probing, resolve_in_base) from agent_env.env.envs.computer.utils import sites as computer_sites from agent_env.env.envs.computer.utils.composer import Composing +from agent_env.store.image_store.registry_api import RegistryRefusedError from agent_env.env.envs.computer.utils.probing import _probe from agent_env.env.envs.computer.env import COMPUTER_TOOLS, ComputerEnv from agent_env.env.envs.computer import app_env @@ -734,6 +735,77 @@ def test_env_build_by_an_installed_alias_composes_under_the_id_of_the_env_it_nam "@local/acme-kit/kit/desktop__computer").image().image_name]) +MIRROR = "ghcr.io/org/computers" + + +def _mirrored(docker, monkeypatch, read=None): + """Move every composed image out of the env's image store into the computer image mirror, as a fresh runner of CI + finds them, and record each copy back: the mirror's refs, read with ``read`` when given.""" + stored = {ref: digest for ref, digest in docker.registry.tags.items() + if f"/{composed.REPOSITORY}:" in ref and ":index-" not in ref} + for ref in stored: + del docker.registry.tags[ref] + mirror = {f"{MIRROR}:{composed.mirror_tag(ref.rpartition(':')[2])}": digest for ref, digest in stored.items()} + copied, real = [], composer.pin_if_served + + def pin(ref, auth): + if ref.startswith(f"{MIRROR}:"): + return read(ref) if read else (f"{ref}@{mirror[ref]}" if ref in mirror else None) + return real(ref, auth) + + def copy(source, target, source_auth, target_auth): + copied.append((source.partition("@")[0], target)) + docker.registry.tags[target] = source.rpartition("@")[2] + + monkeypatch.setattr(composer, "pin_if_served", pin) + monkeypatch.setattr(composer, "copy_image", copy) + monkeypatch.setattr(get_config(), "computer_image_mirror", MIRROR) + return stored, copied + + +def test_an_image_the_store_lacks_is_copied_from_the_computer_image_mirror_and_reused_rather_than_built(base, driver, + docker, + monkeypatch): + first = _put(base, driver) + stored, copied = _mirrored(docker, monkeypatch) + + second = _put(base, driver, id="@local/~/kit/other", environment_name="other") + + reused = Env.get(second.id).metadata["composed"]["container"] + assert len(docker.builds) == 2 and all(r["reused"] for r in reused.values()) + assert sorted(copied) == sorted((f"{MIRROR}:{composed.mirror_tag(ref.rpartition(':')[2])}", ref) for ref in stored) + assert {arch: r["image"] for arch, r in reused.items()} == { + arch: r["image"] for arch, r in Env.get(first.id).metadata["composed"]["container"].items()} + + +def test_a_change_to_the_code_that_composes_an_image_misses_the_mirror_and_builds_it(base, driver, docker, + monkeypatch): + _put(base, driver) + _, copied = _mirrored(docker, monkeypatch) + monkeypatch.setattr(composed, "composition", lambda: "sha256:" + "f" * 64) + + second = _put(base, driver, id="@local/~/kit/other", environment_name="other") + + assert len(docker.builds) == 4 and copied == [] + assert not any(r["reused"] for r in Env.get(second.id).metadata["composed"]["container"].values()) + + +def test_a_computer_image_mirror_that_cant_be_read_leaves_the_image_to_be_built(base, driver, docker, monkeypatch, + caplog): + _put(base, driver) + + def refused(ref): + raise RegistryRefusedError(f"{ref}: ghcr.io refused to serve it (HTTP 403)") + + _, copied = _mirrored(docker, monkeypatch, read=refused) + with caplog.at_level(logging.WARNING, logger=composer.__name__): + second = _put(base, driver, id="@local/~/kit/other", environment_name="other") + + built = Env.get(second.id).metadata["composed"]["container"] + assert len(docker.builds) == 4 and not any(r["reused"] for r in built.values()) and copied == [] + assert f"the computer image mirror {MIRROR} couldn't give" in caplog.text + + def test_builds_of_one_env_at_once_keep_each_others_pairs_and_a_failure_doesnt_replace_a_built_one(base, driver, docker, monkeypatch): diff --git a/tst/unit/store/registry_api_test.py b/tst/unit/store/registry_api_test.py index aec0d54fe..120992dd8 100644 --- a/tst/unit/store/registry_api_test.py +++ b/tst/unit/store/registry_api_test.py @@ -327,3 +327,108 @@ def ghcr(request): assert json.loads(request.content) == {"schemaVersion": 2, "mediaType": written, "manifests": [ amd64_entry, *(children if arm64 == "index" else [{**amd64_entry, "digest": OTHER, "platform": {"architecture": "arm64", "os": "linux"}}])]} + + +def _sha(data: bytes) -> str: + return f"sha256:{hashlib.sha256(data).hexdigest()}" + + +def test_an_image_is_copied_by_its_digest_with_only_the_blobs_the_target_lacks_and_its_manifests_byte_for_byte( + fake_registry): + config, kept, missing = b'{"architecture": "amd64"}', b"layer the target has", b"layer it lacks" + image = json.dumps({"mediaType": OCI_IMAGE, "config": {"digest": _sha(config)}, + "layers": [{"digest": _sha(kept)}, {"digest": _sha(missing)}]}).encode() + index = json.dumps({"mediaType": OCI_INDEX, "manifests": [{"digest": _sha(image)}]}).encode() + source = {_sha(index): (OCI_INDEX, index), _sha(image): (OCI_IMAGE, image)} + blobs = {_sha(blob): blob for blob in (config, kept, missing)} + target_blobs, target_manifests = {_sha(kept): kept}, {} + + def registries(request): + path = request.url.path + if request.url.host == "ghcr.io": + if "/blobs/" in path: + return httpx.Response(200, content=blobs[path.rpartition("/")[2]]) + media, body = source[path.rpartition("/")[2]] + return httpx.Response(200, content=body, headers={"Content-Type": media}) + if request.method == "HEAD": + return httpx.Response(200 if path.rpartition("/")[2] in target_blobs else 404) + if request.method == "POST": + return httpx.Response(202, headers={"Location": "/v2/agentenv-computers/blobs/uploads/u1?_state=s"}) + if "/blobs/uploads/" in path: + assert request.url.params["_state"] == "s" + target_blobs[request.url.params["digest"]] = request.read() + return httpx.Response(201) + target_manifests[path.rpartition("/")[2]] = (request.headers["content-type"], request.content) + return httpx.Response(201) + + seen = fake_registry(registries) + registry_api.copy_image(f"ghcr.io/org/computers:k@{_sha(index)}", "localhost:5000/agentenv-computers:k", None, None) + + assert target_blobs == blobs + assert [r.url.params["digest"] for r in seen if r.method == "PUT" and "/blobs/" in r.url.path] == [ + _sha(config), _sha(missing)] + assert target_manifests == {_sha(image): (OCI_IMAGE, image), _sha(index): (OCI_INDEX, index), "k": (OCI_INDEX, index)} + + +def test_a_copy_needs_the_digest_it_copies(fake_registry): + seen = fake_registry(lambda request: httpx.Response(500)) + + with pytest.raises(ValueError, match="names none"): + registry_api.copy_image("ghcr.io/org/computers:k", "localhost:5000/agentenv-computers:k", None, None) + assert seen == [] + + +def test_a_copy_to_ghcr_asks_again_for_the_push_token_its_first_challenge_didnt_give(fake_registry): + config = b'{"architecture": "amd64"}' + image = json.dumps({"mediaType": OCI_IMAGE, "config": {"digest": _sha(config)}, "layers": []}).encode() + pushed = [] + + def registries(request): + if request.url.host == "localhost": + if "/blobs/" in request.url.path: + return httpx.Response(200, content=config) + return httpx.Response(200, content=image, headers={"Content-Type": OCI_IMAGE}) + if request.url.path == "/token": + assert request.headers["authorization"] == "Basic " + base64.b64encode(b"bot:t").decode() + return httpx.Response(200, json={"token": request.url.params["scope"]}) + writes = request.method in ("POST", "PUT") + scope = "repository:org/computers:pull,push" if writes else "repository:org/computers:pull" + if request.headers.get("authorization") not in (f"Bearer {scope}", "Bearer repository:org/computers:pull,push"): + return httpx.Response(401, headers={"WWW-Authenticate": f'Bearer realm="https://ghcr.io/token",scope="{scope}"'}) + if request.method == "HEAD": + return httpx.Response(404) + if request.method == "POST": + return httpx.Response(202, headers={"Location": "/v2/org/computers/blobs/uploads/u1"}) + pushed.append(request.url.path) + return httpx.Response(201) + + fake_registry(registries) + registry_api.copy_image(f"localhost:5000/agentenv-computers:k@{_sha(image)}", "ghcr.io/org/computers:k", None, + RegistryAuth("ghcr.io", "bot", "t")) + + assert pushed == ["/v2/org/computers/blobs/uploads/u1", f"/v2/org/computers/manifests/{_sha(image)}", + "/v2/org/computers/manifests/k"] + + +@pytest.mark.parametrize("location", ["https://elsewhere.example/v2/org/computers/blobs/uploads/u1", + "http://ghcr.io/v2/org/computers/blobs/uploads/u1"], + ids=["another-host", "plain-http"]) +def test_an_upload_the_registry_sends_elsewhere_is_refused_before_its_credentials_go(fake_registry, location): + config = b'{"architecture": "amd64"}' + image = json.dumps({"mediaType": OCI_IMAGE, "config": {"digest": _sha(config)}, "layers": []}).encode() + + def registries(request): + if request.url.host == "localhost": + if "/blobs/" in request.url.path: + return httpx.Response(200, content=config) + return httpx.Response(200, content=image, headers={"Content-Type": OCI_IMAGE}) + assert request.url.host == "ghcr.io" and request.url.scheme == "https", request.url + if request.method == "HEAD": + return httpx.Response(404) + return httpx.Response(202, headers={"Location": location}) + + fake_registry(registries) + + with pytest.raises(ValueError, match="which its credentials don't go to"): + registry_api.copy_image(f"localhost:5000/agentenv-computers:k@{_sha(image)}", "ghcr.io/org/computers:k", None, + RegistryAuth("ghcr.io", "bot", "t")) diff --git a/tst/unit/test_computer_image_mirror_scripts.py b/tst/unit/test_computer_image_mirror_scripts.py new file mode 100644 index 000000000..54f91da22 --- /dev/null +++ b/tst/unit/test_computer_image_mirror_scripts.py @@ -0,0 +1,109 @@ +"""The CI computer image mirror's two scripts: what a runner shares into it, and which of its versions a prune deletes.""" + +from __future__ import annotations + +import datetime +import io +import json +import sys + +import pytest + +from tst.util.scripts import load_script + +NOW = datetime.datetime(2026, 11, 20, 12, tzinfo=datetime.timezone.utc) +CUTOFF = NOW - datetime.timedelta(days=7) +KEY, OTHER = "c" * 16 + "-" + "a" * 64, "c" * 16 + "-" + "b" * 64 +RAW, RAW_OTHER = "a" * 64, "b" * 64 + + +def _version(name: str, created: str, *tags: str) -> dict: + return {"id": hash(name) % 10_000, "name": name, "created_at": created, "metadata": {"container": {"tags": list(tags)}}} + + +@pytest.fixture +def prune(): + return load_script("prune_computer_images.py") + + +def test_an_image_last_seen_within_the_window_is_kept_and_one_last_seen_before_it_is_deleted(prune): + used = _version("sha256:1", "2026-10-01T00:00:00Z", KEY, f"{KEY}-seen-20261001", f"{KEY}-seen-20261115") + unused = _version("sha256:2", "2026-10-01T00:00:00Z", OTHER, f"{OTHER}-seen-20261112") + + assert prune.stale_versions([used, unused], CUTOFF, lambda digest: []) == [unused] + + +def test_an_image_seen_on_the_cutoffs_day_is_kept_through_that_day(prune): + edge = _version("sha256:1", "2026-10-01T00:00:00Z", KEY, f"{KEY}-seen-20261113") + + assert prune.stale_versions([edge], CUTOFF, lambda digest: []) == [] + + +def test_a_version_uploaded_within_the_window_is_kept_though_no_run_has_tagged_it_seen(prune): + fresh = _version("sha256:1", "2026-11-19T00:00:00Z") + old = _version("sha256:2", "2026-11-01T00:00:00Z") + + assert prune.stale_versions([fresh, old], CUTOFF, lambda digest: []) == [old] + + +def test_the_platform_manifests_a_kept_index_lists_are_kept_and_a_deleted_ones_are_not(prune): + kept = _version("sha256:k", "2026-10-01T00:00:00Z", KEY, f"{KEY}-seen-20261119") + dropped = _version("sha256:d", "2026-10-01T00:00:00Z", OTHER, f"{OTHER}-seen-20261001") + children = {"sha256:k": ["sha256:k-amd64", "sha256:k-attestation"], "sha256:d": ["sha256:d-amd64"]} + untagged = [_version(digest, "2026-10-01T00:00:00Z") for digest in ("sha256:k-amd64", "sha256:k-attestation", + "sha256:d-amd64")] + + stale = prune.stale_versions([kept, dropped, *untagged], CUTOFF, lambda digest: children.get(digest, [])) + + assert [version["name"] for version in stale] == ["sha256:d", "sha256:d-amd64"] + + +def test_a_prune_without_the_package_deletes_nothing_passes_and_says_why(prune, monkeypatch, capsys): + def missing(method, url): + raise prune.urllib.error.HTTPError(url, 404, "Not Found", {}, io.BytesIO(b"")) + + monkeypatch.setattr(prune, "_github", missing) + monkeypatch.setattr(sys, "argv", ["prune_computer_images.py", "org", "agentenv-ci-computers"]) + + assert prune.main() == 0 + assert "no package org/agentenv-ci-computers, or this repository has no access to it" in capsys.readouterr().out + + +def test_a_runner_shares_each_image_under_its_mirror_tag_and_todays_seen_tag_and_leaves_its_indexes(monkeypatch, + capsys): + share = load_script("share_computer_images.py") + tags = {"tags": [RAW, f"index-{RAW_OTHER}", RAW_OTHER]} + copies = [] + monkeypatch.setattr(share.urllib.request, "urlopen", lambda url: io.BytesIO(json.dumps(tags).encode())) + monkeypatch.setattr(share, "pin_digest", lambda ref, auth: f"{ref}@sha256:{ref[-64:]}") + monkeypatch.setattr(share, "mirror_tag", lambda key: f"{'c' * 16}-{key}") + monkeypatch.setattr(share, "copy_image", lambda source, target, source_auth, auth: copies.append( + (source.rpartition("@")[2], target, auth.username))) + monkeypatch.setattr(share, "pin_if_served", lambda ref, auth: f"{ref}@sha256:{RAW}") + monkeypatch.setattr(share.datetime, "datetime", type("Fixed", (datetime.datetime,), {"now": staticmethod(lambda tz: NOW)})) + monkeypatch.setenv("GITHUB_ACTOR", "bot") + monkeypatch.setenv("GITHUB_TOKEN", "t") + monkeypatch.setattr(sys, "argv", ["share_computer_images.py", "ghcr.io/org/agentenv-ci-computers"]) + + assert share.main() == 0 + assert copies == [(f"sha256:{raw}", f"ghcr.io/org/agentenv-ci-computers:{tag}", "bot") + for raw, key in ((RAW, KEY), (RAW_OTHER, OTHER)) for tag in (key, f"{key}-seen-20261120")] + assert "::warning::" not in capsys.readouterr().out + + +def test_a_mirror_the_composer_cant_read_anonymously_warns_to_make_it_public(monkeypatch, capsys): + share = load_script("share_computer_images.py") + monkeypatch.setattr(share.urllib.request, "urlopen", lambda url: io.BytesIO(json.dumps({"tags": [RAW]}).encode())) + monkeypatch.setattr(share, "pin_digest", lambda ref, auth: f"{ref}@sha256:{RAW}") + monkeypatch.setattr(share, "copy_image", lambda *args: None) + + def private(ref, auth): + raise ValueError(f"{ref}: ghcr.io refused to serve it (HTTP 403)") + + monkeypatch.setattr(share, "pin_if_served", private) + monkeypatch.setenv("GITHUB_ACTOR", "bot") + monkeypatch.setenv("GITHUB_TOKEN", "t") + monkeypatch.setattr(sys, "argv", ["share_computer_images.py", "ghcr.io/org/agentenv-ci-computers"]) + + assert share.main() == 0 + assert "make the package public" in capsys.readouterr().out diff --git a/tst/unit/test_prune_registry_script.py b/tst/unit/test_prune_registry_script.py deleted file mode 100644 index 2b842562c..000000000 --- a/tst/unit/test_prune_registry_script.py +++ /dev/null @@ -1,97 +0,0 @@ -"""``.github/scripts/prune_registry.py``, which keeps only the computer images a slow-tier run used before the local -registry's storage is cached: their tags, the manifests those reach, and the layer links garbage collection leaves.""" - -from __future__ import annotations - -import ast -import hashlib -import json -import sys -from pathlib import Path - -import pytest - -from tst.util.scripts import SCRIPTS, load_script - -SCRIPT = SCRIPTS / "prune_registry.py" -REPOSITORY = "agentenv-computers" -_USED, _STALE = "a" * 64, "b" * 64 - - -@pytest.fixture -def script(): - return load_script(SCRIPT.name) - - -def test_it_imports_only_the_standard_library(): - """It runs under the runner's python3 with sudo, outside the project's environment.""" - imported = set() - for node in ast.walk(ast.parse(SCRIPT.read_text())): - if isinstance(node, ast.Import): - imported |= {alias.name.split(".")[0] for alias in node.names} - elif isinstance(node, ast.ImportFrom) and node.level == 0: - imported.add(node.module.split(".")[0]) - assert imported <= set(sys.stdlib_module_names) | {"__future__"} - - -def test_a_tag_is_used_when_the_log_has_a_request_for_its_manifest_by_it(script): - log = "\n".join([ - f'time="t" level=info msg="response completed" http.request.method=HEAD ' - f'http.request.uri="/v2/{REPOSITORY}/manifests/{_USED}" http.response.status=200', - f'172.17.0.1 - - [10/Nov/2026:05:00:00 +0000] "PUT /v2/{REPOSITORY}/manifests/{"c" * 64} HTTP/1.1" 201 0', - f'172.17.0.1 - - [10/Nov/2026:05:00:01 +0000] "GET /v2/{REPOSITORY}/manifests/sha256:{"d" * 64} HTTP/1.1" 200', - f'172.17.0.1 - - [10/Nov/2026:05:00:02 +0000] "HEAD /v2/other/manifests/{_STALE} HTTP/1.1" 200 0', - ]) - - assert script.used_tags(log, REPOSITORY) == {_USED, "c" * 64} - - -def test_pruning_keeps_the_used_tags_and_every_manifest_their_index_lists(script, tmp_path): - platforms = [_blob(tmp_path, {"layers": [], "n": n}) for n in range(2)] - index = _blob(tmp_path, {"manifests": [{"digest": digest} for digest in platforms]}) - stale = _blob(tmp_path, {"layers": [], "n": "stale"}) - _tag(tmp_path, _USED, index) - _tag(tmp_path, _STALE, stale) - for digest in (index, *platforms, stale): - _revision(tmp_path, digest) - - kept, dropped = script.prune(tmp_path, REPOSITORY, {_USED}) - - manifests = tmp_path / "docker/registry/v2/repositories" / REPOSITORY / "_manifests" - assert (kept, dropped) == (1, 1) - assert sorted(p.name for p in (manifests / "tags").iterdir()) == [_USED] - assert {p.name for p in (manifests / "revisions/sha256").iterdir()} == {d.removeprefix("sha256:") - for d in (index, *platforms)} - - -def test_links_to_layers_garbage_collection_freed_are_dropped(script, tmp_path): - kept = _blob(tmp_path, {"layer": "kept"}) - layers = tmp_path / "docker/registry/v2/repositories" / REPOSITORY / "_layers/sha256" - for digest in (kept, "sha256:" + "e" * 64): - (layers / digest.removeprefix("sha256:")).mkdir(parents=True) - (layers / digest.removeprefix("sha256:") / "link").write_text(digest) - - assert script.drop_freed_links(tmp_path, REPOSITORY) == 1 - assert [p.name for p in layers.iterdir()] == [kept.removeprefix("sha256:")] - - -def _blob(storage: Path, content: dict) -> str: - data = json.dumps(content).encode() - hex_digest = hashlib.sha256(data).hexdigest() - path = storage / "docker/registry/v2/blobs/sha256" / hex_digest[:2] / hex_digest / "data" - path.parent.mkdir(parents=True) - path.write_bytes(data) - return f"sha256:{hex_digest}" - - -def _tag(storage: Path, tag: str, digest: str) -> None: - current = storage / "docker/registry/v2/repositories" / REPOSITORY / "_manifests/tags" / tag / "current" - current.mkdir(parents=True) - (current / "link").write_text(digest) - - -def _revision(storage: Path, digest: str) -> None: - revision = (storage / "docker/registry/v2/repositories" / REPOSITORY / "_manifests/revisions/sha256" - / digest.removeprefix("sha256:")) - revision.mkdir(parents=True) - (revision / "link").write_text(digest)