chore(i18n): refresh es translations
This commit is contained in:
parent
a75e8b51d8
commit
2a34c9ba23
@ -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`.
|
||||
|
||||
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user