Instalar
Un comando, en la máquina donde van a vivir los agentes. No pregunta nada y termina imprimiendo una sola dirección — que es la consola, y la llave para entrar.
Un comando, y no pregunta nada
$ curl -fsSL https://squad.mormon.garden/install.sh | sh
Docker si la máquina no tiene, dos imágenes bajadas en vez de construidas, una configuración con un agente y un techo de cinco dólares al día, y un plano andando. Medio minuto en una máquina que ya tiene Docker.
No pide ninguna clave. Una clave no es algo que necesites antes de que la cosa corra — es lo que este plano puede pagar, cambia, y un plano la acepta mientras está andando. Tres secretos en el primer minuto las hacían parecer requisitos, que es lo único que no son. Se dan en la consola, en la pantalla que existe para eso, y el plano la está usando desde el turno siguiente al que la escribís.
Volver a correrlo es la actualización: baja, cambia el plano, y deja config.yaml intacto junto con cada clave y cada edición de .env. Solo se ponen al día las líneas que dicen qué es esta instalación — qué imágenes, qué dominio — porque un archivo que no coincide con el plano que está corriendo es un docker compose up a mano que arranca el código del mes pasado sin decir nada.
Que no haya nada publicado para bajar no es un error. Un registry caído, un tag que todavía no existe, un fork que no publica nada — el instalador lo dice y construye desde las fuentes, que están ahí mismo. Ese camino es más lento y sigue funcionando, que es justamente para qué se lo mantiene.
El plano sirve su propia consola
Getting in
Open this, or paste it into a console you host yourself:
http://127.0.0.1:8789/?t=MekEy-WJ4RPyWP3PjCEntVGhlJN56bE0uNffl2Obhls
La abrís y la consola está ahí — los agentes, las conversaciones, qué está haciendo cada uno y cuánto gastó. La sirve el plano mismo, desde su propio contenedor, así que no hay nada más que instalar ni nada nuestro entre vos y eso.
Esa dirección es la llave. El plano escribe un token al arrancar, en un archivo 0600 que solo root puede leer, y tenerlo es ser el operador — el protocolo no tiene login propio ni lo quiere. Por eso se pega, no se postea. La página lo cambia por una cookie y limpia la URL, porque una dirección se copia y queda en un historial y una cookie no.
Las claves entran desde ahí: el selector de ambiente, y después Keys. Cada proveedor dice si este plano tiene una y si se la escribió acá o la exportó la máquina, y ninguno muestra nunca un valor — el plano contesta con el nombre de la clave que puso y jamás con la clave. Un plano que no tiene ninguna lo dice arriba de la conversación, porque la alternativa es un turno que se muere en el modelo por un motivo que la pantalla ya sabía.
Hay una copia de la misma consola en https://squad.mormon.garden/app/. Sostiene varios ambientes a la vez y se mueve entre ellos, que es lo único que hace y que la que sirve tu plano no. Es una comodidad, no la puerta: lo que sabe vive en tu navegador y en ningún otro lado, tampoco acá.
Dale un nombre, o reenviá el puerto
El puerto de la consola se publica en el loopback del servidor y en ningún otro lado. Es a propósito: el plano tiene el socket de Docker, así que equivale a root en esa máquina, y publicarlo sería poner root en internet detrás de un token que viaja en texto plano. Hay dos maneras de alcanzarla, y la primera es mejor.
$ curl -fsSL https://squad.mormon.garden/install.sh | sh -s -- --domain=agents.example.com
Apuntá el DNS a la máquina primero, y después corré eso. Un proxy delante del plano obtiene un certificado y lo renueva, y la instalación termina con un solo https://agents.example.com/?t=…. Nada que reenviar, nada que dejar abierto, y funciona desde cualquier navegador.
El proxy es un profile de compose, así que una máquina a la que nunca le dieron un dominio no lo arranca y nunca toma el puerto 80 esperando un certificado que no va a llegar. La exposición del plano no cambia: lo que se publica es el proxy.
Y si no tiene ni dominio ni una terminal que quieras dejar abierta, el plano puede discar para afuera: --relay=https://relay.example.com hace que se encuentre con una consola en un punto de encuentro, que funciona detrás de un NAT porque no se publica nada en ninguna de las dos puntas. Termina imprimiendo un código en vez de una dirección, porque no hay dirección que dar.
Lo que cruza un relay va sellado con una clave que las dos puntas derivan del token de este plano. Al relay se le da un número de sala derivado en un solo sentido de ese mismo token — suficiente para unir dos sockets y no para recuperar nada — así que lleva el tráfico y no puede leerlo. Está apagado salvo que lo pidas, y el que corremos nosotros es el que está en el repositorio, que es la única razón para creer algo de esto.
Sin dominio, un servidor contesta en la dirección que tiene. Ese es el default ahí, porque la alternativa es un puerto reenviado que alguien tiene que sostener. Significa que el token es lo único entre un desconocido y estos agentes, y sobre http cruza internet donde se puede leer — así que la instalación imprime, justo abajo, la línea que lo arregla sin comprar nada:
$ curl -fsSL https://squad.mormon.garden/install.sh | sh -s -- --domain=74-207-235-42.sslip.io
sslip.io resuelve cualquier nombre con forma de dirección a esa dirección, así que Let's Encrypt le emite y no hay nada que registrar. Su límite lo comparte con todos los que lo usan, así que puede negarse donde un nombre propio nunca lo hace — que es toda la diferencia. --open=no devuelve la consola al loopback.
$ ssh -N -L 18789:127.0.0.1:8789 vos@tu-servidor
Eso corrélo en tu propio equipo — no en el servidor, donde ese puerto ya es del plano. Después abrís http://127.0.0.1:18789/?t=… con el token que imprimió el servidor: tu puerto, su llave. Sirve cualquier puerto local libre.
La dirección dice 127.0.0.1 porque, después del reenvío, eso es el servidor. En el servidor no se abre nada — comprobalo allá con ss -ltn — y no hay nada nuevo a lo que loguearse, porque los bytes cruzan la conexión que ya tenías.
Cinco flags, y ningún archivo de configuración que escribir antes
--name=casa | un segundo despliegue en una misma máquina — sus propios contenedores, volúmenes, redes, puertos y agentes |
--domain=agents.example.com | ponelo detrás de ese nombre, con un certificado que renueva solo |
--domain= | devolvé el nombre: se quita el proxy y el plano vuelve a estar en loopback |
--relay=https://relay.example.com | alcanzalo por un punto de encuentro: nada publicado, nada reenviado, sin dominio |
--open=no | dejá la consola en loopback, que es lo que un servidor sin dominio deshace |
--verbose | cada ruta, cada archivo que escribió, y el build en vivo en lugar de guardado |
--build | construí las imágenes acá en vez de bajarlas, que es lo que hace falta para trabajar en el código |
$ curl -fsSL https://squad.mormon.garden/install.sh | sh -s -- --name=casa
Flags y no solo variables de entorno, por la forma en que esto se corre: SQUAD_NAME=casa curl … | sh le pone la variable a curl, y el shell que lee el script nunca la ve. La instalación se va al nombre por defecto y no dice nada, que se parece exactamente a un éxito hasta que hay dos despliegues y el segundo pisó al primero. Un flag cruza el pipe. Las variables siguen funcionando cuando no hay pipe.
Un segundo despliegue no comparte nada con el primero salvo el demonio de Docker — sus propios contenedores, volúmenes, redes, estado y agentes, en puertos derivados de su nombre. SQUAD_VERSION fija qué build publicado correr; si no lo tocás, es el release más nuevo.
La máquina cuesta cinco dólares al mes
Un vCPU, un gigabyte de memoria y diez de disco alcanzan para unos cuantos agentes, y eso es lo más barato de la lista de cualquier proveedor. Necesita un Linux con SSH y nada más — el instalador trae Docker. Una laptop vieja abajo del escritorio también sirve.
| Hetzner | más máquina por el dinero — unos 4,50 € compran dos núcleos y 4 GB, si te sirve una región europea o de EE. UU. |
| Vultr | desde unos $5, y en más sitios que los otros dos juntos |
| DigitalOcean | unos dólares más, y sobre el que más se ha escrito — vale la pena si es tu primer servidor |
Los precios se mueven; la forma de la cuenta no. La máquina es un número fijo al mes, y el único otro costo es aquello con lo que piensan los agentes — medido por el proveedor de modelo al que le des una clave, y limitado por limitUsd a cinco dólares al día por agente. Por squad no se paga nada.
Un gigabyte alcanza porque la instalación baja en vez de construir. Construir en esa máquina significa un árbol de dependencias y un bundler corriendo ahí, que en el fondo de la lista es un build muerto por memoria — y muerto en el medio, que es el peor lugar donde una instalación puede pararse.
Decí qué puede alcanzar un agente
/opt/squad/deploy/config.yaml es toda la superficie: los agentes, qué puede alcanzar cada uno, qué modelos hay para pensar, cuándo despierta cada cual, qué webhooks existen, y — bajo defaults — de qué parte un agente creado después en el teclado. El instalador escribe uno que ya funciona; esta es su forma.
models:
- id: deepseek-v4-flash # naming the provider says the rest
provider: deepseek
- id: sonnet
provider: anthropic
model: claude-sonnet-4-6
- id: gpt-5
provider: openai
defaults:
model: deepseek-v4-flash # /model moves one agent onto another
limitUsd: 5 # dollars a day, reset at midnight UTC
grants:
- id: web # the road: npm, PyPI, git, anywhere
host: "*"
injection:
kind: none # and no key of yours goes down it
- id: search
host: api.openai.com
pathPrefix: /v1/responses # the one endpoint that searches
methods: [POST]
injection:
kind: bearer
token: { ref: OPENAI_API_KEY }
Qué se puede alcanzar y qué se puede gastar son dos preguntas, y solo la primera se contesta "en cualquier lado". Un grant sobre * que llevara una credencial se rechaza cuando se lee el archivo: el camino está abierto, las claves se dan a un lugar por su nombre. Borrá el grant web y el plano vuelve a negar por defecto, host por host.
No hay ningún secreto adentro. Nombra variables de entorno y el proceso tiene los valores, así que el archivo que describe qué puede alcanzar un agente se puede commitear y diffear — un grant que nadie notó que se agregaba es el modo en que esto falla.
Los tres modelos aparecen tenga o no este plano sus claves, porque listar uno es la aprobación y la clave es solo lo que lo hace contestar. Keys en la consola es donde se pega una, y rige desde el turno siguiente sin reiniciar nada y sin tocar este archivo.
Se lee cuando el plano arranca, así que una edición toma efecto con docker compose restart control-plane desde /opt/squad/deploy.
El techo ya está puesto. Dejalo. Un agente puede agendar su propio turno siguiente, así que sin limitUsd lo primero que alguien sabe de un bucle es la factura. Está bajo defaults para que también cubra a los agentes creados después en el teclado, que son justo los que nadie se acuerda de limitar.
Despertar a un agente con un webhook
El puerto 8787 es lo único publicado a la red, y solo acepta pedidos firmados. El instalador genera el secreto y lo pone en .env como HOOK_SECRET. La firma cubre ${timestamp}.${body} y se compara en tiempo constante dentro de una ventana de frescura; un id de hook desconocido contesta exactamente igual que una firma mala, y solo después de leer el cuerpo, para que el endpoint no se pueda enumerar.
BODY='{"text":"the nightly build failed"}'
TS=$(date +%s)
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$HOOK_SECRET" -r | cut -d' ' -f1)"
curl -X POST https://your-vps:8787/hooks/ping \
-H "x-squad-timestamp: $TS" \
-H "x-squad-signature: $SIG" \
-d "$BODY"
Un webhook no puede llevar confianza de operador, por bien firmado que esté. El secreto prueba qué sistema mandó el pedido, nunca que una persona quiso decir lo que hay adentro — así que el cuerpo llega vallado, como datos. Los eventos se encolan por agente y se juntan en un turno, y un turno que falla deja sus eventos encolados en vez de confirmarlos, así que una clave mala cuesta un reintento y no el mensaje.
Las otras dos puertas no necesitan publicar nada, porque salen en vez de ser alcanzadas: /telegram <token> conecta un bot al agente que estás mirando, y /email <dirección> conecta un buzón a todos los agentes del plano. Las dos se aparean a una persona con una frase, y las dos pueden instruir una vez apareadas.
La consola también fue siempre un comando
El mismo plano, el mismo protocolo, otra pantalla. El instalador deja squad en el PATH de la máquina donde corrió, y esto pone la misma consola en el equipo ante el que estás sentado — donde pregunta qué plano manejar y se guarda la respuesta.
$ curl -fsSL https://squad.mormon.garden/client.sh | sh
$ squad
Necesita Node 22.18 o más nuevo y nada más — ningún Docker, esté donde esté el plano. A un plano en un servidor se lo alcanza por ssh vps squad relay, que es el mismo protocolo sobre la conexión que ya tenés, así que tampoco para esto se abre nada allá.
/serve 3000 es esa conexión leída al revés: la red del sandbox no rutea, así que la consola abre el puerto en tu loopback y escribe http://scout.localhost:3000 — un enlace que funciona en la máquina donde se imprimió y en ninguna otra, y que se cierra cuando cerrás la consola.
O probá todo esto en tu equipo
La demo construye las imágenes, levanta un plano de control en una red descartable, muestra qué puede y qué no puede alcanzar el agente, lo despierta con un webhook firmado e imprime el turno. Pide una clave de modelo cuando llega a la parte que la necesita.
$ git clone https://github.com/mormonnegro/squad
$ cd squad
$ ./deploy/demo.sh up
./deploy/demo.sh down borra los contenedores, las redes, el volumen y el estado. La única diferencia con un despliegue real es dónde vive el estado: bajo el árbol de trabajo, porque /var/lib necesita root y no está compartido con Docker Desktop en macOS.
La misma instalación, a mano
El repositorio, los dos archivos que el instalador habría escrito por vos, y un up. Todo lo de arriba sigue aplicando — esto es solo la parte que baja y arranca.
$ git clone https://github.com/mormonnegro/squad /opt/squad && cd /opt/squad/deploy
$ cp .env.example .env # ports, the webhook secret, the origins
$ cp config.example.yaml config.yaml # what each agent may reach
$ docker compose up -d
SQUAD_IMAGE y SQUAD_SANDBOX_IMAGE en .env dicen qué imágenes correr; si no las tocás son las construidas localmente, y docker compose build las hace. Apuntalas a las publicadas para saltear el build.
config.example.yaml es la referencia, con cada opción comentada, y su agente de ejemplo alcanza hosts que no son tuyos — leelo entero antes de arrancar y no después. Sin el instalador tampoco hay squad en el PATH, así que la consola es docker compose exec control-plane squad.
Dos cosas son estructurales
- El plano de control corre en la red de los agentes, no en el host. Los contenedores de una red interna no pueden alcanzar el host de ninguna manera, así que un proxy en el host es uno que los agentes no pueden usar.
- El directorio de estado se monta en su propia ruta. El plano le pasa esa ruta al demonio cuando monta la CA en un sandbox, y el demonio resuelve los orígenes del bind en el host, así que una ruta de contenedor más cómoda produce montajes que el demonio no encuentra.