# The Environment Card (https://www.agentenvframework.com/docs/environments/environment-card)

> An Open Protocol for Environments

## The Open Protocol for Using RL Environments

```python title="email/server.py"
import json
from pathlib import Path

from agentenv_protocol import (
    AgentEnvEnvironment,
    DataPart,
    FilePart,
    add_data,
    environment_card,
    get_data,
    reset_data,
    tool,
)


@environment_card(name="email")
class EmailEnv(AgentEnvEnvironment):
    def __init__(self) -> None:
        self.inbox: list[dict] = []
        self.sent: list[dict] = []

    @reset_data
    async def _reset(self) -> None:
        self.inbox.clear()
        self.sent.clear()

    @add_data
    async def _add(self, parts: list) -> None:
        for part in parts:
            if isinstance(part, DataPart):
                self.inbox.extend(part.data.get("inbox", []))
                self.sent.extend(part.data.get("sent", []))
            elif isinstance(part, FilePart):
                seed = json.loads(Path(part.file.uri.removeprefix("file://")).read_text())
                self.inbox.extend(seed.get("inbox", []))
                self.sent.extend(seed.get("sent", []))

    @get_data
    async def _state(self) -> list:
        return [DataPart(data={"inbox": self.inbox, "sent": self.sent})]

    @tool(name="{environment_name}_search")
    async def search(self, query: str) -> dict:
        """Find inbox emails whose subject or body contains the query."""
        q = query.lower()
        return {"emails": [e for e in self.inbox if q in (e["subject"] + " " + e["body"]).lower()]}

    @tool()
    async def send(self, to: str, subject: str, body: str) -> dict:
        """Send an email."""
        self.sent.append({"to": to, "subject": subject, "body": body})
        return {"sent": len(self.sent)}


if __name__ == "__main__":
    EmailEnv().serve()
```

```json title="GET /.well-known/agent-env.json"
{
  "name": "office",
  "protocolVersion": "1.0",
  "url": "/agentenv",
  "preferredTransport": "JSONRPC",
  "additionalInterfaces": [
    {
      "url": "/mcp",
      "transport": "mcp"
    }
  ],
  "capabilities": {
    "extensions": [
      { "uri": "urn:agentenv:disable-tool/v1", … },
      { "uri": "urn:agentenv:enable-tool/v1", … },
      { "uri": "urn:agentenv:triggers/v1", … },
      { "uri": "urn:agentenv:clock/v1", … },
      { "uri": "urn:agentenv:trajectory/v1", … },
      { "uri": "urn:agentenv:step/v1", … },
      { "uri": "urn:agentenv:state/v1", … }
    ]
  },
  "children_environments": [
    {
      "name": "email",
      "protocolVersion": "1.0",
      "url": "/svc/mcp-email/agentenv",
      "preferredTransport": "JSONRPC",
      "additionalInterfaces": [],
      "capabilities": {
        "extensions": null,
        "tools": [
          {
            "name": "email_search",
            "description": "Find inbox emails whose subject or body contains the query.",
            "inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
          },
          {
            "name": "send",
            "description": "Send an email.",
            "inputSchema": {"type": "object", "properties": {"to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}}, "required": ["to", "subject", "body"]}
          }
        ],
        "operations": [
          "data/reset",
          "data/add",
          "data/get"
        ]
      },
      "children_environments": null
    },
    { "name": "contacts", … }
  ]
}
```

Every environment describes itself in a small JSON document, its environment card, served at
`/.well-known/agent-env.json`. The card says which tools an environment has, how its state is reset,
loaded and read, and which extensions it supports, so the framework can drive any environment that
serves one without code written for it.

## What's on the card

`AgentEnvEnvironment` builds the card from your class, so you rarely write one by hand:

| On the card                 | From the class                                                                         |
| --------------------------- | -------------------------------------------------------------------------------------- |
| `name`                      | `ENVIRONMENT_NAME`, then `@environment_card(name=...)`, then the class name            |
| `url`, `preferredTransport` | the data plane, JSON-RPC at `/agentenv`                                                |
| `additionalInterfaces`      | the MCP endpoint, `/mcp`, where agents call the tools                                  |
| `capabilities.tools`        | each `@tool` method, with its docstring and input schema                               |
| `capabilities.operations`   | `@reset_data`, `@add_data` and `@get_data`, as `data/reset`, `data/add` and `data/get` |
| `capabilities.extensions`   | each `@extension` method, with its URN and the route that serves it                    |
| `children_environments`     | the servers' cards, when the environment is composed                                   |

Every deploy passes the name the env was registered with in `ENVIRONMENT_NAME`, so the card, the
stored env and the tool prefixes agree.

## Extensions

An extension is a URN on the card with the route and methods that serve it, such as
`urn:agentenv:set-errors/v1` for forced errors. The framework looks one up by URN and method before
calling it: a step that needs one the card doesn't list stops before sending anything, or skips that
environment when it's told to tolerate the gap.

The `urn:agentenv:` extensions are the ones the framework's own steps call. Any other URN works too, for
extensions your own tasks call.

## Env Topology Extensions

An env's topology can provide extensions out of the box, on top of the ones your class implements. The
[gateway topology](https://www.agentenvframework.com/docs/environments/gateway-topology.md), the default, puts the gateway's own card in
front of your server's, with per-role tool access, triggers and a virtual clock already on it; the
`server` [topology](https://www.agentenvframework.com/docs/environments/topology.md) serves your server's card alone.

## Get an Env Instance's Card

A deploy stores the instance's card on its record, which outlives the instance:

```python title="Python"
import json

from agent_env.env import get_env_instance_store

record = get_env_instance_store().get("email-mmtflq6t")
print(json.dumps(record.environment_card, indent=2))
```

From then on, task steps find what they need on that stored copy, and a few, such as the card validator,
fetch a fresh one.