Control UI (navegador)

El Control UI es una pequeña app de página única con Vite + Lit servida por el Gateway:

  • por defecto: http://<host>:18789/
  • prefijo opcional: configura gateway.controlUi.basePath (p. ej. /openclaw)

Se comunica directamente con el WebSocket del Gateway en el mismo puerto.

Acceso rápido (local)

Si el Gateway se ejecuta en el mismo equipo, abre:

Si la página no carga, inicia el Gateway primero: openclaw gateway.

La autenticación se proporciona durante el handshake WebSocket mediante:

  • connect.params.auth.token
  • connect.params.auth.password El panel de ajustes del dashboard mantiene un token para la sesión actual de la pestaña del navegador y la URL del gateway seleccionada; las contraseñas no se persisten. El asistente de onboarding genera un token de gateway por defecto, así que pégalo aquí en la primera conexión.

Vinculación de dispositivo (primera conexión)

Cuando te conectas al Control UI desde un nuevo navegador o dispositivo, el Gateway requiere una aprobación de vinculación única — incluso si estás en la misma Tailnet con gateway.auth.allowTailscale: true. Esta es una medida de seguridad para prevenir el acceso no autorizado.

Lo que verás: “disconnected (1008): pairing required”

Para aprobar el dispositivo:

# Listar solicitudes pendientes
openclaw devices list

# Aprobar por ID de solicitud
openclaw devices approve <requestId>

Una vez aprobado, el dispositivo se recuerda y no requerirá re-aprobación a menos que lo revoques con openclaw devices revoke --device <id> --role <role>. Consulta CLI de dispositivos para rotación y revocación de tokens.

Notas:

  • Las conexiones locales (127.0.0.1) se aprueban automáticamente.
  • Las conexiones remotas (LAN, Tailnet, etc.) requieren aprobación explícita.
  • Cada perfil de navegador genera un ID de dispositivo único, así que cambiar de navegador o borrar los datos del navegador requerirá re-vinculación.

Soporte de idiomas

El Control UI puede localizarse automáticamente en la primera carga según el locale de tu navegador, y puedes cambiarlo después desde el selector de idioma en la tarjeta de Acceso.

  • Locales soportados: en, zh-CN, zh-TW, pt-BR, de, es
  • Las traducciones no inglesas se cargan de forma lazy en el navegador.
  • El locale seleccionado se guarda en el almacenamiento del navegador y se reutiliza en futuras visitas.
  • Las claves de traducción faltantes recurren al inglés.

Qué puede hacer (actualmente)

  • Chatear con el modelo vía WebSocket del Gateway (chat.history, chat.send, chat.abort, chat.inject)
  • Transmitir llamadas de herramientas + tarjetas de salida de herramientas en vivo en el Chat (eventos del agente)
  • Canales: WhatsApp/Telegram/Discord/Slack + canales de plugin (Mattermost, etc.) estado + login QR + configuración por canal (channels.status, web.login.*, config.patch)
  • Instancias: lista de presencia + actualización (system-presence)
  • Sesiones: lista + sobreescrituras de thinking/fast/verbose/reasoning por sesión (sessions.list, sessions.patch)
  • Trabajos cron: listar/agregar/editar/ejecutar/habilitar/deshabilitar + historial de ejecución (cron.*)
  • Skills: estado, habilitar/deshabilitar, instalar, actualización de claves API (skills.*)
  • Nodos: lista + capacidades (node.list)
  • Aprobaciones exec: editar allowlists del gateway o nodo + política ask para exec host=gateway/node (exec.approvals.*)
  • Configuración: ver/editar ~/.openclaw/openclaw.json (config.get, config.set)
  • Configuración: aplicar + reiniciar con validación (config.apply) y despertar la última sesión activa
  • Las escrituras de configuración incluyen un guard de hash base para prevenir sobreescrituras de ediciones concurrentes
  • Esquema de configuración + renderizado de formulario (config.schema, incluyendo esquemas de plugin + canal); el editor JSON raw sigue disponible
  • Debug: snapshots de estado/salud/modelos + log de eventos + llamadas RPC manuales (status, health, models.list)
  • Logs: tail en vivo de logs de archivo del gateway con filtro/exportación (logs.tail)
  • Actualización: ejecutar actualización de paquete/git + reinicio (update.run) con reporte de reinicio

Notas del panel de trabajos cron:

  • Para trabajos aislados, la entrega por defecto es resumen de anuncio. Puedes cambiar a ninguno si quieres ejecuciones solo internas.
  • Los campos de canal/destino aparecen cuando se selecciona anuncio.
  • El modo webhook usa delivery.mode = "webhook" con delivery.to configurado a una URL HTTP(S) de webhook válida.
  • Para trabajos de sesión principal, los modos de entrega webhook y ninguno están disponibles.
  • Los controles de edición avanzada incluyen eliminar después de ejecutar, limpiar sobreescritura de agente, opciones exact/stagger de cron, sobreescrituras de modelo/thinking del agente, y toggles de entrega best-effort.
  • La validación del formulario es inline con errores a nivel de campo; los valores inválidos deshabilitan el botón de guardar hasta que se corrijan.
  • Configura cron.webhookToken para enviar un token bearer dedicado; si se omite, el webhook se envía sin header de auth.
  • Respaldo deprecado: los trabajos legacy almacenados con notify: true pueden seguir usando cron.webhook hasta que se migren.

Comportamiento del chat

  • chat.send es no bloqueante: confirma inmediatamente con { runId, status: "started" } y la respuesta se transmite vía eventos chat.
  • Re-enviar con la misma idempotencyKey devuelve { status: "in_flight" } mientras se ejecuta, y { status: "ok" } tras completarse.
  • Las respuestas de chat.history tienen límite de tamaño para seguridad de la UI. Cuando las entradas de transcripción son demasiado grandes, el Gateway puede truncar campos de texto largos, omitir bloques de metadata pesados, y reemplazar mensajes sobredimensionados con un placeholder ([chat.history omitted: message too large]).
  • chat.inject agrega una nota del asistente a la transcripción de la sesión y transmite un evento chat para actualizaciones solo de UI (sin ejecución del agente, sin entrega al canal).
  • Detener:
    • Clic en Detener (llama a chat.abort)
    • Escribe /stop (o frases de aborto independientes como stop, stop action, stop run, stop openclaw, please stop) para abortar fuera de banda
    • chat.abort soporta { sessionKey } (sin runId) para abortar todas las ejecuciones activas de esa sesión
  • Retención parcial al abortar:
    • Cuando se aborta una ejecución, el texto parcial del asistente puede seguir mostrándose en la UI
    • El Gateway persiste el texto parcial del asistente abortado en el historial de transcripción cuando existe salida en buffer
    • Las entradas persistidas incluyen metadata de aborto para que los consumidores de transcripción puedan distinguir parciales abortados de salida de completado normal

Acceso por Tailnet (recomendado)

Tailscale Serve integrado (preferido)

Mantén el Gateway en loopback y deja que Tailscale Serve lo proxee con HTTPS:

openclaw gateway --tailscale serve

Abre:

  • https://<magicdns>/ (o tu gateway.controlUi.basePath configurado)

Por defecto, las solicitudes Serve del Control UI/WebSocket pueden autenticarse vía headers de identidad de Tailscale (tailscale-user-login) cuando gateway.auth.allowTailscale es true. OpenClaw verifica la identidad resolviendo la dirección x-forwarded-for con tailscale whois y comparándola con el header, y solo acepta esto cuando la solicitud llega a loopback con los headers x-forwarded-* de Tailscale. Configura gateway.auth.allowTailscale: false (o fuerza gateway.auth.mode: "password") si quieres requerir token/contraseña incluso para tráfico Serve. La auth Serve sin token asume que el host del gateway es de confianza. Si hay código local no confiable que pueda ejecutarse en ese host, requiere auth por token/contraseña.

Bind a tailnet + token

openclaw gateway --bind tailnet --token "$(openssl rand -hex 32)"

Luego abre:

  • http://<tailscale-ip>:18789/ (o tu gateway.controlUi.basePath configurado)

Pega el token en los ajustes de la UI (se envía como connect.params.auth.token).

HTTP inseguro

Si abres el dashboard sobre HTTP plano (http://<lan-ip> o http://<tailscale-ip>), el navegador se ejecuta en un contexto no seguro y bloquea WebCrypto. Por defecto, OpenClaw bloquea las conexiones del Control UI sin identidad de dispositivo.

Solución recomendada: usa HTTPS (Tailscale Serve) o abre la UI localmente:

  • https://<magicdns>/ (Serve)
  • http://127.0.0.1:18789/ (en el host del gateway)

Comportamiento del toggle de auth insegura:

{
  gateway: {
    controlUi: { allowInsecureAuth: true },
    bind: "tailnet",
    auth: { mode: "token", token: "replace-me" },
  },
}

allowInsecureAuth es un toggle de compatibilidad solo local:

  • Permite que las sesiones del Control UI en localhost procedan sin identidad de dispositivo en contextos HTTP no seguros.
  • No omite las verificaciones de vinculación.
  • No relaja los requisitos de identidad de dispositivo remoto (no-localhost).

Solo para emergencias:

{
  gateway: {
    controlUi: { dangerouslyDisableDeviceAuth: true },
    bind: "tailnet",
    auth: { mode: "token", token: "replace-me" },
  },
}

dangerouslyDisableDeviceAuth deshabilita las verificaciones de identidad de dispositivo del Control UI y es una degradación de seguridad severa. Revierte rápidamente después del uso de emergencia.

Consulta Tailscale para guía de configuración HTTPS.

Compilar la UI

El Gateway sirve archivos estáticos desde dist/control-ui. Compílalos con:

pnpm ui:build # auto-instala dependencias de UI en la primera ejecución

Base absoluta opcional (cuando quieres URLs de assets fijas):

OPENCLAW_CONTROL_UI_BASE_PATH=/openclaw/ pnpm ui:build

Para desarrollo local (servidor dev separado):

pnpm ui:dev # auto-instala dependencias de UI en la primera ejecución

Luego apunta la UI a tu URL del WebSocket del Gateway (p. ej. ws://127.0.0.1:18789).

Debugging/testing: servidor dev + Gateway remoto

El Control UI son archivos estáticos; el destino WebSocket es configurable y puede ser diferente del origen HTTP. Esto es útil cuando quieres el servidor dev de Vite localmente pero el Gateway se ejecuta en otro lugar.

  1. Inicia el servidor dev de la UI: pnpm ui:dev
  2. Abre una URL como:
http://localhost:5173/?gatewayUrl=ws://<gateway-host>:18789

Auth única opcional (si es necesario):

http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-token>

Notas:

  • gatewayUrl se almacena en localStorage tras la carga y se elimina de la URL.
  • token se importa del fragmento de URL, se almacena en sessionStorage para la sesión actual de la pestaña del navegador y la URL del gateway seleccionada, y se elimina de la URL; no se almacena en localStorage.
  • password se mantiene solo en memoria.
  • Cuando gatewayUrl está configurado, la UI no recurre a credenciales de configuración o entorno. Proporciona token (o password) explícitamente. Las credenciales explícitas faltantes son un error.
  • Usa wss:// cuando el Gateway está detrás de TLS (Tailscale Serve, proxy HTTPS, etc.).
  • gatewayUrl solo se acepta en una ventana de nivel superior (no embebida) para prevenir clickjacking.
  • Los despliegues del Control UI no-loopback deben configurar gateway.controlUi.allowedOrigins explícitamente (orígenes completos). Esto incluye configuraciones de desarrollo remotas.
  • gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true habilita el modo de fallback de origen por header Host, pero es un modo de seguridad peligroso.

Ejemplo:

{
  gateway: {
    controlUi: {
      allowedOrigins: ["http://localhost:5173"],
    },
  },
}

Detalles de configuración de acceso remoto: Acceso remoto.