
GhostyCode y goose traen servidor de red: los levantas y ya hablan ACP por WebSocket. Los otros cincuenta agentes del registro oficial —Gemini CLI, Codex, Copilot, cline, qwen-code— no. Y no es descuido de nadie: el transporte de red de ACP sigue siendo un RFD en borrador. La especificación estable define un solo transporte, stdio, con el cliente lanzando al agente como proceso hijo.
Cruzando la red no hay padre ni hijo. Ochenta líneas de Node restituyen esa relación: son el padre del agente de un lado y el WebSocket del otro. Con eso, cualquier agente del registro entra al chat de tu equipo.
Vamos a montarlo con Gemini CLI dentro de una microVM y a conectarlo a Ghosty Teams.
El adaptador
Guarda esto como adaptador.mjs. Node trae WebSocket cliente desde la v22, pero no servidor, así
que la única dependencia es ws.
Primero el servidor y quién puede entrar:
/health va sin credencial porque es lo que consulta quien da de alta la caja: sirve para saber que
llegaste al lugar correcto antes de autenticarte. El token se comprueba en el upgrade, que es un
GET y por tanto CORS no lo cubre.
Después el cruce. Cada conexión levanta su propio agente:
Ese búfer de pendiente es obligatorio. stdout llega en pedazos arbitrarios, y sin acumular hasta
el salto de línea vas a partir un mensaje a la mitad y mandar JSON roto por el socket. Con mensajes
cortos parece que funciona; se rompe cuando el agente empieza a producir respuestas largas.
El stderr no es del protocolo: va al log del adaptador y nunca al socket.
Y ahí está la trampa que cuesta una tarde: agente.kill() mata el proceso, no su descendencia. Los
CLI de agentes suelen ser un script que arranca otros procesos, y gemini deja dos vivos. Con
detached: true el agente estrena grupo propio, y el - de process.kill(-pid) mata el grupo
entero. El SIGKILL de respaldo es para el agente ocupado que tarda en atender el SIGTERM, o que no
lo atiende nunca.
Sin eso quedan dos procesos por cada conversación, y en una caja que vive semanas se comen la memoria sin que nadie sepa por qué.
¿Y si un solo agente atendiera a todos?
Cada conexión levanta su propio proceso. La otra opción es compartir uno, y cuesta más de lo que parece.
Cada mensaje de ACP lleva un número para saber qué respuesta corresponde a qué pregunta. El problema
es que cada cliente empieza a contar desde 1. Si dos hilos escriben al mismo agente, los dos mandan
su id 1, y cuando llega la respuesta —también con id 1— nadie sabe a quién devolvérsela.
Se resuelve con una tabla: renumeras cada mensaje que entra, guardas de quién era, y traduces de vuelta al salir. Unas treinta líneas más, contando que los permisos viajan al revés —del agente al cliente— y también hay que enrutarlos.
Ganas un proceso en vez de N. Pierdes el aislamiento: un solo agente es un solo workspace para todos los que le escriban, y si se cae, se cae para todos.
Por eso aquí va uno por conexión: se cierra el socket, muere el proceso, y no hay nada que desenredar.
Montarlo en la caja
Nunca entras a la caja: /exec corre un comando adentro y devuelve su salida, /bg deja algo
corriendo. Necesitas una llave de EasyBits de
Dashboard → Developer y una de
Google AI Studio para el modelo.
Una caja, con vida suficiente para trabajar:
Nace con 30 minutos, así que ese extend a cuatro horas va antes de invertirle trabajo. El detalle
de cada llamada está en el post anterior.
Dentro: el agente, la dependencia del adaptador, y el directorio donde va a trabajar.
/data/work no es capricho: es el cwd con el que Ghosty Teams abre la sesión, y Gemini valida que
exista. Si falta, el primer turno muere con Directory does not exist: /data/work. El LEEME.txt es
para tener algo que pedirle; en su lugar puedes clonar un repo si quieres darle trabajo de verdad.
Ahora el archivo del adaptador. Sin SSH y sin scp, viaja en el mismo /exec, codificado en base64
para que ni las comillas ni los saltos de línea peleen con el JSON:
Ese node --check comprueba que el archivo llegó entero. Un base64 truncado produce un error de
sintaxis al arrancar que parece cualquier otra cosa. (Si vas a editar adentro seguido, EasyBits
también abre SSH con sandbox_ssh_enable, en plantillas que declaren el puerto 22.)
Gemini CLI dentro de una caja va por llave: su login con Google abre un navegador, y ahí no hay
ninguno. Lo levantas con /bg, que es la única forma de dejar algo corriendo — un nohup … & dentro
de /exec muere con el comando que lo lanzó:
Y a la calle:
El token no se prueba con curl: la puerta está en el upgrade, que curl no hace. Se ve al abrir el
socket, y sin la cabecera el cliente recibe un 401 crudo en vez de una conexión.
Conectarlo a Ghosty Teams
En Ajustes → Agentes → Agregar agente → Por ACP va la misma URL con esquema de socket y la ruta
del protocolo — echo "${CAJA/https/wss}/acp" te la imprime lista para pegar—, el $ACP_TOKEN en su
campo, un @handle y listo.
Al darle Probar, Teams abre el WebSocket y hace initialize — el mismo handshake del primer
mensaje real — y te muestra lo que el agente responde de sí mismo:
Si ves eso, el circuito completo funciona. Escríbele en un hilo y contesta desde la caja, con las herramientas del espacio a la mano.
El modelo vive en la sesión
Gemini CLI no declara modelSelection en sus agentCapabilities, así que parece que el modelo no se
elige. Pero la respuesta de session/new trae un catálogo:
El default es auto: el CLI decide por turno entre el pro y el flash. Cuál te tocó lo dice el
resultado del session/prompt, que viene con la cuenta:
Para fijarlo, una bandera más al final del /bg — el adaptador pasa los argumentos tal cual al
agente. Tumbas el proceso viejo y levantas con el modelo que quieras:
Ojo con ese pkill: el patrón lleva corchetes a propósito. pkill -f adaptador.mjs mataría también
al shell que /exec levanta para correrlo, porque su propia línea de comando contiene esa cadena —
se suicida antes de hacer nada, y en absoluto silencio.
Importa si el agente va a buscar en la web: el grounding de la familia 2.5 cuesta $35 por cada 1,000
búsquedas contra $14 de la 3.x, y con auto no eliges cuál te toca.
Lo que este adaptador no hace
Ochenta líneas alcanzan para un agente por conexión y poco más. Faltan cosas que un servidor de
verdad sí trae: multiplexar sesiones sobre un proceso, límites de tamaño de frame, reconexión con
reanudación de sesión, y el Acp-Connection-Id que el RFD define para identificar conexiones.
Justo eso implementa el SDK oficial (agent-client-protocol-http), y por eso GhostyCode y goose no
necesitan adaptador: el servidor ya viene adentro. Si el agente te da igual y lo que quieres es el
montaje sólido, empieza por ahí y sáltate este archivo:
- GhostyCode en una caja, conectado a Ghosty Teams
—
ghosty servepublica ACP en red por su cuenta, con token y/healthincluidos. - El mismo agente, conectado a VS Code
— con goose y su
goose serve, por si tu cliente es el editor y no el chat.
Este adaptador es para el resto: los cincuenta agentes que hablan solo stdio y que, mientras el RFD no aterrice, necesitan a alguien que les preste el cable. El día que lo adopten, el archivo se borra.

Integrando MongoDB Vector Search con Effect-TS: Una Guía para Principiantes
Checa este otro Post

