Transporte de Socket RPC
El daemon expone una interfaz JSON-RPC 2.0 sobre un flujo IPC local, un socket de dominio Unix en Unix y un named pipe en Windows. Este es el transporte principal para clientes locales como zerocode. El gateway HTTP/WS se mantiene para webhooks, el panel de control web y los consumidores REST remotos.
Resolución de endpoint
Cada directorio de datos obtiene su propio endpoint, por lo que múltiples instancias del daemon en la misma máquina no colisionan. El directorio de datos se deriva del directorio de configuración (--config-dir / ZEROCLAW_CONFIG_DIR, o ZEROCLAW_DATA_DIR).
| SO | Endpoint predeterminado |
|---|---|
| Linux | <data_dir>/daemon.sock (socket de dominio Unix) |
| macOS | <data_dir>/daemon.sock (socket de dominio Unix) |
| Windows | \\.\pipe\zeroclaw-<hash> donde <hash> se deriva de data_dir |
Anula esto con la variable de entorno ZEROCLAW_SOCKET en cualquiera de las plataformas:
sh
export ZEROCLAW_SOCKET=/tmp/my-zeroclaw.sock
zeroclaw daemon
PowerShell
$env:ZEROCLAW_SOCKET = '\\.\pipe\my-zeroclaw'
zeroclaw daemon
Protocolo de cable
NDJSON (JSON delimitado por saltos de línea). Cada línea es un mensaje JSON-RPC 2.0 completo. Sin entramado HTTP, sin prefijo de longitud. El entramado es idéntico en todas las plataformas; las canalizaciones con nombre transportan el mismo flujo de bytes que los sockets de Unix.
{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1},"id":1}\n
{"jsonrpc":"2.0","result":{"protocolVersion":1,"serverVersion":"0.8.6"},"id":1}\n
Límites de conexiones
El endpoint local limita lo que un cliente puede mantener:
| Límite | Valor | Lo que ve el cliente |
|---|---|---|
| Tamaño del marco | 8 MiB por línea | Un error -32600 con id: null y data: {"reason": "frame_too_large", "limit_bytes": 8388608}, seguido del fin del flujo. El resto de la línea excedida nunca se lee, por lo que la conexión no puede continuar. |
| Inicializar | 30 s desde connect | Una conexión que no ha completado initialize 30 s después de conectarse se cierra sin respuesta, de modo que un cliente que se conecta y no envía nada no puede retener un slot de conexión. El plazo también cubre la autenticación: un initialize enviado a tiempo cuya autenticación sigue en curso cuando vence el plazo, como una verificación OIDC lenta, se cierra en lugar de aceptarse tarde. |
| Marco inacabado | 30 s desde el primer byte del fotograma | Una vez que comienza un frame, la línea completa debe llegar en un plazo de 30 s desde su primer byte, sin importar lo constantemente que llegue el resto. Transcurrido ese tiempo, el cliente recibe un único error -32600 con id: null y data: {"reason": "frame_timeout", "limit_ms": 30000}, y luego el fin del flujo. La espera entre frames no tiene límite, por lo que un cliente inicializado puede permanecer inactivo. |
| Bloqueo de escritura | 30 s por fotograma | Un cliente que deja de leer durante 30 s mientras el daemon tiene salida en cola para él se desconecta. Las demás conexiones no se ven afectadas. Desconectar termina los turnos que inició esa conexión, como ocurre con cualquier otra desconexión. Esto incluye a un cliente suspendido: un zerocode detenido con Ctrl-Z, o congelado con Ctrl-S, mientras un turno transmite, tiene ese turno cancelado tras 30 s, cuando antes terminaba una vez que el cliente se reanudaba. |
| Conexiones abiertas | rpc.max_local_connections, valor predeterminado 512 | Una conexión que supera el límite recibe un único error -32004 con id: null y data: {"reason": "connection_limit", "limit": N}, y después se cierra el flujo. El daemon registra una advertencia cuando empieza a rechazar conexiones. El valor se lee cuando se inicia el listener. |
Las cargas útiles mayores que un frame se envían como una subida fragmentada en lugar de en una sola línea. Consulta Subidas fragmentadas.
Saludo inicial
La primera llamada RPC debe ser initialize. El daemon rechaza todos los demás métodos hasta que initialize se complete correctamente. Una discrepancia en la versión del protocolo produce un error estructurado con el código -32011.
{
"jsonrpc": "2.0",
"método": "inicializar",
"parámetros": {
"protocolVersion": 1
},
"id": 1
}
El endpoint no requiere un token de emparejamiento. El control de acceso lo gestiona el sistema operativo:
- Unix: el socket es
0o600, el directorio padre es0o700. - Windows: la ACL de la canalización con nombre se establece de forma predeterminada para el usuario creador y
SYSTEM.
Métodos
| Método | Dirección | Descripción |
|---|---|---|
initialize | cliente -> daemon | Autenticar y negociar la versión del protocolo |
session/new | cliente -> daemon | Crear una sesión de agente (requiere agentAlias; opcionalmente cwd, sessionId; un ID que ya está activo vincula al llamador con esa sesión canónica en memoria en lugar de reemplazar su historial de agente, y se rechaza con FORBIDDEN cuando el entorno reenviado que el llamador puede usar con sus concesiones actuales difiere del que tenía la sesión al crearse; el parámetro opcional keep_siblings suprime el desalojo de sesiones hermanas inactivas del mismo modo, para clientes con varias sesiones que gestionan el ciclo de vida de sus hermanas por sí mismos) |
session/close | cliente -> daemon | Elimina el propietario de la sesión activa; el historial duradero de ACP sigue siendo reanudable |
session/kill | cliente -> daemon | Eliminar la sesión activa y colocar un marcador de eliminación en su historial duradero de ACP |
session/delete | cliente -> daemon | Eliminar la sesión activa y su historial duradero seleccionado |
sesión/indicación | cliente -> daemon | Ejecutar un turno (transmitido mediante notificaciones session/update) |
session/cancel | cliente -> daemon | Cancelar un turno en curso |
session/state | cliente -> daemon | Leer el estado del ciclo de vida de la sesión en tiempo real, la identidad del turno activo y el plan actual opcional; el trabajo activo o en cola se representa con state: "running" para que los clientes de recuperación puedan confirmar el estado terminal antes de liberar el trabajo retenido |
status | cliente -> daemon | Versión del servidor, versión del protocolo, lista de sesiones activas |
file/upload/begin, file/upload/chunk, file/upload/commit | cliente -> daemon | Sube un archivo en fragmentos ordenados; consulta Cargas fragmentadas |
session/update | daemon -> client | Notificación de streaming durante un turno (fragmentos de texto, llamadas a herramientas, aprobaciones) |
elicitation/create | daemon -> client | Solicitar entrada interactiva para los flujos de ask-user y poll |
Escrituras atómicas de configuración
config/set-many acepta un arreglo sets ordenado que contiene de 1 a 256 objetos, cada uno con prop y value. Utiliza la misma sintaxis de propiedades y las mismas reglas de valores que config/set. Por ejemplo, después de crear un perfil de permisos llamado operator, un administrador puede crear un usuario completo en una sola solicitud:
{"jsonrpc":"2.0","método":"config/set-many","parámetros":{sets:[{prop:users.example.uid,"valor":1001},{prop:users.example.permission_profiles,"valor":[operador]}]},"id":2}
El éxito devuelve {"props":["users.example.uid","users.example.permission_profiles"],"set":true}. Todos los campos se preparan en un único candidato y se validan antes de una única confirmación persistente. Las entradas posteriores para la misma propiedad prevalecen. Un fallo de preparación o de confirmación deja sin cambios la configuración persistida y la configuración activa; un error de entrada no válida indica su índice con base cero.
El llamador necesita Config:Update y autorización para cada ruta solicitada. El daemon resuelve la autoridad vigente mientras mantiene el bloqueo de escritura de la configuración, antes de preparar cualquier entrada. Si una ruta está prohibida, se rechaza todo el lote con FORBIDDEN, incluido el índice de la entrada, aunque las rutas anteriores estuvieran permitidas. La revocación mientras se espera ese bloqueo también rechaza el lote. Las vistas de proveedor/modelo se preparan a partir del candidato completo y se instalan después de que la confirmación sea exitosa. Las operaciones de eliminación y de clave de mapa no forman parte de este método.
Solicitudes bidireccionales
Cualquiera de las partes puede enviar una solicitud a través del socket establecido. El receptor debe responder con el mismo id y exactamente uno de result o error. Cada par utiliza su propia secuencia zc-out-<number> para las solicitudes salientes; el prefijo no es un espacio de nombres globalmente único. La correlación sigue siendo direccional: cada par compara las respuestas únicamente con su propio mapa de solicitudes pendientes, por lo que el mismo ID textual puede estar en curso de forma independiente en direcciones opuestas.
{"jsonrpc":"2.0","método":"elicitation/create","parámetros":{"mensaje":"¿Continuar?"},"id":zc-out-0}
{"jsonrpc":"2.0","resultado":{acción:aceptar,"contenido":{respuesta:sí}},"id":zc-out-0}
Un "result": null explícito es una respuesta correcta y aun así resuelve al llamador pendiente. Un objeto error lo resuelve como un fallo. Si el par no responde, la operación ask-user o poll que la inició mantiene su comportamiento de tiempo de espera existente.
Cada trama debe ser un objeto JSON con "jsonrpc": "2.0". Las solicitudes requieren un method de tipo cadena; cuando está presente, params debe ser un objeto o una matriz. Las respuestas requieren un id de tipo cadena, numérico o nulo, y exactamente un miembro de respuesta. El JSON no válido produce -32700 (error de análisis). Una envolvente con forma de solicitud malformada produce -32600 (solicitud no válida), utilizando su ID de solicitud válido cuando se pueda recuperar. Una envolvente con forma de respuesta malformada se registra sin el contenido de la trama y se descarta sin responder, porque hacer eco de su ID podría completar una solicitud no relacionada en la dirección opuesta. Las envolventes cuya dirección es ambigua usan id: null. Los ID de respuesta válidos desconocidos también se registran y se ignoran en lugar de responder, lo que evita bucles de respuesta.
Activar streaming
session/prompt devuelve el resultado final cuando el turno se completa. Durante la ejecución, el daemon envía notificaciones session/update con eventos incrementales:
{"jsonrpc":"2.0","método":"session/update","parámetros":{"sessionId":"...","tipo":agent_message_chunk,"texto":"Hola"}}
{"jsonrpc":"2.0","método":"session/update","parámetros":{"sessionId":"...","tipo":"llamada_herramienta",toolCallId:tc_1,"nombre":bash,rawInput:{...}}}
{"jsonrpc":"2.0","método":"session/update","parámetros":{"sessionId":"...","tipo":"resultado_herramienta",toolCallId:tc_1,"nombre":bash,rawOutput:"..."}}
Tipos de evento: agent_message_chunk, agent_thought_chunk, tool_call, tool_result, approval_request.
Ciclo de vida duradero de ACP
El RPC nativo mantiene la transcripción visible original separada del contexto del proveedor conservado. El recorte automático no elimina ni renumera las filas de la transcripción original. Una notificación de recorte se envía solo después de que su instantánea del contexto conservado y el límite del punto de control cubierto se hayan confirmado juntos; si la escritura falla, la notificación se suprime. La instantánea excluye los prompts del sistema de tiempo de ejecución, la inyección de memoria recuperada y el razonamiento oculto. Al producirse una interrupción, la recuperación añade el progreso visible del punto de control una sola vez y restaura el modelo a partir de la instantánea conservada más reciente más los eventos de punto de control posteriores. Una instantánea conservada explícitamente vacía sigue siendo autoritativa. Las sesiones sin instantánea conservan la ruta heredada de reproducción segura para el proveedor.
ACP las sesiones pueden conservar historial duradero antes de alcanzar un estado terminal. Cuando se elimina el propietario en memoria, la recuperación de prompts RPC solo considera las filas duraderas cuyo interaction_surface persistido es compatible; una superficie no compatible se deja intacta para que una recuperación directa posterior pueda inspeccionar el checkpoint original.
Recovery añade un marcador de interrupción visible para el cliente en un turno sin terminar. La repetición hacia el proveedor excluye ese marcador sintético, y la salida de herramientas persistida sigue sujeta a los límites existentes de la transcripción.
Las operaciones del ciclo de vida tienen significados duraderos distintos:
session/closepuede conservar un historial duradero y reanudable.session/killconserva el historial, pero marca la fila como tombstone, por lo que no es elegible para la rehidratación en tiempo de ejecución.session/deleteelimina la fila de ACP y su punto de control recuperable antes de desregistrar y eliminar la sesión activa cuando el destino es una sesión ACP activa, o cuando no existe ningún modo activo y se selecciona una fila duradera de ACP. Una sesión Chat activa con el mismo ID permanece aislada del almacenamiento de ACP.
La cancelación por señal de kill y delete se realiza antes de intentar su operación de SQLite, que puede fallar. Si esa operación falla para un destino inactivo, el RPC informa un error interno y conserva el propietario activo y el registro del canal. Si la cancelación forzada ya eliminó un propietario activo, el RPC no puede restaurar esa generación activa. La fila duradera y el punto de control recuperable seguirán disponibles cuando el almacenamiento vuelva a funcionar.
Subidas por fragmentos
Ataque file/attach transporta un archivo completo en un solo frame, lo que lo limita bastante por debajo del límite por archivo una vez contabilizado el base64. file/upload/begin, file/upload/chunk y file/upload/commit transportan la misma subida en fragmentos y la dejan exactamente como file/attach: en el workspace del agente de la sesión, bajo su hash de contenido, deduplicada por sesión, con el mismo marcador en el resultado.
file/upload/beginconsession_id,size_bytesy, opcionalmente,filenameysha256(hex). El resultado devuelve unupload_id,chunk_bytes(1 MiB) ymax_bytes(el límite de 10 MB por archivo).file/upload/chunkconupload_id,offsetydata_b64, en este orden.offsetdebe ser igual a los bytes recibidos hasta el momento; el resultado informareceived_bytes. Reenviar un fragmento aceptado con los mismos bytes se confirma sin cambios, por lo que un cliente puede reintentar después de una respuesta perdida.file/upload/commitconupload_iduna vez que hayan llegado todos los bytes declarados. Unsha256declarado debe coincidir. El resultado es una entrada de archivofile/attach.
Una carga pertenece a la conexión que la inició: ninguna otra conexión puede referenciarla, y se descarta cuando esa conexión se cierra. Los límites:
- Una conexión puede preparar cuatro cargas a la vez.
- El demonio almacena en staging como máximo 256 MiB en total entre todas las conexiones. El cargo cubre la carga útil de cada subida y los metadatos que conserva (id de sesión, propietario, agente, nombre de archivo y hash declarado), de modo que una carga útil vacía no puede retener memoria sin coste. Un
filenamede más de 255 bytes se rechaza. - Una carga inactiva durante cinco minutos se descarta. Cuando una nueva carga no cabe en el presupuesto, el daemon primero recupera todas las cargas inactivas más allá de ese plazo, incluso aquellas cuya conexión sigue abierta, para que un cliente silencioso no pueda retener el presupuesto.
Los tres métodos se sirven solo en conexiones locales; un par WSS recibe -32012. Necesitan el permiso files:create, y begin comprueba que el principal es propietario de la sesión (para un principal sin admin) y que puede usar el agente de la sesión.
Una subida está vinculada a la sesión exacta para la que se inició: la sesión activa con ese id, su propietario y su agente. commit la almacena en un solo paso bajo el bloqueo de la sesión. Primero confirma que la sesión sigue siendo la misma y, a continuación, verifica la credencial del llamador, files:create, la propiedad de la sesión y el derecho de uso del agente según la política vigente en ese momento; solo entonces escribe e indexa el archivo. Una concesión de permiso revocada mientras la subida estaba en curso, o una sesión cerrada o recreada con el mismo id, hace que el commit falle antes de escribir nada. La verificación y la escritura mantienen fijos la política de autorización aceptada y el conjunto de tokens emparejados, de modo que un cambio de política o un desemparejamiento que llegue después de completarse la verificación solo se aplica una vez almacenado el archivo: la subida se ordena antes que él, nunca entre la verificación y la escritura. file/attach almacena mediante el mismo paso y exige el mismo requisito de propiedad.
Una nueva subida de un archivo que la sesión ya indexó se compara con el archivo en disco. Si el archivo fue editado, eliminado o reemplazado por un enlace, los bytes subidos se escriben de nuevo en la ruta indexada antes de devolverlo.
Modo efímero
zeroclaw daemon --ephemeral rastrea los clientes conectados y se termina automáticamente cuando el último se desconecta (tras un período de gracia de 1 segundo). Una reconexión durante el período de gracia cancela el apagado. El daemon no saldrá hasta que al menos un cliente se haya conectado.
Los daemons iniciados sin --ephemeral ignoran el recuento de clientes y se ejecutan hasta que se detienen explícitamente.
Seguridad
- Directorio de socket Unix:
0o700(solo el propietario) - Archivo de socket Unix:
0o600(solo propietario) - Canalización con nombre de Windows: la ACL predeterminada concede acceso al usuario creador y a
SYSTEM SO_PEERCREDen Linux proporciona el PID y el UID del proceso que se conecta para el registro de auditoría; Windows registrapipe:localcomo etiqueta del par
Prueba rápida
Inicie el daemon en una terminal:
sh
zeroclaw daemon
En una segunda terminal en Unix, conéctate con socat:
sh
socat READLINE UNIX-CONNECT:~/.zeroclaw/data/daemon.sock
Pegue las líneas una a la vez:
{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":1},"id":1}
{"jsonrpc":"2.0","method":"status","params":{},"id":2}
En Windows, usa cualquier cliente de named-pipe (PowerShell [System.IO.Pipes.NamedPipeClientStream], nc mediante WSL, o simplemente ejecuta zerocode).
Internos
La capa de despacho se encuentra en crates/zeroclaw-runtime/src/rpc/:
| Archivo | Rol |
|---|---|
transport.rs | rasgo RpcTransport |
turn.rs | execute_turn() ejecutor de turnos compartido |
session.rs | RpcSession, SessionStore |
dispatch.rs | Enrutamiento de métodos de RpcDispatcher |
local.rs | LocalTransport + listener (socket Unix / canalización con nombre de Windows) |
wss.rs | WSS (WebSocket Secure) transporte + aceptador TLS |
attachments.rs | Procesamiento de carga de archivos, deduplicación, generación de marcadores |
upload.rs | Ordenamiento de la preparación de cargas por fragmentos: límites por conexión y globales |
El trait RpcTransport está diseñado para que transportes adicionales (vsock, IPC personalizado) se integren sin tocar la lógica de despacho ni de sesión. El módulo local.rs envuelve las primitivas de Unix y Windows tras una única struct LocalTransport usando tokio::io::split, de modo que el bucle de lectura/escritura se comparte entre ambas plataformas.