Choose the environment's topology
The shapes an env instance can take, from the default gateway to a server on its own or a topology of your own
Every instance on Deploying your environment 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
covers what it builds.
The server topology
server runs your server on its own. To register EmailEnv in the server topology:
agent-env env mcp-server put --id email-server \
--dockerfile email/Dockerfile --env-provider-type server
agent-env env deploy --id email-serverDeployed!
Instance ID: email-server-s4x3fduv
Env MCP Url: http://localhost:61761/mcp
Expires At (UTC): 2026-09-29 09:24 UTCThere 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:
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:
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:
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:
[project.entry-points."agent_env.env_providers"]
k8s = "k8s_topology.provider:K8sProvider"agent-env env mcp-server put --id email-k8s \
--dockerfile email/Dockerfile --env-provider-type k8sEnvironment gateway topology covers the default topology in depth.
Last updated on