Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
471d33d
feat(agents): ship Claude Code in the wheel, as the agents bundle's c…
earakely-scale Oct 19, 2026
dd78e30
test(agents): the Claude Code agent's card, turns, retries, trajector…
earakely-scale Oct 19, 2026
e413674
test(agents): run the shipped Claude Code against a fake Messages end…
earakely-scale Oct 19, 2026
ed200cd
feat(config)!: the default agent is the agents bundle's Claude Code
earakely-scale Oct 19, 2026
2e24153
test(agents): read the CLI's latest user turn for its tool result, an…
earakely-scale Oct 19, 2026
8aa8308
fix(agents): the CLI gets the model key once, as its bearer token
earakely-scale Oct 19, 2026
f8ca06f
test(installer): agent-env's own plugin row lists the agents bundle too
earakely-scale Oct 19, 2026
ea1580a
fix(agents): a turn cancelled while the CLI starts kills the CLI and …
earakely-scale Oct 19, 2026
3bd1981
fix(agents): a snapshot load keeps the other conversations, a failed …
earakely-scale Oct 19, 2026
def7431
feat(agents): Claude Code serves install/v1, into a task container or…
earakely-scale Oct 19, 2026
f025888
fix(agents): two agents installed in one container keep their own ins…
earakely-scale Oct 19, 2026
d778f43
fix(agents): a retried install replaces the agent an earlier attempt …
earakely-scale Oct 19, 2026
245cead
fix(agents): the CLI keeps LITELLM_API_KEY, so the model's scripts ca…
earakely-scale Oct 19, 2026
87fbe1d
feat(agents): the image gives the model's scripts node and a pinned b…
earakely-scale Oct 19, 2026
0b75d6a
test(agents): a project id posted over agent-config attributes every …
earakely-scale Oct 19, 2026
ba255c1
test(agents): a file part naming a stored object reaches the CLI thro…
earakely-scale Oct 20, 2026
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
5 changes: 3 additions & 2 deletions .agentenv/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ agent_default = "local" # agents too
# team_id = "your-team"
# project_id = "your-project"

[agents]
default_a2a_agent_id = "a2a-default" # what a deploy_agent step that names no agent deploys
# [agents]
# default_a2a_agent_id = "my-agent" # what a deploy_agent step that names no agent deploys; by default the
# # agents bundle's Claude Code, @local/agentenv-framework/agents/claude-code

# [model]
# base_url = "http://localhost:4000" # a model proxy on this machine; local containers reach it as host.docker.internal
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ 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"
# Each shipped example's image installs the protocol this release ships with, keeping its extras.
for requirements in src/agent_env/examples/*/*/*/requirements.txt; do
grep -q "^agentenv-framework-protocol" "$requirements" || continue
sed -i -E "s/^agentenv-framework-protocol(\[[^]]*\])?.*/agentenv-framework-protocol\1==${protocol}/" "$requirements"
grep -qE "^agentenv-framework-protocol(\[[^]]*\])?==${protocol}$" "$requirements"
done
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
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,17 @@ or one of them only where it is used.
`install_commands_host` on this machine, with your privileges. Every command a local sandbox runs
carries `AGENTENV_SANDBOX=<sandbox id>`, and teardown stops the processes that carry it, then removes
what steps staged in `/tmp` under that id. Those processes are found through `/proc`, so on macOS
they keep running; files the install wrote elsewhere (`/opt`, packages) stay.
they keep running; files the install wrote elsewhere (`/opt`, packages) stay. Such an agent fetches the
local store's grants only when `grant_advertise_host` names an address this machine reaches, the
bridge address say: `host.docker.internal`, which grant URLs name by default, resolves only in containers.
- `[agents] default_a2a_agent_id`: the agent a `deploy_agent` step without an id deploys; built-in
`a2a-default`, overridable by `configure(default_a2a_agent_id=...)`. Any step's agent field left unset
falls back to it (`entity.refs.default_agent_refs`), so a bundle run plans the default for it.
`@local/agentenv-framework/agents/claude-code`, the `agents` bundle's Claude Code
(`src/agent_env/examples/agents/`), overridable by `configure(default_a2a_agent_id=...)`. Any step's
agent field left unset falls back to it (`entity.refs.default_agent_refs`), so a bundle run plans the
default for it, pulling the built-in one from the installed `agents` bundle. Claude Code needs a model
endpoint serving Anthropic's Messages API, such as a LiteLLM gateway. It declares `install/v1` too: its
folder's `install.sh` installs it into a task container or onto a VM sandbox's host, with its own Claude
Code, uv and Python, adding bash only to a target with neither bash nor zsh.
- Secrets never live in the file: use `secret:KEY` or `env:NAME` references, resolved through
`[stores.secret]`.
- agent-env has no stage concept: "dev" versus "prod" is which file `AGENT_ENV_CONFIG` names. A
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ smol = [
agent-env = "agent_env.cli:cli"

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

Expand Down
4 changes: 2 additions & 2 deletions src/agent_env/config/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _default_sandbox_mode() -> str:
_HUMAN_A2A_CONFIG_KEY = "default_human_a2a_url"
_AGENTS_CONFIG_SECTION = "agents"
_AGENTS_CONFIG_KEY = "default_a2a_agent_id"
_DEFAULT_A2A_AGENT_ID = "a2a-default"
_DEFAULT_A2A_AGENT_ID = "@local/agentenv-framework/agents/claude-code" # the agents bundle's Claude Code


def _validated_human_a2a_url(value: object, source: str) -> str:
Expand Down Expand Up @@ -383,7 +383,7 @@ def get_default_human_a2a_url(self) -> str:

def get_default_a2a_agent_id(self) -> str:
"""The agent a ``deploy_agent`` step without an id deploys:
``configure(default_a2a_agent_id=...)`` > ``[agents] default_a2a_agent_id`` > ``a2a-default``."""
``configure(default_a2a_agent_id=...)`` > ``[agents] default_a2a_agent_id`` > the agents bundle's Claude Code."""
key = _KEY_BY_NAME[f"{_AGENTS_CONFIG_SECTION}.{_AGENTS_CONFIG_KEY}"]
for layer in self.key_candidates(key):
if layer.kind == KIND_INSTALLED:
Expand Down
22 changes: 22 additions & 0 deletions src/agent_env/examples/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Agents that come with agent-env, each an agent folder a bundle builds and deploys like its own.

`agents/claude-code/` runs Anthropic's [Claude Code](https://code.claude.com/docs/en/headless) CLI behind an A2A
server: the prompt goes to `claude -p`, the task env's MCP servers to `--mcp-config`, and its stream-json output is the
turn's trajectory. It's the default agent, so a `deploy_agent` or an agent judge that names no agent deploys it. Name it
from another bundle as `@local/agentenv-framework/agents/claude-code`.

It needs Docker, and a model endpoint serving Anthropic's Messages API, `<base>/v1/messages`, which a LiteLLM gateway
does: the `[model]` base URL and key reach it as `ANTHROPIC_BASE_URL` and a bearer token, and `[model.headers]` are sent
on every model call. The `LITELLM_*` variables stay in its environment too, so a script the model runs can call the
endpoint. Building the image downloads Claude Code, which is Anthropic's software under its
[Commercial Terms](https://www.anthropic.com/legal/commercial-terms); the image is built where it runs and never
published. The CLI's version is the Dockerfile's `CLAUDE_CODE_VERSION`. The image gives the scripts the model runs
`node`, without npm or npx, and the Python packages `model-requirements.txt` pins.

`install_agent` installs it too, into a container a task already runs or onto a `vm` sandbox's Linux host, with
`install.sh` from its folder: Claude Code's native build for the target's CPU and libc, uv, and the agent's requirements
in a Python of its own, so the target's Python is left alone. The CLI runs its commands in bash or zsh, so a target with
neither, Alpine's for one, gets bash from its own package manager. The agent works in the container's working directory,
or in `/app` on the host.

Run its smoke task with `agent-env run agents`.
36 changes: 36 additions & 0 deletions src/agent_env/examples/agents/agents/claude-code/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Node for the scripts the model runs, without npm or npx; Claude Code itself is a native build.
FROM public.ecr.aws/docker/library/node:22-slim@sha256:c3de60bf2f9dd0ac6370e6117950ff62d6e339527e7472301c9c78a017978392 AS node

FROM public.ecr.aws/docker/library/python:3.11-slim@sha256:c20888b6acdd1e63e1c433a185bf3ad162c0288fe484616ce062e0d28add2900
# poppler-utils: the CLI's Read tool runs pdftoppm for a PDF's pages.
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl poppler-utils \
&& rm -rf /var/lib/apt/lists/*
# Claude Code's native build for this machine's platform, checked against its release manifest.
ARG CLAUDE_CODE_VERSION=2.1.280
RUN set -eu; \
case "$(uname -m)" in x86_64) platform=linux-x64 ;; aarch64) platform=linux-arm64 ;; \
*) echo "Claude Code has no build for $(uname -m)" >&2; exit 1 ;; esac; \
release="https://downloads.claude.ai/claude-code-releases/${CLAUDE_CODE_VERSION}"; \
curl -fsSL --retry 3 "$release/manifest.json" -o /tmp/manifest.json; \
checksum=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["platforms"][sys.argv[2]]["checksum"])' \
/tmp/manifest.json "$platform"); \
curl -fsSL --retry 3 "$release/$platform/claude" -o /usr/local/bin/claude; \
echo "$checksum /usr/local/bin/claude" | sha256sum -c -; \
chmod 0755 /usr/local/bin/claude; \
rm /tmp/manifest.json
COPY --from=node /usr/local/bin/node /usr/local/bin/node
WORKDIR /opt/agent
COPY requirements.txt model-requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt -r model-requirements.txt
# install.sh and run.sh are install/v1's: in the image, so they stay in its build context.
COPY a2a_server.py claude_bridge.py kit.py entrypoint.sh install.sh run.sh ./
# The CLI's cwd, /app, and its state, fixed so neither follows the uid: Modal runs the image as root.
RUN useradd --create-home --uid 10001 agent && mkdir -p /app /home/agent/.claude \
&& chown agent:agent /app /home/agent/.claude && chmod 0755 entrypoint.sh
ENV A2A_PORT=8000 PYTHONPATH=/opt/agent CLAUDE_CONFIG_DIR=/home/agent/.claude \
DISABLE_AUTOUPDATER=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
USER agent
WORKDIR /app
EXPOSE 8000
# CMD rather than ENTRYPOINT: Modal passes a sandbox's env to the image's CMD only.
CMD ["/opt/agent/entrypoint.sh"]
Loading
Loading