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ódigo | Motivo |
|---|---|
| 404 | Versión no encontrada en el volumen app_media, usuario no autenticado, o versión con formato inválido |
| 400 | Formato 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:
- Django session (usuario logueado)
- JWT Bearer Token (Agente con token JWT válido)
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
- [[feature—terminal—lanzador-estable-f1]]
- [[entity—core—service—pkg-updater]]
- [[entity—core—endpoint—download-agent]]