WebShell
WebShell é um terminal voltado ao browser que alcança cada edge registrado pelo mesmo tunnel geminio que o resto da plataforma usa. Não há bastion SSH separado, sem jumpbox, sem porta inbound. O edge continua discando para fora; o manager abre uma classe de stream multiplexada para I/O de shell.
Casos de uso:
- O agent sugere um fix; você clica em "Open shell on edge-prod-04" e confirma a mudança sem sair do SPA.
- Vendor / contratado precisa de uma olhada one-off em um host sem enrollment de VPN.
- Resposta a incidente: cada comando é gravado com a linha de auditoria da sessão originária.
Arquitetura
browser ──WebSocket──> manager:/v1/webshell/ws
│
├─ Router.Register(sessionID, sink, ActiveSession)
│
└─ geminio Stream (shell class)
│
▼
edge agent
└─ pty.Start("/bin/bash")O router do lado manager está em internal/manager/biz/webshell/router.go. Mantém um diretório sessionID → Sink: handlers WebSocket registram no connect, o dispatcher tunnel-incoming roteia os pushes de saída / exit do edge para o browser certo.
// 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
}O handler HTTP / WebSocket vive ao lado em internal/manager/server/webshell para que o router permaneça HTTP-agnostic e testável em unit.
As duas classes de stream
O tunnel geminio multiplexa:
- Classe de controle — RPCs JSON (execução de skill, sinalização de plugin, probes do evaluator de alerta).
- Classe de shell — streams brutos de bytes (um por sessão WebShell, um por follower de
tail -f, etc.).
Separar no nível do tunnel importa porque I/O de shell é bursty e não-framed; misturar com os RPCs de controle deixa estes famélicos. Cada classe tem seu próprio budget de backpressure.
Metadados de sessão
Cada sessão viva tem uma 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 tique em cada tecla (Router.TouchInput). Um watchdog de idle-timeout despeja sessões mais velhas que o limite configurado sem input recente — defende contra o vazamento "fechei a aba do browser com um comando rodando".
Gravação de auditoria
Duas camadas:
- Linha de cabeçalho — tabela
webshell_sessions: quem, quando, qual edge, exit code, total de bytes in/out. - Gravação de stream — a interface
Recorderdo lado manager pega cada byte que cruza o fio (ambas direções, com timestamp) e anexa a um arquivo cast asciinema-compatible em/var/lib/ongrid/webshell-recordings/<session_id>.cast. A página admin/admin/webshellos reproduz.
A interface Recorder é estreita de propósito — produção usa um sink de arquivo; testes injetam um fake; backends futuros de cloud-blob entram sem tocar no resto da stack.
Limites de concorrência
Cap por usuário: Router.CountByUser é chamado do handler de open do WebSocket; conexões acima do cap são rejeitadas com HTTP 429. Cap padrão é 5 (configurável). Cap por edge defende contra um agent runaway abrindo 100 shells concorrentes.
Matando sessões
Três caminhos matam uma sessão:
- Browser close — desconexão do WebSocket propaga ao edge, que dá
kill -HUPno pty. - Admin kill — o admin SPA chama
Killer.Kill(reason="admin terminated")noSink, que tuneliza um close até o edge. A razão é gravada na linha de exit da sessão. - Despejo por idle — o watchdog dispara
Kill("idle timeout")em sessões cujoLastInputAtultrapassou o cap.
// router.go:50
type Killer interface {
Kill(reason string)
}O handler do lado manager instala um Killer quando registra o Sink. Qualquer Sink que opt-in vira admin-killable; o resto só é browser-close-killable.
Gate de role
WebShell é gateado na role admin (RBAC do ADR-022). A role user pode conversar com o agent mas não pode abrir shells; a role viewer pode ler gravações de sessões passadas mas não pode abrir novas. O gate roda na entrada do handler HTTP, antes do upgrade do WebSocket.
Veja também
- Skills — a skill
bashé o equivalente one-shot do WebShell (um único comando, sem pty). Mesmo substrato de auditoria. - Instalação do edge — subir o edge agent de um host para o WebShell conseguir alcançá-lo.
- Arquitetura — onde o tunnel geminio se senta.