Aprobaciones

Ver en Markdown

Cómo OpenWAF Tasks deja las escrituras en manos de una persona. La guardia de solo lectura, los pasos tipados, el hash, la fase de actuar, la caducidad y el webhook.

En esta página

Una tarea nunca escribe sola en un sitio. Propone una acción, una persona aprueba esa acción exacta y solo entonces el runner la realiza, sin tu código ni el modelo.

La guardia de solo lectura

Mientras una ejecución prepara, el runner mantiene su propia conexión DevTools con el navegador, enganchada a cada página, marco y worker. Hace fallar toda petición cuyo método no sea GET, HEAD u OPTIONS y la registra como evento blocked (se cuenta en blocked_requests de la ejecución). Ni tu script ni el agente pueden quitarla: es una conexión aparte que nunca ven.

Un envío de formulario, un fetch con POST o un DELETE desde la página fallan en esta fase. Las páginas que necesitan un POST solo para leerse no funcionan en una tarea.

Pasos

Una acción es una lista de 1 a 50 pasos tipados. El primero debe ser goto. Los selectores son selectores de Playwright (css=, text=, role=…; CSS atraviesa las shadow roots abiertas).

opCamposQué hace
gotourlAbre una URL https:// de uno de los dominios permitidos de la tarea
clickselectorHace clic en el elemento
fillselector, textFija el valor de un campo o área de texto
typeselector, textEscribe el texto tecla a tecla
presskey, selector opcionalPulsa una tecla, por ejemplo Enter
selectselector, valueElige una opción de un <select>
checkselectorMarca una casilla o botón de opción
wait_forselector o textEspera a que aparezca
waitmsEspera de 1 a 10000 ms
assert_urlurlSe para con error salvo que la URL de la página empiece así (https://, dominio permitido)
assert_texttextSe para con error salvo que la página lo muestre

Los textos admiten hasta 10000 caracteres, los selectores y valores hasta 2000 y las URL hasta 4000. Una propuesta lleva además un summary de una línea (hasta 300 caracteres), el target (la URL https:// donde cae la escritura; su host es el dominio de la acción) y una captura de la página tal como la vio la tarea.

Usa assert_url y assert_text para que una acción falle en vez de hacer otra cosa si la página ha cambiado desde que se propuso.

El hash de la carga

OpenWAF valida los pasos contra los dominios permitidos de la tarea y guarda payload_sha256: el SHA-256 en hexadecimal del JSON canónico de {"steps": [...]}, con los campos en el orden de la tabla (op, url, selector, text, key, value, ms), sin los campos vacíos, sin escapar HTML y sin salto de línea final.

Para aprobar devuelves ese hash con POST /v1/orgs/{org}/task-approvals/{id}/approve. Si no coincide con el guardado la respuesta es 409 payload_changed: apruebas lo que has visto y nada más. El runner vuelve a calcular el hash antes de actuar.

La fase de actuar

Tras la aprobación el runner abre un navegador nuevo con la clave y el perfil de la tarea, comprueba el hash y repite él mismo los pasos. No se ejecuta ni tu script ni el modelo. Las peticiones con métodos de escritura solo se permiten hacia el dominio de la acción y sus subdominios. La aprobación recibe executed_at, una captura final y un result: ok, final_url y error.

Si un límite por dominio aún no permite la acción, la aprobación sigue approved con not_before y se ejecuta entonces.

Estados

AprobaciónSignificado
pendingEspera a una persona
approvedAprobada; espera al runner o a not_before
executedLos pasos se ejecutaron hasta el final
failedFalló un paso; result.error dice cuál
rejectedUna persona la rechazó
expiredNadie decidió a tiempo

Una ejecución con propuestas sigue en awaiting_approval hasta que no queda ninguna pendiente. Entonces termina succeeded si se ejecutó alguna acción, failed si alguna falló, rejected si se rechazaron todas o expired si caducaron.

Caducidad

Una aprobación caduca tras approval.expire_minutes: 120 minutos (2 horas) por defecto, entre 10 y 1440.

Políticas

approval.policySignificado
requiredCada acción espera a una persona. Es la única política que puedes fijar.
relaxedLas propuestas hacia relaxed_domains se aprueban solas. Solo la fija un operador, con motivo, y solo para destinos internos no públicos.

Webhook

Dale a la tarea un webhook.url (una URL https:// pública) para recibir los eventos approval.requested y run.finished. El secreto (owts_…) se muestra una vez al fijar el webhook; "rotate_webhook_secret": true en un PATCH genera otro.

Cada entrega es un POST con un cuerpo JSON y estas cabeceras:

X-OpenWAF-Signature: t=1761735600,v1=5d2f…
X-OpenWAF-Event: approval.requested
X-OpenWAF-Delivery: 0b9d5c3e-…
{"id": "0b9d5c3e-…", "type": "approval.requested", "created_at": "2026-10-29T10:00:00Z", "org_id": "…",
 "task": {"id": "…", "name": "guestbook"},
 "run": {"id": "…", "number": 12, "status": "awaiting_approval", "error": "", "finished_at": null},
 "approval": {"id": "…", "status": "pending", "summary": "Sign the guestbook…", "target": "https://example.com/guestbook",
              "domain": "example.com", "payload_sha256": "9c1e…", "expires_at": "2026-10-29T12:00:00Z"}}

approval es null en run.finished. Una respuesta 2xx cuenta como entregada; si no, la entrega se reintenta 3 veces, tras 1, 5 y 30 minutos. X-OpenWAF-Delivery no cambia entre reintentos, así que puedes descartar duplicados.

t es la hora Unix de la entrega y v1 el HMAC-SHA256 en hexadecimal, con el secreto como clave, de t, un punto y el cuerpo tal cual. Recalcúlalo, compara en tiempo constante y rechaza entregas de más de 5 minutos:

verify.mjs
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(parts.v1 ?? "", "hex");
  return got.length === want.length && timingSafeEqual(got, want);
}

Última actualización:

↑↓ para moverte↵ para abriresc para cerrar