Suporte remoto (WebSocket)#
Canal bidirecional remote-support.v1 para atendimento remoto assistido. Um
cliente local autorizado abre um WebSocket e a API coordena a sessão:
autentica, autoriza o escopo, oferece comandos de uma allowlist do servidor,
exige consentimento explícito por comando e retransmite a saída dentro de
limites publicados.
Não é shell remoto e não pode virar um. A API não executa nada — quem executa é o cliente local, e só depois do consentimento. Um comando é sempre um identificador da allowlist + argumentos estruturados; não existe campo no protocolo capaz de carregar uma linha de shell, pipe ou redirecionamento.
Desligado por padrão. Enquanto o recurso não estiver habilitado no deployment, a rota responde
503 REMOTE_SUPPORT_DISABLEDe nunca faz upgrade. Habilitar não basta: sem allowlist configurada, nenhum comando é autorizado. O cliente desktop é um componente à parte e ainda não faz parte desta entrega — mas fala exatamente este contrato, provado por um teste de integração que roda o cliente real contra esta API.
GETAbrir a sessão#
GET /v1/agent-runtime/{id}/sessions/{sid}/remote-support/ws
X-Agent-Token: prt_...
Mesmas garantias do resto do plano runtime: o token prt_ é ligado ao seu
agente, {sid} tem de ser uma sessão desse agente, e limites de plano e rate
limit continuam valendo. O token vai no header — nunca em query string. O escopo
(conta, agente, sessão) vem da credencial e da URL; nada que o cliente envie o
amplia, e endereçar outra tripla devolve SESSION_NOT_FOUND.
POSTHandshake#
O primeiro frame tem de ser hello; nada é aceito antes do hello_ack.
{"type":"hello","protocol":"remote-support.v1","session_id":"sess_a1b2c3",
"client_id":"desk-9","client_version":"1.4.2"}
O session_id apenas confirma o que a credencial já autorizou — divergência é
SESSION_NOT_AUTHORIZED. Todo frame, nas duas direções, carrega type e
protocol; versão diferente é PROTOCOL_UNSUPPORTED em qualquer frame, não só
no handshake, e não há downgrade. O hello_ack devolve o contrato explícito:
limites, heartbeat, política de reconexão e a allowlist exata de comandos
(sempre presente, mesmo vazia).
{"type":"hello_ack","protocol":"remote-support.v1","session_id":"sess_a1b2c3",
"agent_id":"AGENT_ID","resume":"unsupported","commands":["collect_logs"],
"limits":{"max_message_bytes":65536,"max_output_bytes_per_request":1048576,
"max_output_bytes_per_session":8388608,"max_concurrent_commands":1,
"consent_timeout_ms":60000,"command_timeout_ms":300000,
"idle_timeout_ms":900000,"session_ttl_ms":3600000},
"heartbeat":{"ping_interval_ms":20000,"pong_timeout_ms":10000}}
POSTCiclo de um comando#
cliente → hello servidor → hello_ack
cliente → ready
servidor → command_request (request_id)
cliente → consent_granted | consent_denied
servidor → command_start (só se granted)
cliente → stdout / stderr / exit
qualquer lado → cancel servidor → error | close
command_requestcarregacommand:{name,args},origin:{operator,reason},consent_deadline_msetimeout_ms.originé obrigatório: consentimento sem quem pede e por quê não é consentimento informado.command_starté a autorização de partida — o cliente reporta a decisão, mas só executa depois desse frame.exitcarregastatus(completed,failed,timeout,cancelled),exit_codeetruncated.canceltem a mesma forma nas duas direções:{request_id, reason}.- Saída antes do consentimento é violação de protocolo, não dado: os bytes
são descartados e o cliente recebe
CONSENT_REQUIRED. consent_grantedrepetido é idempotente e nunca inicia o comando duas vezes.request_idé obrigatório emconsent_granted,consent_denied,stdout,stderr,exitecancel.- Tipos aceitos do cliente:
hello,ready,consent_granted,consent_denied,stdout,stderr,exit,cancel. Os demais são só do servidor. Campo desconhecido ou JSON inválido encerra a conexão comMESSAGE_MALFORMED. Como não háerrordo cliente, uma recusa local viraconsent_deniedcomreason, e uma falha em execução viraexitcomstatus:"failed".
Formato de um comando:
{"type":"command_request","protocol":"remote-support.v1","request_id":"rsr_...",
"command":{"name":"collect_logs","args":["--since","1h"]},
"origin":{"operator":"suporte@catcher.one","reason":"investigar chamado 4711"},
"consent_deadline_ms":60000,"timeout_ms":300000}
Máximo de 8 argumentos, 256 bytes cada, 1024 bytes no total; caracteres
permitidos A-Za-z0-9._:/=+@,- e .. é rejeitado. Nome fora da allowlist é
COMMAND_NOT_ALLOWED; argumento inválido é COMMAND_ARGS_INVALID.
GETLimites e reconexão#
| Limite | Padrão |
|---|---|
| Frame de entrada | 64 KiB (acima disso a conexão fecha com WebSocket status 1009, MESSAGE_TOO_LARGE) |
| Saída por comando | 1 MiB |
| Saída por sessão | 8 MiB |
| Comandos simultâneos | 1 |
| Fila servidor→cliente | 32 frames |
| Consentimento | 60 s |
| Execução | 5 min |
| Ocioso | 15 min |
| Vida da sessão | 60 min |
| Ping / pong | 20 s / 10 s |
Um cliente que para de ler encerra a sessão com BACKPRESSURE — o servidor não
enfileira sem limite. O heartbeat é liveness de transporte: um pong prova que o
socket está vivo, não renova o relógio de ocioso.
Não há resume ("resume":"unsupported"). Só um cliente vivo por sessão — um
segundo recebe SESSION_ALREADY_ACTIVE. Ao cair, o comando em voo é cancelado,
nunca herdado; reconectar autentica de novo e cria uma sessão nova, sem
reexecutar nada.
GETErros e fechamento#
{"type":"error","protocol":"remote-support.v1","request_id":"rsr_...",
"code":"CONSENT_REQUIRED","message":"this command has not been consented to",
"retryable":false}
{"type":"close","protocol":"remote-support.v1","code":"SESSION_EXPIRED",
"message":"the session was idle for too long","retryable":true,
"last_request_id":"rsr_..."}
Códigos: MESSAGE_MALFORMED, MESSAGE_TOO_LARGE, UNKNOWN_MESSAGE_TYPE,
REQUEST_ID_REQUIRED, REQUEST_ID_UNKNOWN, HANDSHAKE_REQUIRED,
HANDSHAKE_TIMEOUT, PROTOCOL_UNSUPPORTED, SESSION_NOT_AUTHORIZED,
SESSION_ALREADY_ACTIVE, SESSION_NOT_FOUND, SESSION_EXPIRED,
SESSION_CLOSED, CLIENT_NOT_READY, CONSENT_REQUIRED, CONSENT_DENIED,
CONSENT_TIMEOUT, COMMAND_NOT_ALLOWED, COMMAND_ARGS_INVALID,
COMMAND_LIMIT_EXCEEDED, COMMAND_TIMEOUT, COMMAND_CANCELLED,
OUTPUT_LIMIT_EXCEEDED, BACKPRESSURE, AUDIT_UNAVAILABLE,
REMOTE_SUPPORT_DISABLED, INTERNAL_ERROR. As mensagens são EN, estáveis e
sanitizadas: nunca carregam token, segredo, argumento ou saída.
Cada sessão e cada comando ficam auditados com apenas metadados — conta,
agente, sessão, request_id, identificador do comando, contagem de argumentos,
resultado e exit code. Valores de argumento e stdout/stderr nunca entram em
log nem na auditoria. Se a auditoria falhar, o comando é recusado com
AUDIT_UNAVAILABLE em vez de executar fora do registro.