Suporte remoto (Console)#
O mesmo canal remote-support.v1, visto do lado do operador. São dois planos
com duas credenciais, e essa separação é o contrato:
| Plano | Quem | Credencial |
|---|---|---|
| Runtime | o cliente local de suporte | X-Agent-Token: prt_… (o WebSocket acima) |
| Console | um humano operador, no navegador | JWT de sessão (+ X-CSRF-Token nas mutações) |
O navegador nunca recebe um token prt_, e não existe endpoint, campo ou
corpo capaz de conceder consentimento: essa transição tem um único escritor, o
humano no teclado da máquina remota. O Console oferece; quem consente e executa é
o cliente local. Os dois planos operam a mesma sessão e a mesma allowlist.
Autorização#
Toda rota exige sessão autenticada e a capability remote_support:operate:
owner e admin a têm, o papel agent não, e um grant direto do IdP
(perms: ["remote_support:operate"]) também autoriza. Mutações passam por CSRF.
O escopo (conta, agente, sessão) vem da credencial e da URL; qualquer tripla de
fora recebe 404 SESSION_NOT_FOUND — o mesmo que uma sessão inexistente.
Com o recurso desligado, toda a família responde 503 REMOTE_SUPPORT_DISABLED.
Endpoints#
| Método | Rota | Sucesso |
|---|---|---|
POST |
/v1/agents/{id}/remote-support/sessions |
201 + SessionView |
GET |
/v1/agents/{id}/remote-support/sessions |
200 + {version, sessions[], total} |
GET |
/v1/agents/{id}/remote-support/sessions/{sid} |
200 + SessionView (snapshot) |
POST |
/v1/agents/{id}/remote-support/sessions/{sid}/close |
200 — revoga a sessão |
POST |
/v1/agents/{id}/remote-support/sessions/{sid}/commands |
202 + CommandView |
POST |
/v1/agents/{id}/remote-support/sessions/{sid}/commands/{requestId}/cancel |
202 (ou 200 se já terminal) |
GET |
/v1/agents/{id}/remote-support/sessions/{sid}/events |
200 text/event-stream |
{sid} é o id da conversa do agente — o mesmo que o WebSocket autoriza. Corpos
são decodificados em modo estrito: campo desconhecido é 400.
// POST .../sessions
{ "session_id": "sess_a1b2c3" }
// POST .../sessions/{sid}/commands
{ "command": "diagnostics", // identificador da allowlist, nunca linha de shell
"argv": ["--fast"], // vetor estruturado
"reason": "investigar chamado 4711", // opcional; vai VERBATIM para a tela de consentimento
"idempotency_key": "op-1" } // repetir devolve o MESMO request, com "replayed": true
O request_id é cunhado pelo servidor (rsr_…), não escolhido pelo cliente.
Snapshot e comandos#
SessionView traz version, session_id, agent_id, state, origin,
client_id, os timestamps do ciclo (authorized_at, connected_at, ready_at,
disconnected_at, closed_at), close_code/close_reason, output_bytes,
allowed_commands, limits, commands[], events[], cursor e replay
(oldest_id, latest_id, size, capacity). Nenhum campo carrega credencial
de runtime, linha de comando bruta ou saída fora dos limites.
state da sessão: awaiting_client → connected → ready → disconnected →
closed. state do comando: awaiting_consent → running → exited /
denied / cancelled / failed.
CommandView traz request_id, idempotency_key, command, argv, state,
requested_at, started_at, ended_at, exit_code, truncated,
cancel_requested, output_bytes, code, message, consent_deadline_ms e
timeout_ms.
Stream de eventos (SSE)#
GET /v1/agents/{id}/remote-support/sessions/{sid}/events?after=42
Authorization: Bearer <jwt>
Autenticado pelo mesmo JWT das demais leituras — consuma com fetch (o
EventSource nativo não manda Authorization), nunca credencial em query
string. O cursor vem de ?after= ou do header Last-Event-ID.
Cada evento sai com id: e event:, e o corpo é
{id, version, type, session_id, request_id, at, data}. Tipos:
session.authorized, session.client_connected, session.client_ready,
session.client_disconnected, session.closed, command.accepted,
command.started, command.consent_denied, command.consent_expired,
command.output, command.exited, command.cancel_requested,
command.cancelled, command.failed.
command.started só existe porque o cliente local mandou consent_granted.
O primeiro frame é sempre stream_status, com status ok, replay_expired
(o cursor é anterior à janela) ou cursor_invalid (cursor à frente do log). Nos
dois últimos vem "snapshot_required": true: busque o snapshot e recomponha, em
vez de renderizar uma sessão com buraco no meio. Há : heartbeat a cada 20 s,
stream_end (session_closed ou stream_max_age, aos 30 min) e stream_error
(BACKPRESSURE) quando este navegador parou de ler.
Limites e redação#
Janela de replay de 256 eventos por sessão, 8 streams simultâneos (o
nono é 429 BACKPRESSURE), chunk de saída de 8 KiB por evento (acima disso
truncated: true), 20 comandos no histórico do snapshot e registro retido
por 2 h após o fim. Os limites do canal em si vêm em limits.
Toda saída e toda mensagem passam por redação por forma antes de chegar ao
navegador — prefixos de credencial, chaves de provedor, JWTs, blocos PEM,
headers Authorization e atribuições cujo nome diz que é segredo viram
[REDACTED].
Erros#
REMOTE_SUPPORT_DISABLED (503) · SESSION_NOT_FOUND (404) ·
REQUEST_ID_UNKNOWN (404) · SESSION_NOT_AUTHORIZED (403) ·
COMMAND_NOT_ALLOWED (403) · COMMAND_ARGS_INVALID (400) · BAD_REQUEST (400) ·
SESSION_CLOSED / SESSION_EXPIRED / CLIENT_NOT_READY /
COMMAND_LIMIT_EXCEEDED (409) · BACKPRESSURE (429) · AUDIT_UNAVAILABLE (503) ·
CSRF_INVALID (403).