# Choose the environment's topology (https://www.agentenvframework.com/docs/environments/topology)

> The shapes an env instance can take, from the default gateway to a server on its own or a topology of your own

1. One `email/` folder, registered three times: each env stores the topology its `--env-provider-type` names.
2. Each deploy builds its topology’s shape: a gateway and a database, your server alone, or whatever your provider runs.
3. Each instance serves the card its topology makes, and only the gateway’s lists the gateway’s extensions.
4. The same `email_search` call reaches your server through the gateway, straight, or through whatever your provider puts in front.

Parts of the scene:

- **The gateway topology**: The default. A gateway fronts your server and adds its extensions: a virtual clock, triggers, a log of tool calls, `/step`, per-role tool access and a read of the state. A Postgres database sits beside them.
- **The server topology**: Your server alone in one container, with no gateway and no Postgres; it keeps its state in a SQLite file. It must serve its own card, named after the env, and at least one tool.
- **A topology of your own**: An `EnvironmentProvider` you write and install: its `deploy` starts the instance however you choose and returns its record, and its `close` stops it. Its type is the name you pass to `--env-provider-type`.
- **The sandbox**: Where agent-env runs the instance’s containers: your machine’s Docker unless `--sandbox` names another. The built-in topologies deploy into it.
- **Three envs**: Each is its own env, put from the same `email/` folder with a different `--env-provider-type`, which the env stores. Every deploy of an env takes the topology it stores.
- **An agent**: Connects to the instance’s MCP URL, which its card gives. Its calls reach whatever the topology puts first: the gateway, your server itself, or what your provider puts in front.
- **Your server**: The same `EmailEnv` in all three. Its code does not change with the topology; only what runs around it does.
- **The gateway**: Stands in front of your server: agents reach its tools through the gateway’s `/mcp`, and it records every call in the instance’s trajectory.
- **The state database**: Postgres, with a web UI and an MCP server of its own. Your server gets a schema of its own in it through `DATABASE_URL`, and the gateway reads it too.
- **Whatever your provider runs**: The containers your provider starts around your server, such as a proxy, a sidecar or nothing at all. The framework needs only the record it returns and the card the instance serves.
- **Each instance’s card**: Its topology makes it: the gateway’s card, named at random, with your server’s as a child; your server’s own card; or whatever your provider serves, named after the env or listing a child of that name.

Every instance on [Deploying your environment](https://www.agentenvframework.com/docs/environments/deploying.md) ran behind a
gateway. That is the default topology, not the only one. An env's topology is the shape of its
instances: which containers run and what stands in front of your server. An `EnvironmentProvider`
builds that shape, and the env records which one when you register it, as `env_provider_type` in
its stored document. The built-in providers are `gateway` and `server`, and you can write your
own. The animation above shows the same env under different topologies.

## The gateway topology

`gateway` is what `agent-env env mcp-server put` registers unless you say otherwise. It runs a
gateway in front of your server, with a Postgres database next to them, and the gateway adds its own
extensions to the instance. [Environment gateway topology](https://www.agentenvframework.com/docs/environments/gateway-topology.md)
covers what it builds.

## The server topology

`server` runs your server on its own. To register `EmailEnv` in the `server` topology:

```bash title="Terminal"
agent-env env mcp-server put --id email-server \
  --dockerfile email/Dockerfile --env-provider-type server
agent-env env deploy --id email-server
```

```text title="Output (end)"
Deployed!
Instance ID: email-server-s4x3fduv
Env MCP Url: http://localhost:61761/mcp
Expires At (UTC): 2026-09-29 09:24 UTC
```

There is no gateway URL and no Postgres database, and agents connect straight to your server. Its
card is the one the instance serves, so it must be named after the env, and the server must list at
least one tool.

## Your own topology

A topology of your own is an `EnvironmentProvider` subclass with a `type`, a `deploy` that starts
the instance and returns its record, and a `close` that stops what `deploy` started. This example,
`k8s`, runs each instance on a Kubernetes cluster instead of in an agent-env sandbox:

1. `agent-env env deploy` reads `k8s` from the env and builds your `K8sProvider`, which creates a namespace for the instance.
2. It applies the manifests there: a pod running your server’s image, a volume for its state, and a Service and an Ingress in front.
3. It polls the card through the ingress until your server answers, and returns it on the instance’s record.
4. An agent reaches your server through the ingress and the Service, as any client reaches any instance.
5. `env.close()` runs the provider’s `close()`, which deletes the namespace and everything in it.

Parts of the scene:

- **The commands**: `agent-env env deploy --id email-k8s` deploys the env like any other. `await env.close()`, on the env object that deployed it, runs your provider’s `close()`.
- **agent-env**: Reads the env’s `env_provider_type`, `k8s`, builds the provider installed under that name through the `agent_env.env_providers` entry point, and hands it the env and the deploy’s options.
- **Your K8sProvider**: Your code, from `k8s_topology/provider.py`. It creates the namespace, applies the manifests with `kubernetes_asyncio`, and returns a `DeployedEnv` carrying the card it read.
- **Your cluster**: Any Kubernetes cluster your kubeconfig reaches. It pulls your server’s image from agent-env’s image store, so it needs access to that store. The provider ignores `--sandbox`.
- **The namespace**: One per instance, named after the env plus eight random characters. Everything the deploy creates lives in it, so deleting it tears the instance down.
- **The Ingress**: Routes every path under the instance’s host, `/mcp`, `/agentenv` and the card among them, to the Service. Its host is the URL the provider records.
- **The Service**: Sends port 80 to port 18765 on the pod, where your server’s `serve()` listens.
- **The pod**: The Deployment’s one replica, running your server’s image with `ENVIRONMENT_NAME` set, so the card it serves is named after the env. One replica, because its state lives on one volume.
- **The volume**: A PersistentVolumeClaim mounted at `/data`, where your server keeps its SQLite file through `DATABASE_URL`, so its state survives a restart of the pod.
- **The recorded card**: The card the provider read through the ingress, stored on the instance’s record like any other: your server’s own, named `email`, at the ingress’s host.
- **An agent**: Connects to the MCP URL the record’s card gives, on the ingress’s host, and reaches your server through the Service like any other client.

Every deploy gets a namespace of its own, with your server in a pod, a volume for its state, and a
Service and an Ingress in front:

```yaml title="k8s_topology/instance.yaml"
apiVersion: apps/v1
kind: Deployment
metadata: {name: server}
spec:
  replicas: 1
  selector: {matchLabels: {app: server}}
  template:
    metadata: {labels: {app: server}}
    spec:
      containers:
        - name: server
          image: $image
          ports: [{containerPort: 18765}]
          env:
            - {name: ENVIRONMENT_NAME, value: $environment_name}
            - {name: DATABASE_URL, value: "sqlite:////data/state.sqlite"}
          volumeMounts: [{name: state, mountPath: /data}]
      volumes: [{name: state, persistentVolumeClaim: {claimName: state}}]
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: state}
spec: {accessModes: [ReadWriteOnce], resources: {requests: {storage: 1Gi}}}
---
apiVersion: v1
kind: Service
metadata: {name: server}
spec: {selector: {app: server}, ports: [{port: 80, targetPort: 18765}]}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: server}
spec:
  rules:
    - host: $namespace.envs.example.com
      http:
        paths: [{path: /, pathType: Prefix, backend: {service: {name: server, port: {number: 80}}}}]
```

The provider fills in the env's image and `environment_name`, applies the manifests with the
Kubernetes client, and records the card the ingress serves. `wait_for_card` stands for your own code
that polls `/.well-known/agent-env.json` until the pod answers:

```python title="k8s_topology/provider.py"
import secrets
from pathlib import Path
from string import Template

import yaml
from kubernetes_asyncio import client, config, utils

from agent_env.env.env import DeployedEnv
from agent_env.providers.env_providers import EnvironmentProvider

MANIFESTS = Template((Path(__file__).parent / "instance.yaml").read_text())


class K8sProvider(EnvironmentProvider):
    type = "k8s"

    async def deploy(self, env, sandbox_provider, **options) -> DeployedEnv:
        self.namespace = f"{env.id}-{secrets.token_hex(4)}"
        manifests = MANIFESTS.substitute(
            image=env.docker_image_artifact.image_name,
            environment_name=env.environment_name,
            namespace=self.namespace,
        )
        await config.load_kube_config()
        async with client.ApiClient() as api:
            namespace = client.V1Namespace(metadata=client.V1ObjectMeta(name=self.namespace))
            await client.CoreV1Api(api).create_namespace(namespace)
            for manifest in yaml.safe_load_all(manifests):
                await utils.create_from_dict(api, manifest, namespace=self.namespace)
        url = f"https://{self.namespace}.envs.example.com"
        return DeployedEnv(env_id=env.id, env_version=env.version, env_provider_type=self.type,
                           environment_card_url=f"{url}/.well-known/agent-env.json",
                           environment_card=await wait_for_card(url))

    async def close(self) -> None:
        await config.load_kube_config()
        async with client.ApiClient() as api:
            await client.CoreV1Api(api).delete_namespace(self.namespace)
```

It ignores `sandbox_provider`, because the cluster is where the instance runs, and the cluster must
be able to pull from your image store. The card it records is your server's own, named after the
env as every record's card must be. agent-env's reapers find only its own sandboxes, so ending a
namespace at `ttl_seconds` is the provider's job too, for example with a label that a reaper in the
cluster reads.

You install the provider through an `agent_env.env_providers` entry point named after its `type`,
and pass that name to `--env-provider-type`:

```toml title="pyproject.toml"
[project.entry-points."agent_env.env_providers"]
k8s = "k8s_topology.provider:K8sProvider"
```

```bash title="Terminal"
agent-env env mcp-server put --id email-k8s \
  --dockerfile email/Dockerfile --env-provider-type k8s
```

[Environment gateway topology](https://www.agentenvframework.com/docs/environments/gateway-topology.md) covers the default topology in
depth.