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>
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 198o/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
- Cada release requiere entrada en
CHANGELOG.mdcon fecha y seccionesAdded / Changed / Fixed / Securitysegún corresponda. - El tag se crea después del commit de changelog, no antes.
[Unreleased]acumula todos los cambios hasta que Marcos decide liberar.- Claude propone el número de versión basándose en las reglas de arriba; Marcos lo aprueba antes de tagear.
git tagygit push --tagslos ejecuta Claude solo cuando Marcos lo confirma — son acciones externas (modifican el remoto) que requieren aprobación explícita.- 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.py→app/scraper/con el mismo contrato de métodosstorage.py→ Postgres con el mismo interfaceconfig.py→ se expande con más settingstelegram_bot.py→app/bot/channels/telegram/poller.py→app/poller/utils/sanitize.py→ se mantiene igual
No es código desechable.