Plugin nativo de canal que conecta OpenClaw con la mensajería de Query usada por el frontend React y la aplicación Flutter. Mantiene un WebSocket persistente por bot, envía actividad inmediatamente y conserva respuestas terminales para que un mensaje reenviado por Django no ejecute dos veces las herramientas del agente.
- OpenClaw
2026.7.1-2o posterior. - Node.js
22.22.3+,24.15+o25.9+, dentro de los rangos aceptados por el runtime. - La URL de conexión generada al crear/configurar el agente en Query.
En PowerShell:
cd "C:\Users\julia\Repos\- Query-assets\query-agent-plugin"
npm install
npm run build
openclaw plugins install -l "C:\Users\julia\Repos\- Query-assets\query-agent-plugin"
openclaw plugins enable queryLa opción -l enlaza esta carpeta; después de modificar el código basta con
ejecutar npm run build y reiniciar el Gateway.
Query entrega una URL parecida a esta al revelar las instrucciones del bot:
wss://api.example.com/ws/openclaw-agent/42/?token=SECRETO
La forma más directa es guardar esa URL completa:
openclaw config set channels.query.url 'wss://api.example.com/ws/openclaw-agent/42/?token=SECRETO'
openclaw config set channels.query.enabled true
openclaw gateway restartTambién se puede separar el secreto para que no quede dentro de la URL:
{
channels: {
query: {
enabled: true,
url: "wss://api.example.com/ws/openclaw-agent/42/",
token: "SECRETO",
heartbeatMs: 25000,
reconnectMinMs: 500,
reconnectMaxMs: 15000,
activityMode: "smart",
effortMode: "auto",
},
},
}Si no se configura channels.query.token, el plugin intenta
QUERY_OPENCLAW_TOKEN y, finalmente, el parámetro token de la URL. Nunca
escribe el token en sus logs.
Para dirigir este canal a un agente distinto del predeterminado se usa el
sistema normal de bindings del runtime, con channel: "query" y
accountId: "default".
Para conectar varios bots Query a varios agentes OpenClaw en el mismo gateway,
usa channels.query.accounts. Cada cuenta mantiene su propio WebSocket,
reconexion, cache durable por client_msg_id y estado.
{
channels: {
query: {
enabled: true,
accounts: {
query: {
url: "wss://apius.itsquery.com/ws/openclaw-agent/3/?token=SECRETO",
origin: "https://us.itsquery.com",
stateFile: "/home/ubuntu/.openclaw/workspace/tenants/query/state/query-plugin-response-cache.json",
},
"director-asocapitales": {
url: "wss://apiasocapitales.itsquery.com/ws/openclaw-agent/1/?token=SECRETO",
origin: "https://us.itsquery.com",
stateFile: "/home/ubuntu/.openclaw/workspace/tenants/asocapitales/state/query-plugin-response-cache.json",
},
},
},
},
bindings: [
{
type: "route",
agentId: "query",
match: { channel: "query", accountId: "query" },
},
{
type: "route",
agentId: "director",
match: { channel: "query", accountId: "director-asocapitales" },
},
],
}El bloque de configuracion que entrega Query describe el contrato WebSocket. Para OpenClaw, los campos importantes son:
connection.url: va enchannels.query.urlsi hay una sola cuenta, o enchannels.query.accounts.<accountId>.urlsi hay varios agentes.identity.agent: va en el bindingagentId.identity.name: sirve como nombre humano; no enruta por si solo.protocol: es informativo; Query no negociaSec-WebSocket-Protocol.
Una cuenta Query mantiene un solo WebSocket por agente, pero puede transportar
muchos canales. El plugin usa thread_id como clave de sesión y de
idempotencia:
general: contexto compartido por quienes tienen acceso al agente;topic: contexto compartido por los miembros autorizados del canal;private: contexto exclusivo del usuario dueño y del soporte visible que haya entrado explícitamente.
Cada evento entrante debe incluir thread_id; cada actividad y respuesta lo
devuelve junto con client_msg_id. El plugin nunca cae silenciosamente al canal
General si no reconoce el hilo. El protocolo v1 continúa aceptándose durante el
despliegue, pero no ofrece aislamiento multihilo.
Query conserva la fuente de verdad de permisos, historial, autoría y notificaciones. El plugin recibe el autor real y el tipo/nombre del canal para construir el turno del agente, pero no decide quién puede leer o escribir.
Los cambios del servicio cron de OpenClaw se sincronizan con Query mediante
schedule.sync. Query materializa una entrega por usuario/hilo y puede enviar
schedule.cancel cuando se revoca el acceso de su último destinatario.
El hilo donde se pide una tarea y el hilo donde se entrega son datos distintos.
Por defecto se conserva el canal actual. Si la persona pide otro destino, el
agente carga de forma diferida query_delivery_targets mediante tool_search
y usa exactamente el thread_id y query_account_id autorizados que devuelve:
un administrador puede elegir otros canales del mismo agente y un usuario
normal solo canales a los que ya tiene acceso. Una cancelación queda registrada
en Query y se reenvía si el plugin estaba desconectado.
Las herramientas google_* no se ejecutan en una sesión de Query hasta que
Query confirma que esa cuenta es de quien está conversando. El guard corre en
before_tool_call, antes de que exista un cliente de Google, y bloquea cuando:
- la llamada no dice explícitamente qué
accountIdusa (no hay cuenta por defecto, ni siquiera con una sola configurada); - no hay credencial delegada vigente para el canal —incluido el caso de un cron cuya tarea no tiene un autor con acceso vigente;
- Query responde que esa cuenta no es de esa persona, está pendiente de revisión o fue revocada;
- no se puede consultar a Query (un fallo de red no abre el paso).
QUERY_EXTERNAL_ACCOUNT_GUARD_TOOLS=gmail,gcal amplía los prefijos vigilados.
El mensaje de bloqueo le dice al agente qué cuentas sí tiene esa persona, así que el reintento suele ser el correcto. La identidad sale de la credencial que firmó Query, no del prompt: el agente puede equivocarse de nombre sin que el error llegue a Google. Un subagente lanzado desde un turno de Query hereda el canal, y con él la misma restricción.
Query devuelve además el correo con el que espera que esa cuenta esté
autenticada. El guard lo usa para preguntar y para contrastar lo que la llamada
ya traiga —si no coincide, bloquea aunque la cuenta sí sea de esa persona— pero
no reescribe los parámetros de la tool: hoy las herramientas de
openclaw-google-workspace sólo reciben accountId, y el correo esperado vive
en la configuración interna de cada cuenta de ese plugin, que lo valida por su
lado. QUERY_GOOGLE_GUARD_EXPECTED_EMAIL_PARAM existe por si ese contrato
cambia; déjalo apagado.
Las cuentas de Google se configuraron en OpenClaw antes de que Query llevara la tabla de vínculos, así que la primera vez que alguien usa la suya desde Query no hay nada vinculado y la respuesta sería 403. Pedir una migración manual por persona convierte el aislamiento en un trámite, y un trámite que estorba se termina saltando por lo ancho.
Antes de preguntar, el guard busca en la configuración local de Google Workspace
el expectedEmail de ese accountId y lo manda a Query en configured_email.
Si coincide exactamente con el correo del usuario que firmó el token, Query crea
el vínculo verified en ese momento y deja pasar. Si no coincide, no autoriza y
no crea nada.
Los dos correos que viajan al backend no valen lo mismo, y por eso van en campos distintos:
| campo | de dónde sale | qué puede hacer |
|---|---|---|
configured_email |
expectedEmail en la config de la máquina |
fundar el vínculo la primera vez |
authenticated_email |
parámetros de la tool, o sea el modelo | sólo bloquear si no cuadra |
El modelo elige el accountId y puede escribir el correo que quiera en los
parámetros; lo que no puede es editar la configuración de la máquina. Esa es la
única razón por la que configured_email sirve como prueba.
El atajo no se aplica cuando la cuenta ya tiene una decisión tomada: si está
vinculada a otra persona, revocada, o esperando revisión, sigue bloqueada. Y si
la cuenta local se reconfigura a otro buzón, el guard lo delata —Query responde
email_mismatch en vez de seguir apuntando a donde ya no apunta.
Esto no reemplaza la verificación de Google. Después de que Query autorice,
openclaw-google-workspace sigue comprobando que el token OAuth real pertenece
al expectedEmail configurado; si el token es de otro correo, la tool falla
igual.
El dato se lee de:
plugins.entries["openclaw-google-workspace"].config.accounts[accountId].expectedEmail
plugins.entries es un objeto indexado por id de plugin y accounts un mapa por
accountId — ésa es la forma que hay en la máquina. El lector también acepta
plugins.entries como lista y accounts como lista, además de algunos alias del
nombre del correo, porque ese formato lo fija openclaw-google-workspace y puede
cambiarlo sin avisar: si dejara de reconocerse, el fallo sería mudo —todo el
mundo bloqueado sin un error que lo explique—. Si esa versión guarda las cuentas
en otro sitio, hay dos escapes sin tocar código:
QUERY_GOOGLE_WORKSPACE_PLUGIN_IDS=mi-plugin-google— id exacto de la entrada enplugins.entriescuando el nombre no contiene "google workspace".QUERY_GOOGLE_WORKSPACE_ACCOUNTS_FILE=/ruta/accounts.json— archivo aparte con las cuentas, leído con las mismas reglas.
Si no se encuentra correo, no se manda nada y todo se comporta como antes: sin vínculo previo, 403.
El patrón google_* cubre las herramientas actuales —google_gmail_*,
google_calendar_*, google_drive_*, google_tasks_*, google_sheets_*,
google_contacts_*, google_workspace_*— incluidas las de autenticación, que
también quedan protegidas en sesiones Query: la migración de cuentas se hace por
admin y auditoría, no por lo que el agente decida en un turno.
QUERY_EXTERNAL_ACCOUNT_GUARD_TOOLS sólo hace falta si aparece una herramienta
con otro prefijo.
openclaw plugins inspect query --runtime --json
openclaw channels status --probe
openclaw logs --followAl enviar hola desde React o Flutter, el flujo esperado es:
Query REST/WS -> plugin: message(client_msg_id)
plugin -> Query: activity(state=working) inmediato
plugin -> OpenClaw: turno del agente
plugin -> Query: activity/tool/heartbeat mientras sigue trabajando
plugin -> Query: message(client_msg_id) respuesta terminal
En los logs debe aparecer connected to Query. Si el socket cae, el plugin se
reconecta con espera exponencial de 0.5 a 15 segundos. Además envía un ping cada
25 segundos, por debajo del cierre inactivo típico de 60 segundos de Nginx.
- Cada respuesta conserva el
client_msg_idoriginal. - Un mensaje duplicado recibe la respuesta ya calculada, sin volver a ejecutar al agente.
- Las últimas 2.000 respuestas por
thread_id + client_msg_idse guardan durante hasta 30 días en<OPENCLAW_STATE_DIR>/query-channel/default/responses.json. - El archivo se escribe de forma atómica y con permisos restringidos.
responseTimeoutMsvale0de forma predeterminada (sin timeout artificial).- El plugin envía estados operativos seguros y un heartbeat de actividad cada
20 segundos. Puede ajustarse con
QUERY_ACTIVITY_HEARTBEAT_MS(mínimo 5000). - No se expone el stream privado de pensamiento. Sí se transporta el comentario público que el propio agente escribe antes de una tarea o herramienta.
channels.query.activityMode decide cuánto ve la persona mientras el agente
trabaja. Si no se configura se usa QUERY_AGENT_ACTIVITY_MODE y, en último
lugar, smart.
| Modo | Qué muestra |
|---|---|
off |
Nada. El latido sigue saliendo marcado como interno para que Query sepa que el turno vive. |
lite |
Acuse de recibo; si pasan 4 s, también el último comentario público concreto del agente. |
smart |
Bitácora pública inmediata: comentarios, planes y ciclos de herramientas, sin descartar pasos por tiempo. |
verbose |
La misma bitácora completa y además los pasos de mantenimiento marcados admin. |
debug-internal |
Todo, incluido lo marcado internal. Solo para el panel interno. |
Cada evento viaja con kind (paso canónico), run_id, sequence, source y
visibility (public / admin / internal). Query filtra por visibility antes de
reenviar al chat, así que un paso admin llega al panel pero no a la persona.
La secuencia permite que Query persista reintentos de forma idempotente sin
confundir dos pasos iguales que sí ocurrieron en momentos distintos.
Los estados operativos salen de un catálogo fijo en src/activity-policy.ts y
no requieren una segunda llamada al modelo. El evento público
commentary/preamble conserva la frase real del agente —por ejemplo, qué va a
contrastar o validar—, pero pasa por el mismo saneador que descarta
credenciales, rutas del servidor, trazas, prompts y payloads crudos. Los eventos
privados thinking/reasoning nunca se reenvían.
channels.query.effortMode (o QUERY_AGENT_EFFORT_MODE) acepta fast,
normal, careful, exhaustive y auto. Query Core puede enviar un override
por agente en data.effort_mode; ese valor manda para el turno actual y evita
tener que reiniciar el gateway al editar un agente.
auto es el valor predeterminado. El router es determinista y no llama a otro
LLM: saludos y preguntas simples parten en fast, una consulta ordinaria en
normal, escrituras/finanzas/comunicaciones/configuración suben como mínimo a
careful, y auditorías, cierres, migraciones o producción suben a
exhaustive. Pedir "hazlo rápido" nunca reduce ese piso de seguridad.
Cada actividad incluye solamente enums saneados:
effort_mode_configured, effort_mode_effective, effort_escalated y
effort_escalation_reason. Los logs de fin de turno agregan esos campos,
latencia total, cantidad de herramientas y tamaño aproximado del contexto.
QUERY_TOOLS_CACHE_TTL_MS (60000 por omisión, 0 desactiva) controla cuánto
se reutiliza la metadata de módulos entre llamadas de una misma credencial.
- Los adjuntos entrantes se entregan al contexto multimedia del agente; las URLs multimedia devueltas por el agente regresan como adjuntos de Query.
Cada turno del canal Query recibe una directiva central que exige entregar los
archivos generados mediante attachments/assets y prohÃbe presentar rutas del
workspace, localhost, redes privadas o Tailscale como resultado final. La regla
vive en src/inbound.ts: aplica desde el primer mensaje de cualquier agente y
no depende de AGENTS.md ni TOOLS.md. Los LocalPath de adjuntos entrantes
siguen disponibles para que el agente pueda leerlos.
La tool query_attachment_send recibe file_path y opcionalmente thread_id,
message, name, mime_type y kind. Reutiliza uploadArtifactToQuery() con
la credencial delegada, devuelve la URL opaca de Query y publica media
estructurada para que OpenClaw la incorpore a mediaUrls; el socket la entrega
finalmente en data.attachments. En outbound y cron se usa el mismo uploader
con la credencial del bot mediante uploadOutboundArtifactToQuery().
Como defensa final, src/private-links.ts reescribe rutas Linux/Windows, hosts
privados y URLs fabricadas con rutas locales. Si el archivo no existe, falta
credencial o falla la subida, elimina la referencia insegura, conserva una
explicación visible y registra solo un código seguro del error.
uploadArtifactToQuery() y uploadOutboundArtifactToQuery() aceptan el campo
opcional replaceAttachmentId. Al enviarlo usan PUT y reemplazan el blob del
adjunto conservando su ID; al omitirlo mantienen el POST de creación.
Reemplazar es el comportamiento por defecto. El canal recuerda, por hilo y
ruta local, qué asset salió de cada archivo (src/artifact-store.ts,
persistido en <OPENCLAW_STATE_DIR>/query-artifacts.json). Reenviar el mismo
archivo reemplaza su contenido: la URL pública no cambia, así que el enlace que
la persona ya tiene sigue sirviendo y muestra la versión al día. Sin esto, una
tarde de correcciones dejaba diez copias y nueve enlaces obsoletos.
Conservar la versión anterior es una decisión explícita, no un accidente: el
agente llama a query_artifact_new_version con la ruta antes de reenviar, y ese
envío crea un asset nuevo dejando el anterior intacto.
Dos salvaguardas: un asset fijado (pinned) no se reemplaza nunca —es un
template o algo ya publicado, y Query no lo impide del lado del servidor, así
que la regla vive aquí—, y si Query rechaza el reemplazo se olvida la ruta y se
sube como nuevo, que es peor que reutilizarlo pero mucho mejor que perder el
archivo.
QUERY_ARTIFACT_REUSE_TTL_MS permite caducar el reuso (0 = sin límite, el
valor por defecto).
Para templates u otros artifacts permanentes se puede pasar pinned: true al
crear o reemplazar. Query responderá is_pinned: true y expires_at: null, y
el asset quedará excluido de la limpieza automática. Para cambiar
un asset existente sin volver a subir su archivo están disponibles
setQueryArtifactPinned() y setOutboundQueryArtifactPinned().
npm run check
npm test
npm run build
npm packLas pruebas levantan un servidor WebSocket real y comprueban el ACK inmediato, la respuesta correlacionada, la persistencia y la deduplicación de reintentos.