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: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ and deserialized through a registry.

| Package | Contents |
|---|---|
| `env/` | `Env` base class: a subclass declares `type`, implements `from_dict` and `async deploy(...) -> DeployedEnv`, and may override `reset`, `close` and `load_file_artifact_universe`; teardown closes each env a run deployed before terminating its sandboxes by id. Built-ins in `env/envs/`: `MCPServerEnv` (`mcp_server`), `MultiEnv` (`multi`, MCP servers plus websites), `ServiceDBEnv` (`service_db`, PostgreSQL), `WebsiteEnv` (`website`), `GatewayEnv` (`gateway_server`), `ComputerDriverEnv` (`computer_driver`, `env/envs/computer/driver_env.py`: a computer driver's files and what its class declares for each platform it supports, read at put in a process of its own, `agentenv_protocol.computer._declare`, where the driver's own dependencies needn't be installed, with the `port` it serves on and its install by OS, `[install.<os>]`: files, a `python` requirements file for a venv, and the services the base's init runs; it deploys only as a computer env's driver), `ComputerEnv` (`computer`, in `env/envs/computer/`: a base computer and a driver, matched at put by gate 1 before anything is stored, then composed by `composer.py` into an image for each form and arch with this machine's Docker, the other arch through its emulation, tagged `agentenv-computers:<key>` in the image store by a key of the image's Dockerfile and build context so any env with the same inputs reuses it; each pair's status goes into its metadata, `composed`, the arches built are written as its computer, a `computer` artifact over one index, and `agent-env env build` resumes what a put left; gate 3 then boots each native build on a configured sandbox provider and probes it at `full`, making it `ready` or `failed`, and a build no provider runs natively stays `unverified`. Its deploy, through the `computer` env provider (`providers/env_providers/env_computer_provider.py`), boots its first `ready` build on a sandbox provider that runs its arch natively (`native_platforms()`), with no gateway in front and the driver's card as the env card, then checks its `display`; `deploy_env` `env_params` take a `form`, `computer_settings` (deploy-phase values of the base's settings; a secret one, here or on the env, only as a bare `secret:`/`env:` reference) and `driver_settings` (the driver's `ComputerSettings`, such as its default coordinate space); a `network` that allows nothing runs it on local Docker's internal network behind a relay (`providers/sandbox_providers/local_docker/relay/`); `reset` and `close` have the driver end its recordings first (`computer-recording/v1` `end_all`, each uploading what's left and its manifest under the grant it started with), and `reset` then boots a fresh container in its place (`reset_container`), keeping the computer's address but not an agent's MCP session to the old driver; its `load_file_artifact_universe` writes files under the session user's home through the driver's `data/add`, `Downloads` by default, each file's bytes through a read grant when the store's grants reach it and its network allows everything, else inline (`env/envs/computer/utils/inside.py`)), `ComputerAppEnv` (`computer_app`, `env/envs/computer/app_env.py`: an app on a computer, a `ComputerAppEnvironment` class its env.toml's `module` names, read at put by the same `_declare`; deployed alone it is a one-app computer env, a `ComputerEnv` of the computer and driver its env.toml names, the composer installing it under `/opt/agentenv/apps/<name>/` with its services as supervisord programs, its card named after it, and `env_params` `app_settings` handing it values of its `Settings` in `AGENTENV_APP_SETTINGS`, a secret one only as a bare reference). An env type whose services take environment data defines `load_environment_artifact` and `load_environment_universe_artifact`; `load_artifact` refuses an environment artifact aimed at a type that doesn't, at save and before it loads. An env built on the protocol SDK can serve agents a skill (`@agent_skill`, `skill/v1`), which `add_skills` `env_ids` installs under its environment name (behind a gateway, each server's under that server's name). `env/gateway/gateway.py` is the env-side gateway server, container-only code. While its clock is armed it serves agents `get_time` (`env/gateway/get_time.py`), which answers in UTC and, given an IANA `time_zone`, also in that zone; executors' working `hours` (`env/gateway/triggers.py`) take a fixed `utc_offset` or an IANA `time_zone`. |
| `env/` | `Env` base class: a subclass declares `type`, implements `from_dict` and `async deploy(...) -> DeployedEnv`, and may override `reset`, `close` and `load_file_artifact_universe`; teardown closes each env a run deployed before terminating its sandboxes by id. Built-ins in `env/envs/`: `MCPServerEnv` (`mcp_server`), `MultiEnv` (`multi`, MCP servers plus websites), `ServiceDBEnv` (`service_db`, PostgreSQL), `WebsiteEnv` (`website`), `GatewayEnv` (`gateway_server`), `ComputerDriverEnv` (`computer_driver`, `env/envs/computer/driver_env.py`: a computer driver's files and what its class declares for each platform it supports, read at put in a process of its own, `agentenv_protocol.computer._declare`, where the driver's own dependencies needn't be installed, with the `port` it serves on and its install by OS, `[install.<os>]`: files, a `python` requirements file for a venv, and the services the base's init runs; it deploys only as a computer env's driver), `ComputerEnv` (`computer`, in `env/envs/computer/`: a base computer and a driver, matched at put by gate 1 before anything is stored, then composed by `composer.py` into an image for each form and arch with this machine's Docker, the other arch through its emulation, tagged `agentenv-computers:<key>` in the image store by a key of the image's Dockerfile and build context so any env with the same inputs reuses it; each pair's status goes into its metadata, `composed`, the arches built are written as its computer, a `computer` artifact over one index, and `agent-env env build` resumes what a put left; gate 3 then boots each native build on a configured sandbox provider and probes it at `full`, making it `ready` or `failed`, and a build no provider runs natively stays `unverified`. Its deploy, through the `computer` env provider (`providers/env_providers/env_computer_provider.py`), boots its first `ready` build on a sandbox provider that runs its arch natively (`native_platforms()`), with no gateway in front and the driver's card as the env card, then checks its `display`; `deploy_env` `env_params` take a `form`, `computer_settings` (deploy-phase values of the base's settings; a secret one, here or on the env, only as a bare `secret:`/`env:` reference) and `driver_settings` (the driver's `ComputerSettings`, such as its default coordinate space); a `network` that allows nothing runs it on local Docker's internal network behind a relay (`providers/sandbox_providers/local_docker/relay/`); `reset` and `close` have the driver end its recordings first (`computer-recording/v1` `end_all`, each uploading what's left and its manifest under the grant it started with), and `reset` then boots a fresh container in its place (`reset_container`), keeping the computer's address but not an agent's MCP session to the old driver; its `load_file_artifact_universe` writes files under the session user's home through the driver's `data/add`, `Downloads` by default, each file's bytes through a read grant when the store's grants reach it and its network allows everything, else inline (`env/envs/computer/utils/inside.py`)), `ComputerAppEnv` (`computer_app`, `env/envs/computer/app_env.py`: an app on a computer, a `ComputerAppEnvironment` class its env.toml's `module` names, read at put by the same `_declare`; deployed alone it is a one-app computer env, a `ComputerEnv` of the computer and driver its env.toml names, the composer installing it under `/opt/agentenv/apps/<name>/` with its services as supervisord programs beside its own server (`agentenv_protocol.computer.serve_app`, in a venv with the protocol's `app` extra, on `.agentenv/server.sock` in its folder), which adds its launcher, launches a `boot` app once per boot and says through `computer-app/v1` whether it's ready, as the driver's `computer-ready` asks, its card named after it, and `env_params` `app_settings` handing it values of its `Settings` in `AGENTENV_APP_SETTINGS`, a secret one only as a bare reference). An env type whose services take environment data defines `load_environment_artifact` and `load_environment_universe_artifact`; `load_artifact` refuses an environment artifact aimed at a type that doesn't, at save and before it loads. An env built on the protocol SDK can serve agents a skill (`@agent_skill`, `skill/v1`), which `add_skills` `env_ids` installs under its environment name (behind a gateway, each server's under that server's name). `env/gateway/gateway.py` is the env-side gateway server, container-only code. While its clock is armed it serves agents `get_time` (`env/gateway/get_time.py`), which answers in UTC and, given an IANA `time_zone`, also in that zone; executors' working `hours` (`env/gateway/triggers.py`) take a fixed `utc_offset` or an IANA `time_zone`. |
| `artifact/` | Immutable versioned artifacts; every `put()` creates a new version. The document goes to the document store, binary payloads to the object store. Every artifact but an environment universe, whose `metadata` key holds its legacy metadata file ids, carries a `metadata` dict outside its versioned fields, changed by compare-and-swap (`merge_metadata`). Built-ins: `CliArtifact` (`cli`), `ComputerArtifact` (`computer`, a base computer: its architectures, OS, session user, settings and container form, whose image `put` reads from its registry into `metadata.validated_images`), `FileArtifact` (`file`), `FileArtifactUniverse` (`file_artifact_universe`), `DockerImageArtifact` (`docker_image`), `EnvironmentArtifact` (`environment`), `EnvironmentUniverseArtifact` (`environment_universe`), `SkillArtifact` (`skill`); `[artifacts] type_aliases` maps another stored spelling of a type onto one of them. A `DockerImageArtifact` holds a tar.gz of its image, or, written by `put_ref` (`--image-ref` on `env mcp-server put` and `a2a-agent put`), only a registry reference with its tag pinned to a digest, which sandboxes pull, or a published image's tag with its `image_digest` and its build context, which sandboxes pull by the digest and build from the context when they can't (the infra images, and a bundle agent built from a shipped agent's sources, `Plan.published`), or, written by `put_context`, only its build context, which a VM sandbox builds before running it (`VmSandbox.load_docker_images`) and Modal builds before creating its container (`ModalSandboxProvider.prepare_image`, outside a fallback chain's deadline); anything else that runs images by name (a container on the local provider, a lone server on any provider but Modal) refuses such an image. A build context (`utils/docker/context.py`) is what `docker build` would send, `.dockerignore` applied, written so equal files give equal bytes and an equal `source_digest`. |
| `task/`, `task_step/` | A `Task` holds its `TaskStep`s inline; `Task.run()` executes them as a DAG. `depends_on` (None means all prior steps) gates a step, independent steps run concurrently, `fail_task_on_error` makes a failure fatal or tolerated, `retry_config` rolls a failed span back through the step journal and re-dispatches it. Built-in steps live in `task_step/task_steps/` (`deploy_env`, `deploy_agent`, `prompt_agent`, the verifiers under `verifiers/`, `start_computer_recording` and `end_computer_recording`, and more). On an env whose card serves computer-exec/v1 (a computer), `run_code` with `env_id` and `run_container_unit_tests_verifier` with `env_id` run inside it as its session user through the driver: files staged with `data/add` and read with `data/get`, commands run with `run`; `env_outcome_verifier` hands a `verify` that takes an `env_url` keyword the env's address. `teardown_run` and the `teardown_sandboxes` step share one core (`task/teardown.py` `tear_down`): the recordings the run left open end with fresh grants, each env still up closes (`Env.close`; a computer env ends any recording still open), what stayed open is registered from its namespace, then every sandbox is terminated by id. `task/episode/` runs a task with some of its agents played from outside, as a training loop does (`EpisodeSpec`). |
| `store/` | Four store ABCs with local and cloud implementations: `DocumentStore` (SQLite, MongoDB, DynamoDB, Firestore with MongoDB compatibility), `ObjectStore` (filesystem, S3, Cloud Storage), `ImageStore` (local OCI registry, ECR), `SecretStore` (env vars or file, AWS Secrets Manager, Google Cloud Secret Manager). `VersionedEntityStore` implements the shared versioned get/put logic, `QueryBuilder` is the immutable chained query API, `store/base.py` holds the error types. `store/routing.py` is the `@local` namespace: while the CLI or a `build_app` server runs, an `@local/…` id and what an `@local` task's run records go to local stores beside the configured ones, and that run's writes to the configured stores are refused. A new backend must pass the conformance kits in `tst/store/`. |
Expand Down
42 changes: 31 additions & 11 deletions packages/agentenv-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,8 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
class's `notes`, markdown for what its card can't say.
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
with `recording`, `computer-recording/v1` (below).
Expand Down Expand Up @@ -541,9 +541,10 @@ and gives it back.

A computer app is software an agent uses on a computer, beside the driver: a desktop app, a service
or both. Write one as a `ComputerAppEnvironment` subclass from `agentenv_protocol.computer`, an
`AgentEnvEnvironment` whose class also says what it is to a computer. It needs only the plain
package: `agentenv_protocol.computer` imports what a driver needs (mcp, uvicorn, Pillow, PyAV) only
when a driver's names are used.
`AgentEnvEnvironment` whose class also says what it is to a computer. Writing one and reading what
it declares need only the plain package: `agentenv_protocol.computer` imports what a driver needs
(mcp, uvicorn, Pillow, PyAV) only when a driver's names are used. On a computer it runs as its own
server, which needs the `app` extra (below).

```python
from pydantic import BaseModel, SecretStr
Expand All @@ -564,18 +565,37 @@ class MailApp(ComputerAppEnvironment):
`permissions` it needs of the computer. Its env.toml names the rest: the app itself
(`environment_name`) and the `computer` and `driver` it deploys alone on.
- **`linux = Linux(...)`** is how a Linux computer installs it: `files`, a folder of the app's
copied under its prefix; `python`, a requirements file there installed into a venv beside them;
`services`, programs the computer's init runs from the prefix as the session user.
- **`launch = Launch(...)`** says how it starts in the desktop session, with it (`at="boot"`) or when
the agent opens it (`"demand"`), and `ready=Ready(...)` when it's ready; `@ready_check` marks a
method for readiness that needs logic, and `@install_step(scope=...)` one a computer runs as it
installs the app.
copied under its prefix, which holds the class's file; `python`, a requirements file there,
installed into the venv every app gets beside them; `services`, programs the computer's init runs
from the prefix as the session user.
- **`launch = Launch(...)`** says how it starts in the desktop session: with it (`at="boot"`) or when
the agent opens it (`"demand"`). `ready=Ready(...)` says when it's ready, within `timeout_s` of its
launch: a window matching `launch.window` by its class, instance or title, a port answering on
loopback, or a command exiting 0. `@ready_check` marks a method for readiness that needs logic (a
sync one runs in a thread), and `@install_step(scope=...)` one a computer runs as it installs the
app.
- **Its skill** is a file of its folder (`skill = "skill.md"`) or an `@agent_skill` method, not both;
its data hooks are the usual `@add_data`, `@get_data` and `@reset_data`.
- **`Settings`** is typed; a computer hands each app the values a deploy gives it, as one JSON value
in `APP_SETTINGS_ENV` (`AGENTENV_APP_SETTINGS`, the apps' values by name), and the base class reads
its own, by `ENVIRONMENT_NAME`, as `self.settings`. A `SecretStr` field is a secret.

On a computer each app is its own server: `python -m agentenv_protocol.computer.serve_app
<file>:<Class>`, a program the computer's init runs from the app's folder in the app's venv, which
has the `app` extra (`agentenv-framework-protocol[app]`: mcp and uvicorn). It serves the class's
card and tools on the Unix socket `.agentenv/server.sock` in that folder, with `computer-app/v1`:
the app's `launch` as its params, and the methods `launch` (POST), which starts the app in the
desktop session, and `ready` (GET: an `AppReadiness` of `ready`, `problems`, `launched`, `waited_s`
and `timeout_s`), which runs the app's readiness checks then, at once, each within 10 s. A server
that can't start, or whose start in the session fails (adding the launcher, say), writes why to
`.agentenv/server.failure` in the folder and exits, and the driver reports that in its place. Once
the session is up the server adds the app's launcher to the session user's menu, which runs `python
-m agentenv_protocol.computer.launch <folder>` to ask the server, so the server starts every copy of
the app, and launches a `boot` app, once per boot. The driver finds each app by the socket in its
folder: the computer is ready once every app's server answers and each `boot` app, and each app with
no launch, says it's ready; a `demand` app counts once something launches it. What the server does
in the session (`agentenv_protocol.computer.session`) is Linux's.

`MailApp.declaration()` returns what the class declares, an `AppDeclaration`, which a `computer_app`
env stores, and `python -m agentenv_protocol.computer._declare <folder> <file> <class>` prints it as
JSON, or a driver's `DriverDeclaration`, without the class's own dependencies installed.
Expand Down
6 changes: 6 additions & 0 deletions packages/agentenv-protocol/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ agent = [
"regex>=2023.12.25",
"uvicorn>=0.30",
]
# A computer app's own server: ComputerAppEnvironment serves its card and computer-app/v1 over mcp's FastMCP, on a Unix
# socket through uvicorn.
app = [
"mcp>=1.25,<2",
"uvicorn>=0.30",
]
# A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP, crops and encodes frames, and
# records the display as H.264 with PyAV (computer-recording/v1).
computer = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import types
from pathlib import Path

from pydantic import ValidationError

from .app import AppDeclaration, ComputerAppEnvironment
from .card import COMPUTER
from .driver import ComputerDriverEnvironment, DriverDeclaration, DriverPlatform
Expand Down Expand Up @@ -95,6 +97,10 @@ def main(argv: list[str]) -> int:
try:
with contextlib.redirect_stdout(sys.stderr):
declared = declare(Path(folder), file, name)
except ValidationError as e:
print("ValueError: " + "; ".join(error["msg"].removeprefix("Value error, ") for error in e.errors()),
file=sys.stderr)
return 1
except Exception as e:
print(f"{type(e).__name__}: {e}", file=sys.stderr)
return 1
Expand Down
Loading
Loading