Files
hechakuaa_bot/CLAUDE.md
markosbenitez f5f7fdc179 docs: arquitectura con seguridad en CLAUDE.md + SECURITY.md dedicado
CLAUDE.md: diagrama Mermaid reemplaza file-tree desactualizado; incluye
todos los componentes actuales con controles de seguridad anotados.
Stack actualizado con aiohttp y Docker. Referencia a SECURITY.md.

SECURITY.md (nuevo): diagrama defensa en profundidad (7 vectores → controles),
diagrama vault.py v3 con capas KEK+DEK, diagrama de ciclo de vida de
credenciales (secuencia), tabla de controles transversales y limitaciones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:14:33 -03:00

13 KiB

Pedrito Hechakuaa — Instrucciones para Claude Code

Qué es esto

Pedrito Hechakuaa: un bot de Telegram que monitorea notificaciones pendientes en el sistema judicial de Paraguay (apps.csj.gov.py) y avisa cuando aparece algo nuevo. Uso propio de un solo abogado (single-tenant).

Versión actual: v0.3.1 — ver historial completo en CHANGELOG.md.

Scope implementado:

  • Login al PJ + polling de notificaciones pendientes (multi-tenant)
  • Diff vs snapshot por tenant → solo notifica lo nuevo
  • Telegram como canal de salida con onboarding, tono personalizable y timezone
  • Búsqueda de expedientes y descarga de PDFs
  • Infraestructura: Docker, health check HTTP, schema migrations, webhook opt-in

Fuera del scope actual (no agregar sin consulta explícita):

  • Motor de reglas de plazos
  • LLM / conversación libre
  • Web de onboarding
  • Actuaciones de expedientes (es v1)
  • Acuse de notificaciones (nunca — efecto procesal irreversible)

⚠️ Invariantes de seguridad — NUNCA se violan, aunque sea v0

✅ Credenciales cifradas con Fernet en .env — desencriptar en memoria, usar, descartar
✅ .env en .gitignore desde el primer commit
✅ Todo log pasa por scrub() en utils/sanitize.py antes de escribirse
✅ Audit trail mínimo: quién hizo qué, cuándo, resultado — SIN payload sensible
✅ El bot solo responde al TELEGRAM_CHAT_ID configurado (no es público)

❌ PUT /Notificaciones/{id}/recibir — NUNCA. Acusa notificaciones, efecto procesal irreversible
❌ Password, bearer token, o cualquier credencial en logs (ni en DEBUG)
❌ print() para debug — SIEMPRE usar logger
❌ .env con valores reales en el repo

scrub() (from utils.sanitize import scrub): función en utils/sanitize.py que recibe cualquier string y reemplaza patrones sensibles (tokens, credenciales) antes de loggear. Toda llamada a logger.*() con datos externos pasa por acá.


Arquitectura

graph TD
    ABOGADO["👤 Abogado"]
    TG(["Telegram API"])
    CSJ(["apps.csj.gov.py · PJ"])

    subgraph VPS ["VPS — Ubuntu 22.04"]
        NGX["🔐 Nginx\nSSL · 30 r/s webhook · 10 r/min health\nX-Content-Type-Options · X-Frame-Options"]

        subgraph DOCKER ["🐳 Docker container"]
            WH["POST /webhook\n→ 404 si token inválido\nsecrets.compare_digest"]
            HL["GET /health\npúblico: status + timestamp"]
            HH["GET /health/history\nBearer auth · chat_id enmascarado SHA256"]
            ONB["Onboarding FSM · 8 pasos\ninvite atómico UPDATE WHERE usado=0\ntiming ≥800ms · compare_digest"]
            CMD["Comandos\n/notif · /exp · /pdf · /estado · /ayuda"]
            POLLER["Poller · APScheduler\nmulti-tenant · lun-vie 7-18h"]
            VAULT["🔐 vault.py\nPBKDF2-SHA256 600k iter\nDEK 32B por cifrado · envelope v3"]
            CFG["🔐 config.py · Settings\nenv vars borradas post-init"]
            SCRUB["scrub()\ntodo dato externo a logs\npasa por acá antes de escribir"]
        end

        DB[("pasante.db\ntenants · audit_log\ninvite_codes · migrations")]
        SNAP["snapshot_{chat_id}.json\ndiff notificaciones por tenant"]
    end

    ABOGADO <-->|"HTTPS"| TG
    TG -->|"HTTPS + X-Secret-Token"| NGX
    NGX --> WH
    NGX --> HL
    NGX --> HH
    WH --> ONB
    WH --> CMD
    ONB -->|"cifra credenciales"| VAULT
    VAULT <-->|"credentials_enc"| DB
    CMD --> DB
    HH -->|"audit_log — chat_id enmascarado"| DB
    POLLER -->|"HTTPS + Bearer + usuario-rol:16"| CSJ
    CSJ -->|"notificaciones JSON"| POLLER
    POLLER --> SNAP
    POLLER -->|"alertas"| TG
    POLLER -->|"descifra por tenant"| VAULT
    CFG -.->|"configura"| VAULT
    CFG -.->|"configura"| POLLER

    classDef secure fill:#fef2f2,stroke:#991b1b,color:#450a0a
    class NGX,VAULT,CFG,SCRUB secure

Ver SECURITY.md para el diagrama de arquitectura de seguridad detallado.


Stack (sin cambios hasta nuevo aviso)

Capa Tecnología
Lenguaje Python 3.12 con type hints estrictos
HTTP client httpx async
Bot python-telegram-bot v21 async
Scheduler APScheduler 3
Cifrado cryptography (Fernet + PBKDF2)
HTTP server aiohttp>=3.9 (health check + webhook)
Config pydantic-settings
Logging structlog (JSON en prod, ConsoleRenderer en dev)
Infraestructura Docker + docker-compose

Sin Playwright. Sin Postgres. Sin Redis. Sin LLM.

No introducir librerías fuera de esta lista sin justificación explícita.


Sistema judicial target

URL base: https://apps.csj.gov.py Docs: docs/reconocimiento/SISTEMA_NUEVO_RECON.md y MANUAL_SANITIZADO.md

Flujo de autenticación (confirmado por reconocimiento)

1. GET https://apps.csj.gov.py/login
   → Cookie: cookiesession1={hex32}

2. POST https://apps.csj.gov.py/autenticador/login
   Headers: Cookie: cookiesession1={hex32}
   Body JSON: {"usuario": "{cedula}", "clave": "{password}", "temporal": false}
   → bearerToken (TTL 1h), refreshToken, timestampExpiracionUtc

3. Todos los requests siguientes llevan:
   Cookie: cookiesession1={hex32}
   Authorization: Bearer {bearerToken}
   usuario-rol: 16   ← 16 = Abogados

Endpoints de notificaciones (confirmados)

GET /api/gestion-partes/Notificaciones/CantidadNotificacionesPorRecibir
→ int (cantidad de notificaciones pendientes)

GET /api/gestion-partes/Notificaciones/PorRecibir?offset=0&cantidad=50
→ lista de NotificacionPendiente

Campos relevantes de NotificacionPendiente

{
  "codNotificacionOrigen": 1234567,
  "nroExpedienteNumero": 1234,
  "nroExpedienteAnio": 2024,
  "caratula": "Rodríguez c/ Municipalidad Capital",
  "descripcionActuacion": "Auto Interlocutorio",
  "descripcionTipoActuacion": "Providencia",
  "fecha": "2026-05-20",
  "juzgado": "1er Juzgado Civil Capital",
  "permisoFirmado": "JWT...",
  "codCasoJudicial": 103708,
  "origen": 0
}

Estados de conversación

El bot es simple pero tiene estados implícitos que deben manejarse explícitamente:

UNREGISTERED          → usuario no autorizado (responder a cualquier message)
REGISTERED_IDLE       → autorizado, sin operación en curso (estado normal)
CHECKING              → revisión de notificaciones en curso (no disparar otra)
ERROR_AUTH            → fallo de auth del PJ (bot pausado hasta intervención)
ERROR_PASSWORD_TEMP   → password temporal detectada (bot pausado)

Manejo de errores — mensajes accionables

Nunca: "Error inesperado" o mensajes técnicos al usuario. Siempre: qué pasó + qué puede hacer el usuario.

Situación Mensaje al abogado
Login fallido por credenciales "No pude conectarme al PJ. Las credenciales podrían haber cambiado. Contactá al administrador."
Password temporal "Tu contraseña del PJ es temporal. Cambiala en apps.csj.gov.py y luego reiniciá Pedrito Hechakuaa."
Error de red transitorio "No pude conectarme al sistema judicial. Voy a reintentar en la próxima ronda ({hora})."
5 errores consecutivos "El sistema del PJ estuvo inaccesible por un tiempo. Si el problema persiste, revisá apps.csj.gov.py directamente."

Emojis con criterio: éxito, error, ⚠️ advertencia, procesando. No decorativos.


Estructura del snapshot (storage.py)

JSON local en data/snapshot.json:

{
  "last_check": "2026-05-20T10:00:00Z",
  "notificaciones_vistas": [1234567, 1234568, 1234569]
}

Solo guardamos los IDs (codNotificacionOrigen) ya notificados. Diff: cualquier ID en la respuesta del PJ que no esté en esta lista = novedad.


Comandos de Telegram

  • /start — muestra estado del bot
  • /notif — revisa notificaciones ahora (no espera al siguiente polling)
  • /estado — último check, cantidad de notificaciones activas
  • /ayuda — lista comandos
  • /exp <número> — detalle de expediente por número (ej: /exp 198 o /exp 198/2026)
  • /exp <texto> — busca expedientes por carátula (ej: /exp rodríguez)

Formato de mensajes al abogado

Notificación proactiva nueva

📋 Nueva notificación judicial

{caratula}
Exp. {numero}/{anio} — {juzgado}

Tipo: {descripcionActuacion}
Fecha: {fecha formateada DD/MM/YYYY}

⚠️ Para acusar esta notificación entrá directamente
en https://apps.csj.gov.py

Sin novedades (solo cuando el abogado lo pide con /notif)

✅ Sin novedades desde las {hora} de hoy.
Tenés {N} notificaciones pendientes en total.

Iniciativa agéntica — clasificación de cambios

Nivel Tipo de cambio Acción
1 Refactor interno, naming, código muerto Ejecutar libremente
2 Cambio visible en mensajes del bot o UX conversacional Ejecutar y reportar
3 Seguridad, credenciales, dependencias nuevas, scope del bot Consultar antes

Argumentos NO válidos para evitar consulta de Nivel 3:

  • "Es una mejora de legibilidad"
  • "Es trivial de revertir"
  • "Resuelve un bug menor"

Definition of Done — cada feature o fix

  • Happy path funciona en ejecución real (python main.py)
  • Credenciales no aparecen en logs (verificar con LOG_LEVEL=DEBUG)
  • Maneja desconexión del PJ con retry + mensaje claro al usuario
  • Maneja timeout de sesión: relogin automático transparente
  • Maneja datos vacíos (cero notificaciones, response inesperado)
  • credential_scrubber() usado en cualquier log con datos externos
  • Type hints completos en funciones nuevas o modificadas
  • Sin print() en código nuevo

Alerta especial: fragilidad del cliente HTTP

El csj_client.py es la parte más probable de romperse. Ante cualquier reporte de "el cliente funciona", verificar:

  • ¿Se probó con credenciales reales o con mocks?
  • ¿Los campos del JSON del PJ son los actuales o los del último test?
  • ¿El timeout fue suficiente para la red real?

Ante cambio de schema del PJ: actualizar csj_client.py y documentar en OBSERVATIONS.md qué cambió y cuándo.


Archivos del proyecto

Archivo Propósito
CLAUDE.md Este archivo — reglas para Claude Code
CHANGELOG.md Historial de versiones — actualizar antes de cada release
DEPLOY.md Guía de despliegue en VPS: Docker, Nginx, SSL, DNS
OBSERVATIONS.md Hallazgos durante el desarrollo (schema real del PJ, etc.)
TECH_DEBT.md Deuda técnica anotada
migrations/ Archivos .sql de schema — nunca editar los ya aplicados
data/pasante.db Base de datos SQLite (tenants, audit_log, invite_codes)
data/snapshot_*.json Estado del diff de notificaciones por tenant

Gestión de versiones

Esquema: Semantic Versioning (semver 2.0)

MAJOR.MINOR.PATCH — tags git con prefijo v (ej: v0.3.1).

Número Incrementar cuando... Ejemplos
MAJOR Cambio incompatible con versiones anteriores: migración de DB no retrocompatible, cambio de protocolo del PJ, rediseño de arquitectura v1.0.0 (migración a Postgres)
MINOR Feature nuevo sin romper lo existente: comando nuevo, integración nueva, mejora visible al usuario /recordatorio, modo silencioso
PATCH Bug fix, security patch, mejora interna, documentación operacional corrección de campo del PJ, hardening

Reglas que Claude Code sigue siempre

  1. Cada release requiere entrada en CHANGELOG.md con fecha y secciones Added / Changed / Fixed / Security según corresponda.
  2. El tag se crea después del commit de changelog, no antes.
  3. [Unreleased] acumula todos los cambios hasta que Marcos decide liberar.
  4. Claude propone el número de versión basándose en las reglas de arriba; Marcos lo aprueba antes de tagear.
  5. git tag y git push --tags los ejecuta Claude solo cuando Marcos lo confirma — son acciones externas (modifican el remoto) que requieren aprobación explícita.
  6. Tags retroactivos son válidos para dar historia a commits anteriores; se crean con git tag -a vX.Y.Z <hash>.

Proceso de release (copy-paste)

# 1. CHANGELOG.md ya actualizado (mover [Unreleased] → [X.Y.Z] con fecha)
git add CHANGELOG.md
git commit -m "chore: release vX.Y.Z"

# 2. Tag anotado
git tag -a vX.Y.Z -m "vX.Y.Z — resumen en una línea"

# 3. Push incluyendo el tag
git push origin main --tags

Cómo arrancar

# Setup (una sola vez)
pip install uv && uv sync
cp .env.example .env
# Completar .env: FERNET_KEY, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID

# Cifrar credenciales del PJ
python scripts/cifrar_credenciales.py

# Arrancar
python main.py

# Tests
pytest
pytest -k "poller" -v
pytest --co -q   # solo listar tests sin correr

Cómo migrar de v0 a v1

La v0 está diseñada para migración incremental:

  • csj_client.pyapp/scraper/ con el mismo contrato de métodos
  • storage.py → Postgres con el mismo interface
  • config.py → se expande con más settings
  • telegram_bot.pyapp/bot/channels/telegram/
  • poller.pyapp/poller/
  • utils/sanitize.py → se mantiene igual

No es código desechable.