agent-env
agent-env is a Python SDK and CLI for building, deploying and running agentic environments and the tasks that grade agents inside them. Environments are containerized servers that speak the open agentenv-protocol; agent-env builds them into versioned images, deploys them behind a gateway, points an agent at them, and scores what the agent did.
Documentation: www.agentenvframework.com/docs covers environments, artifacts, agents, tasks, the registry and plugins.
Install
Both packages are on PyPI. You need Python 3.11 or newer and, to run environments locally, a running Docker daemon:
pip install agentenv-framework
The distribution is named agentenv-framework, the import package is agent_env and the command is agent-env. It depends on agentenv-framework-protocol, whose import package is agentenv_protocol, and installs it too.
To work on agent-env itself, install from a clone with uv:
git clone https://github.com/scaleapi/agentenv-framework && cd agentenv-framework
uv sync --extra dev
source .venv/bin/activate
Plugin contract
What a package that extends agent-env relies on. Unit tests and the plugin-API check in CI hold the code to these sections, so a pull request that changes the contract changes this README too.
Register types from an installed package
An installed distribution registers envs, task steps, artifacts, sandbox, state and environment providers and explorer plugins by declaring entry points, with no config. The group says what kind of thing it is, the entry-point name is the registry key, and the value is the class:
[project.entry-points."agent_env.envs"]
browser = "agentenv_browser.env:BrowserEnv"
[project.entry-points."agent_env.task_steps"]
browser_navigate = "agentenv_browser.steps:NavigateTaskStep"
| Group | Value | Name |
|---|---|---|
agent_env.envs |
Env subclass with its own type, implementing from_dict |
must equal the class's type, the spelling its documents are written under |
agent_env.task_steps |
TaskStep subclass with its own type, implementing execute and from_dict |
must equal the class's type |
agent_env.artifacts |
Artifact subclass with its own type field default |
the registry key; a class may also register under extra names, such as a legacy spelling, as long as its own type default resolves to it and its type field accepts each extra name |
agent_env.sandbox_providers |
SandboxProvider subclass implementing create_sandbox |
the registry key; it must equal the .type of the sandboxes the provider produces, checked on the first create_* call |
agent_env.state_providers |
EnvStateProvider subclass implementing acquire and _teardown |
must equal the class's type, checked at registration |
agent_env.env_providers |
EnvironmentProvider subclass implementing deploy and close |
must equal the class's type, which its records carry as env_provider_type; checked at registration |
agent_env.explorer_plugins |
ExplorerPlugin subclass with its own type, implementing router |
must equal the class's type |
- The class must implement every abstract method of its base, and, for envs and task steps,
from_dictas a classmethod, which the base defines only to raise. A plugin that does not isfailedwithinvalid-plugin, naming what is missing, for exampleGradeTaskStep must implement execute and from_dict. Animplsentry or provider table naming such a class is aConfigError. - Each registry takes the built-ins first, then plugins, then config. A plugin cannot replace a built-in: it is skipped with a warning, however many distributions claim that name.
- A name that two installed distributions register in one group, or that one declares twice, is left out of the registry with a warning, and the rest of the group, built-ins included, loads. An explorer plugin under that name is not mounted, so its routes are absent. Resolving the name fails like a plugin that did not load, naming each claimant and its version, for example
Unknown env type: browser (2 installed plugins register 'browser' in agent_env.envs: …), and says to remove all but one withagent-env plugin remove PACKAGE, or, when one package declares the name twice, to report it to the package's author. Neither claimant is picked, because the order entry points are found in is not fixed. - Config cannot settle a conflicted name. An
implsentry, a provider table, an[explorer.plugins]impl or an[artifacts] type_aliasesentry mapping to that name is checked as usual, then skipped with a warning. Atype_aliasesentry whose old spelling is that name is aConfigError, as it is when one plugin registers it. - A plugin that fails to import, fails the checks above, or (for explorer plugins) fails to construct is skipped with a warning and the others still load. So is one whose requirement on agent-env excludes the installed version, before it is imported (see Plugin compatibility). Resolving its name then reports the recorded error, for example
Unknown env type: browser (registered by 'browser' from agentenv-web 2.1.0 (…) but failed to load: ModuleNotFoundError(…)). - Config replaces a plugin's class with a warning, except under a conflicted name: an
implsentry of the sametype, or a[sandbox.providers.<name>]/[state.providers.<name>]table carryingimpl. Naming the plugin's own class is silent. A provider table withoutimplconfigures the plugin's provider the way it configures a built-in's, which is where per-deployment settings such as a secret name belong. A config-only provider table, or an[artifacts] type_aliasesentry, that points at a plugin which failed to load is skipped with a warning rather than failing the whole registry. agent_env.plugins.load_failures()lists every plugin that did not take effect in the registries the currentConfighas built (failed to load, validate or construct, clashed with a built-in, or claims a name another entry point claims), by group and name, with the reason. The record belongs to theConfig, soreset_config()starts a new one. A deployment that ships its plugins can build its registries at startup and assert it is empty.agent_env.plugins.inventory()lists every installed distribution that declares entry points in these groups, with a status for each contribution and acodesaying why (see Plugin report format):active;replaced(config names a different class, andreplaced_bysays where);failed;skipped(a built-in owns the name);conflict;blocked(the group's real build fails, for example because its config table is invalid, so nothing in the group loads); orunloaded. It builds the registries on a throwawayConfigthat reads the same document, so theConfigin use keeps its registries and itsload_failures().inventory(load=False)reads installed metadata only and runs no plugin code, so a status that needs a build isunloaded. With the defaultload=Truethe plugins are imported and the explorer plugins constructed, with whatever process-wide effects that has.discovery_errorsnames each group whose installed entry points could not be read at all, so none of its plugins is listed.- Discovery reads installed metadata and imports nothing. When entry points are loaded is unspecified: today each group is imported when its registry is first built, but a plugin must not depend on that.
- Loading a plugin never changes which config a
Configreads: theConfigresolves its document before it imports any plugin. A plugin package that setsAGENT_ENV_CONFIGon import affects only configs built afterwards, such as afterreset_config().
The distribution that declares the entry points is the plugin, so pip uninstall removes it completely, apart from any [plugins.<package>] table in the config file. CLI commands are a separate contribution, described next.
CLI plugins, root options, explorer routes
Installed packages add commands through two entry-point groups:
[project.entry-points."agent_env.cli_plugins"]
my-tools = "mycorp_demo.cli:my_tools"
[project.entry-points."agent_env.cli_root_options"]
tenant = "mycorp_demo.cli:tenant_option"
agent_env.cli_plugins entries are click commands or groups added next to the built-ins, under the entry-point name whatever the command object is called: my-tools above is agent-env my-tools. agent_env.cli_root_options entries are optional click.Option instances with expose_value=False; their callback runs before the subcommand, so it can set AGENT_ENV_CONFIG and call agent_env.config.reset_config() to select the config for the whole process. Plugins load when agent_env.cli is imported, after the built-ins. Clash rules: a plugin command whose entry-point name core already uses, or a flag core owns, is skipped with a warning on stderr, and core wins. Two different root options on the same flag are both left off with a warning, and agent-env plugin list reports each as a conflict; the CLI still starts, so agent-env plugin remove can settle it. The same click.Option object exported by two entry points, from one package or two, is attached once. Nothing grafts onto existing groups.
Explorer routes: subclass agent_env.explorer.plugin.ExplorerPlugin, set the type ClassVar, and return a FastAPI APIRouter from the router property. List it under [explorer.plugins] impls or declare an agent_env.explorer_plugins entry point; from_config() takes no arguments, and a plugin reads its own settings with agent_env.plugins.settings (see Plugin settings). agent-env up --no-bootstrap mounts it before the core routers and needs no Docker.
Bundles from installed packages
A package ships bundles by naming the package that holds their folders:
[project.entry-points."agent_env.bundles"]
triage = "mycorp_demo.bundles"
- The bundle
triageis the foldermycorp_demo/bundles/triage/. The entry-point name is both the bundle's name and its folder's name, so it may contain-. - The value names a package, never an object. The folder is found without importing any of the package's code, so listing bundles runs nothing.
- The folder ships in the wheel as package data, and the package must be installed unpacked.
- An installed bundle's ids are rooted at its distribution, wherever the package is installed:
triageabove writes@local/mycorp-demo/triage/.... agent-env run triageruns it, andagent-env runwith no argument lists the installed bundles, each with its folder.- When two packages install bundles of one name, run each as
<package>/<name>, using the canonical distribution name, for examplemycorp-demo/triage. agent-env's own bundles keep their bare names, so a package'shelloruns only as<package>/hello. agent-env plugin listshows each bundle as a contribution of its package, agent-env's ownhelloasagentenv-framework … bundle hello.plugin checkfails when a bundle doesn't resolve to a folder, fails a checkagent-env runmakes before it reads a store (it isn't a valid bundle, a type it names doesn't resolve, or a step's fields don't build), is registered twice by one package (conflict), or comes from a package whose agent-env requirement isn't met (incompatible-core).list --no-loadandshow --no-loadonly parse it.
Plugin settings
A plugin that needs settings of its own reads them from [plugins.<package>], where <package> is its distribution name: the name pip install takes and agent-env plugin list prints. That table belongs to the plugin. agent-env reads nothing in it and checks none of its keys. Every other top-level table belongs to agent-env, which warns about one it does not read in agent-env config show, so a plugin keeps nothing of its own anywhere else.
[plugins.acme-agentenv-browser]
endpoint = "env:BROWSER_URL?http://localhost:9222"
timeout = 30
[plugins.acme-agentenv-browser.viewport]
width = 1280
from agent_env.plugins import settings
def browser_timeout() -> int:
browser = settings("acme-agentenv-browser") # {} when the file has no such table
return browser.get("timeout", 10) # the plugin keeps its own defaults
- Pass the distribution name, not the module's
__name__or__package__:acme-agentenv-browsermay import asacme_browser. - Call
settings()where the value is used, infrom_config,executeor a command's body, not at import. Entry points are imported before a root option can select the config file, so a value read at import may come from another file. - The key is matched by canonical name (PEP 503: lowercase, with each run of
-,_and.read as one-), so[plugins.acme_agentenv_browser]is the same table. Write the canonical form. It never needs quoting, whereas a dotted name written bare ([plugins.acme.browser]) nests. Two tables that name one package are aConfigError, not a guess. settings(name, *, config=None)returns a copy of the table, read fromconfig's document (default: the processConfig), withenv:andsecret:references resolved as in every other table. It raisesagent_env.config.ConfigErrorwhen[plugins]or the plugin's entry is not a table, when two keys name the package, or when a reference without a?defaultcannot be resolved; the message names the table. agent-env never reads the table itself, so a broken entry fails only its own plugin's read, andconfig showreports it in place.- No environment variable overrides a plugin setting. For a value that differs per deployment, write an
env:reference in the table. agent-env config showlists each table under its package: with the installed version, with(declares no agent_env entry point)when the distribution is installed but is not a plugin, which usually means a misspelled entry-point group, with(agent-env itself)foragentenv-framework, whose table agent-env does not read, or with(not installed).config explain plugins.<package>.<key>says whose table holds the key; agent-env does not read or check it, so it cannot tell whether the plugin reads that key, and a misspelled key is still shown. A table for a plugin that is not installed is reported, never an error, because one config file is often shared by processes that install different plugins.- Both commands mask a plugin's table the way they mask agent-env's, and a service may log what they report. A literal value is printed as
***when its key, or a key above it in the table, containspassword,passwd,passphrase,secret,token,credential,api_key,private_key,authorbearerin any case, and a connection URI's userinfo is masked. Any other literal is printed, so keep credentials behindsecret:orenv:references. A reference prints as written, with any?defaultmasked like a literal. - A provider the plugin registers is still configured in
[sandbox.providers.<name>]or[state.providers.<name>], under the provider's name, and an explorer plugin class is still listed in[explorer.plugins]. agent-env reads those tables itself.[plugins.<package>]holds only what the plugin reads. - Uninstalling a plugin leaves its table in the config file.
- A top-level table one letter from
[plugins], such as[plugin], gets a warning inconfig show: nothing reads it, so the plugin would get no settings.
Manage plugins
agent-env plugin shows what the installed plugins contribute and whether each piece took effect, and adds or removes them.
agent-env plugin list # every plugin package, what it provides, and its status
agent-env plugin show PACKAGE # each contribution and why it is in that state
agent-env plugin check # exit 1 if any contribution did not take effect
agent-env plugin add SPEC... # install through this environment's installer, then check
agent-env plugin remove PACKAGE # uninstall through the same installer, after safety checks
agent-env 0.9.1193 · uv tool at ~/.local/share/uv/tools/agentenv-framework
config: (none)
PACKAGE VERSION PROVIDES STATUS
agentenv-browser 1.0.0 env browser, task step browser_navigate ok
agentenv-framework 0.9.1193 bundle hello ok
agentenv-grader 0.3.1 task step grade_essay 1 failed
| Status | Meaning |
|---|---|
active |
registered and in use |
replaced |
config names a different class for the name; show says where. Not a failure |
failed |
failed to import, validate or construct |
skipped |
a built-in, a core command or root option, or (for CLI commands) a plugin loaded first owns the name |
conflict |
another entry point, from another package or this one, claims the same name, so none of them is used. For a bundle, only a second claim from the same package is a conflict; another package's bundle of that name is qualified-only |
blocked |
the group cannot load at all, for example because its config table is invalid, so this contribution does not either |
unloaded |
not loaded (--no-load). When loading, it means the status could not be determined, and check fails on it |
A contribution that is not active also has a code saying why, shown in brackets after its reason; the codes are listed under Plugin report format.
- The header says how agent-env is installed (uv tool, pipx, uv project, virtualenv or system Python) and where: a plugin has to be installed into that same environment.
listandshowtake--jsonand--no-load.--no-loadreads installed metadata only and imports no type plugin; CLI plugins are already loaded, because the CLI loads them when it starts. Without it,list,showandcheckimport every type plugin and construct the explorer plugins, so that plugin code runs.showalso reports whether importing the package setsAGENT_ENV_CONFIG, checked in a fresh interpreter.checkbuilds every registry the way a process does, adds the CLI's own plugins, and fails onfailed,skipped,conflict,blockedorunloaded, or when the config file or the installed entry points cannot be read (a malformedentry_points.txthides every plugin, so it fails rather than passing empty).check --jsonprints thelist --jsonreport plusokandproblems. Areplacedcontribution passes: config chose it.pluginis a core command: a CLI plugin that names a commandpluginis skipped, like any clash with a core command.- The Python equivalent is
agent_env.plugins.inventory()(see Register types from an installed package).
Plugin report format
plugin list --json, plugin show --json and plugin check --json, and the agent_env.plugins types they are built from, are the interface for scripts, CI and image builds. The human output of these commands is not: parse the JSON.
{
"format_version": 1,
"agent_env": {"version": "0.9.1217", "environment": "uv tool", "location": "/home/me/.local/share/uv/tools/agentenv-framework"},
"config": {"path": "/work/.agentenv/config.toml", "error": null},
"loaded": true,
"group_errors": {},
"discovery_errors": {},
"plugins": [
{"name": "agentenv-grader", "version": "0.3.1", "contributions": [
{"group": "agent_env.task_steps", "name": "grade_essay", "value": "agentenv_grader.steps:GradeEssay",
"status": "failed", "code": "load-failed", "reason": "failed to load: ModuleNotFoundError(\"No module named 'openai'\")",
"replaced_by": null, "conflicts_with": []}
]}
]
}
- Every key is always present; an empty value is
null,[]or{}.group_errorsanddiscovery_errorsmap an entry-point group to{code, reason}, andconfig.erroris{code, reason}ornull. - A distribution whose metadata cannot be read is listed under the label
(unreadable metadata: DIR), whereDIRis its.dist-infodirectory. The label is not a package name, soplugin removecannot take it: reinstall the package with its installer, or delete that directory. - A contribution's
statussays what happened to it (the table above) and itscodesays why.codeandreasonare set together: on every contribution that is notactive, and on anactiveone only for information.replaced_byis set exactly when the status isreplaced:{file, table, impl}, wheretableis the config table naming the other class (envs,task_steps,artifacts,explorer.plugins,sandbox.providers.NAMEorstate.providers.NAME).conflicts_withlists the other entry points that claim the name, as{package, version, value}. show --jsonaddsconfig_effect, a sentence saying whether importing the package setsAGENT_ENV_CONFIG(nullwith--no-load).check --jsonaddsokandproblems, each a contribution with itspackageandversion.checkexits 0 whenokand 1 otherwise; a usage error exits 2.showexits 1 with no report when no installed plugin package has that name. A run that does not print exactly one JSON document on stdout, such as a crash while agent-env starts, is not a report: count it as a failure whatever its exit status.
format_version changes only for a change a consumer cannot ignore: a key removed, renamed or given another type, a new status, a code redefined or reused, or a new meaning for an exit status. A new key, a new code, reworded reason or config_effect text, and a change in list order leave it as it is. So a consumer should ignore keys it does not know, handle a code it does not know by its status, refuse a format_version it does not know, and never parse reason. Which situation gets which status and code, and which group error is reported when several apply, is behaviour rather than format: a release that changes one says so in its notes. A code is never removed or reused.
| Code | Where | Meaning | What to do |
|---|---|---|---|
load-failed |
failed |
The plugin's code raised: when it was imported (SystemExit included), when an explorer plugin was constructed, or in a root option's callback with the flag absent |
Install what it needs, or report it to the plugin's author |
invalid-plugin |
failed |
It imported but does not fit its group: not a subclass of the group's base class; a type that is missing, inherited or not the entry-point name; a method its base requires left unimplemented; not a click.Command or click.Option; a root option that is required or exposes a value; a command click refused; an extra artifact name that reads no document. A bundle is never imported: it is invalid when its value isn't an installed, unpacked package holding that folder, when its metadata can't be read, or when the folder isn't a valid bundle (with plugins loading, one whose step, env or artifact types don't resolve) |
Report it to the plugin's author; for a bundle whose package is installed zipped, reinstall it unpacked. For an extra name whose class's own type is in conflict, settle that conflict |
incompatible-core |
failed |
The plugin's requirement on agentenv-framework or agentenv-framework-protocol excludes the installed version, so it was not imported. See Plugin compatibility |
Upgrade agent-env, or install a version of the plugin that fits |
builtin-name |
skipped |
agent-env owns the name: a built-in type, a core command or a core root option | The plugin has to rename it |
name-conflict |
conflict, skipped |
More than one entry point claims the name, from two packages or twice from one; for bundles, only twice from one package (see qualified-only). In a type group none of them is registered, and the rest of the group loads; two different root options on one flag are each a conflict, and neither is attached. A CLI command whose name a plugin loaded earlier took is skipped, and the earlier one stays |
Remove all but one: agent-env plugin remove PACKAGE. A package that declares a name twice has to be fixed by its author |
replaced-by-config |
replaced |
The config registers another class under the name | Nothing, unless you did not mean it |
already-attached |
active |
A second entry point, from the same package or another, exports the same click.Option object, which is attached once |
Nothing |
not-loaded |
unloaded |
--no-load, or inventory(load=False): whether it takes effect needs an import |
Run without --no-load |
status-unknown |
unloaded |
It was loaded, but nothing was recorded for it. This should not happen | Report it as an agent-env bug |
config-not-found |
config error, group error, blocked |
AGENT_ENV_CONFIG names a file that does not exist |
Fix or unset AGENT_ENV_CONFIG |
config-unreadable |
config error, group error, blocked |
The config file could not be read or parsed | Fix the file |
config-invalid |
group error, blocked |
The file parsed, but what it says for this group is invalid | Fix the table the reason names |
group-build-failed |
group error, blocked |
Building the group raised something else | See the reason |
entry-points-unreadable |
discovery error | The group's installed entry points could not be read, so none of its plugins is listed | Reinstall the package the reason names |
qualified-only |
active |
Another package installs a bundle of the same name, so this one runs only as <package>/<name>. agent-env's own bundles keep the bare name |
Run it by its qualified name |
A blocked contribution carries its group error's code, so it says what to fix rather than only that the group failed.
Add and remove plugins
agent-env never installs anything itself. add and remove run the installer that owns the environment agent-env runs in, so that installer's next rebuild keeps the change:
| agent-env installed as | add runs |
remove runs |
|---|---|---|
| uv tool | uv tool install agentenv-framework --with ... --with SPEC, passing back the receipt's requirements (editable and git ones included), Python and options |
the same, without the package |
| pipx | pipx inject VENV SPEC, with the venv's own pip arguments (and --force to upgrade a package it already has) |
pipx uninject --leave-deps VENV PACKAGE |
| uv project | uv add --no-sync --project ROOT SPEC and uv sync --inexact, then shows the pyproject.toml diff |
uv remove --no-sync --project ROOT PACKAGE and uv pip uninstall PACKAGE |
| virtualenv | python -m pip install SPEC (or uv pip install when the venv has no pip) |
python -m pip uninstall -y PACKAGE |
| Poetry or PDM project | prints poetry add / pdm add to run yourself |
prints poetry remove / pdm remove |
| Hatch environment | says to add it to the environment's dependencies | the same, for removal |
| system Python with the PEP 668 marker, or read-only site-packages | refuses, with guidance | refuses |
SPEC is anything the installer accepts, so a plugin can come from an index, a git repository or a file:
| From | SPEC |
|---|---|
| PyPI, or the index the installer is configured with | agentenv-grader, 'agentenv-grader==0.3.0', 'agentenv-grader>=0.3,<0.4' |
| a git tag, commit or branch | 'agentenv-grader @ git+https://github.com/mycorp/agentenv-grader@v0.3.0' |
| one package in a monorepo | 'agentenv-grader @ git+https://github.com/mycorp/plugins@v1#subdirectory=agentenv-grader' |
| a private repository | 'agentenv-grader @ git+ssh://git@github.com/mycorp/agentenv-grader.git@v0.3.0' |
| a release asset | https://github.com/mycorp/agentenv-grader/releases/download/v0.3.0/agentenv_grader-0.3.0-py3-none-any.whl |
| a local wheel or checkout | ./dist/agentenv_grader-0.3.0-py3-none-any.whl, ./agentenv-grader |
-
Write a git URL as
NAME @ URL, and a local checkout asNAME @ file:///absolute/path. agent-env cannot read the name from a baregit+https://…or a directory, so it cannot tell pipx or a uv tool which installed plugin the spec replaces: pipx leaves the installed one as it is, andaddsays so; a uv tool reports conflicting URLs, and the add is rolled back. A git install records the commit it resolved to, so a rollback reinstalls that commit. -
addnever replaces agent-env itself. A spec that namesagentenv-framework, as a name, a wheel orNAME @ URL, is refused before anything runs. A bare URL or directory that turns out to build it is rolled back after the install: agent-env may move to a newer release from the index when a plugin requires one, but not to a URL, git or directory source. Upgrade agent-env with the installer that owns the environment. -
Credentials stay with git and the installer: an SSH key or a git credential helper (
gh auth setup-git) for a private repository, and the installer's own config for a private index. A token written intoSPECis kept in the installer's record of the environment. -
--installeroverrides the detected installer,--index-urlpasses an index through to it (a uv tool keeps it as its default index),--dry-runshows what would run, and--yesskips the prompt. -
uv takes a pre-release or a yanked version only when you name it. If the installer reports that a plugin needs one, name that exact version in the same command, for example
agent-env plugin add agentenv-grader 'agentenv-grader-core==0.2.0b1';plugin removeremoves it later the same way. -
A uv project whose environment is set by
UV_PROJECT_ENVIRONMENTis found from the current directory, as uv finds it, so runaddandremoveinside the project. In a uv workspace whose root has no[project]table,addgoes to the one member that declaresagentenv-framework(uv add --package MEMBER).removedrops a package from every member and group that declares it (--package,--group,--optional,--dev). -
A uv project is never synced exactly, so packages from extras and anything installed outside the lock, agent-env included, stay installed.
-
pipx and a virtualenv leave a removed plugin's own dependencies installed, as
pip uninstalldoes. Left to itself,pipx uninjectwould also uninstall everything nothing else requires, agent-env included. -
A uv tool installed with constraints, overrides or executables from another package is refused:
uv tool installcannot take those back from agent-env, so make that change with uv. -
One
addorremoveruns at a time per environment; a second one is refused until the first finishes. The lock is a file in$XDG_STATE_HOME/agent-env/locks(by default~/.local/state/agent-env/locks). If the installer's record changes some other way while a change waits for confirmation, the change is refused rather than run from its outdated plan. -
A plain virtualenv first shows the installer's own dry run of what would change. Every mode reports what did change afterwards, calling out a change to agent-env itself.
-
addthen checks each new plugin package in a fresh interpreter: it must declare at least oneagent_env.*entry point, and every contribution must beactiveorreplaced.showreports whether importing it setsAGENT_ENV_CONFIG. If the check fails, the installer fails or removes another plugin, or the change is interrupted (Ctrl-C or SIGTERM),addputs the environment back as it was: every package at its old version from its old source, and the uv tool receipt, pipx metadata, orpyproject.tomlanduv.lock, byte for byte. The installer runs with Ctrl-C ignored, since one stopped halfway leaves packages no installer can read: it finishes, and then the change is rolled back. A second Ctrl-C stops it at once. It keeps the terminal, so its own prompts, such as git's for credentials, still work. When the environment cannot be put back exactly,addsays which packages differ and the installer command that reinstalls them. Adding what is already installed changes nothing and succeeds, and a bare name that a uv tool already lists keeps its version pin.--keepkeeps a change that failed the check instead. -
removerefuses, unless--force:- while another installed package requires the package;
- while the config file names something only it provides (
[sandbox] defaultoragent_default, a provider table, atype_aliasestarget), or animplslist, provider, store or runnerimplnames one of its modules; - while stored documents use env, artifact or task-step types only it provides. This is checked for a local document store, and for a remote one only with
--check-usage; a store that cannot be read also blocks, until--force.
It never removes agent-env itself. A package that declares no
agent_env.*entry point is removed only when the installer records it on its own (a uv tool's--with, a pipx injection, a project dependency), as it does for a package named next to a plugin inadd; after removing a plugin,removenames any such package that only the plugin needed. In a uv tool or a uv project, removing a plugin also removes the plugins that only it needed, as the installer's rebuild would, soremovenames them first and checks them like the package itself. If the installer fails, removes a plugin it did not name, or the change is interrupted,removeputs the environment back the same wayadddoes. -
Plugins are trusted code: nothing installs one implicitly, from a task document, a config file or anywhere else.
-
A plugin that breaks CLI startup does not lock you out. A root option whose callback raises or exits while its flag is absent is skipped, with a warning, and reported as
failed; a flag you pass still fails, naming the plugin. A CLI plugin that exits while being imported is skipped like any load failure. For a plugin that hangs or kills the interpreter while being imported, uninstall it with the installer directly: the commandplugin remove --dry-runwould print.
Building a platform plugin
One installable package can combine all of the above: bundled config files, a root option that selects one per invocation, store and provider classes, a Runner, custom steps, and explorer routers. The --tenant demo above is that pattern in miniature. Its callback points AGENT_ENV_CONFIG at a bundled tenants/<name>.toml; a name that does not exist fails loud with ConfigError on first use, while --help still works. A hosted control plane serves the explorer app from its own server process and lists its public hostnames under [explorer] allowed_hosts (see the comments in .agentenv/config.example.toml). A durable runner is the plugin's responsibility: this repository ships only LocalRunner.
Plugin compatibility
What a plugin can build on, how to declare the agent-env it needs, and what agent-env does when the installed one does not fit.
The plugin surface. A plugin may rely on these. Anything else, including every underscored name other than a method a subclass must implement (EnvStateProvider._teardown), is internal and can change in any release.
- The entry-point groups and their rules, in Register types from an installed package and CLI plugins, root options, explorer routes.
- The base class each group names, with its public methods and attributes:
agent_env.env.env.Env,agent_env.task_step.task_step.TaskStepand theagent_env.task_step.context.TaskStepContexta step runs with,agent_env.artifact.artifact.Artifact,agent_env.providers.sandbox_providers.sandbox_provider.SandboxProvider,agent_env.providers.env_state.env_state_provider.EnvStateProvider,agent_env.providers.env_providers.env_provider.EnvironmentProvider, andagent_env.explorer.plugin.ExplorerPlugin. - The two functions an env that uses an environment provider calls:
agent_env.providers.env_providers.env_provider.build_env_providerandagent_env.env.store.register_env_instance. - What an environment provider reads to deploy a built-in env:
agent_env.env.envs.mcp_server.MCPServerEnv.docker_image_artifactandagent_env.env.envs.mcp_server.MCPServerEnv.environment_name;agent_env.env.envs.website.WebsiteEnv.backend_docker_image_artifact,agent_env.env.envs.website.WebsiteEnv.frontend_docker_image_artifactandagent_env.env.envs.website.WebsiteEnv.environment_name;agent_env.env.envs.multi_env.MultiEnv.mcp_server_envs,agent_env.env.envs.multi_env.MultiEnv.website_envsandagent_env.env.envs.multi_env.MultiEnv.name; and each image'sagent_env.artifact.artifacts.docker_image.DockerImageArtifact.image_name. - The top level of
agent_env.plugins, includingsettings, and the[plugins.<package>]table it reads (Plugin settings). - The
plugin --jsonoutput, which has its own rules: Plugin report format.
The classes a config impl names, such as stores and runners, are not on the list yet.
Changes before 1.0. Every merged change can ship as a release, several a day. A change that breaks the plugin surface is marked with ! after the scope in its pull request title, which becomes its commit title, as in feat(plugins)!: …. Where the old behaviour can be kept for a while, it is deprecated first: it keeps working and emits a DeprecationWarning that names what replaces it. How long that lasts is not fixed before 1.0.
The plugin-api CI job holds pull requests to this. It compares the listed base classes, TaskStepContext, the two functions, the env attributes and the top level of agent_env.plugins with the pull request's base (.github/scripts/check_plugin_api.py), and fails on a break the title does not mark; with the !, it lists the breaks and passes, and editing the title re-runs it. A break is what fails code written against the old surface:
- for a caller, a name, parameter or
__all__entry that is removed or renamed, a new required parameter, a parameter that can no longer be passed as before, or a changed default or constant, including the group-name constantsagent_env.pluginsexports, compared by value; - for a subclass of a base class, a new abstract or required method, a method that becomes abstract or required, a new
ClassVarwith no value, a method that changes between plain,async,classmethod,staticmethodand property, and a base-class method that accepts more than before: a new parameter, even an optional one, a parameter that stops being required, a new*argsor**kwargs, or a keyword-only parameter that can now be passed by position, since an override written for the old signature fails when core passes it; - for construction, a changed
@dataclass(...)option or pydanticmodel_config, a new metaclass, or a new__init_subclass__that can raise, which refuses a subclass.
A member every subclass must implement is declared @abstractmethod, or listed in its registry's _MUST_IMPLEMENT, so that the check enforces it; a new method that only raises NotImplementedError gets a note but does not fail the job.
agent-env ships py.typed, so mypy and pyright check a plugin against its annotations. Import each name from the module the list above gives, such as TaskStepContext from agent_env.task_step.context: pyright, and mypy under --strict, treat a name a module only imports as private to that module. Type annotations are not compared, so a release can correct one without a !. The check can report a change no plugin notices, such as a new parameter core never passes to an override; mark it all the same. It does not see a change in behaviour, in a type the surface only names in a signature, such as DeployedEnv, in a pydantic field's requirements or the order of dataclass fields, in an abstract method a new base from outside agent-env brings, or in a registration rule other than abstract methods and _MUST_IMPLEMENT; review covers those. A ! can also mark a break outside the plugin surface.
Declaring the agent-env your plugin needs. Declare a floor, agentenv-framework>=X, where X is the oldest release you test against. Leave out a ceiling such as <1: before 1.0 it would not guard against a change in a 0.9 release, and it would keep your plugin from installing with the next major one. Pin exact versions in the application or image that installs your plugin, not in the plugin. If your plugin imports agentenv_protocol itself, declare that as well.
What agent-env checks. Before it imports a plugin, agent-env compares the plugin's requirements on agentenv-framework and agentenv-framework-protocol with the versions installed. A plugin they exclude is not loaded:
plugin list,showandcheckreport each of its contributions asfailedwith the codeincompatible-core, with or without--no-load, andcheckexits 1;- using one of its types names the requirement, as in
needs agentenv-framework>=0.9.1220 (installed: 0.9.1218); - it claims none of its names, so a plugin that can load and registers the same name does not conflict with it.
An installer that resolves dependencies never gets you there; pip install --no-deps or a forced install can. There is no override: upgrade agent-env, or install a version of the plugin that fits. A requirement under an extra, or whose environment marker is false, does not count, and an agent-env with no installed metadata, such as a source tree on sys.path, is not checked. Requirements between plugins are the installer's to check; pip check lists any that are unmet.
Contribute, release, license
CONTRIBUTING.md is the contributor guide: open an issue before anything larger than a bug fix, one logical change per pull request with tests, pull request titles in the form type(scope): summary, an approving review from a code owner (CODEOWNERS) and green CI. AGENTS.md is the repository map and conventions file for contributors and coding agents; CLAUDE.md imports it.
Documentation map
| Document | Covers |
|---|---|
| www.agentenvframework.com/docs | the user guide: environments, artifacts, agents, tasks, the registry and plugins |
packages/agentenv-protocol/README.md |
wire contract, environment server SDK, A2A agent framework |
CONTRIBUTING.md |
development setup, test tiers, CI jobs, pull request rules |
AGENTS.md |
repository map, configuration and extension-point summary, conventions for contributors and coding agents |
SECURITY.md |
private vulnerability reporting and supported versions |
CODE_OF_CONDUCT.md |
Contributor Covenant |
.agentenv/config.example.toml |
the all-local configuration to copy |
.env.example |
a commented reference of the AGENT_ENV_* variables; agent-env never loads this file, export what you need yourself. Its AGENT_ENV_ENVIRONMENT line is read only by an installed plugin, never by agent-env |
Versioning and compatibility
The agentenv-framework distribution and agentenv-framework-protocol are versioned separately (0.9.x and 0.1.x today), both in their pyproject.toml; agent-env releases carry a vX.Y.Z tag, and agentenv-framework-protocol is bumped in the same commit and has no separate tag today. There is no agent_env.__version__ attribute; agent-env --version prints the installed version. Protocol extensions carry their version in the URI (urn:agentenv:clock/v1, urn:agentenv:agent-config/v1, urn:agentenv:trajectory/v1). One version can take more than one request shape: under v1 the skill, trajectory, snapshot and changelog extensions accept object-transfer requests next to their older shapes, and the request field lists on an agent's card say which ones that agent takes. Renamed CLI commands are removed outright; no deprecated aliases exist at this version. What plugins may rely on, and how changes to it are made, is in Plugin compatibility; the plugin commands' --json output has its own format version and rules (Plugin report format). Beyond those, a written compatibility and deprecation policy does not exist yet.
Releases
A release is a version bump in both pyproject.toml files plus a vX.Y.Z tag. Maintainers cut releases: the bump is automated when a labelled pull request merges, so contributors do not edit version or push tags (see the Releases section of CONTRIBUTING.md). Neither package is published to a public index yet, and there is no CHANGELOG.md.
Support, security, license
Report bugs and gaps as issues against this repository, with the installed agentenv-framework version and the sandbox backend in use. Report vulnerabilities privately through the contact in SECURITY.md, not in public issues; only the latest release is supported, so reproduce against it first. Contributors follow CONTRIBUTING.md and the Code of Conduct; every pull request needs a code-owner review. agent-env and agentenv-framework-protocol are licensed under the Apache License 2.0; see LICENSE and NOTICE. Their third-party dependencies and those dependencies' licenses are listed in THIRD_PARTY_NOTICES.md.
Metadata
Release files for agentenv-framework 0.9.1255
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agentenv_framework-0.9.1255.tar.gz | 2.2 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agentenv_framework-0.9.1255-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.3 MB
Release files / agentenv_framework-0.9.1255.tar.gz
| Download URL | agentenv_framework-0.9.1255.tar.gz |
|---|---|
| Size | 2.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3cc2fe26880add32c173278d066b36b037946dab8066a9fd9d9a1d871a12c099
|
|
BLAKE2b-256 checksum How to use checksums |
4be5fd55ad1ca1815c516691056397a6c291f8610d48f89d6739cab8192f7b5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / agentenv_framework-0.9.1255-py3-none-any.whl
| Download URL | agentenv_framework-0.9.1255-py3-none-any.whl |
|---|---|
| Size | 1.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ca3227a9e2c12ebb6f770bfd5503324c0227b8d33a13fc4bead48327b19ecff8
|
|
BLAKE2b-256 checksum How to use checksums |
e6ac4f785a59a76bce4380d37db31c8e3b1f98abf46a9f2506c55a1738a780da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|