Runbook: Firma y distribución de paquetes del Agente (Lanzador Estable)
Propósito
Documentar el ciclo completo de build, firma y publicación de paquetes del Agente (app-X.Y.Z.zip) bajo el Lanzador Estable (F1).
Audiencia: Edo, desarrolladores, infra, quien maneje releases del Agente.
Preparación inicial (una sola vez)
1. Generar clave privada Ed25519
Máquina: la que ejecute build_agent.bat (típicamente la de Edu).
# En PowerShell
python scripts/agent/generate_signing_key.py
Salida esperada:
Clave privada generada: C:\Users\edu\.crearack\keys\agent_signing_ed25519.pem
PUBKEY_HEX (para PACKAGE_PUBKEYS_HEX del bootstrap): <64 chars hex>
COPIAR la PUBKEY_HEX.
2. Actualizar bootstrap con la clave pública
Edita terminal/agent/main.py:
# Línea ~50
PACKAGE_PUBKEYS_HEX = [
"abc123def456...", # ← pega aquí la pubkey_hex de arriba
]
Commit este cambio (el bootstrap 2.14.0 se compila CON la clave pública).
3. Hacer backup de la clave privada
# Cifra y copia
gpg --symmetric ~/.crearack/keys/agent_signing_ed25519.pem
# → agent_signing_ed25519.pem.gpg
# Guarda en:
# - USB cifrada (backup físico)
# - Vault empresa (1Pass, Bitwarden, etc.)
# - JAMÁS en Git, SaaS público, o email
Si se pierde: no se pueden firmar futuras releases. Generar par nuevo, actualizar PACKAGE_PUBKEYS_HEX en bootstrap, y compilar bootstrap 2.14.1+.
Build normal de release
1. Cambiar version.py
Edita terminal/agent/version.py:
AGENT_VERSION = "1.45.14" # ← incrementa
2. Ejecutar build_agent.bat
cd C:\ruta\al\repo
build_agent.bat
El script hace:
Paso 1-3: Verifica Python, estructura módulos, ambiente.
Paso 4.1-4.6: Compile el .exe (PyInstaller), self-test.
Paso 4.7 (nuevo, Lanzador Estable):
python scripts\agent\build_app_package.py --output "%OUTPUT_DIR%"
Internamente:
- Empaqueta
app-1.45.14.zip(config, app_main, core/, routes/, assets) - Calcula SHA-256
- FIRMA con la clave privada en
~/.crearack/keys/agent_signing_ed25519.pem - Emite
app-1.45.14.meta.json:
{
"version": "1.45.14",
"sha256": "a1b2c3d4e5f6...",
"sig_ed25519": "abcdef0123456789...",
"min_bootstrap_api": 1,
"built_at": "2026-07-07T14:23:45+00:00"
}
Paso 4.8: Deploy a OUTPUT_DIR (típicamente static/downloads/).
Paso 5: Publica a Hetzner via scp:
scp -o ConnectTimeout=20 \
"%OUTPUT_DIR%\CreaRackAgent.exe" \
"%OUTPUT_DIR%\app-1.45.14.zip" \
"%OUTPUT_DIR%\app-1.45.14.meta.json" \
root@crearack.com:/tmp/
Luego docker cp al volumen del web:
ssh root@crearack.com "
docker cp /tmp/app-1.45.14.zip \
<web-container>:/app/media/downloads/app-1.45.14.zip && \
docker cp /tmp/app-1.45.14.meta.json \
<web-container>:/app/media/downloads/app-1.45.14.meta.json && \
docker cp /tmp/CreaRackAgent.exe \
<web-container>:/app/media/downloads/CreaRackAgent.exe && \
docker exec <web-container> sh -c \
'printf 1.45.14 > /app/media/downloads/CreaRackAgent.exe.version' && \
rm -f /tmp/{CreaRackAgent.exe,app-1.45.14.zip,app-1.45.14.meta.json}
"
Orden crítico: zip+meta PRIMERO, luego .exe, luego .version.
3. Verificar en el servidor
ssh root@crearack.com
docker exec <web-container> ls -lh /app/media/downloads/app-1.45.14.*
# Debe listar:
# - app-1.45.14.zip (0.5 MB típico)
# - app-1.45.14.meta.json (0.1 KB)
4. Test de descarga
Desde el cliente Agente (con bootstrap 2.14.0):
# Simula heartbeat
manifest = {
"version": "1.45.14",
"package_version": "1.45.14",
"package_sha256": "a1b2c3d4e5f6...", # de .meta.json
"package_sig": "abcdef0123456789...", # de .meta.json
"package_url": "/downloads/agent-app/1.45.14",
}
# pkg_updater descarga y verifica
await download_and_verify_package("1.45.14", manifest)
# → Debe retornar True si firma OK
O manualmente:
curl -H "Authorization: Bearer <JWT_AGENT>" \
https://crearack.com/downloads/agent-app/1.45.14 \
-o app-1.45.14.zip
Troubleshooting
“Clave de firma NO encontrada”
[!] Clave de firma NO encontrada: C:\Users\edu\.crearack\keys\agent_signing_ed25519.pem
Solución: ejecuta python scripts/agent/generate_signing_key.py (ver Preparación inicial).
“FIRMA ED25519 FALLO”
En el bootstrap:
if not verify_ed25519_signature(meta["sig_ed25519"], meta["sha256"]):
raise ValueError("FIRMA ED25519 INVÁLIDA")
Causas:
- Clave privada no coincide con PUBKEY_HEX del bootstrap
- Paquete se alteró en tránsito
- Bootstrap tiene pubkey vieja (después de rotación, no actualizada)
Solución:
- Verifica
PACKAGE_PUBKEYS_HEXenterminal/agent/main.pyvs. la que se usó para firmar - Si rotaste la clave, asegúrate que
generate_signing_key.py --rotategeneró.new.pemy actualizaste PUBKEY_HEX - Recompila bootstrap 2.14.1+
- Publica nuevo paquete con la clave nueva
“SHA-256 MISMATCH”
sha256_actual = hashlib.sha256(open("app-1.45.14.zip", "rb").read()).hexdigest()
if sha256_actual != meta["sha256"]:
raise ValueError("SHA-256 MISMATCH")
Causas:
- ZIP se corrompió en tránsito (red, SaaS filesystem)
- Alguien alteró el ZIP sin actualizar .meta.json
Solución:
- Verifica integridad del ZIP en el servidor:
sha256sum app-1.45.14.zipvs. meta.json - Si está corrupto, borra y republica desde local (
build_agent.batnuevamente)
“Descarga falla” (cliente sin internet / SaaS inaccesible)
En pkg_updater:
if not download_and_verify_package(...):
log.warning("Descarga/verificación falló, mantener current")
return
Agente sigue funcionando con paquete anterior.
Próximo intento: en 6 horas (heartbeat normal).
Rotación de claves
Escenario: la clave privada se compromete o lleva >1 año.
Paso 1: Generar nueva clave
python scripts/agent/generate_signing_key.py --rotate
# → genera agent_signing_ed25519.new.pem (no toca la vieja)
# → imprime PUBKEY_HEX_NEW
Paso 2: Actualizar bootstrap (2.14.1+)
Edita terminal/agent/main.py:
PACKAGE_PUBKEYS_HEX = [
"abc123...", # ← vieja (permite validar paquetes viejos aún)
"def456...", # ← nueva (se usa para firmar nuevas releases)
]
Compila bootstrap 2.14.1.
Paso 3: Publica bootstrap 2.14.1
Una vez que clientes migren a 2.14.1, puedes retirar la clave vieja en 2.14.2:
PACKAGE_PUBKEYS_HEX = [
"def456...", # ← solo nueva
]
Paso 4: Firma nuevas releases con la clave nueva
# Renombra
mv ~/.crearack/keys/agent_signing_ed25519.new.pem \
~/.crearack/keys/agent_signing_ed25519.pem
# Próxima ejecución de build_agent.bat usa la nueva
Backup: copiar la vieja a un lugar seguro (USB cifrada, vault) antes de retirarla.
Validación de firma (offline)
Si necesitas validar la firma sin ejecutar el Agente:
# Usar cryptography de Python
python -c "
from cryptography.hazmat.primitives import serialization
import json, hashlib
meta = json.load(open('app-1.45.14.meta.json'))
zip_data = open('app-1.45.14.zip', 'rb').read()
# Verifica SHA
if hashlib.sha256(zip_data).hexdigest() != meta['sha256']:
print('SHA-256 FALLO')
exit(1)
# Verifica firma Ed25519
pubkey_hex = meta.get('sig_ed25519')
pubkey_bytes = bytes.fromhex(pubkey_hex[:64]) # primeros 64 chars
priv = serialization.load_pem_private_key(...) # si tienes la privada
pub = priv.public_key()
pub.verify(bytes.fromhex(meta['sig_ed25519']), meta['sha256'].encode())
print('FIRMA OK')
"
Véase también
- [[feature—terminal—lanzador-estable-f1]]
- [[entity—core—service—pkg-updater]]
- [[entity—terminal—service—app-main]]