WebShell
WebShell es una terminal cara al navegador que alcanza cada edge registrado a través del mismo túnel geminio que usa el resto de la plataforma. No hay bastión SSH separado, no hay jumpbox, no hay puerto entrante. El edge sigue marcando hacia afuera; el manager abre una clase de stream multiplexado para I/O de shell.
Casos de uso:
- El agente sugiere un fix; haces clic en "Abrir shell en edge-prod-04" y confirmas el cambio sin salir de la SPA.
- Un vendor / contratista necesita una mirada one-off a un host sin enrolarse en la VPN.
- Respuesta a incidentes: cada comando queda grabado con la fila de audit de la sesión originante.
Arquitectura
browser ──WebSocket──> manager:/v1/webshell/ws
│
├─ Router.Register(sessionID, sink, ActiveSession)
│
└─ geminio Stream (shell class)
│
▼
edge agent
└─ pty.Start("/bin/bash")El router del lado del manager está en internal/manager/biz/webshell/router.go. Mantiene un directorio sessionID → Sink: los handlers de WebSocket se registran al conectar, el dispatcher tunnel-incoming rutea las pushes de salida / exit del edge al navegador correcto.
// internal/manager/biz/webshell/router.go:57
type Router struct {
mu sync.RWMutex
sinks map[string]Sink
meta map[string]*ActiveSession // sessionID → metadata
stdoutBytes sync.Map // sessionID → *uint64
}El handler HTTP / WebSocket vive al lado en internal/manager/server/webshell para que el router se mantenga agnóstico de HTTP y unit-testable.
Las dos clases de stream
El túnel geminio multiplexa:
- Clase de control — JSON RPCs (ejecución de skill, señalización de plugin, probes del evaluator de alertas).
- Clase de shell — streams de bytes crudos (uno por sesión WebShell, uno por follower de
tail -f, etc.).
Dividir a nivel de túnel importa porque el I/O de shell es bursty y sin framing; mezclarlo con los RPCs de control mata a estos últimos. Cada clase tiene su propio presupuesto de backpressure.
Metadata de sesión
Cada sesión viva tiene un ActiveSession:
// router.go:37
type ActiveSession struct {
SessionID string
OngridUserID uint64
SSHUser string
DeviceID uint64
EdgeID uint64
StartedAt time.Time
LastInputAt time.Time // updated on every browser → edge frame
}LastInputAt tickea en cada keystroke (Router.TouchInput). Un watchdog de idle-timeout desaloja sesiones más viejas que el límite configurado sin input reciente — defiende contra el leak "cerré la pestaña del browser con un comando corriendo".
Grabación de auditoría
Dos capas:
- Fila de header — tabla
webshell_sessions: quién, cuándo, qué edge, exit code, total de bytes in/out. - Grabación de stream — la interfaz
Recorderdel lado del manager toma cada byte que cruza el wire (ambas direcciones, timestamped) y anexa a un archivo cast compatible con asciinema bajo/var/lib/ongrid/webshell-recordings/<session_id>.cast. La página admin/admin/webshelllos reproduce.
La interfaz Recorder es estrecha a propósito — producción usa un sink de archivo; los tests inyectan un fake; futuros backends de cloud-blob caen sin tocar el resto del stack.
Límites de concurrencia
Tope por-usuario: Router.CountByUser se llama desde el handler de WebSocket open; las conexiones por encima del tope son rechazadas con HTTP 429. El tope default es 5 (configurable). El tope por-edge defiende contra un agente runaway abriendo 100 shells concurrentes.
Matando sesiones
Tres rutas matan una sesión:
- Cierre del browser — la desconexión del WebSocket se propaga al edge, que hace
kill -HUPal pty. - Kill por admin — la SPA admin llama
Killer.Kill(reason="admin terminated")en elSink, que tunelea un close hasta el edge. La razón queda grabada en la fila de exit de la sesión. - Eviction por idle — el watchdog dispara
Kill("idle timeout")en sesiones cuyoLastInputAtexcedió el tope.
// router.go:50
type Killer interface {
Kill(reason string)
}El handler del lado del manager instala un Killer cuando registra el Sink. Cualquier Sink que opte queda admin-killable; el resto solo es browser-close-killable.
Gating por rol
WebShell está gated en el rol admin (RBAC de ADR-022). El rol user puede chatear con el agente pero no puede abrir shells; viewer puede leer grabaciones de sesiones pasadas pero no puede abrir nuevas. El gate corre en la entrada del handler HTTP, antes del upgrade a WebSocket.
Ver también
- Skills — el skill
bashes el equivalente one-shot de WebShell (un comando, sin pty). Mismo sustrato de audit. - Instalación de edge — levantar el agente edge de un host para que WebShell pueda alcanzarlo.
- Arquitectura — dónde se sienta el túnel geminio.