Ciclo de Aprobación por Operador
Un agente que se comunica con contrapartes externas nunca debe enviar por su propio criterio y debe mantener informado al operador internamente. Este skill define el contrato: cada borrador saliente se archiva como una obligación, un operador decide sobre el texto exacto y un libro de entregas prueba lo que salió.
Cuándo Usarlo
- Un agente redacta respuestas a clientes, proveedores, inversores o socios en un canal compartido, correo electrónico o chat, y un humano debe aprobar antes de enviar.
- Necesita un registro de auditoría que vincule cada mensaje enviado con el texto exacto del borrador, el operador que lo aprobó y la hora de la decisión.
- Ha visto cómo una aprobación obsoleta libera un borrador reescrito, o dos trabajadores entregan el mismo mensaje aprobado dos veces.
- Los borradores siguen re-preguntando a las contrapartes por hechos que el libro ya contiene.
Cómo Funciona
Objetos
| Objeto | Significado |
|---|---|
| Obligación | Algo que debemos a una contraparte. El estado avanza: drafted, luego approved o rejected, luego sent. Lleva direction, counterparty, channel y una época updated_at. |
| Borrador | Fila adjunta que contiene el texto exacto del borrador, un sha256 de ese texto, coordenadas de origen (plataforma, canal, hilo, usuario) y prioridad (P0 a P3). Uno por obligación, reemplazado al re-archivar. |
| Decisión | Aprobación o rechazo de un operador, registrado con el id del operador, un nonce y la época del borrador contra la que se tomó. |
| Instantánea de aprobación | Texto, hash, época y destino inmutables registrados por el escritor de decisiones ya autorizado. Las instantáneas faltantes no pueden conceder el envío. |
| Reclamo | Reserva duradera con un token aleatorio y estado; como máximo un reclamo activo por obligación. |
| Entrega | Fila del libro que demuestra un envío o aviso para un par (obligación, decisión). |
El esquema de referencia está en references/approval-ledger.sql.
Archivar un borrador
- Limpiar entradas. Eliminar caracteres de control, colapsar espacios en blanco en campos de una sola línea y aplicar límites de longitud (borrador, resumen, contexto, contraparte). Los campos vacíos o demasiado grandes se rechazan, no se truncan silenciosamente.
- Ejecutar la puerta de línea base (abajo). Puede rechazar el archivado.
- Hashear el texto del borrador con sha256. El prefijo del hash va al resumen para que el panel de aprobación muestre qué texto está aprobando.
- Actualizar o insertar. Si ya existe una obligación abierta con estado
draftedpara el mismo par (contraparte, canal), reemplazar el borrador adjunto y avanzar elupdated_atde la obligación. Ese avance es la rotación de época: cualquier decisión vinculada a la época anterior ya no puede liberar el nuevo texto. En caso contrario, insertar una nueva obligación con estadodrafted. - Enrutar el recibo de archivado solo a un destino interno de operaciones verificado y configurado. Si el origen es ese destino interno, acusar recibo allí. Nunca silencioso significa reporte interno, no una respuesta externa automática. Mantener los hashes de los borradores, el estado de aprobación, la identidad del operador y los metadatos del flujo de trabajo fuera de los canales visibles para la contraparte. Los orígenes desconocidos o no clasificados permanecen en silencio; un mensaje directo no es automáticamente interno.
Si un destino interno verificado no está disponible, conservar el resultado del archivado en el resultado de la herramienta interna o en la superficie del operador. Nunca recurrir a un origen externo o desconocido. Un resultado de herramienta expuesto a externos no es una superficie interna.
Archivar un borrador no autoriza una respuesta externa. Cualquier pregunta aclaratoria o respuesta neutral permitida por la política es una decisión saliente separada, sujeta a las restricciones existentes de mención, canal, solo-borrador, congelado y nunca en la disciplina de canales de contraparte. No debe divulgar metadatos internos de aprobación.
Puerta de línea base
Antes de archivar cualquier borrador, consultar la línea base actual de la contraparte (un libro temporal, almacén de contratos o CRM):
- Contrato firmado o entregado registrado: rechazar el archivado con la evidencia y una recomendación. Preguntar a una contraparte sobre especificaciones después de firmar es el fallo exacto que esta puerta existe para evitar.
- Anulación por operador:
force_despite_signed_contractpermite el archivado y marca[BASELINE_OVERRIDE_SIGNED_CONTRACT]en el contexto del borrador. - Servicio de puerta inalcanzable: el archivado continúa y el contexto se marca
[BASELINE_CHECK_UNAVAILABLE]. El panel ve que la protección estuvo desactivada. Los fallos nunca desactivan silenciosamente la puerta. - Cuando los hechos están disponibles, adjuntar los más recientes al contexto como un resumen
[BASELINE FACTS: ...]para que el borrador llegue con la verdad actual.
Decidir
El panel de aprobación lista las obligaciones con estado drafted y dirección we_owe_them. Aprobar o rechazar escribe una fila de decisión que lleva la época del borrador (draft_updated_ts) y cambia el estado de la obligación en la misma transacción. Una decisión cuya época no coincide con el updated_at actual está obsoleta y no debe liberar nada.
Para una decisión de aprobación ya autorizada, la misma transacción inserta una fila inmutable obligation_approval_snapshots: IDs de decisión y obligación, época actual del borrador, texto exacto y SHA-256, plataforma/canal/hilo y tipo draft_sent. El escritor de la decisión debe establecer la autorización antes de escribir; la referencia nunca autentica a un operador ni fabrica una decisión. La política de aprobación automática no se habilita ni expande mediante la referencia. Las decisiones heredadas sin instantáneas requieren conciliación explícita o una nueva aprobación; nunca retroceder el permiso del borrador mutable actual.
Entregar
La referencia SQLite está en references/approval_claims.py. Concede permiso de envío pero nunca llama al transporte. Usar una base de datos de referencia local existente inicializada desde el fixture SQL; el módulo no aplica migraciones de esquema o producción. Solo un escritor de decisiones de confianza puede poblar los registros de aprobación. Todos los escritores deben habilitar claves foráneas y disparadores recursivos y respetar las protecciones del esquema; la manipulación administrativa de la base de datos está fuera de este modelo.
- Descubrir borradores aprobados vinculados. El descubrimiento no es permiso.
claim()abre su propia transacciónBEGIN IMMEDIATE, valida la época aprobada actual, el texto exacto, el SHA-256 calculado y el destino completo contra la instantánea, e inserta un reclamo único antes de devolver su token. Un conflicto detiene al trabajador antes del transporte. Los recibos completados no pueden reclamarse de nuevo. begin_dispatch()revalida el vínculo y cambia atómicamenteclaimedadispatchingusando el token. Solo su llamante ganador recibe eldraft_textexacto y el destino después del commit. Nunca regenerar texto, releer un adjunto mutable para el transporte ni reutilizar la carga para otro intento. Se rechaza una transacción de llamante anidada; el permiso no puede depender de un commit posterior del llamante. No queda ninguna transacción de base de datos abierta durante el transporte.- Un resultado confirmado exitoso va a
complete(), que registra atómicamente la coordenada de entrega, marca el reclamo como entregado y cambia la obligación asent. Una finalización idéntica es un no-op; las coordenadas conflictivas fallan. La clave UNIQUE del recibo deduplica registros, no efectos externos previos. - Excepciones, tiempos de espera, muerte del trabajador después de begin-dispatch o fallo de persistencia del recibo dejan un intento bloqueado.
mark_unknown()registra incertidumbre. Los reclamos desconocidos nunca expiran, se reabren, se reintentan automáticamente ni permiten que otra decisión para esa obligación los evite. Un llamante de confianza puede usarreconcile()con coordenada confirmada exitosa y evidencia; el módulo no verifica esa evidencia. Un recibo ausente no es prueba de no entrega.
La garantía es un intento de envío automático por decisión aprobada, no una entrega externa exactamente una vez. Un crash después de begin-dispatch pero antes del transporte puede dejar cero envíos y un reclamo retenido. Liberar un resultado desconocido para un nuevo intento requeriría cercar al ejecutor original y verificar la semántica del proveedor; esta referencia deliberadamente no proporciona tal operación de reintento.
| Estado del reclamo | Estados siguientes permitidos |
|---|---|
| claimed | dispatching o cancelled antes del envío |
| dispatching | delivered o unknown |
| unknown | delivered solo mediante conciliación de confianza |
| delivered, cancelled | terminal; la clave de decisión no se puede reutilizar |
Mientras un reclamo está activo, las protecciones de la base de datos congelan las escrituras de obligación, borrador y decisión, incluyendo reemplazos. Las instantáneas y los reclamos no se pueden borrar. Cancelar una operación reclamada con su token antes de re-archivar; el token obsoleto entonces no concede nada. Después de que comience el envío, mantener nuevas ediciones o revocaciones para conciliación. Esto serializa los cambios en lugar de pretender retirar una operación en vuelo.
Las decisiones rechazadas y las filas heredadas sin borradores/instantáneas nunca entran en esta ruta de envío de borradores externos. Reportarlas en la superficie del operador interno para manejo manual. Los pies de recibo internos siguen siendo internos:
aprobado por <operador> · recibo <decision_id> · borrador sha256 <prefijo>.
Nunca alterar el texto externo ya aprobado para adjuntar metadatos del flujo de trabajo.
La validación local enfocada usa bases de datos temporales, conexiones separadas y un contador de intentos simulado, no un proveedor o mensaje real:
python3 -m unittest discover -s tests/skills -p 'test_approval_delivery_claims.py'.
Las pruebas requieren Python 3.11+ con soporte de serialización SQLite; la referencia usa solo la biblioteca estándar. Las comprobaciones de contrato del patrón de escritorio existentes siguen siendo una verificación de compatibilidad separada.
Aprobación automática con tiempo límite (opcional)
Un borrador puede llevar auto_send_after (segundos de época). Un barrido aprueba borradores cuyo plazo ha pasado sin decisión, registrando al operador auto-ttl, luego la entrega procede por la ruta normal. Las acciones del operador siempre ganan: una decisión cambia el estado antes de que el barrido lo vea, y un re-archivo rota la época y mueve o borra el plazo. El barrido re-comprueba el estado y la época dentro de la transacción de escritura, por lo que una carrera se resuelve como un no-op. Los borradores sin plazo permanecen con puerta dura para siempre.
Vinculación de señal
Un borrador puede nombrar la obligación entrante a la que responde (signal_obligation_id). Este es el único enlace veraz para la medición de latencia (señal entrante a respuesta redactada) y permite que el escaneo de SLA trate ese elemento entrante como respondido. Rechazar el archivado si la fila referenciada no existe.
Ejemplos
Archivar un borrador
file_request(
draft="Gracias, podemos mantener el espacio hasta el viernes. ¿Qué fecha de inicio te funciona?",
counterparty="acme-supplier",
context="respuesta a pregunta sobre ventana de entrega",
origin_platform="slack", origin_channel="#acme-compartido",
origin_thread="1712345678.000100", priority="P1",
signal_obligation_id=412)
-> {obligation_id: 431, draft_sha256: "9f2c...", refiled: false}
El destino interno de operaciones verificado y configurado ve:
Borrador archivado para aprobación (P1, sha 9f2c8a1b). Esperando al operador.
El canal de origen visible para la contraparte no recibe ningún aviso de archivado. Si no hay un destino interno verificado disponible, el recibo permanece en el resultado de la herramienta interna o en la superficie del operador, sin recurrir a externos.
Re-archivar después de una indicación
El operador pide un borrador más corto. Archivar de nuevo para el mismo par (contraparte, canal) devuelve refiled: true, el texto y el hash del adjunto cambian, y updated_at avanza. Un clic de aprobación en la fila antigua del panel lleva la época antigua y se ignora.
Rechazo por puerta
DeskApprovalError: la puerta de línea base rechazó este borrador: el libro muestra un
contrato firmado para 'acme-supplier'. Evidencia: acuerdo principal ejecutado
2026-08-14. Recomendación: no preguntar. Re-archivar con
force_despite_signed_contract=true si esto es genuinamente un hilo nuevo.
Pie de recibo en un canal interno
Confirmado para el viernes, fecha de inicio 2026-09-08.
aprobado por operador-a · recibo 118 · borrador sha256 9f2c8a1b2d3e4f50
Invariantes a probar
- Los recibos de archivado van solo a operaciones internas verificadas y configuradas; el origen recibe uno solo cuando es ese destino interno verificado.
- Un origen desconocido permanece en silencio. Un destino interno no disponible usa el resultado de la herramienta interna o la superficie del operador, sin recurrir a externos.
- El mismo par (contraparte, canal) archivado dos veces produce una obligación, dos épocas.
- Una decisión con época obsoleta nunca resulta en una fila de entrega.
- Dos reclamantes concurrentes producen un permiso de envío; los perdedores nunca intentan el transporte.
- Los resultados desconocidos y la persistencia fallida del recibo nunca habilitan un reintento automático.
- La finalización exitosa registra el recibo y el estado enviado en una transacción.
- Una época, texto, hash o destino alterados no pueden adquirir ni iniciar un reclamo.
- Los reclamos activos bloquean el re-archivo; solo la cancelación previa al envío puede liberar esa retención.
- La puerta no disponible marca el marcador; la puerta con contrato firmado rechaza sin fuerza.
- Auto-ttl nunca se activa contra texto que el operador ha re-archivado desde entonces.