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
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ updates:
- "/src/agent_env/env/envs/service_db"
- "/src/agent_env/env/envs/website_browser"
- "/src/agent_env/agents/agents/claude-code"
- "/src/agent_env/agents/agents/openhands"
- "/src/agent_env/agents/agents/pi"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Expand Down
10 changes: 8 additions & 2 deletions .github/scripts/infra_build.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""What the release workflow's jobs that hold a token to push images or publish to PyPI run for the images agent-env
publishes, its infra images and the Claude Code agent's. It uses only the standard library and the docker CLI, so those
publishes, its infra images and the shipped agents'. It uses only the standard library and the docker CLI, so those
jobs install nothing; what they're handed by the jobs that run agent-env (``infra_images.py``) is checked against the
images agent-env ships before it's built, pushed or recorded.

Expand Down Expand Up @@ -32,6 +32,8 @@
"agentenv-db-mcp": "src/agent_env/env/envs/service_db/Dockerfile.db-mcp",
"agentenv-website-browser": "src/agent_env/env/envs/website_browser/Dockerfile",
"agentenv-claude-code": "src/agent_env/agents/agents/claude-code/Dockerfile",
"agentenv-openhands": "src/agent_env/agents/agents/openhands/Dockerfile",
"agentenv-pi": "src/agent_env/agents/agents/pi/Dockerfile",
}
_TAG = re.compile(r"^src-[0-9a-f]{64}$")
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
Expand All @@ -49,7 +51,8 @@
_RETRY_WAITS_SECONDS = (30, 60, 120)

# Each image's check, beyond its build: the gateway imports its server, Postgres turns healthy, the sidecars and the
# browser answer their own commands, and Claude Code's agent runs its CLI and imports its server.
# browser answer their own commands, Claude Code's and pi's agents run their CLIs and import their servers, and OpenHands'
# imports its server, which loads the OpenHands SDK.
_SMOKES = {
"agentenv-gateway": ["docker", "run", "--rm", "--entrypoint", "python", "{image}", "-c", "import gateway.entrypoint"],
"agentenv-db-web": ["docker", "run", "--rm", "--entrypoint", "/usr/bin/pgweb", "{image}", "--version"],
Expand All @@ -59,6 +62,9 @@
" && ls -d /root/.cache/ms-playwright/chromium-*"],
"agentenv-claude-code": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c",
'claude --version && python -c "import a2a_server"'],
"agentenv-openhands": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", 'python -c "import a2a_server"'],
"agentenv-pi": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c",
'pi --version && python3 -c "import a2a_server"'],
}


Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/infra_images.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""The images agent-env publishes, its infra images and the Claude Code agent's (``agent_env.env.published_images``), for
"""The images agent-env publishes, its infra images and the shipped agents' (``agent_env.env.published_images``), for
the release workflow's jobs that only read: this imports agent-env, so it runs where agent-env and its dependencies are
installed, and no job that holds a token to push images or publish to PyPI runs it (those run ``infra_build.py``).

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ and every job that reads those folders runs it first, so an image's tag, its bui
and creates the GitHub release, whose notes are the titles of the pull
requests merged since the previous release.
Once the repository variable `PUBLISH_INFRA_IMAGES` is `true`, it also publishes the infra images agent-env
ships and the Claude Code agent's (`agent_env.env.published_images`) whose `src-<hex>` tags GHCR lacks: after `main`'s CI passes, each is
ships and the shipped agents', one per agent folder (`agent_env.env.published_images`), whose `src-<hex>` tags GHCR lacks: after `main`'s CI passes, each is
built on a runner of each platform, checked, and tagged as one multi-platform image, and a tag that exists is
never moved. The infra images are published before the protocol, and a failed infra image job sends neither package
to PyPI. An agent's `requirements.txt` keeps its protocol floor, unpinned, so its image is built again only when its
Expand Down
10 changes: 5 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ the previous release, so a clear pull request title is also the release note. A
`main`'s CI is not published and gets no release; its pull requests are in the next release's notes.

A release also publishes the infra images agent-env ships, once the `PUBLISH_INFRA_IMAGES` repository variable is
`true`: one per Dockerfile under `src/agent_env/env`, and the Claude Code agent's (`agentenv-claude-code`, its CLI
included), at `ghcr.io/scaleapi/agentenv-<name>:src-<hex>` for linux/amd64
`true`: one per Dockerfile under `src/agent_env/env`, and one per shipped agent (`agentenv-claude-code`, its CLI
included, `agentenv-pi` and `agentenv-openhands`), at `ghcr.io/scaleapi/agentenv-<name>:src-<hex>` for linux/amd64
and linux/arm64, where the tag is the build context's source digest, so an unchanged Dockerfile is not built again.
The Claude Code image installs the newest `agentenv-framework-protocol` at or above the floor in its
`requirements.txt`, which a release doesn't pin, so it is built again only when its folder changes: once the protocol is
on PyPI, as a raised floor may name it, and before `agentenv-framework` is published.
The agents' images install the newest `agentenv-framework-protocol` at or above the floor in their
`requirements.txt`, which a release doesn't pin, so each is built again only when its folder changes: once the protocol
is on PyPI, as a raised floor may name it, and before `agentenv-framework` is published.
Each of them pins its base images by digest, and Dependabot bumps those pins within each base's major and minor
version (an upgrade, such as postgres 16 to 18, is made by hand), so a base's fixes reach the published
image: a bump changes the sources, and the next release builds it again.
Expand Down
21 changes: 14 additions & 7 deletions src/agent_env/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ does: the `[model]` base URL and key reach it as `ANTHROPIC_BASE_URL` and a bear
on every model call. The `LITELLM_*` variables stay in its environment too, so a script the model runs can call the
endpoint. The image holds Claude Code, which is Anthropic's software under its
[Commercial Terms](https://www.anthropic.com/legal/commercial-terms); a release also publishes the image, CLI included,
at `ghcr.io/scaleapi/agentenv-claude-code:src-<hex>`, built from this folder with the protocol pinned as the wheel
ships it. A run that deploys the agent registers that image when it's published for the sources installed: each deploy
at `ghcr.io/scaleapi/agentenv-claude-code:src-<hex>`, built from this folder, and again only when
the folder changes. A run that deploys the agent registers that image when it's published for the sources installed: each deploy
pulls it by its digest and builds it from this folder only when it can't, and a provider that runs images by name can
run it. The CLI's version is the Dockerfile's
`CLAUDE_CODE_VERSION`. The image gives the scripts the model runs
Expand All @@ -28,17 +28,24 @@ or in `/app` on the host.

`agents/pi/` runs the [pi coding agent](https://github.com/earendil-works/pi/tree/main/packages/coding-agent) behind
the protocol's A2A agent framework: each turn is one `pi --mode json` run, the task env's MCP servers go to its
`mcp.json`, and its JSON events are the turn's trajectory. Name it as `@local/agentenv-framework/agents/pi`. It calls
`mcp.json`, and its JSON events are the turn's trajectory. Name it as `pi`, its alias, or by its id,
`@local/agentenv-framework/agents/pi`. It calls
the `[model]` endpoint as an OpenAI-compatible one and takes its model from the prompt, so its smoke task needs
`[model] default` (or the `agent` role) set. It declares `install/v1` too, for Debian or Ubuntu targets.
Its folder's `README.md` covers the extensions it implements.

`agents/openhands/` runs the [OpenHands](https://github.com/OpenHands/software-agent-sdk) agent behind the
protocol's A2A agent framework, driving the OpenHands SDK in process rather than a CLI: each turn is one run of a
persisted SDK conversation, the task env's MCP servers go to its `mcp_config`, and its SDK events are the turn's
trajectory. Name it as `@local/agentenv-framework/agents/openhands`. It calls the `[model]` endpoint through LiteLLM
and takes its model from the prompt, so its smoke task needs `[model] default` (or the `agent` role) set. It declares
`install/v1` too, for Debian or Ubuntu targets. Its folder's `README.md` covers the extensions it implements.
trajectory. Name it as `openhands`, its alias, or by its id, `@local/agentenv-framework/agents/openhands`. It
calls the `[model]` endpoint through LiteLLM and takes its model from the prompt, so its smoke task needs
`[model] default` (or the `agent` role) set. It declares `install/v1` too, for Debian or Ubuntu targets. Its folder's
`README.md` covers the extensions it implements.

`agent-env run agents` runs every agent's smoke task, building each image; `--task claude-code-smoke` runs Claude
A release publishes pi's and OpenHands' images too, at `ghcr.io/scaleapi/agentenv-pi:src-<hex>` and
`ghcr.io/scaleapi/agentenv-openhands:src-<hex>`, each built again only when its folder changes, and a run deploys
them as it does Claude Code's: it pulls the image
published for the sources installed, by its digest, and builds it from the agent's folder only when it can't.

`agent-env run agents` runs every agent's smoke task, pulling or building each image; `--task claude-code-smoke` runs Claude
Code's alone, `--task pi-smoke` pi's, `--task openhands-smoke` OpenHands'.
3 changes: 3 additions & 0 deletions src/agent_env/agents/agents/openhands/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# The image installs the newest agentenv-framework-protocol at or above this floor; a release doesn't pin it, so the
# image is built again only when this folder changes. The floor is the first that serves peer-agents/v1, which the
# agent leaves to the SDK; raise it when the agent needs a newer protocol.
agentenv-framework-protocol[agent]>=0.1.351
openhands-sdk==1.57.0
openhands-tools==1.57.0
1 change: 1 addition & 0 deletions src/agent_env/agents/aliases.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
[agents]
claude-code = ["claude-code"]
pi = ["pi"]
openhands = ["openhands"]
5 changes: 3 additions & 2 deletions src/agent_env/env/published_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ def ref(self) -> str:
return f"{self.repository}:{self.tag()}"


# The images of the agents agent-env ships that its releases publish, Claude Code's CLI included: the default agent's.
AGENT_IMAGES = (PublishedImage("agentenv-claude-code", _AGENTS / "claude-code" / "Dockerfile"),)
# The images of the agents agent-env ships that its releases publish: one per agent folder, Claude Code's CLI included.
AGENT_IMAGES = tuple(PublishedImage(f"agentenv-{folder.name}", folder / "Dockerfile")
for folder in sorted(_AGENTS.iterdir()) if (folder / "Dockerfile").is_file())


def published_digest(image: PublishedImage) -> str | None:
Expand Down
3 changes: 2 additions & 1 deletion tst/unit/bundle/shipped_bundles_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def test_the_shipped_aliases_are_agent_envs_alone_and_claude_code_names_the_defa

assert table.refused == {} and table.refused_claims == {}
assert {alias: named.agent_id for alias, named in table.aliases.items()} == {
"claude-code": "@local/agentenv-framework/agents/claude-code", "pi": "@local/agentenv-framework/agents/pi"}
"claude-code": "@local/agentenv-framework/agents/claude-code", "pi": "@local/agentenv-framework/agents/pi",
"openhands": "@local/agentenv-framework/agents/openhands"}
assert table.target("claude-code").agent_id == get_config().get_default_a2a_agent_id()


Expand Down
4 changes: 2 additions & 2 deletions tst/unit/env/published_images_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@


def test_every_shipped_dockerfile_is_published_once_under_a_name_of_its_own():
"""The infra envs' and the default agent's, Claude Code."""
"""The infra envs' and every shipped agent's."""
shipped = [dockerfile for dockerfiles in bootstrap._DOCKERFILES.values() for dockerfile in dockerfiles]
shipped.append(published_images._AGENTS / "claude-code" / "Dockerfile")
shipped += published_images._AGENTS.glob("*/Dockerfile")

assert sorted(image.dockerfile for image in bootstrap.PUBLISHED_IMAGES) == sorted(shipped)
assert len({image.name for image in bootstrap.PUBLISHED_IMAGES}) == len(bootstrap.PUBLISHED_IMAGES)
Expand Down
2 changes: 1 addition & 1 deletion tst/unit/test_infra_images_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_plan_only_lists_the_infra_images_or_the_agents_images(script, monkeypat
agents = [image.name for image in AGENT_IMAGES]
assert planned == {"infra": [image.name for image in bootstrap.PUBLISHED_IMAGES if image.name not in agents],
"agents": agents}
assert "agentenv-gateway" in planned["infra"] and agents == ["agentenv-claude-code"]
assert "agentenv-gateway" in planned["infra"] and agents == ["agentenv-claude-code", "agentenv-openhands", "agentenv-pi"]


def test_plan_missing_lists_the_images_the_registry_lacks_or_refuses_to_show(script, monkeypatch, capsys):
Expand Down
Loading