Ceryx logo
Menu
View as Markdown

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:

*.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:

# 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:

{
  "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 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. The OpenAPI spec describes the whole API in one file.

Esc

Type to search the docs.