From 2a34c9ba2314d0b5d0f85fa7415a2784ee8bf87f Mon Sep 17 00:00:00 2001 From: "openclaw-docs-i18n[bot]" Date: Thu, 16 Apr 2026 05:16:56 +0000 Subject: [PATCH] chore(i18n): refresh es translations --- docs/es/gateway/protocol.md | 541 ++++++++++-------- ...exec-duplicate-completion-investigation.md | 132 +++++ 2 files changed, 429 insertions(+), 244 deletions(-) create mode 100644 docs/es/refactor/async-exec-duplicate-completion-investigation.md diff --git a/docs/es/gateway/protocol.md b/docs/es/gateway/protocol.md index 0c35c0383..a5f93e346 100644 --- a/docs/es/gateway/protocol.md +++ b/docs/es/gateway/protocol.md @@ -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 `...`, `...`, - `...`, `...` 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. + `...`, `...`, 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.` como `enabled`, + el espacio de trabajo del agente predeterminado. + - El modo de configuración aplica un parche a valores `skills.entries.` 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`. diff --git a/docs/es/refactor/async-exec-duplicate-completion-investigation.md b/docs/es/refactor/async-exec-duplicate-completion-investigation.md new file mode 100644 index 000000000..d1497f890 --- /dev/null +++ b/docs/es/refactor/async-exec-duplicate-completion-investigation.md @@ -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=, 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:`). + +### 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.