diff --git a/AGENTS.md b/AGENTS.md index 55aa1c75..6c7dffc9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,6 +132,7 @@ and deserialized through a registry. | `cli/` | Click CLI with the groups `a2a-agent`, `artifact`, `bundle`, `config`, `env`, `eval`, `plugin`, `run`, `task`, `up`. `task run`, `task run-batch` and `eval run` hand their runs to the configured `[runner]` and follow them. | | `agents/` | The `agents` bundle: the agents agent-env ships, one folder each under `agents/agents/`, with a smoke task each under `agents/tasks/`. Its Claude Code, `@local/agentenv-framework/agents/claude-code`, is the default agent. An agent's Python packages are its folder's `requirements.txt`, installed in its image, never in agent-env's own environment. A run that pulls one of its agents reads the whole bundle, so `tst/unit/bundle/shipped_bundles_test.py` parses and checks every shipped bundle. | | `computers/` | The `computers` bundle: the base computers agent-env ships as `computer` artifacts, `ubuntu-24.04` (XFCE on Xvfb, supervisord as the session user, settings as `AGENTENV_SETTING_`), aliased through `[artifacts]`. It has no tasks either. | +| `browsers/` | The `browsers` bundle: the website browsers agent-env ships, today `chromium`, a `computer_app` env (Chrome for Testing at the build Playwright pins, for amd64 and arm64, set up for unattended use by managed policies in `/etc/opt/chrome_for_testing/policies/managed`, opened at boot on its `start_urls`, its profile its data). All of it, its pin, install step, policies and command, is in `envs/chromium/chromium.py`; `tst/integration/computer/chromium_app_test.py` runs it. Like `drivers`, it has no tasks. | | `drivers/` | The `drivers` bundle: the computer drivers agent-env ships as `computer_driver` envs, the reference driver (X11 on Linux, recording through `x11grab`) aliased `reference`, and trycua's cua-driver aliased `cua-driver` (its daemon from the pinned PyPI wheel, its own instructions, tools and skill resources served to agents through `@server_instructions`, `forwarded_tools` and `@resource`, its tools beside `computer_*`, `computer/v1` on its desktop-scope calls, its skill pack leading the computer skill; `tst/integration/computer/cua_driver_test.py` runs it). It has no tasks: a bundle with neither tasks nor evals is refused unless its `aliases.toml` names something for other bundles to use. The shipped `desktop` example composes it with the `ubuntu-24.04` base into a computer env, which `tst/integration/computer/` deploys, probes, records and has an agent use. | | `examples/` | The example bundles agent-env ships (`hello`, and `echo-mcp`, which needs Docker, and `desktop`, a computer env of `ubuntu-24.04` and `reference` with a probe task and an agent task). Tests run them, but they are examples for users, not fixtures. Every shipped bundle is registered under `agent_env.bundles` in `pyproject.toml`, ` = ""` shipping `src///`. | diff --git a/pyproject.toml b/pyproject.toml index 0720a1de..0aa99458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,6 +158,7 @@ agent-env = "agent_env.cli:cli" [project.entry-points."agent_env.bundles"] agents = "agent_env" +browsers = "agent_env" computers = "agent_env" drivers = "agent_env" desktop = "agent_env.examples" diff --git a/src/agent_env/browsers/README.md b/src/agent_env/browsers/README.md new file mode 100644 index 00000000..777085b4 --- /dev/null +++ b/src/agent_env/browsers/README.md @@ -0,0 +1,13 @@ +The website browsers that come with agent-env, today `chromium`, a `computer_app` env a computer env lists in its +`apps`. + +`envs/chromium/` is Chromium for an X11 desktop on Linux (`amd64` and `arm64`): Chrome for Testing 156.0.8078.4, the +build Playwright 1.64.1 pins, fetched by sha256 when a computer env that lists it is put and unpacked into +`/opt/agentenv/apps/chromium/` by its install step, with managed policies in +`/etc/opt/chrome_for_testing/policies/managed/` for unattended use: no first-run screens, sign-in, sync, promotional +tabs, default-browser prompt, telemetry, password, autofill or translation prompts, updates, location or notification +prompts, and no warning bar. It runs without Chromium's sandbox and `/dev/shm`, which a container can't give it, and +with no proxy. It opens at boot on its `start_urls` setting, a list, the first tab active, none by default +(`env_params.app_settings = {chromium = {start_urls = [...]}}`). Its data is its profile, `~/.config/chromium`, which the +image has none of, so each deploy and each reset starts with a clean one. Name it from another bundle as `chromium`, +the alias `aliases.toml` gives it, or by its id, `@local/agentenv-framework/browsers/chromium`. diff --git a/src/agent_env/browsers/aliases.toml b/src/agent_env/browsers/aliases.toml new file mode 100644 index 00000000..36afb975 --- /dev/null +++ b/src/agent_env/browsers/aliases.toml @@ -0,0 +1,3 @@ +# Ids a computer env's apps, or a task, can name these browsers by; a store's env of the id wins over an alias. +[envs] +chromium = ["chromium"] diff --git a/src/agent_env/browsers/envs/chromium/chromium.py b/src/agent_env/browsers/envs/chromium/chromium.py new file mode 100644 index 00000000..296a628f --- /dev/null +++ b/src/agent_env/browsers/envs/chromium/chromium.py @@ -0,0 +1,95 @@ +"""agentenv's chromium app: Chrome for Testing at the build Playwright 1.64.1 pins, for amd64 and arm64 (Google ships +no Linux arm64 Chrome), set up for unattended use by managed policies and opened at boot on the deploy's +``start_urls``. Its data is its profile, which the image has none of, so every deploy and every reset starts with a +clean one; its HTTP cache is kept out of it. Run as a script, it is the app's launch.""" +import json +import os +import subprocess +from pathlib import Path +from typing import Iterable + +from pydantic import BaseModel + +from agentenv_protocol.computer import (ComputerAppEnvironment, Download, Launch, Packages, Ready, computer_app, + install_step, linux) + +VERSION = "156.0.8078.4" +_ARCHIVES = {"amd64": ("linux64/chrome-linux64.zip", + "43fd31766c7533a02dda2ded5ad50c2f861cd5400be7509c0abad71129f79d79"), + "arm64": ("linux-arm64/chrome-linux-arm64.zip", + "910a12fff6170202195f72ef3d353801c7063d301ab80b1f51b77d791fda60ce")} +DOWNLOADS = [Download(url=f"https://storage.googleapis.com/chrome-for-testing-public/{VERSION}/{path}", sha256=sha256, + to=f"chromium/chrome-{arch}.zip", arch=arch) + for arch, (path, sha256) in _ARCHIVES.items()] +#: What it links against that Ubuntu 24.04 lacks, a font for the pages it shows, and unzip for its install step. +PACKAGES = ["fonts-liberation", "libasound2t64", "libatk-bridge2.0-0t64", "libatk1.0-0t64", "libatspi2.0-0t64", + "libcairo2", "libcups2t64", "libdbus-1-3", "libdrm2", "libexpat1", "libgbm1", "libglib2.0-0t64", + "libgtk-3-0t64", "libnspr4", "libnss3", "libpango-1.0-0", "libvulkan1", "libx11-6", "libxcb1", + "libxcomposite1", "libxdamage1", "libxext6", "libxfixes3", "libxkbcommon0", "libxrandr2", "libxshmfence1", + "unzip"] +WINDOW_CLASS = "Chromium" +#: Where Chrome for Testing reads managed policies; it reads neither Chromium's folder nor Chrome's. +POLICY_DIR = Path("/etc/opt/chrome_for_testing/policies/managed") +#: No sign-in, sync, welcome or promotional tabs, default-browser prompt, telemetry, password or autofill prompts, +#: translation offers, updates, or location and notification prompts; and no warning bar for the flags a container +#: needs, which also takes away Chrome for Testing's own bar. +POLICIES = { + "BrowserSignin": 0, + "SyncDisabled": True, + "PromotionalTabsEnabled": False, + "DefaultBrowserSettingEnabled": False, + "MetricsReportingEnabled": False, + "PasswordManagerEnabled": False, + "AutofillAddressEnabled": False, + "AutofillCreditCardEnabled": False, + "TranslateEnabled": False, + "ComponentUpdatesEnabled": False, + "DefaultGeolocationSetting": 2, + "DefaultNotificationsSetting": 2, + "CommandLineFlagSecurityWarningsEnabled": False, +} +PROFILE = ".config/chromium" +CACHE = ".cache/chromium" + + +@computer_app(supports=[{"os": {"type": "linux", "display": ["x11"]}, "arch": ["amd64", "arm64"]}]) +@linux(packages=Packages(apt=PACKAGES), downloads=DOWNLOADS, paths=[PROFILE], + launch=Launch(at="boot", command=["venv/bin/python", "chromium.py"], window={"class": WINDOW_CLASS}, + ready=Ready(window=True))) +class Chromium(ComputerAppEnvironment): + class Settings(BaseModel): + #: Opened in tabs at launch, the first active; none opens a new tab. + start_urls: list[str] = [] + + @install_step(scope="system", os="linux") + def chromium(self) -> None: + install_chromium(Path.cwd()) + + +def install_chromium(folder: Path, policies: Path = POLICY_DIR) -> None: + """Unpack the arch's archive under ``folder``'s ``chromium/`` to ``chromium/chrome/``, with unzip, which keeps its + execute bits, in its place, and write ``POLICIES`` to ``policies``.""" + [archive] = (folder / "chromium").glob("chrome-*.zip") + unpacked = folder / "chromium" / "unpacked" + subprocess.run(["unzip", "-q", str(archive), "-d", str(unpacked)], check=True) + [top] = unpacked.iterdir() + top.rename(folder / "chromium" / "chrome") + unpacked.rmdir() + archive.unlink() + policies.mkdir(parents=True, exist_ok=True) + (policies / "agentenv.json").write_text(json.dumps(POLICIES, indent=1) + "\n", encoding="utf-8") + + +def chromium_command(folder: Path, *, home: Path, urls: Iterable[str]) -> list[str]: + """Chromium's command: no sandbox, which a container can't give it, and no /dev/shm, which no computer sizes; no + first run or default-browser check; ``WINDOW_CLASS`` for its windows; its profile and, outside it, its cache, under + ``home``; and ``urls`` in tabs, the first active. It takes no proxy: a computer has no site proxy, and its sites on + loopback must stay reachable.""" + return [str(folder / "chromium" / "chrome" / "chrome"), "--no-sandbox", "--disable-dev-shm-usage", "--no-first-run", + "--no-default-browser-check", f"--class={WINDOW_CLASS}", f"--user-data-dir={home / PROFILE}", + f"--disk-cache-dir={home / CACHE}", *urls] + + +if __name__ == "__main__": + command = chromium_command(Path(__file__).resolve().parent, home=Path.home(), urls=Chromium().settings.start_urls) + os.execv(command[0], command) diff --git a/src/agent_env/browsers/envs/chromium/env.toml b/src/agent_env/browsers/envs/chromium/env.toml new file mode 100644 index 00000000..f2c05cc7 --- /dev/null +++ b/src/agent_env/browsers/envs/chromium/env.toml @@ -0,0 +1,4 @@ +type = "computer_app" +module = "chromium.py:Chromium" +computer = "ubuntu-24.04" +driver = "reference" diff --git a/tst/installer/test_container_journey.py b/tst/installer/test_container_journey.py index 94d0edfc..d13237c0 100644 --- a/tst/installer/test_container_journey.py +++ b/tst/installer/test_container_journey.py @@ -133,7 +133,7 @@ def test_a_new_user_installs_agent_env_adds_plugins_runs_a_task_and_removes_them container.ok("python", "-m", "pip", "install", "-q", "--no-index", "--find-links", "/wheels", "uv") container.ok("uv", "tool", "install", "agentenv-framework") rows = container.ok("agent-env", "plugin", "list").stdout.split("STATUS\n", 1)[1] - assert re.fullmatch(r"agentenv-framework +\S+ +9 bundles +ok\n", rows), rows + assert re.fullmatch(r"agentenv-framework +\S+ +10 bundles +ok\n", rows), rows added = container.ok("agent-env", "plugin", "add", browser, grader, "--yes") assert "toy_navigate active" in added.stdout and "toy_grade active" in added.stdout diff --git a/tst/integration/computer/chromium/envs/desktop/env.toml b/tst/integration/computer/chromium/envs/desktop/env.toml new file mode 100644 index 00000000..cce7e20f --- /dev/null +++ b/tst/integration/computer/chromium/envs/desktop/env.toml @@ -0,0 +1,4 @@ +type = "computer" +computer = "ubuntu-24.04" +driver = "reference" +apps = ["chromium"] diff --git a/tst/integration/computer/chromium/tasks/desktop.json b/tst/integration/computer/chromium/tasks/desktop.json new file mode 100644 index 00000000..67f3b315 --- /dev/null +++ b/tst/integration/computer/chromium/tasks/desktop.json @@ -0,0 +1,3 @@ +[ + {"id": "env", "type": "deploy_env", "env_id": "desktop"} +] diff --git a/tst/integration/computer/chromium_app_test.py b/tst/integration/computer/chromium_app_test.py new file mode 100644 index 00000000..59bee68d --- /dev/null +++ b/tst/integration/computer/chromium_app_test.py @@ -0,0 +1,80 @@ +"""agentenv's chromium app on the ubuntu-24.04 base, as the chromium test bundle's desktop lists it: gate 3 makes this +machine's arch ready with it; deployed on two start_urls, it shows one Chromium window, on the first, with no first-run, +sign-in or permission window beside it, and the second a tab away, which an agent reaches with Control+Tab; its managed +policies are where Chrome for Testing reads them; and a reset of its data clears its profile and opens it again. Needs +Docker and the local registry on localhost:5000.""" + +import asyncio +import json +import platform +import re + +import pytest +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +from agentenv_protocol import client as protocol_v1 + +from agent_env.env import Env +from agent_env.env.envs.computer.utils import inside +from agent_env.store.routing import namespace_routing +from agent_env.task.teardown import teardown_run +from agent_env.task_step.context import TaskStepContext + +pytestmark = [pytest.mark.integration, pytest.mark.int_test_slow] + +ARCH = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}[platform.machine().lower()] +ALPHA = "data:text/html,Alpha page

Alpha

" +BRAVO = "data:text/html,Bravo page

Bravo

" +# Each window the window manager manages, its class and its title. +WINDOWS = ('for w in $(xprop -root _NET_CLIENT_LIST | sed "s/.*# //; s/,//g"); do ' + 'xprop -id "$w" WM_CLASS _NET_WM_NAME; done') + + +def test_gate_3_makes_this_machines_arch_ready_with_chromium(chromium_desktop): + results = chromium_desktop.metadata["composed"]["container"] + + assert results[ARCH]["status"] == "ready" and results[ARCH]["probe"]["passed"], results[ARCH] + assert [app.environment_name for app in chromium_desktop.composed_apps()] == ["chromium"] + + +@pytest.mark.asyncio +async def test_a_deploy_opens_its_start_urls_unprompted_and_a_reset_clears_its_profile(chromium_desktop): + with namespace_routing(): + deployed = await Env.get(chromium_desktop.id).deploy( + env_params={"app_settings": {"chromium": {"start_urls": [ALPHA, BRAVO]}}}) + context = TaskStepContext(deployed_envs=[deployed]) + try: + opened = await _chromium_windows(deployed, "Alpha page") + async with streamablehttp_client(f"{deployed.environment_url}/mcp") as (read, write, _): + async with ClientSession(read, write) as session: + await session.initialize() + keyed = await session.call_tool("computer_key", {"keys": ["Control", "Tab"]}) + tabbed = await _chromium_windows(deployed, "Bravo page") + policies = await inside.run( + deployed, ["cat", "/etc/opt/chrome_for_testing/policies/managed/agentenv.json"], timeout_s=30) + await inside.run(deployed, ["sh", "-c", "touch ~/.config/chromium/marker"], timeout_s=30) + await protocol_v1.reset_data(f"{deployed.environment_url}/svc/chromium", headers=deployed.control_headers) + marker = await inside.run(deployed, ["sh", "-c", "test -e ~/.config/chromium/marker"], timeout_s=30) + reopened = await _chromium_windows(deployed, "Alpha page") + finally: + report = await teardown_run(context) + + assert opened == ["Alpha page - Google Chrome for Testing"] + assert not keyed.isError and tabbed == ["Bravo page - Google Chrome for Testing"] + assert {"BrowserSignin": 0, "CommandLineFlagSecurityWarningsEnabled": False}.items() <= json.loads( + policies["stdout"]).items() + assert marker["exit_code"] != 0 and reopened == ["Alpha page - Google Chrome for Testing"] + assert not report.failed + + +async def _chromium_windows(deployed, title: str) -> list[str]: + """The titles of the computer's Chromium windows once one of them starts with ``title``.""" + for _ in range(60): + listed = (await inside.run(deployed, ["sh", "-c", WINDOWS], timeout_s=30))["stdout"] + classes = re.findall(r'WM_CLASS\(STRING\) = .*"([^"]*)"\n_NET_WM_NAME\(UTF8_STRING\) = "(.*)"', listed) + titles = [name for cls, name in classes if cls == "Chromium"] + if any(name.startswith(title) for name in titles): + return titles + await asyncio.sleep(1) + raise TimeoutError(f"no Chromium window titled {title!r}: {listed}") diff --git a/tst/integration/computer/conftest.py b/tst/integration/computer/conftest.py index 4bfef907..ff25cc3f 100644 --- a/tst/integration/computer/conftest.py +++ b/tst/integration/computer/conftest.py @@ -1,5 +1,6 @@ """The ubuntu-24.04 base computer, built from its folder once for the tests here and tagged for the local registry; the -shipped desktop example's computer env, and the cua-driver test bundle's, each composed once for a module's tests; +shipped desktop example's computer env, the cua-driver test bundle's and the chromium test bundle's, each composed once +for a module's tests; ``state``, local stores for a test that runs a bundle; and ``docker``, which runs the docker CLI for them.""" import logging @@ -25,6 +26,7 @@ BASE = Path(agent_env.__file__).parent / "computers" / "artifacts" / "ubuntu-24.04" CUA_DRIVER = Path(__file__).parent / "cua-driver" +CHROMIUM = Path(__file__).parent / "chromium" @pytest.fixture(scope="session") @@ -50,6 +52,13 @@ def cua_desktop(tmp_path_factory): yield from _composed(tmp_path_factory.mktemp("cua-desktop"), parse_bundle(CUA_DRIVER)) +@pytest.fixture(scope="module") +def chromium_desktop(tmp_path_factory): + """The chromium test bundle's desktop, the ubuntu-24.04 base and the reference driver with agentenv's chromium app, + composed as ``desktop`` is.""" + yield from _composed(tmp_path_factory.mktemp("chromium-desktop"), parse_bundle(CHROMIUM)) + + def _composed(root: Path, bundle): patch = pytest.MonkeyPatch() patch.chdir(root) diff --git a/tst/unit/bundle/installed_test.py b/tst/unit/bundle/installed_test.py index 00238582..035defa3 100644 --- a/tst/unit/bundle/installed_test.py +++ b/tst/unit/bundle/installed_test.py @@ -127,8 +127,8 @@ def test_the_plugin_report_counts_the_bundles_agent_env_ships(monkeypatch, quiet assert result.exit_code == 0, result.output (core,) = json.loads(result.stdout)["plugins"] assert [(c["group"], c["name"], c["status"]) for c in core["contributions"]] == [ - (BUNDLES, "advance-to-next-event", "active"), (BUNDLES, "agents", "active"), (BUNDLES, "computers", "active"), - (BUNDLES, "desktop", "active"), (BUNDLES, "drivers", "active"), + (BUNDLES, "advance-to-next-event", "active"), (BUNDLES, "agents", "active"), (BUNDLES, "browsers", "active"), + (BUNDLES, "computers", "active"), (BUNDLES, "desktop", "active"), (BUNDLES, "drivers", "active"), (BUNDLES, "echo-mcp", "active"), (BUNDLES, "hello", "active"), (BUNDLES, "mail-mcp", "active"), (BUNDLES, "next-env-event", "active")] assert "\n agent-env itself\n" in shown and "requires:" not in shown diff --git a/tst/unit/bundle/shipped_bundles_test.py b/tst/unit/bundle/shipped_bundles_test.py index 42891a4b..9f3bd827 100644 --- a/tst/unit/bundle/shipped_bundles_test.py +++ b/tst/unit/bundle/shipped_bundles_test.py @@ -26,8 +26,8 @@ def _shipped() -> dict: def test_agentenv_framework_ships_its_bundles(): - assert SHIPPED == ["advance-to-next-event", "agents", "computers", "desktop", "drivers", "echo-mcp", "hello", "mail-mcp", - "next-env-event"] + assert SHIPPED == ["advance-to-next-event", "agents", "browsers", "computers", "desktop", "drivers", "echo-mcp", "hello", + "mail-mcp", "next-env-event"] @pytest.mark.parametrize("name", SHIPPED) @@ -53,13 +53,13 @@ def test_the_shipped_aliases_are_agent_envs_alone_and_claude_code_names_the_defa -def test_the_shipped_env_aliases_name_the_mail_env_the_validator_deploys_and_the_drivers(): +def test_the_shipped_env_aliases_name_the_mail_env_the_validator_deploys_the_drivers_and_the_browsers(): table = installed_aliases(tuple(_shipped().values()), ENV_ALIASES) assert table.refused == {} and table.refused_claims == {} assert {alias: named.target_id for alias, named in table.aliases.items()} == { "mail": "@local/agentenv-framework/mail-mcp/mail", "reference": "@local/agentenv-framework/drivers/reference", - "cua-driver": "@local/agentenv-framework/drivers/cua-driver"} + "cua-driver": "@local/agentenv-framework/drivers/cua-driver", "chromium": "@local/agentenv-framework/browsers/chromium"} assert A2AAgent.VALIDATION_ENV_ID == "mail" def test_a_task_naming_no_agent_plans_with_the_shipped_default(tmp_path, monkeypatch, local_stores, cli_routing): diff --git a/tst/unit/env/envs/test_chromium_app.py b/tst/unit/env/envs/test_chromium_app.py new file mode 100644 index 00000000..99cdbb5a --- /dev/null +++ b/tst/unit/env/envs/test_chromium_app.py @@ -0,0 +1,79 @@ +"""agentenv's chromium app as a put reads it: the installed ``browsers`` bundle names it ``chromium``; its class declares +the pinned Chromium archive for each arch and its packages, its profile as its data, a launch matched by its window +class and started on its ``start_urls``, none by default, and its install step, which unpacks the archive keeping its +execute bits and writes the policies; and its command.""" +import json +import os +import sys +import zipfile + +import pytest + +from agentenv_protocol.computer._load import load_class +from agentenv_protocol.computer.app import ComputerAppEnvironment + +from agent_env.bundle import parse_bundle +from agent_env.bundle.installed import find_bundle + + +@pytest.fixture +def chromium(): + sys.modules.pop("chromium", None) + cls = load_class(find_bundle("browsers").root / "envs" / "chromium" / "chromium.py", "Chromium", + ComputerAppEnvironment) + yield sys.modules[cls.__module__] + sys.modules.pop("chromium", None) + + +def test_the_browsers_bundle_names_its_chromium_app_chromium(): + bundle = find_bundle("browsers") + parsed = parse_bundle(bundle.root, id_root=bundle.id_root) + + assert [(entry.id, entry.type) for entry in parsed.entries] == [("@local/agentenv-framework/browsers/chromium", + "computer_app")] + assert parsed.aliases == {"envs": {"chromium": ("chromium",)}} + + +def test_chromium_declares_its_archives_profile_launch_start_urls_and_install_step(chromium): + declared = chromium.Chromium.declaration() + linux = declared.os["linux"] + + assert [(d["arch"], d["to"]) for d in linux.install["downloads"]] == [("amd64", "chromium/chrome-amd64.zip"), + ("arm64", "chromium/chrome-arm64.zip")] + assert all(f"/chrome-for-testing-public/{chromium.VERSION}/" in d["url"] for d in linux.install["downloads"]) + assert {"libnss3", "libasound2t64", "unzip"} <= set(linux.install["packages"]["apt"]) + assert linux.paths == [".config/chromium"] + assert linux.launch.command == ["venv/bin/python", "chromium.py"] and linux.launch.at == "boot" + assert linux.launch.window == {"class": "Chromium"} and linux.launch.ready.window + assert [(step.name, step.scope, step.os) for step in declared.install_steps] == [("chromium", "system", ["linux"])] + assert declared.settings["properties"]["start_urls"]["default"] == [] + assert declared.supports[0]["arch"] == ["amd64", "arm64"] + + +def test_its_install_unpacks_the_archive_with_its_execute_bits_in_its_place_and_writes_the_policies(chromium, tmp_path): + (tmp_path / "chromium").mkdir() + with zipfile.ZipFile(tmp_path / "chromium" / "chrome-amd64.zip", "w") as archive: + executable = zipfile.ZipInfo("chrome-linux64/chrome") + executable.external_attr = 0o100755 << 16 + archive.writestr(executable, "#!/bin/sh\n") + archive.writestr("chrome-linux64/resources.pak", "data") + + chromium.install_chromium(tmp_path, tmp_path / "policies") + + assert sorted(p.relative_to(tmp_path).as_posix() for p in tmp_path.rglob("*") if p.is_file()) == [ + "chromium/chrome/chrome", "chromium/chrome/resources.pak", "policies/agentenv.json"] + assert os.access(tmp_path / "chromium/chrome/chrome", os.X_OK) + policies = json.loads((tmp_path / "policies" / "agentenv.json").read_text()) + assert policies == chromium.POLICIES and policies["BrowserSignin"] == 0 + assert not policies["CommandLineFlagSecurityWarningsEnabled"] + + +def test_its_command_runs_unsandboxed_without_dev_shm_first_runs_or_proxy_and_opens_the_urls_in_order(chromium, + tmp_path): + command = chromium.chromium_command(tmp_path, home=tmp_path / "home", urls=["https://a.test/", "https://b.test/"]) + + assert command == [str(tmp_path / "chromium/chrome/chrome"), "--no-sandbox", "--disable-dev-shm-usage", + "--no-first-run", "--no-default-browser-check", "--class=Chromium", + f"--user-data-dir={tmp_path / 'home/.config/chromium'}", + f"--disk-cache-dir={tmp_path / 'home/.cache/chromium'}", "https://a.test/", "https://b.test/"] + assert not [arg for arg in command if "proxy" in arg] diff --git a/tst/unit/test_clean_install_wheel.py b/tst/unit/test_clean_install_wheel.py index f964e907..c1d40778 100644 --- a/tst/unit/test_clean_install_wheel.py +++ b/tst/unit/test_clean_install_wheel.py @@ -63,7 +63,8 @@ def test_the_gate_checks_the_folder_of_every_shipped_bundle(): roots = {name: root.relative_to(SCRIPT.parents[2]).as_posix() for name, root in clean_install.bundle_roots().items()} assert roots == {"advance-to-next-event": "src/agent_env/examples/advance-to-next-event", - "agents": "src/agent_env/agents", "computers": "src/agent_env/computers", + "agents": "src/agent_env/agents", "browsers": "src/agent_env/browsers", + "computers": "src/agent_env/computers", "desktop": "src/agent_env/examples/desktop", "drivers": "src/agent_env/drivers", "echo-mcp": "src/agent_env/examples/echo-mcp", "hello": "src/agent_env/examples/hello", "mail-mcp": "src/agent_env/examples/mail-mcp",