chore(i18n): refresh es translations

This commit is contained in:
openclaw-docs-i18n[bot] 2026-04-16 05:16:56 +00:00
parent a75e8b51d8
commit 2a34c9ba23
2 changed files with 429 additions and 244 deletions

View File

@ -1,32 +1,32 @@
---
read_when:
- Implementar o actualizar clientes WS del gateway
- Depurar incompatibilidades de protocolo o fallos de conexión
- Regenerar el esquema/modelos del protocolo
summary: 'Protocolo WebSocket del Gateway: handshake, frames y versionado'
title: Protocolo del Gateway
- Implementación o actualización de clientes WS del Gateway
- Depuración de incompatibilidades del protocolo o fallos de conexión
- Regeneración del esquema/modelos del protocolo
summary: 'Protocolo WebSocket del Gateway: saludo inicial, tramas, control de versiones'
title: Protocolo Gateway
x-i18n:
generated_at: "2026-04-11T02:44:52Z"
generated_at: "2026-04-16T05:15:09Z"
model: gpt-5.4
provider: openai
source_hash: 83c820c46d4803d571c770468fd6782619eaa1dca253e156e8087dec735c127f
source_hash: 683e61ebe993a2d739bc34860060b0e3eda36b5c57267a2bcc03d177ec612fb3
source_path: gateway/protocol.md
workflow: 15
---
# Protocolo del Gateway (WebSocket)
# Protocolo Gateway (WebSocket)
El protocolo WS del Gateway es el **plano de control único + transporte de nodos** para
OpenClaw. Todos los clientes (CLI, UI web, app de macOS, nodos iOS/Android, nodos
headless) se conectan por WebSocket y declaran su **role** + **scope** en el momento
del handshake.
OpenClaw. Todos los clientes (CLI, interfaz web, app de macOS, nodos iOS/Android,
nodos sin interfaz) se conectan mediante WebSocket y declaran su **rol** + **alcance** en el
momento del saludo inicial.
## Transporte
- WebSocket, frames de texto con cargas JSON.
- El primer frame **debe** ser una solicitud `connect`.
- WebSocket, tramas de texto con cargas útiles JSON.
- La primera trama **debe** ser una solicitud `connect`.
## Handshake (connect)
## Saludo inicial (`connect`)
Gateway → Cliente (desafío previo a la conexión):
@ -80,10 +80,24 @@ Gateway → Cliente:
"type": "res",
"id": "…",
"ok": true,
"payload": { "type": "hello-ok", "protocol": 3, "policy": { "tickIntervalMs": 15000 } }
"payload": {
"type": "hello-ok",
"protocol": 3,
"server": { "version": "…", "connId": "…" },
"features": { "methods": ["…"], "events": ["…"] },
"snapshot": { "…": "…" },
"policy": {
"maxPayload": 26214400,
"maxBufferedBytes": 52428800,
"tickIntervalMs": 15000
}
}
}
```
`server`, `features`, `snapshot` y `policy` son obligatorios según el esquema
(`src/gateway/protocol/schema/frames.ts`). `auth` y `canvasHostUrl` son opcionales.
Cuando se emite un token de dispositivo, `hello-ok` también incluye:
```json
@ -96,8 +110,8 @@ Cuando se emite un token de dispositivo, `hello-ok` también incluye:
}
```
Durante el traspaso de bootstrap confiable, `hello-ok.auth` también puede incluir
entradas de rol adicionales acotadas en `deviceTokens`:
Durante la transferencia de arranque confiable, `hello-ok.auth` también puede incluir
entradas de rol acotadas adicionales en `deviceTokens`:
```json
{
@ -116,12 +130,13 @@ entradas de rol adicionales acotadas en `deviceTokens`:
}
```
Para el flujo bootstrap integrado de nodo/operator, el token principal del nodo se mantiene con
`scopes: []` y cualquier token de operator transferido sigue acotado a la allowlist
del operator de bootstrap (`operator.approvals`, `operator.read`,
`operator.talk.secrets`, `operator.write`). Las comprobaciones de scope de bootstrap siguen
prefijadas por rol: las entradas de operator solo satisfacen solicitudes de operator, y los roles
que no son operator siguen necesitando scopes bajo su propio prefijo de rol.
Para el flujo integrado de arranque nodo/operador, el token principal del nodo mantiene
`scopes: []` y cualquier token de operador transferido sigue estando acotado a la lista
permitida del operador de arranque (`operator.approvals`, `operator.read`,
`operator.talk.secrets`, `operator.write`). Las comprobaciones de alcance de arranque
siguen teniendo prefijo de rol: las entradas de operador solo satisfacen solicitudes de
operador, y los roles que no son de operador siguen necesitando alcances bajo el prefijo
de su propio rol.
### Ejemplo de nodo
@ -158,24 +173,24 @@ que no son operator siguen necesitando scopes bajo su propio prefijo de rol.
}
```
## Enmarcado
## Estructura de tramas
- **Solicitud**: `{type:"req", id, method, params}`
- **Respuesta**: `{type:"res", id, ok, payload|error}`
- **Evento**: `{type:"event", event, payload, seq?, stateVersion?}`
Los métodos con efectos secundarios requieren **claves de idempotencia** (consulta el esquema).
Los métodos con efectos secundarios requieren **claves de idempotencia** (véase el esquema).
## Roles + scopes
## Roles + alcances
### Roles
- `operator` = cliente del plano de control (CLI/UI/automatización).
- `operator` = cliente del plano de control (CLI/interfaz de usuario/automatización).
- `node` = host de capacidades (camera/screen/canvas/system.run).
### Scopes (operator)
### Alcances (operator)
Scopes comunes:
Alcances comunes:
- `operator.read`
- `operator.write`
@ -187,149 +202,151 @@ Scopes comunes:
`talk.config` con `includeSecrets: true` requiere `operator.talk.secrets`
(o `operator.admin`).
Los métodos RPC del gateway registrados por plugins pueden solicitar su propio scope de operator, pero
Los métodos RPC del Gateway registrados por Plugin pueden solicitar su propio alcance de operador, pero
los prefijos reservados de administración del núcleo (`config.*`, `exec.approvals.*`, `wizard.*`,
`update.*`) siempre se resuelven a `operator.admin`.
`update.*`) siempre se resuelven como `operator.admin`.
El scope del método es solo la primera barrera. Algunos comandos con barra alcanzados mediante
`chat.send` aplican comprobaciones más estrictas a nivel de comando además de eso. Por ejemplo, las escrituras
persistentes de `/config set` y `/config unset` requieren `operator.admin`.
El alcance del método es solo la primera puerta de control. Algunos comandos con barra a los que se accede mediante
`chat.send` aplican comprobaciones más estrictas a nivel de comando además de eso. Por ejemplo, las escrituras persistentes de
`/config set` y `/config unset` requieren `operator.admin`.
`node.pair.approve` también tiene una comprobación de scope adicional en el momento de la aprobación
además del scope base del método:
`node.pair.approve` también tiene una comprobación adicional de alcance en el momento de aprobación además del
alcance base del método:
- solicitudes sin comando: `operator.pairing`
- solicitudes con comandos de nodo que no son exec: `operator.pairing` + `operator.write`
- solicitudes que incluyen `system.run`, `system.run.prepare` o `system.which`:
`operator.pairing` + `operator.admin`
### Caps/commands/permissions (node)
### Capacidades/comandos/permisos (node)
Los nodos declaran las capacidades reclamadas al conectarse:
Los nodos declaran capacidades reclamadas en el momento de conectarse:
- `caps`: categorías de capacidades de alto nivel.
- `commands`: allowlist de comandos para invoke.
- `permissions`: alternadores granulares (p. ej., `screen.record`, `camera.capture`).
- `commands`: lista permitida de comandos para `invoke`.
- `permissions`: controles detallados (por ejemplo, `screen.record`, `camera.capture`).
El Gateway trata estas como **claims** y aplica allowlists del lado del servidor.
El Gateway las trata como **reclamaciones** y aplica listas permitidas del lado del servidor.
## Presencia
- `system-presence` devuelve entradas con clave por identidad del dispositivo.
- Las entradas de presencia incluyen `deviceId`, `roles` y `scopes` para que las UI puedan mostrar una sola fila por dispositivo
incluso cuando se conecta como **operator** y **node**.
- `system-presence` devuelve entradas indexadas por identidad del dispositivo.
- Las entradas de presencia incluyen `deviceId`, `roles` y `scopes` para que las interfaces puedan mostrar una sola fila por dispositivo
incluso cuando se conecta como **operator** y **node** al mismo tiempo.
## Familias comunes de métodos RPC
Esta página no es un volcado completo generado, pero la superficie WS pública es más amplia
que los ejemplos de handshake/auth anteriores. Estas son las principales familias de métodos que el
que los ejemplos de saludo inicial/autenticación anteriores. Estas son las principales familias de métodos que el
Gateway expone hoy.
`hello-ok.features.methods` es una lista conservadora de descubrimiento construida a partir de
`hello-ok.features.methods` es una lista de descubrimiento conservadora construida a partir de
`src/gateway/server-methods-list.ts` más las exportaciones de métodos de plugins/canales cargados.
Trátala como descubrimiento de características, no como un volcado generado de cada helper invocable
Trátela como descubrimiento de funcionalidades, no como un volcado generado de cada helper invocable
implementado en `src/gateway/server-methods/*.ts`.
### Sistema e identidad
- `health` devuelve la instantánea de salud del gateway en caché o sondeada de forma reciente.
- `status` devuelve el resumen del gateway con estilo `/status`; los campos sensibles se
incluyen solo para clientes operator con scope de admin.
- `gateway.identity.get` devuelve la identidad del dispositivo del gateway usada por relay y
los flujos de pairing.
- `system-presence` devuelve la instantánea de presencia actual para dispositivos
- `health` devuelve la instantánea de estado del Gateway almacenada en caché o recién sondeada.
- `status` devuelve el resumen del Gateway estilo `/status`; los campos sensibles
se incluyen solo para clientes operator con alcance de administrador.
- `gateway.identity.get` devuelve la identidad del dispositivo Gateway usada por los flujos
de retransmisión y emparejamiento.
- `system-presence` devuelve la instantánea de presencia actual de los dispositivos
operator/node conectados.
- `system-event` agrega un evento del sistema y puede actualizar/transmitir contexto de presencia.
- `last-heartbeat` devuelve el evento heartbeat persistido más reciente.
- `set-heartbeats` activa o desactiva el procesamiento de heartbeat en el gateway.
- `system-event` agrega un evento del sistema y puede actualizar/transmitir el contexto
de presencia.
- `last-heartbeat` devuelve el evento Heartbeat persistido más reciente.
- `set-heartbeats` activa o desactiva el procesamiento de Heartbeat en el Gateway.
### Modelos y uso
- `models.list` devuelve el catálogo de modelos permitido en tiempo de ejecución.
- `usage.status` devuelve resúmenes de ventanas de uso del proveedor/cuota restante.
- `usage.cost` devuelve resúmenes agregados de uso de costos para un rango de fechas.
- `doctor.memory.status` devuelve el estado de preparación de vector-memory / embeddings para el
workspace activo del agente predeterminado.
- `usage.status` devuelve resúmenes de ventanas de uso/restante de cuota del proveedor.
- `usage.cost` devuelve resúmenes agregados de uso de costo para un intervalo de fechas.
- `doctor.memory.status` devuelve la preparación de memoria vectorial / incrustación para el
espacio de trabajo del agente predeterminado activo.
- `sessions.usage` devuelve resúmenes de uso por sesión.
- `sessions.usage.timeseries` devuelve series temporales de uso para una sesión.
- `sessions.usage.logs` devuelve entradas de log de uso para una sesión.
- `sessions.usage.logs` devuelve entradas del registro de uso para una sesión.
### Canales y helpers de login
### Canales y helpers de inicio de sesión
- `channels.status` devuelve resúmenes de estado de canales/plugins incluidos + empaquetados.
- `channels.logout` cierra sesión en un canal/cuenta específicos cuando el canal
- `channels.status` devuelve resúmenes de estado de canales/plugins integrados y empaquetados.
- `channels.logout` cierra la sesión de un canal/cuenta específico donde el canal
admite cierre de sesión.
- `web.login.start` inicia un flujo de login QR/web para el proveedor actual de canal web
compatible con QR.
- `web.login.wait` espera a que ese flujo de login QR/web se complete e inicia el
- `web.login.start` inicia un flujo de inicio de sesión QR/web para el proveedor de canal web
actual compatible con QR.
- `web.login.wait` espera a que ese flujo de inicio de sesión QR/web se complete e inicia el
canal si tiene éxito.
- `push.test` envía un push APNs de prueba a un nodo iOS registrado.
- `voicewake.get` devuelve los disparadores de palabra de activación almacenados.
- `voicewake.set` actualiza los disparadores de palabra de activación y transmite el cambio.
- `push.test` envía una notificación push APNs de prueba a un nodo iOS registrado.
- `voicewake.get` devuelve los activadores de palabra de activación almacenados.
- `voicewake.set` actualiza los activadores de palabra de activación y transmite el cambio.
### Mensajería y logs
### Mensajería y registros
- `send` es el RPC directo de entrega saliente para envíos dirigidos a canal/cuenta/hilo
- `send` es el RPC de entrega saliente directa para envíos dirigidos a canal/cuenta/hilo
fuera del ejecutor de chat.
- `logs.tail` devuelve la cola configurada del log de archivos del gateway con controles de cursor/límite y
bytes máximos.
- `logs.tail` devuelve el final del registro de archivos configurado del Gateway con cursor/límite y
controles máximos de bytes.
### Talk y TTS
- `talk.config` devuelve la carga efectiva de configuración de Talk; `includeSecrets`
- `talk.config` devuelve la carga útil efectiva de configuración de Talk; `includeSecrets`
requiere `operator.talk.secrets` (o `operator.admin`).
- `talk.mode` establece/transmite el estado actual del modo Talk para clientes de WebChat/Control UI.
- `talk.speak` sintetiza voz mediante el proveedor activo de voz de Talk.
- `tts.status` devuelve el estado de TTS habilitado, el proveedor activo, los proveedores de respaldo
- `talk.mode` establece/transmite el estado actual del modo Talk para clientes
de WebChat/Control UI.
- `talk.speak` sintetiza voz mediante el proveedor de voz Talk activo.
- `tts.status` devuelve el estado de activación de TTS, el proveedor activo, los proveedores de respaldo
y el estado de configuración del proveedor.
- `tts.providers` devuelve el inventario visible de proveedores de TTS.
- `tts.enable` y `tts.disable` activan o desactivan el estado de preferencia de TTS.
- `tts.setProvider` actualiza el proveedor de TTS preferido.
- `tts.providers` devuelve el inventario visible de proveedores TTS.
- `tts.enable` y `tts.disable` activan o desactivan el estado de preferencias de TTS.
- `tts.setProvider` actualiza el proveedor TTS preferido.
- `tts.convert` ejecuta una conversión puntual de texto a voz.
### Secrets, config, update y wizard
### Secretos, configuración, actualización y asistente
- `secrets.reload` vuelve a resolver los SecretRefs activos e intercambia el estado secreto en tiempo de ejecución
solo si todo se completa con éxito.
- `secrets.resolve` resuelve asignaciones de secretos dirigidas por comando para un conjunto específico
de comando/destino.
- `config.get` devuelve la instantánea actual de configuración y el hash.
- `config.set` escribe una carga de configuración validada.
- `secrets.reload` vuelve a resolver las SecretRef activas e intercambia el estado de secretos en tiempo de ejecución
solo si todo se completa correctamente.
- `secrets.resolve` resuelve asignaciones de secretos dirigidas a comandos para un conjunto
específico de comando/destino.
- `config.get` devuelve la instantánea y el hash de la configuración actual.
- `config.set` escribe una carga útil de configuración validada.
- `config.patch` fusiona una actualización parcial de configuración.
- `config.apply` valida + reemplaza la carga completa de configuración.
- `config.schema` devuelve la carga del esquema activo de configuración usada por Control UI y
- `config.apply` valida y reemplaza la carga útil completa de configuración.
- `config.schema` devuelve la carga útil del esquema de configuración en vivo usada por Control UI y
herramientas CLI: esquema, `uiHints`, versión y metadatos de generación, incluidos
metadatos de esquema de plugins + canales cuando el runtime puede cargarlos. El esquema
incluye metadatos de campo `title` / `description` derivados de las mismas etiquetas
y texto de ayuda usados por la UI, incluidos objeto anidado, wildcard, elemento de array
y ramas de composición `anyOf` / `oneOf` / `allOf` cuando existe documentación
y texto de ayuda usados por la interfaz, incluidas ramas anidadas de objeto, comodín,
elemento de matriz y composición `anyOf` / `oneOf` / `allOf` cuando existe documentación
de campo coincidente.
- `config.schema.lookup` devuelve una carga de búsqueda acotada por ruta para una ruta de configuración:
ruta normalizada, un nodo superficial del esquema, una sugerencia coincidente + `hintPath`, y
resúmenes inmediatos de hijos para navegación UI/CLI.
- Los nodos del esquema de búsqueda conservan la documentación orientada al usuario y los campos comunes
de validación: `title`, `description`, `type`, `enum`, `const`, `format`, `pattern`,
límites numéricos/de string/de array/de objeto, y flags booleanos como
- `config.schema.lookup` devuelve una carga útil de búsqueda con alcance a ruta para una ruta de configuración:
ruta normalizada, un nodo de esquema superficial, la sugerencia coincidente + `hintPath`, y
resúmenes inmediatos de elementos secundarios para exploración en interfaz/CLI.
- Los nodos de esquema de búsqueda conservan la documentación orientada al usuario y los campos de validación comunes:
`title`, `description`, `type`, `enum`, `const`, `format`, `pattern`,
límites numéricos/de cadena/de matriz/de objeto, y marcas booleanas como
`additionalProperties`, `deprecated`, `readOnly`, `writeOnly`.
- Los resúmenes de hijos exponen `key`, `path` normalizada, `type`, `required`,
`hasChildren`, además de `hint` / `hintPath`.
- `update.run` ejecuta el flujo de actualización del gateway y programa un reinicio solo cuando
la actualización en sí tuvo éxito.
- Los resúmenes de elementos secundarios exponen `key`, `path` normalizada, `type`, `required`,
`hasChildren`, además de `hint` / `hintPath` coincidentes.
- `update.run` ejecuta el flujo de actualización del Gateway y programa un reinicio solo cuando
la actualización en sí se completó correctamente.
- `wizard.start`, `wizard.next`, `wizard.status` y `wizard.cancel` exponen el
asistente de onboarding por WS RPC.
asistente de incorporación mediante WS RPC.
### Familias principales existentes
#### Helpers de agente y workspace
#### Helpers de agente y espacio de trabajo
- `agents.list` devuelve las entradas de agentes configuradas.
- `agents.list` devuelve entradas de agentes configuradas.
- `agents.create`, `agents.update` y `agents.delete` administran registros de agentes y
la conexión del workspace.
la conexión del espacio de trabajo.
- `agents.files.list`, `agents.files.get` y `agents.files.set` administran los
archivos bootstrap del workspace expuestos para un agente.
archivos del espacio de trabajo de arranque expuestos para un agente.
- `agent.identity.get` devuelve la identidad efectiva del asistente para un agente o
sesión.
- `agent.wait` espera a que una ejecución termine y devuelve la instantánea terminal cuando
- `agent.wait` espera a que una ejecución finalice y devuelve la instantánea terminal cuando
está disponible.
#### Control de sesión
@ -339,265 +356,301 @@ implementado en `src/gateway/server-methods/*.ts`.
para el cliente WS actual.
- `sessions.messages.subscribe` y `sessions.messages.unsubscribe` activan o desactivan
las suscripciones a eventos de transcripción/mensajes para una sesión.
- `sessions.preview` devuelve vistas previas acotadas de transcripciones para claves de sesión
- `sessions.preview` devuelve vistas previas acotadas de la transcripción para claves de sesión
específicas.
- `sessions.resolve` resuelve o canoniza un destino de sesión.
- `sessions.create` crea una nueva entrada de sesión.
- `sessions.send` envía un mensaje a una sesión existente.
- `sessions.steer` es la variante de interrumpir y redirigir para una sesión activa.
- `sessions.abort` aborta el trabajo activo de una sesión.
- `sessions.patch` actualiza metadatos/anulaciones de la sesión.
- `sessions.reset`, `sessions.delete` y `sessions.compact` realizan tareas de mantenimiento
de sesión.
- `sessions.patch` actualiza metadatos/sobrescrituras de la sesión.
- `sessions.reset`, `sessions.delete` y `sessions.compact` realizan mantenimiento de
sesión.
- `sessions.get` devuelve la fila completa de la sesión almacenada.
- la ejecución del chat sigue usando `chat.history`, `chat.send`, `chat.abort` y
- la ejecución de chat sigue usando `chat.history`, `chat.send`, `chat.abort` y
`chat.inject`.
- `chat.history` está normalizado para visualización en clientes UI: las etiquetas de directiva inline se
eliminan del texto visible, las cargas XML de llamadas a herramientas en texto plano (incluyendo
- `chat.history` está normalizado para visualización para clientes de UI: las etiquetas de directivas en línea se
eliminan del texto visible, las cargas útiles XML de llamadas a herramientas en texto plano (incluidas
`<tool_call>...</tool_call>`, `<function_call>...</function_call>`,
`<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>` y
bloques truncados de llamadas a herramientas) y los tokens de control del modelo filtrados en ASCII/de ancho completo
se eliminan, se omiten las filas del asistente compuestas solo por tokens silenciosos como `NO_REPLY` /
`no_reply`, y las filas sobredimensionadas pueden reemplazarse con marcadores de posición.
`<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, y
bloques truncados de llamadas a herramientas) y los tokens de control del modelo filtrados en ASCII/ancho completo
se eliminan, las filas del asistente compuestas únicamente por tokens silenciosos como `NO_REPLY` /
`no_reply` exactos se omiten, y las filas sobredimensionadas pueden reemplazarse por marcadores de posición.
#### Emparejamiento de dispositivos y tokens de dispositivo
- `device.pair.list` devuelve dispositivos emparejados pendientes y aprobados.
- `device.pair.list` devuelve los dispositivos emparejados pendientes y aprobados.
- `device.pair.approve`, `device.pair.reject` y `device.pair.remove` administran
los registros de emparejamiento de dispositivos.
- `device.token.rotate` rota un token de dispositivo emparejado dentro de sus límites aprobados
de role y scope.
- `device.token.rotate` rota un token de dispositivo emparejado dentro de los límites
aprobados de su rol y alcances.
- `device.token.revoke` revoca un token de dispositivo emparejado.
#### Emparejamiento de nodos, invoke y trabajo pendiente
#### Emparejamiento de nodos, invocación y trabajo pendiente
- `node.pair.request`, `node.pair.list`, `node.pair.approve`,
`node.pair.reject` y `node.pair.verify` cubren el emparejamiento de nodos y la
verificación bootstrap.
verificación de arranque.
- `node.list` y `node.describe` devuelven el estado de nodos conocidos/conectados.
- `node.rename` actualiza la etiqueta de un nodo emparejado.
- `node.invoke` reenvía un comando a un nodo conectado.
- `node.invoke.result` devuelve el resultado de una solicitud invoke.
- `node.event` transporta eventos originados en el nodo de vuelta al gateway.
- `node.canvas.capability.refresh` actualiza tokens de capacidad de canvas con scope.
- `node.invoke.result` devuelve el resultado de una solicitud de invocación.
- `node.event` transporta eventos originados en el nodo de vuelta al Gateway.
- `node.canvas.capability.refresh` actualiza tokens de capacidad de canvas con alcance limitado.
- `node.pending.pull` y `node.pending.ack` son las API de cola para nodos conectados.
- `node.pending.enqueue` y `node.pending.drain` administran trabajo pendiente duradero
para nodos desconectados/sin conexión.
para nodos sin conexión/desconectados.
#### Familias de aprobaciones
#### Familias de aprobación
- `exec.approval.request`, `exec.approval.get`, `exec.approval.list` y
`exec.approval.resolve` cubren solicitudes puntuales de aprobación exec más la
`exec.approval.resolve` cubren solicitudes puntuales de aprobación de exec además de
búsqueda/repetición de aprobaciones pendientes.
- `exec.approval.waitDecision` espera una aprobación exec pendiente y devuelve
la decisión final (o `null` en caso de timeout).
la decisión final (o `null` por tiempo de espera agotado).
- `exec.approvals.get` y `exec.approvals.set` administran instantáneas de la política
de aprobación exec del gateway.
de aprobación exec del Gateway.
- `exec.approvals.node.get` y `exec.approvals.node.set` administran la política local del nodo para exec
mediante comandos relay del nodo.
mediante comandos de retransmisión del nodo.
- `plugin.approval.request`, `plugin.approval.list`,
`plugin.approval.waitDecision` y `plugin.approval.resolve` cubren
flujos de aprobación definidos por plugins.
flujos de aprobación definidos por Plugin.
#### Otras familias principales
- automatización:
- `wake` programa una inyección de texto de activación inmediata o en el próximo heartbeat
- `wake` programa una inyección de texto de activación inmediata o en el siguiente Heartbeat
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`,
`cron.run`, `cron.runs`
- Skills/herramientas: `commands.list`, `skills.*`, `tools.catalog`, `tools.effective`
### Familias comunes de eventos
- `chat`: actualizaciones de chat de la UI como `chat.inject` y otros
eventos de chat solo de transcripción.
- `chat`: actualizaciones de chat de UI como `chat.inject` y otros eventos de chat
solo de transcripción.
- `session.message` y `session.tool`: actualizaciones de transcripción/flujo de eventos para una
sesión suscrita.
- `sessions.changed`: cambió el índice o los metadatos de la sesión.
- `presence`: actualizaciones de la instantánea de presencia del sistema.
- `tick`: evento periódico de keepalive / actividad.
- `health`: actualización de la instantánea de salud del gateway.
- `heartbeat`: actualización del flujo de eventos heartbeat.
- `cron`: evento de cambio de ejecución/trabajo cron.
- `shutdown`: notificación de apagado del gateway.
- `sessions.changed`: cambió el índice de sesiones o los metadatos.
- `presence`: actualizaciones de instantáneas de presencia del sistema.
- `tick`: evento periódico de keepalive/vivacidad.
- `health`: actualización de instantánea de estado del Gateway.
- `heartbeat`: actualización del flujo de eventos Heartbeat.
- `cron`: evento de cambio de ejecución/trabajo Cron.
- `shutdown`: notificación de apagado del Gateway.
- `node.pair.requested` / `node.pair.resolved`: ciclo de vida del emparejamiento de nodos.
- `node.invoke.request`: difusión de solicitud invoke del nodo.
- `node.invoke.request`: difusión de solicitud de invocación de nodo.
- `device.pair.requested` / `device.pair.resolved`: ciclo de vida del dispositivo emparejado.
- `voicewake.changed`: cambió la configuración de los disparadores de palabra de activación.
- `exec.approval.requested` / `exec.approval.resolved`: ciclo de vida de la aprobación exec.
- `plugin.approval.requested` / `plugin.approval.resolved`: ciclo de vida de la aprobación de plugins.
- `voicewake.changed`: cambió la configuración de activadores de palabra de activación.
- `exec.approval.requested` / `exec.approval.resolved`: ciclo de vida de la
aprobación exec.
- `plugin.approval.requested` / `plugin.approval.resolved`: ciclo de vida de la
aprobación de Plugin.
### Métodos helper de nodo
- Los nodos pueden llamar a `skills.bins` para obtener la lista actual de ejecutables de Skills
para comprobaciones de autoallow.
para comprobaciones de permiso automático.
### Métodos helper de operator
- Los operator pueden llamar a `commands.list` (`operator.read`) para obtener el inventario de comandos en runtime de un agente.
- `agentId` es opcional; omítelo para leer el workspace del agente predeterminado.
- Los operadores pueden llamar a `commands.list` (`operator.read`) para obtener el inventario
de comandos en tiempo de ejecución de un agente.
- `agentId` es opcional; omítalo para leer el espacio de trabajo del agente predeterminado.
- `scope` controla a qué superficie apunta el `name` principal:
- `text` devuelve el token principal del comando de texto sin la `/` inicial
- `native` y la ruta predeterminada `both` devuelven nombres nativos dependientes del proveedor
- `native` y la ruta predeterminada `both` devuelven nombres nativos sensibles al proveedor
cuando están disponibles
- `textAliases` contiene alias exactos con barra, como `/model` y `/m`.
- `nativeName` contiene el nombre nativo dependiente del proveedor cuando existe.
- `provider` es opcional y solo afecta el nombre nativo más la disponibilidad de comandos
nativos del plugin.
- `includeArgs=false` omite de la respuesta los metadatos serializados de argumentos.
- Los operator pueden llamar a `tools.catalog` (`operator.read`) para obtener el catálogo de herramientas en runtime de un
- `textAliases` contiene alias exactos con barra como `/model` y `/m`.
- `nativeName` contiene el nombre de comando nativo sensible al proveedor cuando existe.
- `provider` es opcional y solo afecta al nombre nativo además de la disponibilidad de comandos
nativos del Plugin.
- `includeArgs=false` omite metadatos de argumentos serializados de la respuesta.
- Los operadores pueden llamar a `tools.catalog` (`operator.read`) para obtener el catálogo de herramientas en tiempo de ejecución de un
agente. La respuesta incluye herramientas agrupadas y metadatos de procedencia:
- `source`: `core` o `plugin`
- `pluginId`: propietario del plugin cuando `source="plugin"`
- `optional`: si una herramienta del plugin es opcional
- Los operator pueden llamar a `tools.effective` (`operator.read`) para obtener el inventario efectivo en runtime de herramientas
- `pluginId`: propietario del Plugin cuando `source="plugin"`
- `optional`: si una herramienta de Plugin es opcional
- Los operadores pueden llamar a `tools.effective` (`operator.read`) para obtener el inventario efectivo de herramientas en tiempo de ejecución
para una sesión.
- `sessionKey` es obligatorio.
- El gateway deriva el contexto de runtime confiable del lado del servidor a partir de la sesión, en lugar de aceptar
contexto de auth o de entrega proporcionado por el llamador.
- La respuesta tiene scope de sesión y refleja lo que la conversación activa puede usar en este momento,
incluidas herramientas de core, plugin y canal.
- Los operator pueden llamar a `skills.status` (`operator.read`) para obtener el inventario visible
- El Gateway deriva el contexto confiable de tiempo de ejecución del lado del servidor a partir de la sesión en lugar de aceptar
autenticación o contexto de entrega proporcionados por el llamador.
- La respuesta tiene alcance de sesión y refleja lo que la conversación activa puede usar ahora mismo,
incluidas herramientas core, de Plugin y de canal.
- Los operadores pueden llamar a `skills.status` (`operator.read`) para obtener el inventario visible
de Skills para un agente.
- `agentId` es opcional; omítelo para leer el workspace del agente predeterminado.
- `agentId` es opcional; omítalo para leer el espacio de trabajo del agente predeterminado.
- La respuesta incluye elegibilidad, requisitos faltantes, comprobaciones de configuración y
opciones de instalación saneadas sin exponer valores secretos sin procesar.
- Los operator pueden llamar a `skills.search` y `skills.detail` (`operator.read`) para obtener
- Los operadores pueden llamar a `skills.search` y `skills.detail` (`operator.read`) para
metadatos de descubrimiento de ClawHub.
- Los operator pueden llamar a `skills.install` (`operator.admin`) en dos modos:
- Los operadores pueden llamar a `skills.install` (`operator.admin`) en dos modos:
- Modo ClawHub: `{ source: "clawhub", slug, version?, force? }` instala una
carpeta de skill en el directorio `skills/` del workspace del agente predeterminado.
- Modo instalador del gateway: `{ name, installId, dangerouslyForceUnsafeInstall?, timeoutMs? }`
ejecuta una acción declarada `metadata.openclaw.install` en el host del gateway.
- Los operator pueden llamar a `skills.update` (`operator.admin`) en dos modos:
carpeta de skill en el directorio `skills/` del espacio de trabajo del agente predeterminado.
- Modo instalador del Gateway: `{ name, installId, dangerouslyForceUnsafeInstall?, timeoutMs? }`
ejecuta una acción declarada `metadata.openclaw.install` en el host del Gateway.
- Los operadores pueden llamar a `skills.update` (`operator.admin`) en dos modos:
- El modo ClawHub actualiza un slug rastreado o todas las instalaciones rastreadas de ClawHub en
el workspace del agente predeterminado.
- El modo Config aplica un parche a valores de `skills.entries.<skillKey>` como `enabled`,
el espacio de trabajo del agente predeterminado.
- El modo de configuración aplica un parche a valores `skills.entries.<skillKey>` como `enabled`,
`apiKey` y `env`.
## Aprobaciones exec
- Cuando una solicitud exec necesita aprobación, el gateway transmite `exec.approval.requested`.
- Los clientes operator resuelven llamando a `exec.approval.resolve` (requiere el scope `operator.approvals`).
- Cuando una solicitud exec necesita aprobación, el Gateway difunde `exec.approval.requested`.
- Los clientes operator resuelven llamando a `exec.approval.resolve` (requiere alcance `operator.approvals`).
- Para `host=node`, `exec.approval.request` debe incluir `systemRunPlan` (`argv`/`cwd`/`rawCommand`/metadatos de sesión canónicos). Las solicitudes sin `systemRunPlan` se rechazan.
- Después de la aprobación, las llamadas reenviadas `node.invoke system.run` reutilizan ese
`systemRunPlan` canónico como contexto autoritativo para comando/cwd/sesión.
`systemRunPlan` canónico como el contexto autoritativo de comando/cwd/sesión.
- Si un llamador modifica `command`, `rawCommand`, `cwd`, `agentId` o
`sessionKey` entre prepare y el reenvío final aprobado de `system.run`, el
gateway rechaza la ejecución en lugar de confiar en la carga modificada.
Gateway rechaza la ejecución en lugar de confiar en la carga útil modificada.
## Respaldo de entrega del agente
- Las solicitudes `agent` pueden incluir `deliver=true` para solicitar entrega saliente.
- `bestEffortDeliver=false` mantiene el comportamiento estricto: los destinos de entrega no resueltos o solo internos devuelven `INVALID_REQUEST`.
- `bestEffortDeliver=true` permite volver a la ejecución solo de sesión cuando no puede resolverse ninguna ruta externa entregable (por ejemplo, sesiones internas/webchat o configuraciones multicanal ambiguas).
- `bestEffortDeliver=true` permite recurrir a ejecución solo de sesión cuando no se puede resolver ninguna ruta de entrega externa (por ejemplo, sesiones internas/de webchat o configuraciones ambiguas de múltiples canales).
## Versionado
## Control de versiones
- `PROTOCOL_VERSION` está en `src/gateway/protocol/schema.ts`.
- `PROTOCOL_VERSION` vive en `src/gateway/protocol/schema/protocol-schemas.ts`.
- Los clientes envían `minProtocol` + `maxProtocol`; el servidor rechaza incompatibilidades.
- Los esquemas + modelos se generan a partir de definiciones TypeBox:
- `pnpm protocol:gen`
- `pnpm protocol:gen:swift`
- `pnpm protocol:check`
## Auth
### Constantes de cliente
- La auth del gateway con secreto compartido usa `connect.params.auth.token` o
`connect.params.auth.password`, según el modo de auth configurado.
El cliente de referencia en `src/gateway/client.ts` usa estos valores predeterminados. Los valores son
estables en todo el protocolo v3 y son la línea base esperada para clientes de terceros.
| Constante | Predeterminado | Fuente |
| ----------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` |
| Tiempo de espera de solicitud (por RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
| Tiempo de espera previo a autenticación / desafío de conexión | `10_000` ms | `src/gateway/handshake-timeouts.ts` (límite `250``10_000`) |
| Retroceso inicial de reconexión | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
| Retroceso máximo de reconexión | `30_000` ms | `src/gateway/client.ts` (`scheduleReconnect`) |
| Límite de reintento rápido después de cierre por token de dispositivo | `250` ms | `src/gateway/client.ts` |
| Gracia de parada forzada antes de `terminate()` | `250` ms | `FORCE_STOP_TERMINATE_GRACE_MS` |
| Tiempo de espera predeterminado de `stopAndWait()` | `1_000` ms | `STOP_AND_WAIT_TIMEOUT_MS` |
| Intervalo `tick` predeterminado (antes de `hello-ok`) | `30_000` ms | `src/gateway/client.ts` |
| Cierre por tiempo de espera de `tick` | código `4000` cuando el silencio supera `tickIntervalMs * 2` | `src/gateway/client.ts` |
| `MAX_PAYLOAD_BYTES` | `25 * 1024 * 1024` (25 MB) | `src/gateway/server-constants.ts` |
El servidor anuncia los valores efectivos `policy.tickIntervalMs`, `policy.maxPayload`
y `policy.maxBufferedBytes` en `hello-ok`; los clientes deben respetar esos valores
en lugar de los predeterminados previos al saludo inicial.
## Autenticación
- La autenticación del Gateway con secreto compartido usa `connect.params.auth.token` o
`connect.params.auth.password`, según el modo de autenticación configurado.
- Los modos con identidad, como Tailscale Serve
(`gateway.auth.allowTailscale: true`) o
`gateway.auth.mode: "trusted-proxy"` sin loopback,
satisfacen la comprobación de auth de connect desde encabezados de la solicitud en lugar de `connect.params.auth.*`.
- El modo de ingreso privado `gateway.auth.mode: "none"` omite por completo la auth de connect con secreto compartido; no expongas ese modo en ingresos públicos/no confiables.
- Después del emparejamiento, el Gateway emite un **token de dispositivo** con scope para el
role + scopes de la conexión. Se devuelve en `hello-ok.auth.deviceToken` y el cliente
debe persistirlo para conexiones futuras.
(`gateway.auth.allowTailscale: true`) o `gateway.auth.mode: "trusted-proxy"` sin loopback,
satisfacen la comprobación de autenticación de conexión a partir de los
encabezados de la solicitud en lugar de `connect.params.auth.*`.
- `gateway.auth.mode: "none"` para ingreso privado omite por completo la autenticación de conexión con secreto compartido; no exponga ese modo en ingresos públicos/no confiables.
- Después del emparejamiento, el Gateway emite un **token de dispositivo** con alcance limitado al rol + alcances
de la conexión. Se devuelve en `hello-ok.auth.deviceToken` y el cliente debe
persistirlo para conexiones futuras.
- Los clientes deben persistir el `hello-ok.auth.deviceToken` principal después de cualquier
conexión correcta.
- Al reconectarse con ese token de dispositivo **almacenado**, también deben reutilizarse el conjunto de scopes aprobados
almacenado para ese token. Esto preserva el acceso de lectura/sondeo/estado
conexión satisfactoria.
- Al reconectar con ese token de dispositivo **almacenado**, también se debe reutilizar el conjunto de alcances
aprobados almacenado para ese token. Esto conserva el acceso de lectura/sondeo/estado
que ya se había concedido y evita reducir silenciosamente las reconexiones a un
scope implícito más estrecho de solo admin.
- La precedencia normal de auth de connect es primero token/contraseña compartidos explícitos, luego
`deviceToken` explícito, después token por dispositivo almacenado y, por último, token bootstrap.
- Las entradas adicionales de `hello-ok.auth.deviceTokens` son tokens de traspaso bootstrap.
Persístelas solo cuando la conexión usó auth bootstrap en un transporte confiable
alcance implícito más limitado de solo administrador.
- Ensamblaje de autenticación de conexión del lado del cliente (`selectConnectAuth` en
`src/gateway/client.ts`):
- `auth.password` es ortogonal y siempre se reenvía cuando está establecido.
- `auth.token` se completa en orden de prioridad: primero un token compartido explícito,
luego un `deviceToken` explícito, y después un token por dispositivo almacenado (indexado por
`deviceId` + `role`).
- `auth.bootstrapToken` se envía solo cuando ninguna de las opciones anteriores resolvió un
`auth.token`. Un token compartido o cualquier token de dispositivo resuelto lo suprime.
- La autopromoción de un token de dispositivo almacenado en el reintento puntual
`AUTH_TOKEN_MISMATCH` está restringida a **endpoints confiables únicamente**
loopback, o `wss://` con `tlsFingerprint` fijado. Un `wss://` público
sin fijación no cumple ese requisito.
- Las entradas adicionales `hello-ok.auth.deviceTokens` son tokens de transferencia de arranque.
Persístalos solo cuando la conexión haya usado autenticación de arranque sobre un transporte confiable
como `wss://` o loopback/emparejamiento local.
- Si un cliente proporciona un `deviceToken` **explícito** o `scopes` explícitos, ese
conjunto de scopes solicitado por el llamador sigue siendo autoritativo; los scopes en caché solo se
reutilizan cuando el cliente reutiliza el token almacenado por dispositivo.
- Los tokens de dispositivo pueden rotarse/revocarse mediante `device.token.rotate` y
`device.token.revoke` (requiere el scope `operator.pairing`).
- La emisión/rotación de tokens permanece acotada al conjunto de roles aprobados registrado en
la entrada de emparejamiento de ese dispositivo; rotar un token no puede ampliar el dispositivo a un
role que la aprobación del emparejamiento nunca concedió.
- Para sesiones de token de dispositivo emparejado, la administración del dispositivo tiene scope propio salvo que el
llamador también tenga `operator.admin`: los llamadores que no son admin solo pueden eliminar/revocar/rotar
conjunto de alcances solicitado por el llamador sigue siendo el autoritativo; los alcances en caché solo
se reutilizan cuando el cliente está reutilizando el token por dispositivo almacenado.
- Los tokens de dispositivo se pueden rotar/revocar mediante `device.token.rotate` y
`device.token.revoke` (requiere el alcance `operator.pairing`).
- La emisión/rotación de tokens sigue limitada al conjunto de roles aprobados registrado en
la entrada de emparejamiento de ese dispositivo; rotar un token no puede expandir el dispositivo a un
rol que la aprobación de emparejamiento nunca concedió.
- Para las sesiones de tokens de dispositivos emparejados, la administración del dispositivo tiene alcance propio salvo que el
llamador también tenga `operator.admin`: los llamadores que no son administradores solo pueden eliminar/revocar/rotar
su **propia** entrada de dispositivo.
- `device.token.rotate` también comprueba el conjunto solicitado de scopes de operator frente a los
scopes actuales de la sesión del llamador. Los llamadores que no son admin no pueden rotar un token a
un conjunto más amplio de scopes de operator del que ya poseen.
- Los fallos de auth incluyen `error.details.code` más sugerencias de recuperación:
- `error.details.canRetryWithDeviceToken` (boolean)
- `device.token.rotate` también comprueba el conjunto solicitado de alcances de operador frente a los
alcances actuales de la sesión del llamador. Los llamadores que no son administradores no pueden rotar un token a
un conjunto más amplio de alcances de operador que el que ya poseen.
- Los fallos de autenticación incluyen `error.details.code` además de sugerencias de recuperación:
- `error.details.canRetryWithDeviceToken` (booleano)
- `error.details.recommendedNextStep` (`retry_with_device_token`, `update_auth_configuration`, `update_auth_credentials`, `wait_then_retry`, `review_auth_configuration`)
- Comportamiento del cliente para `AUTH_TOKEN_MISMATCH`:
- Los clientes confiables pueden intentar un único reintento acotado con un token por dispositivo en caché.
- Si ese reintento falla, los clientes deben detener los bucles automáticos de reconexión y mostrar orientación de acción al operator.
- Los clientes confiables pueden intentar un reintento limitado con un token por dispositivo en caché.
- Si ese reintento falla, los clientes deben detener los bucles automáticos de reconexión y mostrar orientación para que el operador actúe.
## Identidad del dispositivo + emparejamiento
- Los nodos deben incluir una identidad de dispositivo estable (`device.id`) derivada de la
huella de un par de claves.
- Los gateways emiten tokens por dispositivo + role.
- Se requieren aprobaciones de emparejamiento para nuevos IDs de dispositivo, a menos que la autoaprobación local
- Los nodos deben incluir una identidad estable de dispositivo (`device.id`) derivada de una
huella digital de par de claves.
- Los Gateways emiten tokens por dispositivo + rol.
- Las aprobaciones de emparejamiento son necesarias para nuevos ID de dispositivo, salvo que la aprobación automática local
esté habilitada.
- La autoaprobación de emparejamiento se centra en conexiones directas de local loopback.
- La aprobación automática de emparejamiento se centra en conexiones directas locales por loopback.
- OpenClaw también tiene una ruta estrecha de autoconexión local de backend/contenedor para
flujos helper confiables con secreto compartido.
- Las conexiones tailnet o LAN en el mismo host siguen tratándose como remotas para el emparejamiento y
- Las conexiones tailnet o LAN del mismo host siguen tratándose como remotas para el emparejamiento y
requieren aprobación.
- Todos los clientes WS deben incluir identidad `device` durante `connect` (operator + node).
Control UI puede omitirla solo en estos modos:
- `gateway.controlUi.allowInsecureAuth=true` para compatibilidad con HTTP inseguro solo en localhost.
- autenticación correcta de operator de Control UI con `gateway.auth.mode: "trusted-proxy"`.
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (opción de emergencia, degradación grave de seguridad).
- autenticación satisfactoria de operator de Control UI con `gateway.auth.mode: "trusted-proxy"`.
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (medida de emergencia, degradación grave de seguridad).
- Todas las conexiones deben firmar el nonce `connect.challenge` proporcionado por el servidor.
### Diagnósticos de migración de auth de dispositivo
### Diagnósticos de migración de autenticación del dispositivo
Para clientes heredados que todavía usan el comportamiento de firma previo al desafío, `connect` ahora devuelve
Para clientes heredados que siguen usando el comportamiento de firma previo al desafío, `connect` ahora devuelve
códigos de detalle `DEVICE_AUTH_*` en `error.details.code` con un `error.details.reason` estable.
Fallos comunes de migración:
| Mensaje | details.code | details.reason | Significado |
| Mensaje | details.code | details.reason | Significado |
| --------------------------- | -------------------------------- | ------------------------ | -------------------------------------------------- |
| `device nonce required` | `DEVICE_AUTH_NONCE_REQUIRED` | `device-nonce-missing` | El cliente omitió `device.nonce` (o lo envió vacío). |
| `device nonce mismatch` | `DEVICE_AUTH_NONCE_MISMATCH` | `device-nonce-mismatch` | El cliente firmó con un nonce obsoleto/incorrecto. |
| `device signature invalid` | `DEVICE_AUTH_SIGNATURE_INVALID` | `device-signature` | La carga de la firma no coincide con la carga v2. |
| `device signature expired` | `DEVICE_AUTH_SIGNATURE_EXPIRED` | `device-signature-stale` | La marca de tiempo firmada está fuera del desfase permitido. |
| `device identity mismatch` | `DEVICE_AUTH_DEVICE_ID_MISMATCH` | `device-id-mismatch` | `device.id` no coincide con la huella de la clave pública. |
| `device public key invalid` | `DEVICE_AUTH_PUBLIC_KEY_INVALID` | `device-public-key` | Falló el formato/canonicalización de la clave pública. |
| `device nonce required` | `DEVICE_AUTH_NONCE_REQUIRED` | `device-nonce-missing` | El cliente omitió `device.nonce` (o lo envió vacío). |
| `device nonce mismatch` | `DEVICE_AUTH_NONCE_MISMATCH` | `device-nonce-mismatch` | El cliente firmó con un nonce obsoleto/incorrecto. |
| `device signature invalid` | `DEVICE_AUTH_SIGNATURE_INVALID` | `device-signature` | La carga útil de la firma no coincide con la carga útil v2. |
| `device signature expired` | `DEVICE_AUTH_SIGNATURE_EXPIRED` | `device-signature-stale` | La marca de tiempo firmada está fuera de la desviación permitida. |
| `device identity mismatch` | `DEVICE_AUTH_DEVICE_ID_MISMATCH` | `device-id-mismatch` | `device.id` no coincide con la huella de la clave pública. |
| `device public key invalid` | `DEVICE_AUTH_PUBLIC_KEY_INVALID` | `device-public-key` | Falló el formato/canonización de la clave pública. |
Objetivo de la migración:
- Espera siempre a `connect.challenge`.
- Firma la carga v2 que incluye el nonce del servidor.
- Envía el mismo nonce en `connect.params.device.nonce`.
- La carga de firma preferida es `v3`, que vincula `platform` y `deviceFamily`
además de los campos de dispositivo/cliente/role/scopes/token/nonce.
- Espere siempre a `connect.challenge`.
- Firme la carga útil v2 que incluye el nonce del servidor.
- Envíe el mismo nonce en `connect.params.device.nonce`.
- La carga útil de firma preferida es `v3`, que vincula `platform` y `deviceFamily`
además de los campos de dispositivo/cliente/rol/alcances/token/nonce.
- Las firmas heredadas `v2` siguen aceptándose por compatibilidad, pero la fijación de metadatos
del dispositivo emparejado sigue controlando la política de comandos al reconectar.
de dispositivos emparejados sigue controlando la política de comandos al reconectar.
## TLS + pinning
## TLS + fijación
- TLS es compatible con conexiones WS.
- Los clientes pueden fijar opcionalmente la huella del certificado del gateway (consulta la configuración `gateway.tls`
más `gateway.remote.tlsFingerprint` o la CLI `--tls-fingerprint`).
- Los clientes pueden fijar opcionalmente la huella digital del certificado del Gateway (véase la configuración `gateway.tls`
además de `gateway.remote.tlsFingerprint` o la CLI `--tls-fingerprint`).
## Alcance
Este protocolo expone la **API completa del gateway** (status, canales, modelos, chat,
agent, sesiones, nodos, aprobaciones, etc.). La superficie exacta está definida por los
Este protocolo expone la **API completa del Gateway** (estado, canales, modelos, chat,
agente, sesiones, nodos, aprobaciones, etc.). La superficie exacta está definida por los
esquemas TypeBox en `src/gateway/protocol/schema.ts`.

View File

@ -0,0 +1,132 @@
---
x-i18n:
generated_at: "2026-04-16T05:15:02Z"
model: gpt-5.4
provider: openai
source_hash: 95e56c5411204363676f002059c942201503e2359515d1a4b409882cc2e04920
source_path: refactor/async-exec-duplicate-completion-investigation.md
workflow: 15
---
# Investigación de finalización duplicada de ejecución asíncrona
## Alcance
- Sesión: `agent:main:telegram:group:-1003774691294:topic:1`
- Síntoma: la misma finalización de ejecución asíncrona para la sesión/ejecución `keen-nexus` se registró dos veces en LCM como turnos de usuario.
- Objetivo: identificar si lo más probable es que se trate de una inyección duplicada en la sesión o de un simple reintento de entrega saliente.
## Conclusión
Lo más probable es que esto sea una **inyección duplicada en la sesión**, no un simple reintento de entrega saliente.
La brecha más importante del lado del Gateway está en la **ruta de finalización de ejecución del Node**:
1. Una finalización de ejecución del lado del Node emite `exec.finished` con el `runId` completo.
2. El `server-node-events` del Gateway convierte eso en un evento del sistema y solicita un Heartbeat.
3. La ejecución del Heartbeat inyecta el bloque de eventos del sistema drenados en el prompt del agente.
4. El ejecutor embebido persiste ese prompt como un nuevo turno de usuario en la transcripción de la sesión.
Si el mismo `exec.finished` llega al Gateway dos veces para el mismo `runId` por cualquier motivo (repetición, reconexión duplicada, reenvío ascendente, productor duplicado), OpenClaw actualmente **no tiene una verificación de idempotencia con clave `runId`/`contextKey`** en esta ruta. La segunda copia se convertirá en un segundo mensaje de usuario con el mismo contenido.
## Ruta exacta del código
### 1. Productor: evento de finalización de ejecución del Node
- `src/node-host/invoke.ts:340-360`
- `sendExecFinishedEvent(...)` emite `node.event` con el evento `exec.finished`.
- La carga incluye `sessionKey` y el `runId` completo.
### 2. Ingesta de eventos en el Gateway
- `src/gateway/server-node-events.ts:574-640`
- Maneja `exec.finished`.
- Construye el texto:
- `Exec finished (node=..., id=<runId>, code ...)`
- Lo encola mediante:
- `enqueueSystemEvent(text, { sessionKey, contextKey: runId ? \`exec:${runId}\` : "exec", trusted: false })`
- Solicita inmediatamente una activación:
- `requestHeartbeatNow(scopedHeartbeatWakeOptions(sessionKey, { reason: "exec-event" }))`
### 3. Debilidad en la deduplicación de eventos del sistema
- `src/infra/system-events.ts:90-115`
- `enqueueSystemEvent(...)` solo suprime **texto duplicado consecutivo**:
- `if (entry.lastText === cleaned) return false`
- Almacena `contextKey`, pero **no** usa `contextKey` para la idempotencia.
- Después del drenado, la supresión de duplicados se reinicia.
Esto significa que un `exec.finished` repetido con el mismo `runId` puede volver a aceptarse más tarde, aunque el código ya tenía un candidato estable para idempotencia (`exec:<runId>`).
### 4. El manejo de activaciones no es el duplicador principal
- `src/infra/heartbeat-wake.ts:79-117`
- Las activaciones se consolidan por `(agentId, sessionKey)`.
- Las solicitudes duplicadas de activación para el mismo objetivo colapsan en una sola entrada pendiente.
Esto hace que **el manejo de activaciones duplicadas por sí solo** sea una explicación más débil que la ingesta duplicada del evento.
### 5. Heartbeat consume el evento y lo convierte en entrada del prompt
- `src/infra/heartbeat-runner.ts:535-574`
- El preflight inspecciona los eventos del sistema pendientes y clasifica las ejecuciones de tipo exec-event.
- `src/auto-reply/reply/session-system-events.ts:86-90`
- `drainFormattedSystemEvents(...)` drena la cola para la sesión.
- `src/auto-reply/reply/get-reply-run.ts:400-427`
- El bloque drenado de eventos del sistema se antepone al cuerpo del prompt del agente.
### 6. Punto de inyección en la transcripción
- `src/agents/pi-embedded-runner/run/attempt.ts:2000-2017`
- `activeSession.prompt(effectivePrompt)` envía el prompt completo a la sesión PI embebida.
- Ese es el punto en el que el prompt derivado de la finalización pasa a convertirse en un turno de usuario persistido.
Así que, una vez que el mismo evento del sistema se reconstruye dos veces dentro del prompt, es esperable que aparezcan mensajes de usuario duplicados en LCM.
## Por qué es menos probable que sea un simple reintento de entrega saliente
Existe una ruta real de fallo saliente en el ejecutor de Heartbeat:
- `src/infra/heartbeat-runner.ts:1194-1242`
- La respuesta se genera primero.
- La entrega saliente ocurre después mediante `deliverOutboundPayloads(...)`.
- Un fallo ahí devuelve `{ status: "failed" }`.
Sin embargo, para la misma entrada de la cola de eventos del sistema, esto por sí solo **no es suficiente** para explicar los turnos de usuario duplicados:
- `src/auto-reply/reply/session-system-events.ts:86-90`
- La cola de eventos del sistema ya se drenó antes de la entrega saliente.
Así que un reintento de envío del canal por sí solo no recrearía exactamente el mismo evento encolado. Podría explicar una entrega externa ausente o fallida, pero por sí solo no una segunda aparición del mismo mensaje de usuario en la sesión.
## Posibilidad secundaria, de menor confianza
Existe un bucle de reintento de ejecución completa en el ejecutor del agente:
- `src/auto-reply/reply/agent-runner-execution.ts:741-1473`
- Ciertos fallos transitorios pueden reintentar toda la ejecución y reenviar el mismo `commandBody`.
Eso puede duplicar un prompt de usuario persistido **dentro de la misma ejecución de respuesta** si el prompt ya se había anexado antes de que se activara la condición de reintento.
La clasifico por debajo de la ingesta duplicada de `exec.finished` porque:
- la brecha observada fue de unos 51 segundos, lo que parece más un segundo turno/activación que un reintento dentro del proceso;
- el informe ya menciona fallos repetidos de envío de mensajes, lo que apunta más a un turno separado posterior que a un reintento inmediato del modelo o del entorno de ejecución.
## Hipótesis de causa raíz
Hipótesis de mayor confianza:
- La finalización de `keen-nexus` llegó a través de la **ruta de eventos de ejecución del Node**.
- El mismo `exec.finished` se entregó dos veces a `server-node-events`.
- El Gateway aceptó ambas porque `enqueueSystemEvent(...)` no deduplica por `contextKey` / `runId`.
- Cada evento aceptado activó un Heartbeat y se inyectó como un turno de usuario en la transcripción de PI.
## Propuesta de corrección mínima y quirúrgica
Si se quiere una corrección, el cambio pequeño de mayor valor es:
- hacer que la idempotencia de eventos de ejecución/sistema respete `contextKey` durante un horizonte corto, al menos para repeticiones exactas de `(sessionKey, contextKey, text)`;
- o añadir una deduplicación específica en `server-node-events` para `exec.finished` con clave `(sessionKey, runId, event kind)`.
Eso bloquearía directamente las repeticiones de `exec.finished` antes de que se conviertan en turnos de sesión.