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: 4 additions & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ jobs:
# workspace members, and the bare name keys [tool.uv.sources]; none of those may be rewritten.
sed -i "s/\"agentenv-framework-protocol[^\"]*\"/\"agentenv-framework-protocol==${protocol}\"/" pyproject.toml
grep -q "\"agentenv-framework-protocol==${protocol}\"" pyproject.toml
# The echo-mcp example's server image installs the protocol this release ships with.
echo_requirements=src/agent_env/examples/echo-mcp/envs/echo/requirements.txt
sed -i "s/^agentenv-framework-protocol.*/agentenv-framework-protocol==${protocol}/" "$echo_requirements"
grep -qx "agentenv-framework-protocol==${protocol}" "$echo_requirements"
uv build --no-sources --out-dir dist
- name: Check its metadata renders
run: uvx --index-url https://pypi.org/simple twine check --strict dist/*
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ and deserialized through a registry.
| `runner/` | The `[runner]` seam: `Runner.submit()` returns `(run_id, instance_id)`; `LocalRunner` is built in. |
| `explorer/` | Optional local web UI: `agent-env up`, needs the `explorer` extra, binds loopback `:8234`. |
| `cli/` | Click CLI with the groups `a2a-agent`, `artifact`, `config`, `env`, `eval`, `plugin`, `run`, `task`, `up`. |
| `examples/` | The bundles agent-env ships (`hello`), registered under `agent_env.bundles` in `pyproject.toml`. Tests run them, but they are examples for users, not fixtures. |
| `examples/` | The bundles agent-env ships (`hello`, and `echo-mcp`, which needs Docker), registered under `agent_env.bundles` in `pyproject.toml`. Tests run them, but they are examples for users, not fixtures. |

## Extension points

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ uv tool install agentenv-framework
agent-env run hello
```

`agent-env run hello` runs the built-in hello task, which needs no Docker, model or configuration. To try it without installing anything, run it with uvx:
`agent-env run hello` runs the built-in hello task, which needs no Docker, model or configuration. `agent-env run echo-mcp` needs Docker: it builds an MCP server from a Dockerfile, deploys it through the gateway and checks it by calling its tool. `agent-env run` lists both. To try hello without installing anything, run it with uvx:

```bash
uvx --from agentenv-framework agent-env run hello
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ agent-env = "agent_env.cli:cli"

[project.entry-points."agent_env.bundles"]
hello = "agent_env.examples"
echo-mcp = "agent_env.examples"

[project.urls]
Homepage = "https://www.agentenvframework.com"
Expand Down
10 changes: 10 additions & 0 deletions src/agent_env/examples/echo-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
An MCP server built from a Dockerfile, deployed through the gateway and checked by calling its tool.

It needs Docker. `envs/echo/` is the server: a `Dockerfile` and a `server.py` whose one tool, `echo`,
returns the text it's sent, named `echo` by its environment card. `deploy_env` builds it and deploys
it behind the gateway, and `env_outcome_verifier` runs `artifacts/check_echo/verify.py` against the
env's MCP endpoint. The first run also builds the infra it deploys on, the gateway and a service
database, which takes minutes. The run tears it all down when it ends.

Run it with `agent-env run echo-mcp`, or copy this folder and run the copy with
`agent-env run ./my-echo`.
14 changes: 14 additions & 0 deletions src/agent_env/examples/echo-mcp/artifacts/check_echo/verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Calls the echo tool through the env's MCP endpoint and checks it sends the text back."""
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

SENT = "hello from agent-env"


async def verify(mcp_url):
async with streamable_http_client(mcp_url) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("echo", {"text": SENT})
text = "".join(getattr(part, "text", "") for part in result.content)
return [{"id": "echoes", "result": not result.isError and text == SENT, "description": f"echo returned {text!r}"}]
8 changes: 8 additions & 0 deletions src/agent_env/examples/echo-mcp/envs/echo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM public.ecr.aws/docker/library/python:3.11-slim@sha256:c20888b6acdd1e63e1c433a185bf3ad162c0288fe484616ce062e0d28add2900
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
RUN useradd --create-home --uid 10001 appuser
USER appuser
CMD ["python", "server.py"]
3 changes: 3 additions & 0 deletions src/agent_env/examples/echo-mcp/envs/echo/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# A release pins agentenv-framework-protocol to the version it was released with.
agentenv-framework-protocol
mcp>=1.25,<2
18 changes: 18 additions & 0 deletions src/agent_env/examples/echo-mcp/envs/echo/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""The echo env: an MCP server with one tool, which returns the text it's sent."""
from typing import Annotated

from pydantic import Field

from agentenv_protocol import AgentEnvEnvironment, environment_card, tool


@environment_card(name="echo")
class EchoEnv(AgentEnvEnvironment):
@tool(name="echo")
def echo(self, text: Annotated[str, Field(description="The text to send back.")]) -> str:
"""Return the text it's sent."""
return text


if __name__ == "__main__":
EchoEnv().serve()
5 changes: 5 additions & 0 deletions src/agent_env/examples/echo-mcp/tasks/smoke.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
{"id": "env", "type": "deploy_env", "env_id": "echo", "sandbox_type": "local"},
{"id": "echoes", "type": "env_outcome_verifier", "env_id": "echo", "file_artifact_id": "check_echo",
"verifier_id": "echoes"}
]
2 changes: 1 addition & 1 deletion tst/installer/test_container_journey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+ +bundle hello +ok\n", rows), rows
assert re.fullmatch(r"agentenv-framework +\S+ +bundle echo-mcp, bundle hello +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
Expand Down
16 changes: 14 additions & 2 deletions tst/integration/cli/run_bundle_built_envs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
under a name of its own, one over a store image, a website, and a multi of bundle and store envs, which is rewritten
when a child is. Environments and a universe the bundle writes load into an MCP server deployed through the gateway,
and an edit to their files loads anew. A universe composed every way a bundle composes one seeds each server of a multi
of bundle and store servers, and a store universe shared across servers seeds the one server it's loaded into. Needs a
Docker daemon and the local registry."""
of bundle and store servers, and a store universe shared across servers seeds the one server it's loaded into. The
echo-mcp example agent-env ships runs by name, and a rerun writes nothing. Needs a Docker daemon and the local
registry."""

import hashlib
import json
Expand Down Expand Up @@ -541,3 +542,14 @@ def test_a_multi_whose_servers_share_a_tool_name_is_refused_at_deploy_naming_the
assert "tasks/suite.json v1: failed at step 'env'" in result.output, result.output
assert "serve duplicate tool names" in result.output, result.output
assert "'list_items' from 'items' and 'stock'" in result.output, result.output


def test_the_shipped_echo_mcp_example_runs_by_name_and_a_rerun_writes_nothing(state):
first, second = _run("echo-mcp"), _run("echo-mcp")

assert first.exit_code == 0, first.output
assert "tasks/smoke.json v1: passed (echoes: 1)" in first.output, first.output
assert second.exit_code == 0, second.output
for entry in ("envs/echo (Dockerfile image)", "envs/echo", "artifacts/check_echo", "tasks/smoke.json"):
assert f"{entry}: v1, unchanged" in second.output, second.output
assert "tasks/smoke.json v1: passed (echoes: 1)" in second.output, second.output
5 changes: 3 additions & 2 deletions tst/unit/bundle/installed_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,16 @@ def test_agent_env_ships_hello_rooted_at_its_own_distribution():
]


def test_the_plugin_report_counts_the_bundle_agent_env_ships(monkeypatch, quiet_logs):
def test_the_plugin_report_counts_the_bundles_agent_env_ships(monkeypatch, quiet_logs):
monkeypatch.setattr(sys.modules["agent_env.cli.plugin"], "config_effect", lambda name: None)

result = CliRunner().invoke(cli, ["plugin", "show", CORE, "--json"])
shown = CliRunner().invoke(cli, ["plugin", "show", CORE]).output

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, "hello", "active")]
assert [(c["group"], c["name"], c["status"]) for c in core["contributions"]] == [
(BUNDLES, "echo-mcp", "active"), (BUNDLES, "hello", "active")]
assert "\n agent-env itself\n" in shown and "requires:" not in shown


Expand Down
21 changes: 21 additions & 0 deletions tst/unit/test_release_protocol_pins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""The files ``.github/workflows/publish-pypi.yml`` rewrites to pin agentenv-framework-protocol on a release: each must
hold exactly one line its sed matches, or the release fails after the protocol is already on PyPI."""

from __future__ import annotations

import re
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
WORKFLOW = (REPO / ".github" / "workflows" / "publish-pypi.yml").read_text()


def test_the_pyproject_holds_the_one_quoted_protocol_requirement_the_release_pins():
assert len(re.findall(r'"agentenv-framework-protocol[^"]*"', (REPO / "pyproject.toml").read_text())) == 1


def test_the_echo_mcp_requirements_the_release_pins_hold_one_protocol_line():
(path,) = re.findall(r"^\s*echo_requirements=(\S+)$", WORKFLOW, re.MULTILINE)
lines = (REPO / path).read_text().splitlines()

assert len([line for line in lines if line.startswith("agentenv-framework-protocol")]) == 1
Loading