Volver a la wiki

Endpoint GET /downloads/agent-app/<version> — Descarga de paquetes de código del Agente

Descripción

Ruta: GET /downloads/agent-app/<version>
Función: core.views.download_agent_package(request, version)
Introducida: v1.45.14 (2026-07-07)
Contexto: Lanzador Estable (F1)

Sirve el paquete de código del Agente (app-X.Y.Z.zip) para clientes con bootstrap 2.14.0+.

Request

GET /downloads/agent-app/1.45.14

Headers:
  Authorization: Bearer <JWT del Agente>  OR  Cookie: sessionid=<Django session>

Response

Éxito (200 OK)

HTTP/1.1 200 OK
Content-Type: application/zip
Content-Length: 512345
Content-Disposition: attachment; filename="app-1.45.14.zip"

[binary: app-1.45.14.zip]

Errores

CódigoMotivo
404Versión no encontrada en el volumen app_media, usuario no autenticado, o versión con formato inválido
400Formato de version inválido (no coincide con \d+(\.\d+){0,3})

Autenticación

Modelo: idéntico a /downloads/CreaRackAgent.exe

if not request.user.is_authenticated:
    # No hay sesión Django
    from terminal.api.auth import get_agent_from_request
    if not get_agent_from_request(request):
        # JWT del Agente inválido/expirado
        raise Http404

Aceptar:

Storage

Ubicación física: MEDIA_ROOT/downloads/app-<version>.zip

Publicada por build_agent.bat (paso 4.7 → script scripts/agent/build_app_package.py):

# Emite en output dir (típicamente static/downloads/):
app-1.45.14.zip          # El paquete (Python, assets)
app-1.45.14.meta.json    # Metadatos: sha256, firma Ed25519, min_bootstrap_api, built_at

Luego scp a Hetzner y docker cp al volumen del contenedor web (/app/media/downloads/).

Integridad

El endpoint no valida la firma Ed25519. Validación ocurre en el bootstrap (terminal/agent/main.py):

# En el bootstrap
meta = json.load(open("app-1.45.14.meta.json"))
sha256_actual = hashlib.sha256(open("app-1.45.14.zip", "rb").read()).hexdigest()
if sha256_actual != meta["sha256"]:
    raise ValueError("SHA-256 MISMATCH")
if not verify_ed25519_signature(meta["sig_ed25519"], meta["sha256"]):
    raise ValueError("FIRMA ED25519 INVÁLIDA — INYECCIÓN DETECTADA")

Ventaja: un SaaS comprometido puede servir un paquete alterado, pero la firma lo detecta. La clave privada está fuera del SaaS.

Carga de la respuesta

Implementación:

def download_agent_package(request, version):
    """Sirve el paquete de código del Agente (app-<version>.zip).
    
    Mismo modelo de auth que download_agent (sesión o JWT del Agente). 
    El zip lo publica build_agent.bat en el volumen app_media junto al .exe; 
    su integridad la garantiza la firma Ed25519 del .meta.json 
    (verificada por el bootstrap del Agente), no este endpoint.
    """
    import os
    import re
    from django.http import FileResponse, Http404
    
    # Validar formato (evita path traversal)
    if not re.fullmatch(r"[0-9]+(\.[0-9]+){0,3}", version):
        raise Http404
    
    # Autenticación
    if not request.user.is_authenticated:
        from terminal.api.auth import get_agent_from_request
        if not get_agent_from_request(request):
            raise Http404
    
    # Archivo existe?
    zip_path = os.path.join(settings.MEDIA_ROOT, "downloads", f"app-{version}.zip")
    if not os.path.isfile(zip_path):
        raise Http404
    
    # Stream
    return FileResponse(
        open(zip_path, "rb"),
        as_attachment=True,
        filename=f"app-{version}.zip"
    )

Método HTTP HEAD

Soportado (para chequeos de existencia sin descargar):

HEAD /downloads/agent-app/1.45.14

→ 200 OK + headers Content-Length, Content-Type (sin body)

Orden de publicación (crítico)

En cada release, build_agent.bat publica en orden FIFO:

1. app-X.Y.Z.zip + app-X.Y.Z.meta.json       ← primero
2. CreaRackAgent.exe (si cambió bootstrap)
3. CreaRackAgent.exe.version (puntero final)

El manifest (core/agent_release.py) anuncia versión leyendo .version y adjunta package_* solo si zip+meta ya están en el volumen:

def agent_update_payload(reported_version: str):
    # ...
    pkg = package_payload(latest)  # ← lee app-X.Y.Z.meta.json
    if pkg:
        payload.update(pkg)  # ← solo si existe
    return payload

Jamás ordena un update cuyos artefactos no estén ya servidos (previene race conditions).

Rate limiting

No implementado (igual que /downloads/CreaRackAgent.exe). Si es necesario proteger contra abuso:

from django.views.decorators.cache import cache_page

@cache_page(60)  # 1 min
@require_http_methods(["GET", "HEAD"])
def download_agent_package(request, version):
    ...

O aplicar rate limit a nivel de LB/Cloudflare.

Véase también

Subir