Skip to content
Open
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: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ and deserialized through a registry.
| `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_<NAME>`), and `omarchy-4.5`, a disk alone built on the composing machine from Omarchy's own VM image (its `[forms.disk]` `source`) by `disk/omarchy/`'s recipe, its services systemd user units of the graphical session that start through `with-seed`, booted by the launcher as a `linux-desktop` guest (`env/envs/computer/utils/omarchy_disk.py`, on `thin_disk.py`, which Windows' disks use too), aliased through `[artifacts]`. It has no tasks either. `disk/linux/` is the Linux disk form's recipe: `boot/`, whose Dockerfile makes a disk form (a GPT disk of an EFI system partition holding one UKI, Ubuntu's kernel with a busybox initramfs whose init reads a NoCloud seed and `switch_root`s into the image's entrypoint, and an ext4 root filesystem that is a container image's, plus `/etc/agentenv/vm/image.env` from its OCI config), `tools/`, the disk tools that make it unprivileged on the builder's own platform (`mkfs.ext4 -d`), and `launcher/`, the QEMU launcher that boots one under KVM. A base's `[forms.disk]` with no image is made from its container form; the composer makes each arch's disk from its composed container, keeping the base disk's EFI system partition, keyed by the container's key, the base disk and the recipe; gate 3 probes it where a provider boots disks natively (`disk_image_platforms()`), and a deploy's first boot with KVM probes one left `unverified`; a deploy naming `form = "disk"` boots it through `create_vm(image=..., env=...)`, its settings in the seed. |
| `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. |
| `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`, the page driver aliased `page`, a browser computer's, whose screen is the browser's active tab and which serves Playwright MCP's `browser_*` tools beside `computer_*` from a Playwright MCP it runs attached to the browser over a CDP address the relay issues it (`browser_evaluate` and `browser_run_code_unsafe` hidden by default), 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`, `<name> = "<package>"` shipping `src/<package as a path>/<name>/`. |

### Training helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,14 @@ def deco(fn: Callable) -> Callable:
class ForwardedTool(NamedTuple):
"""A tool an environment serves on another server's behalf, as that server lists it: its descriptor, its own
schema included, its MCP ``annotations`` and its ``output_schema``; ``call`` takes the call's arguments as keywords
and returns the other server's ``CallToolResult``, which reaches the caller as it is."""
and returns the other server's ``CallToolResult``, which reaches the caller as it is. ``hidden_by_default`` hides it
as ``@tool(hidden_by_default=True)`` hides a tool of the environment's own."""

tool: EnvironmentTool
call: Callable[..., Any]
annotations: dict | None = None
output_schema: dict | None = None
hidden_by_default: bool = False


class _ResourceMarker(NamedTuple):
Expand Down Expand Up @@ -450,7 +452,8 @@ def _register_forwarded(self, app: Any, descriptor: EnvironmentTool, fn: Callabl
fn_metadata=FuncMetadata(arg_model=_passed_through(), output_schema=schema,
output_model=_as_it_came() if schema is not None else None),
is_async=True,
annotations=_mcp_tool_annotations(forwarded.annotations) if forwarded.annotations is not None else None)
annotations=_mcp_tool_annotations(forwarded.annotations) if forwarded.annotations is not None else None,
meta={HIDDEN_BY_DEFAULT_META_KEY: True} if forwarded.hidden_by_default else None)

def _register_resource(self, app: Any, resource: _ServedResource) -> None:
"""A resource as it is, its description absent when it has none, or a template through FastMCP's own
Expand Down Expand Up @@ -1283,6 +1286,8 @@ async def call(**arguments: Any) -> Any:
return await forwarded.call(**arguments)

setattr(call, _FORWARDED_ATTR, forwarded)
if forwarded.hidden_by_default:
setattr(call, _HIDDEN_BY_DEFAULT_ATTR, True)
return call


Expand Down
29 changes: 29 additions & 0 deletions packages/agentenv-protocol/tests/test_tool_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from mcp.types import CallToolResult, TextContent # noqa: E402

from agentenv_protocol import ( # noqa: E402
HIDDEN_BY_DEFAULT_META_KEY,
ROLE_HEADER,
ROLE_META_KEY,
AgentEnvEnvironment,
Expand Down Expand Up @@ -140,6 +141,34 @@ async def click(**arguments):
assert agent_tools == ["read", "write"] and refused.isError


@pytest.mark.asyncio
async def test_a_forwarded_tool_hidden_by_default_stays_hidden_through_every_tool_rule_until_a_rule_names_it():
class Forwarding(_Env):
def forwarded_tools(self):
async def evaluate(**arguments):
return CallToolResult(content=[TextContent(type="text", text="evaluated")])
return [ForwardedTool(EnvironmentTool(name="evaluate", inputSchema={"type": "object"}), evaluate,
hidden_by_default=True)]

with anyio.fail_after(30):
async with _served(Forwarding()) as (rest, session):
async with session("agent") as agent:
listed, refused = await _names(agent), await agent.call_tool("evaluate", {})
await rest.post("/tools/enable", json={"role": "agent", "tools": "*"})
async with session("agent") as agent:
after_every_tool = await _names(agent)
await rest.post("/tools/enable", json={"role": "agent", "tools": ["evaluate"]})
async with session("agent") as agent:
shown = {t.name: t for t in (await agent.list_tools()).tools}
called = await agent.call_tool("evaluate", {})
async with session() as anyone:
anyone_tools = await _names(anyone)
assert listed == after_every_tool == anyone_tools == ["read", "write"]
assert refused.isError and refused.structuredContent["code"] == "permission_denied"
assert sorted(shown) == ["evaluate", "read", "write"] and not called.isError
assert shown["evaluate"].meta == {HIDDEN_BY_DEFAULT_META_KEY: True} and called.content[0].text == "evaluated"


@pytest.mark.asyncio
async def test_everything_off_then_one_tool_on_for_one_role():
with anyio.fail_after(30):
Expand Down
11 changes: 11 additions & 0 deletions src/agent_env/drivers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ a tab that paints nothing shows white. It serves the page group too: `computer_n
it. The browser ending on its own fails the computer for good: its ready check is
`final`, and its tools answer with why. Name it as `page`, or by its id, `@local/agentenv-framework/drivers/page`.

Beside those it serves Playwright MCP's `browser_*` tools (0.0.84, `--caps vision`, on Node 24), as
`playwright-mcp-tools.json` lists them. Its install lays Node and Playwright MCP's three npm packages out from pinned
downloads, and it runs Playwright MCP itself, attached to the browser over a CDP address the browser's relay issues to
the user `playwright-mcp`, so the guard applies to it as to any CDP client; it never starts a browser of its own. Each
caller gets a session of its own there. It starts on the first call, again once it has exited, and again on a fresh
address after the browser's data is reset; it ends with the driver, gets none of the driver's environment, and its file
access is `~/Uploads`. `browser_evaluate` and `browser_run_code_unsafe`, which run the agent's own code, are hidden by
default: a task's tool access shows them by naming them. To move to another version, update the downloads in
`env.toml` and regenerate the manifest from that version's `tools/list`; the browser computer's integration test checks
the two agree.

A run that names it writes it: its files, what its class declares for each platform it supports, read without
`mss`, `python-xlib` or PyAV installed, and how a computer installs it. Its `env.toml`'s `[install.linux]` has a
computer env's composer put the folder in `/opt/agentenv/drivers/reference/`, install `requirements.txt`, resolved and
Expand Down
Loading
Loading