Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/scripts/infra_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
# Docker Hub send.
_RATE_LIMITED = re.compile(r"429 Too Many Requests|toomanyrequests", re.IGNORECASE)
# What pip in an agent image's build answers for the agentenv-framework-protocol its release published moments earlier,
# until PyPI's index, which a CDN caches, lists it.
_NOT_YET_SERVED = re.compile(r"No matching distribution found for agentenv-framework-protocol(\[[^\]]*\])?==")
# until PyPI's index, which a CDN caches, lists it: pinned (an agent's), or a floor it names (the browser's recorder).
_NOT_YET_SERVED = re.compile(r"No matching distribution found for agentenv-framework-protocol(\[[^\]]*\])?(==|>=)")
# How long to wait before each try again, so a burst of builds starting together spreads out.
_RETRY_WAITS_SECONDS = (30, 60, 120)

Expand Down
11 changes: 6 additions & 5 deletions .github/scripts/infra_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
``plan`` prints each image's name, repository, tag, Dockerfile and context as JSON, ``--missing`` only those the
registry hasn't published: it has no image under the tag, or refuses to show one, as GHCR does for a package that
doesn't exist yet, so the first publish can start. ``--only infra`` lists only the infra images, and ``--only agents``
only the agents' images, which a release builds after it publishes the protocol, as an agent's protocol floor may name
it. ``write PATH`` records the digest the registry serves under every
only the images that install the protocol, the agents' and the website browser's, which a release builds after it
publishes the protocol, as their protocol floor may name it. ``write PATH`` records the digest the registry serves under every
image's tag as a release wheel carries them, and fails if any is missing. ``public`` warns about each image a user, who
pulls anonymously, can't. The registry is read with ``GHCR_TOKEN`` (as ``GITHUB_ACTOR``) when it's set, anonymously
otherwise, and always anonymously by ``public``.
Expand All @@ -26,9 +26,10 @@
from agent_env.store.image_store.registry_api import NoSuchImageError, RegistryRefusedError, pin_digest

REPO = Path(__file__).resolve().parents[2]
# "agents" is the shipped bundles' images, the agents' and an example env's, which install the protocol a release
# publishes.
_GROUPS = {"infra": tuple(image for image in PUBLISHED_IMAGES if image not in BUNDLE_IMAGES), "agents": BUNDLE_IMAGES}
# "agents" is the images that install the protocol a release publishes: the shipped bundles', the agents' and an
# example env's, and the website browser's, whose recorder installs it.
_AFTER_PROTOCOL = BUNDLE_IMAGES + tuple(image for image in PUBLISHED_IMAGES if image.name == "agentenv-website-browser")
_GROUPS = {"infra": tuple(image for image in PUBLISHED_IMAGES if image not in _AFTER_PROTOCOL), "agents": _AFTER_PROTOCOL}


def main() -> int:
Expand Down
4 changes: 3 additions & 1 deletion src/agent_env/env/envs/multi_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def _snapshot_after_load_default() -> bool:
)
from agent_env.env.envs.mcp_server import MCPServerEnv
from agent_env.env.envs.website import WebsiteEnv
from agent_env.env.envs.website_browser import WEBSITE_BROWSER_RECORDER_PORT
from agent_env.store.ids import derive_id
from agent_env.providers.sandbox_providers.sandbox_provider import SANDBOX_MODE_CONTAINER
from agent_env.attribution import Attribution
Expand Down Expand Up @@ -615,7 +616,8 @@ async def _load_from_snapshot(self, snapshot) -> None:
]
if website_configs:
website_browser_env = Env.get(config.default_website_browser_env_id)
mcp_servers = list(mcp_servers) + [MCPServerConfig(image=website_browser_env.docker_image_artifact.image_name, environment_name=website_browser_env.environment_name)]
mcp_servers = list(mcp_servers) + [MCPServerConfig(image=website_browser_env.docker_image_artifact.image_name, environment_name=website_browser_env.environment_name,
card_port=WEBSITE_BROWSER_RECORDER_PORT)]

compose_content = gw.create_docker_compose(
mcp_servers=mcp_servers,
Expand Down
12 changes: 9 additions & 3 deletions src/agent_env/env/envs/website_browser/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
FROM public.ecr.aws/docker/library/node:22-slim@sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4

# Chromium runtime dependencies for headless mode, and python3 for the socket probe the gateway's compose
# health-checks every MCP server with.
# health-checks every MCP server with and for the recorder.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-minimal \
python3 \
python3-venv \
libnss3 \
libnspr4 \
libatk1.0-0 \
Expand Down Expand Up @@ -38,7 +39,12 @@ RUN npm init -y && \
npm install @playwright/mcp@${PLAYWRIGHT_MCP_VERSION} && \
npx playwright install chromium

COPY entrypoint.sh /app/entrypoint.sh
# The recorder (recorder.py): computer-recording/v1 of the browser's page, idle until a recording starts.
COPY recorder-requirements.txt /app/
RUN python3 -m venv /opt/recorder && \
/opt/recorder/bin/pip install --no-cache-dir -r /app/recorder-requirements.txt

COPY recorder.py entrypoint.sh /app/
RUN chmod +x /app/entrypoint.sh

CMD ["/app/entrypoint.sh"]
4 changes: 4 additions & 0 deletions src/agent_env/env/envs/website_browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
# The environment_name used when creating MCPServerEnv.
WEBSITE_BROWSER_ENVIRONMENT_NAME = "website-browser"

# The port the image's recorder serves the browser's env card on (recorder.py, entrypoint.sh's RECORDER_PORT).
WEBSITE_BROWSER_RECORDER_PORT = 18766

__all__ = [
"WEBSITE_BROWSER_IMAGE_TAG",
"WEBSITE_BROWSER_ENVIRONMENT_NAME",
"WEBSITE_BROWSER_RECORDER_PORT",
]
10 changes: 10 additions & 0 deletions src/agent_env/env/envs/website_browser/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ set -euo pipefail

# MCP_PORT and MCP_HOST are injected by docker-compose (gateway convention).
# PLAYWRIGHT_MCP_VERSION is baked into the image at build time.

# The recorder serves the browser's card on RECORDER_PORT, restarted whenever it exits; the MCP server starts once
# it listens, so the card is there by the time the server is healthy, or after RECORDER_WAIT_S without it.
export RECORDER_PORT="${RECORDER_PORT:-18766}"
(while true; do /opt/recorder/bin/python /app/recorder.py || true; sleep 1; done) &
for _ in $(seq 1 "$(( ${RECORDER_WAIT_S:-20} * 10 ))"); do
(exec 3<>"/dev/tcp/127.0.0.1/${RECORDER_PORT}") 2>/dev/null && break
sleep 0.1
done

exec npx @playwright/mcp@${PLAYWRIGHT_MCP_VERSION} \
--browser chromium \
--headless \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# The recorder installs the newest agentenv-framework-protocol at or above this floor, the first with computer-recording/v1;
# raise it when the recorder needs a newer protocol.
agentenv-framework-protocol[computer]>=0.1.382
websockets==15.0.1
Loading
Loading