# Agent environments

Let agents publish the services they start on real hostnames, via the Ceryx API.

When an agent starts a web service, for a demo, a test or a whole environment, Ceryx can put it on a real hostname with one API call. Every service gets its own fully qualified domain name, such as `demo-42.agents.example.com`, served over HTTPS as soon as the route exists.

## 1. Prepare a domain for agents

Point a wildcard DNS record at the server, and store a wildcard certificate for it, as in steps 1 and 2 of [Preview deployments](/docs/use-cases/preview-deployments#1-point-a-wildcard-dns-record-at-the-server):

```text
*.agents.example.com.  A  203.0.113.10
```

## 2. Give agents a safe way in

The Ceryx API has no authentication, and anyone who can reach it can change every route. Don't give agents the API itself. Give them a small service of your own that only allows the changes they need.

This small Python service uses the standard library. It lets agents publish a container under `agents.example.com` and nothing else. Run it on the `ceryx` network, where it can reach both the API and the agents' containers:

```python
# publish_service.py
import json
import os
import re
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

CERYX_API = "http://ceryx:8080/api/routes"
DOMAIN = "agents.example.com"
TOKEN = os.environ["PUBLISH_TOKEN"]


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.headers.get("Authorization") != f"Bearer {TOKEN}":
            return self.send_error(403)

        try:
            length = int(self.headers.get("Content-Length", 0))
            if length > 4096:
                return self.send_error(413)
            data = json.loads(self.rfile.read(length))
            name, port = data["name"], data["port"]
            if not re.fullmatch(r"[a-z0-9-]{1,40}", name) or type(port) is not int or not 1 <= port <= 65535:
                return self.send_error(400)
        except (ValueError, KeyError, TypeError):
            return self.send_error(400)

        route = {
            "source": f"{name}.{DOMAIN}",
            "target": f"http://{name}:{port}",
            "settings": {"mode": "proxy", "enforce_https": True},
        }
        try:
            request = Request(CERYX_API, data=json.dumps(route).encode(), headers={"Content-Type": "application/json"})
            with urlopen(request, timeout=5):
                pass
        except (HTTPError, URLError):
            return self.send_error(502)

        body = json.dumps({"url": f"https://{route['source']}"}).encode()
        self.send_response(201)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_DELETE(self):
        if self.headers.get("Authorization") != f"Bearer {TOKEN}":
            return self.send_error(403)
        name = self.path.removeprefix("/")
        if not re.fullmatch(r"[a-z0-9-]{1,40}", name):
            return self.send_error(400)
        try:
            request = Request(f"{CERYX_API}/{name}.{DOMAIN}", method="DELETE")
            with urlopen(request, timeout=5):
                pass
        except (HTTPError, URLError):
            return self.send_error(502)
        self.send_response(204)
        self.end_headers()


HTTPServer(("0.0.0.0", 3000), Handler).serve_forever()
```

Send `POST /` with `{"name": "demo-42", "port": 3000}` to publish a service, or `DELETE /demo-42` to remove it. Both requests require `Authorization: Bearer <PUBLISH_TOKEN>`.

## 3. Describe the tool to your agent

Give your agent a tool that calls the service. Most agent frameworks take a JSON Schema for the input:

```json
{
  "name": "publish_service",
  "description": "Publish a running container on a public HTTPS URL. The container must be on the ceryx Docker network. Returns the URL.",
  "input_schema": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "description": "The container name: lowercase letters, digits and hyphens, up to 40 characters. Also becomes the subdomain."
      },
      "port": {
        "type": "integer",
        "description": "The port the service listens on inside the container."
      }
    },
    "required": ["name", "port"]
  }
}
```

For agents you fully trust, such as one managing your own infrastructure, you can describe the Ceryx API directly with its [OpenAPI spec](/docs/api/openapi.yaml) instead.

## 4. Clean up

Remove routes when environments end, so hostnames don't point at stopped containers. A hostname whose container is gone answers `502 Bad Gateway` until its route is deleted.

## Let agents read these docs

Agents can read every page of these docs as Markdown: add `.md` to its URL, for example [/docs/api/routes.md](/docs/api/routes.md). The [OpenAPI spec](/docs/api/openapi.yaml) describes the whole API in one file.
