Maestri Wire
O Maestri Wire é o protocolo que um computador rodando o Maestri (o host, daqui em diante) fala com outros dispositivos e ferramentas. É o que o app Maestri Remote para iPhone e iPad usa, e está aberto para qualquer coisa que você construir: um script que te avisa quando um agente precisa de atenção, um botão que aprova um prompt, um pequeno dashboard.
Esta página é o contrato como o build atual o aplica, escrita para ser entregue a um agente tanto quanto para ser lida.
Note
O Maestri Wire está em beta, e o host pode ser um Mac ou um PC com Windows. Capabilities e campos opcionais são adicionados conforme o host cresce, e os formatos ainda podem mudar antes do fim do beta; protocolVersion marca uma mudança incompatível.
A versão curta
- O host escuta em uma única porta TCP, 7434 por padrão, e fala apenas HTTPS e WSS. HTTP puro não é servido.
- O certificado é autoassinado. Os clientes fixam (pin) a chave de segurança do host, o SHA-256 da chave pública do seu certificado, em vez de consultar uma autoridade certificadora.
- Um cliente pareia uma vez com um código de seis dígitos (ou com a senha do host) e recebe um token de dispositivo de longa duração, enviado como bearer token em toda requisição.
- Todo dispositivo pareado tem um papel: Controle total (
owner) ou Somente leitura (guest). Convidados podem ler tudo e reajustar um terminal ao tamanho da sua tela; toda escrita é recusada. - Tudo é JSON. Recursos opcionais são anunciados como capabilities; um cliente as consulta e nunca sonda rotas.
- Dois WebSockets: o feed (o estado ao vivo de um workspace) e um stream de terminal (bytes crus do PTY, nos dois sentidos).
Ligando
No host, abra Configurações → Wire. O interruptor no topo inicia o servidor, e o cabeçalho mostra se ele está rodando.
- Pareamento mostra um QR code e um código de seis dígitos. O código vale por cinco minutos, serve para um único uso, e só é emitido enquanto esta aba estiver aberta.
- Manual é para tudo que não consegue escanear um QR code. É protegido por senha: defina uma e a aba mostra os endereços para digitar e a chave de segurança para conferir. Sem senha, o pareamento manual fica desligado.
- Rede mostra onde o host pode ser alcançado: este Wi-Fi, e o endereço da rede privada quando o Tailscale está ativo. "Apenas rede privada" recusa tudo que não chegar pelo Tailscale.
- Dispositivos lista cada pareamento: seu nome (editável ali mesmo), de onde se conectou pela última vez, se está conectado agora, seu papel, e um jeito de revogá-lo.
Endereços e transporte
Não há descoberta. Um cliente aprende o endereço do host pelo QR code (que também carrega os outros endereços do host) ou pela aba Manual, e atualiza a lista com GET /api/info a cada conexão. Entre os endereços, o nome .local resolve pelo próprio mDNS do sistema e sobrevive a um novo lease de DHCP; o endereço 100.64.0.0/10 alcança o host pela rede privada.
Regras de transporte que o host aplica:
- TLS 1.2 no mínimo, apenas HTTP/1.1, sem ALPN. Toda resposta carrega
Connection: close. - Requisições maiores que 8 MiB respondem
413. Uma conexão que não entregou uma requisição completa em 20 segundos é derrubada. No máximo 64 conexões são atendidas ao mesmo tempo. - O cabeçalho
Hostprecisa ser um IP literal,localhost, um nome.localou um nome.ts.net; qualquer outra coisa responde403. Isso derruba DNS rebinding a partir de um navegador. - Nenhum cabeçalho CORS é enviado, então uma página web não consegue ler a API entre origens. Um cliente do Wire é um app ou um script, não uma página em um navegador.
Confiando no certificado
O host gera uma chave uma única vez e a guarda no cofre de credenciais do sistema operacional; o certificado é autoassinado por ela. Os clientes fixam o SHA-256 do SubjectPublicKeyInfo (DER) do certificado. O QR code o carrega em base64 como serverKeyHash; a aba Manual mostra os mesmos 32 bytes em hexadecimal separado por dois-pontos, a Chave de segurança. Compare o que seu cliente vê na primeira conexão com esse valor, e então fixe-o.
Com curl, ou fixe a chave, ou pule a verificação enquanto experimenta:
# Fixe a chave (o base64 do QR code, ou o hexadecimal da aba Manual convertido para base64)
curl --pinnedpubkey 'sha256//BASE64_KEY_HASH' https://192.168.1.20:7434/api/info
# Ou, na sua própria rede, enquanto testa
curl --insecure https://192.168.1.20:7434/api/info
Um host atrás de um proxy reverso com um certificado publicamente confiável também funciona; um cliente pode aceitar qualquer certificado em que seu sistema operacional confie.
Pareamento
O pareamento troca uma credencial de vida curta por um token de dispositivo de longa duração: 64 caracteres hexadecimais minúsculos, o único segredo que um cliente guarda. O host guarda apenas o SHA-256 dele.
O QR code
O QR code da aba Pareamento codifica este JSON:
{
"protocolVersion": 1,
"scheme": "https",
"host": "192.168.1.20",
"port": 7434,
"pairingCode": "483920",
"expiresAt": "2026-08-20T15:04:05Z",
"serverKeyHash": "base64…",
"alternateHosts": ["100.101.102.103"]
}
alternateHosts lista os outros hosts em que o mesmo servidor responde, mesma porta e mesma chave. Guarde-os junto com o pareamento e tente-os quando host parar de responder.
POST /pair
Sem autenticação. Content-Type: application/json é obrigatório.
{ "deviceName": "Painel da sala", "code": "483920" }
ou, com a senha da aba Manual:
{ "deviceName": "Ponte micro", "password": "…" }
Exatamente um entre code e password precisa estar presente. deviceIdentifier (opcional) é um id estável do próprio dispositivo; um dispositivo que pareia de novo com o mesmo identificador substitui o próprio registro no host (mesmo id de dispositivo e mesmo papel, um token novo) em vez de criar outro.
Resposta 200:
{
"token": "64 caracteres hex",
"deviceId": "UUID",
"deviceName": "Painel da sala",
"protocolVersion": 1,
"role": "owner"
}
Erros: 401 credencial errada, 415 content type errado, 429 limitado (com Retry-After em segundos), 503 pareamento por senha não habilitado neste host.
A limitação é por endereço de origem (5 falhas livres, depois 30 segundos dobrando até uma hora, decaindo após 15 minutos ociosos) e global do servidor (10 falhas, mesma escala). Um bloqueio global também aposenta o código de pareamento ativo. Todo pareamento bem-sucedido levanta uma notificação no host, e o novo dispositivo aparece na aba Dispositivos com o nome que enviou.
Tip
Nomeie seu pareamento pelo que ele é. O nome que você envia é o que a aba Dispositivos mostra, e é por ali que a pessoa vai revogá-lo ou rebaixá-lo depois. "Ponte micro" é melhor que "Python 3.12".
Enviando o token
Envie Authorization: Bearer <token> em toda requisição sob /api/. Existem duas exceções, para lugares que não conseguem definir cabeçalhos:
- Requisições
GETpodem levar?token=<token>no lugar. Verbos que modificam precisam usar o cabeçalho. - Upgrades de WebSocket sempre levam
?token=<token>.
Um dispositivo ocioso por 30 dias é esquecido e precisa parear de novo. Um token revogado ou esquecido recebe 401 em todo lugar; pare de tentar e peça para a pessoa parear de novo.
Papéis
Todo dispositivo pareado é um owner (Controle total) ou um guest (Somente leitura). Um convidado é recusado com 403 em toda rota marcada como escrita abaixo, e sua entrada no terminal é descartada; ele pode ler tudo e enviar resize em um stream de terminal, já que reajustar é como uma tela pequena lê um terminal. Papéis são alterados por um owner através de PUT /api/devices/{id}/role ou pela aba Dispositivos. Um dispositivo não pode se tornar convidado sozinho.
Tip
Pareie uma integração como Somente leitura a menos que ela precise digitar ou alterar o canvas. Um notificador, um dashboard ou uma luz de status não precisa de nada que um convidado não possa fazer, e um token de convidado que vazar não consegue fazer nada com seus agentes.
GET /api/info
Sem autenticação; revela mais quando um token válido é apresentado.
{
"name": "Studio",
"protocolVersion": 1,
"capabilities": ["feedSnapshots", "…"],
"requiresPassword": true,
"activeWorkspaceId": "UUID (apenas autenticado)",
"role": "owner (apenas autenticado)",
"hosts": ["192.168.1.20", "100.101.102.103"]
}
Confira protocolVersion == 1 primeiro. name é o nome do próprio host, o título que um cliente dá a tudo que vem dele. hosts lista todo endereço em que este servidor responde; atualize por ele os endereços alternativos que você guarda junto ao pareamento. Nomes de capability desconhecidos devem ser ignorados.
Capabilities
As capabilities são como o protocolo cresce sem uma mudança de versão. Condicione todo recurso opcional à flag correspondente, e nunca sonde uma rota para descobrir se ela existe.
| Capability | O que significa |
|---|---|
feedSnapshots | GET …/feed e o socket do feed existem |
terminalStreaming | o socket do terminal existe |
terminalInputBytes | o socket do terminal aceita inputBytes e frames binários |
terminalInputText | o socket do terminal aceita input (texto UTF-8) |
terminalResize | o socket do terminal honra resize (limitado a 20…250 colunas por 5…120 linhas) |
terminalChat | o snapshot e o socket do chat do terminal, seu histórico e suas rotas de modo, leitura, thread e exclusão, além dos anexos retidos, existem (Chat do terminal, abaixo); os cards de terminal podem carregar chatPreview |
promptSegments | POST …/prompt aceita segments |
attachmentStaging, attachmentDiscard | as rotas de anexo existem |
mentionCatalog | GET …/mentions existe |
terminalThemes | cards de terminal carregam theme |
nativeNotes | rotas de nota endereçadas por nó, com revision / ifRevision |
pairingCodes | pareamento por código de seis dígitos |
canvasMirroring | o snapshot do feed carrega um canvas |
canvasWrites | todas as rotas de escrita do canvas existem; a ausência significa não escrever nada. PUT …/nodes/{id}/note/lock está dentro dessa flag em vez de carregar uma própria, então um host anterior a essa rota responde 404 onde um mais novo aceita a escrita |
noteStackWrites | as rotas da trilha do fichário existem |
noteStackPages | páginas de fichário podem ser adicionadas e removidas |
noteStackFiling | fichários podem ser criados a partir de uma seleção, uma nota arquivada dentro ou movida entre eles, uma página retirada recebendo um ponto de pouso |
deviceRoles | os papéis são aplicados e /api/info informa o de quem chamou |
drawingWrites | desenhos podem ser adicionados, movidos e apagados |
workspaceActions | as rotas de fixar, renomear, acordar, descarregar e limpar atenção existem; os metadados do workspace carregam workingDirectory |
workspaceManagement | o Novo, Editar e Excluir Workspace do host existem: POST /api/workspaces, POST …/workspaces/{ws}/update, DELETE …/workspaces/{ws}; GET /api/workspaces carrega layout, GET /api/directories completa ou lista pastas no próprio disco do host, e os metadados do workspace carregam environment. Somente para owner |
terminalDrafts | POST …/terminals aceita os campos da folha de Novo Terminal |
roleManagement | as rotas de responsabilidade de agente existem; as configurações do terminal carregam icon, color, monitorActivity |
nodeGroups | o snapshot carrega groups, os nós carregam groupId, e as rotas de grupo existem |
partituras | a biblioteca de partituras do host pode ser listada, pré-visualizada e estampada em um andar |
nodeBatchDelete | POST …/nodes/delete remove uma seleção em uma única requisição |
canvasFiles | POST …/files coloca um arquivo do cliente em um andar como nó de arquivo, a regra de soltar-do-Finder do host com os bytes embutidos; GET …/nodes/{id}/file/preview responde a um nó de imagem com uma imagem do tamanho do card, JPEG ou PNG conforme ela tenha transparência a preservar |
deviceRenaming | PUT /api/devices/{id}/name renomeia este dispositivo, ou outro dispositivo se você for owner |
presence | o socket do feed aceita presence; o host desenha um cursor para o dispositivo no seu canvas |
textStyling | POST …/nodes/{id}/text aceita as opções da barra de texto junto com o texto |
nodeUnload | terminais e portais podem ser descarregados; restart e reload os acordam |
terminalFocus | POST …/terminals/{id}/focus existe: o host vai até o terminal como faz ao clicar na própria notificação |
cameraControl | o socket do feed aceita camera: um pan em pontos de tela e um fator de zoom conduzem a câmera do próprio host, para um joystick, um gamepad ou uma roda |
nodeFocus | POST …/nodes/{id}/focus existe: o revelar do host para qualquer nó do canvas |
terminalFileMentions | GET …/terminals/{id}/files existe: o próprio índice de arquivos do compositor do host para um terminal, para menções com @ a partir de um cliente |
fileTrees | as rotas de árvore de arquivos existem (abaixo): a pasta de um nó de árvore navegada e pesquisada, os bytes de um arquivo obtidos com Range, as mudanças e diffs do repositório lidos, e o próprio menu de git do host acionado por ações nomeadas; POST …/nodes aceita o kind fileTree, GET …/workspaces/{ws}/directories completa um caminho de pasta, e nós do canvas do kind fileTree carregam um payload fileTree. Somente para owner |
cableTies | as abraçadeiras do andar viajam no snapshot do canvas como cableTies, e um cliente cria, reposiciona e corta uma (POST …/ties, POST …/ties/{id}/position, DELETE …/ties/{id}) |
routines | as rotinas do host são listadas, criadas, substituídas, removidas e executadas a partir de um cliente (abaixo): GET/POST /api/routines, PUT/DELETE /api/routines/{id}, POST …/routines/{id}/action, DELETE /api/routines/history. Somente para owner |
floorHooks | os hooks de andar do host são lidos, substituídos e executados a partir de um cliente (abaixo): GET/PUT …/hooks, POST …/floors/{id}/hooks. Somente para owner |
floorManagement | o Novo, Aterrissar, Excluir e Descarregar andar do host existem (abaixo): POST …/floors, GET …/branches, GET …/floors/{id}/landing, POST …/floors/{id}/land, POST …/floors/{id}/delete, POST …/floors/{id}/update, POST …/floors/{id|ground}/unload, DELETE …/floors/pending/{id}; os andares no feed carregam isGitIsolated, branch e isCloneMissing, e clones ainda sendo copiados viajam nele como pendingFloors. Somente para owner |
floorStatus | relatos de andar e resumos da barra lateral no feed e na lista de workspaces (abaixo); POST …/floors/{id|ground}/status substitui ou limpa um relato. Leituras seguem a visibilidade do workspace; escritas são somente para owner |
pullRequestChecks | POST …/floors/{id}/pull-request/check pergunta agora ao host do projeto sobre o pull request do andar (abaixo), e o pullRequest dos andares carrega attention e url. Somente para owner |
pullRequestLanding | o lado de Pull Request da folha de aterrissagem (abaixo): GET …/floors/{id}/pull-request/context, POST/DELETE …/floors/{id}/pull-request, POST …/pull-request/merge, /remove, /push, /hand-off; o pullRequest dos andares carrega title, base, review, checks, hasConflicts, mergeMethods. Somente para owner |
pullRequestReview | um andar feito para revisar um pull request aberto (abaixo): GET …/workspaces/{ws}/pull-requests[?query=], POST …/floors com pullRequest; o pullRequest dos andares carrega isReview e isReadOnly, e /hand-off aceita assess. Somente para owner |
agentAccounts | as contas de agente são escolhidas a partir de um cliente como as folhas de terminal do host as escolhem (Contas de agente, abaixo): GET …/terminals/{id}/settings carrega accountId, accounts, accountNotice, accountApplies e accountRestart, PUT …/settings aceita accountId e restartForAccount, POST …/workspaces/{ws}/terminals aceita accountId, GET …/workspaces/{ws}/agent-accounts e GET …/terminals/{id}/agent-accounts as listam para um comando, e os presets carregam accountId e accountName. Adicionar, entrar e remover contas fica no host |
fileTreePullRequest | o Open Pull Request… de uma árvore de arquivos (abaixo): GET …/nodes/{id}/tree/pull-request/context e POST …/nodes/{id}/tree/pull-request abrem um a partir do repositório da árvore como a folha do host faz para uma pasta; o próprio checkout de um andar responde com esse andar, aberto por pullRequestLanding. Somente para owner |
chatThreadPreviews | GET …/terminals/{id}/chat/threads existe (Chat do terminal, abaixo): as entradas mais recentes das outras threads de um chat, só leitura, para uma olhada antes de trocar |
push | notificações push pelo relay do Maestri: PUT /api/push/registration aceita {grant, key, keyId, kinds} e substitui o registro do próprio dispositivo que chama, DELETE /api/push/registration desliga. Qualquer dispositivo pareado, só para si mesmo; convidados recebem apenas attention, maestroRequest, floorStatus e pullRequest. Nada é enviado enquanto a pessoa está no computador do host |
Convenções
- JSON em tudo, UTF-8. Datas são strings ISO 8601. UUIDs são strings hexadecimais maiúsculas, como o Foundation as imprime: repita os ids literalmente, nunca os reformate.
- Corpos de sucesso são
{"ok": true}, salvo quando um formato mais rico estiver listado. - Erros são
{"error": {"code": "…", "message": "…", "reason"?: "…"}}, comcodesendo um deinvalidRequest,unauthorized,forbidden,notFound,conflict,preconditionFailed,payloadTooLarge,rateLimited,internalError,unavailable,unknown. O status HTTP carrega a mesma informação.messageé para logs, exceto sob umreason: uma string aberta que nomeia uma recusa que o cliente pode explicar nas palavras do host, e cujamessageé então a própria frase do host, no idioma dele, para mostrar como está. Hoje o único motivo éaccountRefused(Contas de agente); ignore qualquer outro. - Ids de caminho que não são UUIDs respondem
400; caminhos que não casam com nada respondem404. - Enumerações são strings abertas do ponto de vista de um cliente: trate um valor desconhecido como "algum outro tipo", nunca como uma falha. O servidor é mais rígido e rejeita valores desconhecidos em requisições com
400. - Campos opcionais são omitidos, nunca enviados como
null. - Nas tabelas de rotas abaixo, leitura significa qualquer dispositivo pareado, e escrita significa apenas Controle total. Nos caminhos,
{ws}é um id de workspace e{id}é o nó, terminal, portal ou dispositivo de que a seção trata.
Workspaces e o feed
| Rota | Auth | Descrição |
|---|---|---|
GET /api/workspaces | leitura | {"workspaces": [WorkspaceMeta], "layout"?: SidebarLayout}, na ordem da barra lateral do host; layout (workspaceManagement) é {folders: [{id, name}], groups: [{id, name, folders: [{id, name}]}]}, as pastas de nível superior e os grupos da barra lateral, os lugares onde um workspace pode ser arquivado |
GET /api/workspaces/{ws}/feed?floor=… | leitura | um FeedSnapshot |
GET /api/workspaces/{ws}/icon | leitura | a imagem personalizada do workspace em PNG, com um ETag; 304 em If-None-Match, 404 quando o ícone é um glifo |
POST /api/workspaces/{ws}/activate | escrita | torna-o o workspace ativo do host (404 desconhecido, 409 bloqueado pela licença) |
POST /api/workspaces/{ws}/floors/{floor|ground}/activate | escrita | troca também o andar ativo do host |
POST /api/workspaces/{ws}/pin | escrita | {pinned}; idempotente |
POST /api/workspaces/{ws}/rename | escrita | {name}; 400 vazio |
POST /api/workspaces/{ws}/wake | escrita | inicia os terminais do workspace sem ativá-lo no host |
POST /api/workspaces/{ws}/unload | escrita | para todos os terminais do workspace |
POST /api/workspaces/{ws}/attention/clear | escrita | limpa o badge de atenção do workspace |
POST /api/workspaces | escrita | {name, icon?, color?, workingDirectory?, createDirectory?, environmentOf?, placement?} → {ok, workspaceId}: a folha de Novo Workspace do host. Roda no host, ou com environmentOf no mesmo ambiente daquele workspace, cuja conexão é copiada por inteiro; workingDirectory é nesse ambiente, com ~ permitido. placement é {groupId?, folderId?} de layout: uma pasta, senão um grupo, senão o nível superior. 404 um diretório de trabalho que não existe, o sinal para tentar de novo com createDirectory: true; 400 nome vazio, cor inválida, um caminho que é um arquivo, ou um environmentOf, grupo ou pasta desconhecido; 403 quando o plano do host não permite mais |
POST /api/workspaces/{ws}/update | escrita | {name?, icon?, color?, workingDirectory?, createDirectory?, runsOn?: {environmentOf?}, placement?} → {ok}: a folha de Editar Workspace do host; um campo ausente permanece como está. runsOn move o workspace para o ambiente do workspace nomeado, ou para o host quando environmentOf está ausente dentro dele. 409 quando um workspace com andares isolados por git, ou um andar sendo criado, mudaria sua pasta ou sua conexão (do host para um ambiente, de um ambiente para o host ou para outro): esses checkouts pertencem ao projeto dele onde ele está agora; a própria recusa da folha, mantida de novo quando a edição pousa |
DELETE /api/workspaces/{ws} | escrita | {ok}: o Excluir Workspace do host, sem a confirmação, que fica por conta do cliente |
GET /api/directories?prefix= / ?path= | owner | no próprio disco do host: DirectorySuggestions para prefix, ou uma DirectoryListing para path ({path, name, directories: [{name, path}], isTruncated}, pastas ocultas deixadas de fora, "" para a pasta pessoal); o painel que um cliente navega com Novo e Editar Workspace |
WS /api/feed/stream?ws={ws}&floor=… | leitura | snapshots ao vivo e eventos de mutação |
floor pode estar ausente (espelha o andar ativo do host), ser ground, ou um UUID de andar. Ver um andar pelo Wire nunca move o host; só as rotas de activate fazem isso. Assinar o feed de um workspace como owner acorda seus terminais, então um workspace que o host não está mostrando continua transmitindo; a assinatura de um convidado não acorda nada.
FeedSnapshot
{
workspace: WorkspaceMeta,
floors: [ FloorMeta ],
pendingFloors?: [ PendingFloor ],
items: [ FeedItem ],
canvas: CanvasSnapshot,
epoch: UUID
}
WorkspaceMeta: id, name, icon?, color?, activeFloorId?, ropeRouting ("avoidNodes"|"behindNodes"|"circuit"), selectionStyle?, isPinned, groupName?, folderName?, terminalCount, runningTerminalCount, attentionCount, hasActivity, isLoaded, isLocked?, iconRevision?, workingDirectory?, environment?. icon é o nome de um SF Symbol ou um único emoji. iconRevision está presente quando o host tem uma imagem personalizada para o ícone; seu valor muda junto com a imagem, então use-o como chave de um cache. environment (workspaceManagement) é {kind, name}, o ambiente em que o workspace roda além do host: kind é um de ssh, docker, sandbox, customRuntime (tolere outros), name o próprio rótulo do host para ele; ausente para o próprio host.
floorStatus acrescenta floors, pendingFloors, groundStatus e groundFloorBranch opcionais a WorkspaceMeta. FloorMeta é {id?: UUID, name, color?, isActive, isGitIsolated?, branch?, isCloneMissing?, status?: FloorStatus, pullRequest?: FloorPullRequest, attentionCount?, hasActivity?, isDormant?, cloneKind?, seeding?, folder?, isRemoving?}. cloneKind é como a pasta de um andar isolado por git guarda o checkout (worktree, copy, remoteWorktree; tolere outros), seeding como os arquivos foram depositados (copyOnWrite, checkout; ausente quando não foi registrado), folder o caminho dela no host ou na máquina do workspace, e isRemoving que o host está removendo o andar agora. O térreo não tem id. Veja Andares para os campos de criação pendente e Status do andar e resumos da barra lateral para status, visibilidade do resumo, ordem e compatibilidade.
Um workspace que o plano gratuito do host bloqueou é listado com isLocked: true, e nada mais sobre ele é servido: seu feed, nós, notas, terminais, portais e ambos os sockets respondem 403.
FeedItem é discriminado por kind:
terminal:{ kind, terminal: TerminalCard }pendingPrompt:{ kind, terminal: TerminalCard, prompt: String }: o agente está esperando uma resposta S/nnote:{ kind, note: NoteCard }
Os itens abrangem o térreo e todos os andares do workspace; o canvas abaixo é um andar só.
TerminalCard: id, name, agentType, icon, color?, status, floorId?, floorName, lastActiveAt, isRunning, cols, rows, preview: [String], isManager, needsAttention, isLive, roleName?, roleColor?, roleIcon?, theme?: TerminalTheme, nodeId, isUnloaded?, isActive?, accountName?, maestroRequest?.
idé o id do terminal, que as rotas de terminal aceitam;nodeIdé o nó dele no canvas, que as rotas de nó aceitam. São ids diferentes.accountNameé a conta de agente em que o terminal roda quando não é a Default da CLI dele, mostrada depois do nome ("· Work"); está ausente para Default e num host anterior às contas.maestroRequest(push) é{message, createdAt}: o que um Maestro pediu à pessoa enquanto esse pedido está de pé.needsAttentioné verdadeiro junto com ele, e a mensagem é o que o distingue de um agente que apenas terminou. Está ausente quando não há pedido e num host anterior a ele.previewsão as últimas linhas do terminal em texto puro: o bastante para mostrar o que um agente está fazendo sem um emulador de terminal.needsAttentioné o estado de atenção do próprio host (o agente terminou, ou chamou alguém).isActiveé a palavra do monitor de atividade para um agente trabalhando agora.isRunningdiz que existe um processo;isLive, que o host tem o terminal carregado.isUnloadedé o Descarregar do host: o terminal colocado para dormir manualmente até quePOST …/restarto acorde.
NoteCard: nodeId, fileName, displayName, color, customColor?, floorId?, floorName, preview: [String], lastModifiedAt, isExternal, isContentLocked?, hasCustomName?. O host nomeia uma nota pela primeira linha dela para que o arquivo sempre tenha um nome, mas só mostra o nome no card quando hasCustomName (ou isExternal); desenhe o cabeçalho pela mesma regra.
TerminalTheme: background, foreground, cursor?, cursorText?, selectionBackground?, selectionForeground?, ansiPalette: [16 strings hex].
CanvasSnapshot
{
origin: {x, y}, zoom, bounds?: {x, y, width, height},
nodes: [CanvasNode], connections: [Connection], drawings: [Drawing],
groups?: [{id, name, colorHex?}],
cableTies?: [{id, position: {x, y}, memberConnectionIds: [UUID]}]
}
cableTies (cableTies) são as abraçadeiras do andar: cada uma une as cordas que ela nomeia por Connection.id, na ordem da fita, para que um cliente rodando a física de cordas do host as prenda por position, lado a lado, como o host faz. Uma abraçadeira sem nenhuma corda é descartada pelo host.
CanvasNode: id, frame {x,y,width,height}, zIndex, isNodeLocked, kind, title, subtitle?, icon?, color?, groupId? mais exatamente um payload por tipo:
terminal: TerminalCardnote: NoteCardtext {text, fontSize, isMonospaced, color?, fontFamily?, fontWeight?, fontName?}: o bloco de texto guardado pelo host;fontFamilyé"serif"ou ausente,fontWeighté"medium","bold"ou ausente,fontNameé uma fonte que o host temfile {displayName, caption?, isImage, isVideo}link {url, title?}fileTree: FileTreeNode(veja Árvores de arquivos, abaixo)portal {portalId, name, url?, currentURL?, canGoBack, canGoForward, status, chromeHidden, isUnloaded?, runtime?, runtimeDetail?, device?: {platform, deviceName?, symbol}}.devicepresente significa um portal de dispositivo (um simulador, um emulador ou um celular via adb): sem endereço, sem histórico, e o snapshot dele é a tela do dispositivo;platforméios,ipadosouandroid.runtimeé o que o nó do host mostra agora: uma página éloading,ready,failedouempty; um dispositivo éidle,booting,connecting,liveouunavailable, com a frase do próprio host emruntimeDetail. Sóreadyelivetêm um snapshot que valha buscar.isUnloadedé o Descarregar do host: uma página parada, ou o display de um dispositivo desconectado enquanto ele roda.connector {remoteTerminalId, remoteFloorId?, remoteWorkspaceId?}: a plaquinha que representa um terminal em outro andar ou, quandoremoteWorkspaceIdestá presente, em outro workspace, caso em que oicone acolordela são os daquele workspace e osubtitleé o nome dele. Segui-la significa abrir aquele workspace noremoteFloorIde parar no terminalnoteStack {name, hasCustomName, tabs: [{nodeId, title, color?}], frontNoteId?, front?: NoteCard, uniformColor?}: um fichário
kind é um de terminal, note, text, file, link, fileTree, portal, connector, noteStack. Um nó de tipo desconhecido ainda tem um frame e um título; desenhe-o como uma caixa simples. Notas arquivadas em um fichário não estão em nodes; elas continuam em items e seguem endereçáveis por id de nó através de todas as rotas de nota. groups são os grupos de nós do andar; um membro nomeia seu grupo por groupId.
Connection: id, fromNodeId, toNodeId, kind (terminal|note|noteToNote|portal|portalToPortal|crossFloor|crossWorkspace), isActive, points: [{x,y}], fromNoteNodeId?, toNoteNodeId?. Quando a ponta desenhada é um fichário representando uma de suas páginas, o campo …NoteNodeId nomeia a página real. Uma corda crossFloor vai de um terminal até a plaquinha connector de um terminal em outro andar; o id dela é o do conector, e as rotas de conectar e desconectar não aceitam essas pontas. Uma corda crossWorkspace é o mesmo desenho para um terminal em outro workspace: a metade da conexão que pertence a este workspace, do terminal dele até o conector que representa o agente distante, chaveada por esse conector e cortada ao excluí-lo. Quando ropeRouting é circuit, points pode estar desatualizado; desenhe a partir das pontas. isActive é verdadeiro enquanto algo viaja pela corda; o host desenha uma corda assim acesa.
Drawing é o DrawingPath guardado pelo host, literalmente: id, tool, points: [[x, y]], origin: [x, y], originalSize: [w, h], size: [w, h], color, lineWidth, opacity, zIndex, isLocked, rotation, textContent?, textFontSize, sourceConnection?: {drawingId, edge}, targetConnection?, strokePattern?, fillStyle?, cornerRadius?, controlPointOffset?: [x, y], groupId?. Os pontos são locais a origin e escalam por size / originalSize. tool é um de pen, brush, smartDraw, arrow, highlighter, line, rectangle, ellipse, triangle, diamond, hexagon, oval, parallelogram, star, cloud, heart, xBox, checkBox, blockArrowLeft, blockArrowUp, blockArrowDown, blockArrowRight; edge é top, bottom, left, right; strokePattern é solid, dashed, dotted; fillStyle é noFill, hachure, crossHatch, tint, solidOpaque.
O passo da grade é de 20 pontos; o host encaixa nela os frames que guarda.
Socket do feed: servidor para cliente
{"type": "feed", "snapshot": FeedSnapshot}
{"type": "mutation", "mutation": MutationEvent}
{"type": "pong"}
Um snapshot completo chega ao conectar, a cada mudança (agrupados com pelo menos 750 ms de intervalo), e fora isso não chega: snapshots inalterados não são reenviados. Ignore types desconhecidos.
MutationEvent:
{
mutationId?: String, // repetido literalmente da requisição de origem
sequence: Int, // por workspace, denso, monotônico dentro de um epoch
epoch: UUID, // identidade desta execução do servidor
floorId?: UUID, // por onde a requisição passou; nunca filtre por ele
actor?: { deviceId, deviceName, role? },
kind: { kind: "…", … }
}
Tipos: nodeMoved {nodeId, frame}, nodeRenamed {nodeIds, name}, nodeCreated {nodeId, nodeKind}, nodeRemoved {nodeId}, nodeLockChanged {nodeId, isLocked}, noteColorChanged {nodeId, color, customColor?}, connectionAdded {fromNodeId, toNodeId}, connectionRemoved {fromNodeId, toNodeId}, noteStackRailChanged {stackNodeId, memberNodeIds, frontNoteId?}, drawingAdded {drawingId}, drawingRemoved {drawingId}, drawingMoved {drawingId, origin}.
Os eventos existem para atribuição e para aposentar escritas otimistas. Eles não são um log de replicação: mudanças feitas no próprio host não produzem evento, e o snapshot segue sendo a fonte da verdade. Deduplique por (epoch, sequence), nunca por mutationId. Um epoch novo significa ressincronizar a partir do snapshot.
Socket do feed: cliente para servidor
{"type": "ping"}
{"type": "presence", "nodeId": "…"}
{"type": "presence", "point": {"x": 120, "y": 340}}
{"type": "presence"}
{"type": "camera", "pan": {"dx": 12, "dy": -4}, "zoom": 1.02}
presence (capability presence) diz onde no workspace o dispositivo está, e o host desenha um cursor para ele no seu canvas, na cor do dispositivo e com o nome dele. Envie o nodeId do nó que a pessoa tem aberto (o id de nó de um terminal, não o id de terminal), ou um point em coordenadas do canvas se o seu cliente tiver um ponteiro de verdade. Os dois ausentes significa lugar nenhum; envie assim ao sair de um nó. O relato vive junto do socket, então reenvie o último depois de cada reconexão. Qualquer papel pode relatar.
camera (capability cameraControl) é um frame de um joystick, um gamepad ou uma roda, conduzindo a câmera do próprio host. pan está em pontos de tela e move a viewport (dx para a direita, dy para baixo; o conteúdo desliza para o outro lado, como sob um trackpad); zoom multiplica o zoom do host em torno do centro da viewport. Cada um é opcional. O host limita um frame (um pan a 4000 pontos, um fator entre 0,25 e 4) e o próprio zoom à sua faixa (0,1 a 3). Como conduz a tela, exige um owner e só surte efeito enquanto o workspace do socket for o ativo no host e o andar dele for o que está na tela (um socket sem floor espelha o host e sempre se qualifica); qualquer outro caso é descartado sem resposta. Envie deltas pequenos no ritmo em que o dispositivo os produz, um stick a trinta ou sessenta frames por segundo; não há resposta, e o origin e o zoom do snapshot acompanham pelo autosave do host, não a cada frame.
Escritas no canvas
Todas de escrita. Cada corpo pode levar mutationId (qualquer string que você inventar), que volta literalmente no evento transmitido para você aposentar uma mudança otimista.
| Rota | Corpo | Resposta |
|---|---|---|
POST /api/workspaces/{ws}/nodes/{id}/frame | {x, y, width, height, bringToFront?, mutationId?} | {ok, frame}: o frame como foi guardado (encaixado, limitado; um bloco de texto ou um connector mantém o próprio tamanho); 409 travado |
POST /api/workspaces/{ws}/nodes | {kind: "note"|"text"|"fileTree", floorId?, x?, y?, text?, color?, path?, mutationId?} | {ok, nodeId} (nota 260×200, texto 240×60, fileTree 320×400); fileTree (fileTrees) aceita path, a pasta absoluta no ambiente do workspace, com ~ permitido; no próprio disco do host, um caminho que não é uma pasta responde 400 |
POST /api/workspaces/{ws}/terminals | {presetId?, floorId?, x?, y?, name?, command?, icon?, color?, monitorActivity?, maestroMode?, roleId?, accountId?, mutationId?} | {ok, nodeId, terminalId} (600×420); 404 preset ou responsabilidade não encontrada, 400 nada para rodar ou uma cor que não seja #RRGGBB, 400 um accountId em que o terminal não pode rodar (agentAccounts, Contas de agente) |
POST / DELETE /api/workspaces/{ws}/connections | {fromNodeId, toNodeId, mutationId?, fromWorkspaceId?} | {ok}; 400 para um par que o canvas não liga, ou entre andares. No POST, toNodeId pode ser um connector: o terminal é ligado ao agente distante que aquele conector representa, no andar ou no workspace dele, e esta ponta da corda para nesse conector, então vários agentes podem compartilhar um. fromWorkspaceId nomeia o workspace da origem quando não é {ws}: uma conexão entre workspaces, só no POST, só entre terminais, resolvida como {fromWorkspaceId}/fromNodeId → {ws}/toNodeId, com o host criando um conector em cada workspace. O DELETE recusa (400): exclua o conector no lugar |
DELETE /api/workspaces/{ws}/nodes/{id} | {ok}; as páginas de um fichário permanecem no canvas | |
POST …/nodes/delete | {nodeIds, mutationId?} | {ok}; a regra de lote do host: um passo de desfazer, e um fichário leva suas páginas junto; todo id precisa existir |
POST …/nodes/{id}/duplicate | {ok, nodeId} | |
POST …/nodes/{id}/lock | {locked, mutationId?} | {ok} |
POST …/nodes/{id}/focus | — (nodeFocus) | {ok}: o revelar do próprio host, o mesmo que suas barras de ferramentas e os resultados de busca executam — o app vem para a frente, o workspace e o andar ficam ativos, a câmera vai até o nó, um terminal recebe o foco, e qualquer outra coisa é selecionada; 404 nó desconhecido |
POST …/nodes/{id}/rename | {name, mutationId?} | {ok}; 400 não renomeável, 409 nota externa |
POST …/nodes/{id}/text | {text, fontSize?, fontWeight?, fontFamily?, isMonospaced?, color?, fontName?, mutationId?} | {ok}; apenas nós de texto. Os campos de estilo precisam de textStyling; um campo ausente deixa o bloco como está. fontSize é limitado a 8…200, fontWeight é regular/medium/bold, fontFamily é sans/serif, color é #RRGGBB ou "" para a cor do tema, fontName é uma fonte pelo nome ou "" para tirar uma fonte customizada |
PUT …/nodes/{id}/note/color | {color, customColor?} | NoteContent |
POST /api/workspaces/{ws}/groups | {nodeIds, drawingIds?, name?, floorId?} | {ok, groupId?}: o comando Agrupar do host; dois ou mais nós viram um grupo de nós, dois ou mais desenhos um grupo de desenhos |
POST /api/workspaces/{ws}/ungroup | {nodeIds, drawingIds, floorId?} | {ok} |
DELETE /api/workspaces/{ws}/groups/{id} | {ok}; os membros ficam onde estão | |
POST …/groups/{id}/rename | {name} | {ok}; vazio limpa |
PUT …/groups/{id}/color | {colorHex?} | {ok}; #RRGGBB ou null para o padrão |
PUT …/nodes/{id}/stack/front | {frontNoteId, mutationId?} | {ok, memberNodeIds, frontNoteId?}: vira o fichário para uma página |
PUT …/nodes/{id}/stack/order | {memberNodeIds, mutationId?} | o mesmo; uma permutação das páginas atuais |
POST …/nodes/{id}/stack/pages | {afterNoteId?, mutationId?} | {ok, nodeId}: uma página em branco, virada para a frente |
DELETE …/nodes/{id}/stack/pages/{noteId} | {x, y, mutationId?} (opcional) | {ok, memberNodeIds, frontNoteId?}; a nota permanece no canvas, em x,y quando informado |
PUT …/nodes/{id}/stack/pages/{noteId} | {mutationId?} | o mesmo; arquiva uma nota solta, ou traz para cá uma página de outro fichário |
POST …/note-stacks | {nodeIds, floorId?, mutationId?} | {ok, stackNodeId}: o "Colocar no Fichário" do host, para uma seleção |
POST …/files?filename=&x=&y=&floor=&mutationId= | os bytes do arquivo sob o seu Content-Type (8 MB) | {ok, nodeId}: um nó de arquivo centrado em x,y; um .md/.txt vira uma nota |
POST /api/workspaces/{ws}/drawings | {floorId?, drawing: Drawing, mutationId?} | {ok, drawingId}; guardado sob o id com que foi enviado; 409 se esse id já existir |
DELETE /api/workspaces/{ws}/drawings/{id} | {ok}; 409 travado | |
POST …/drawings/{id}/origin | {x, y, mutationId?} | {ok}; o novo canto superior esquerdo do desenho |
POST /api/workspaces/{ws}/ties | {floorId?, tie: {id, position, memberConnectionIds}} | {ok, tieId}; o option-drag do host sobre as cordas, guardado sob o id com que foi enviado, no andar nomeado (ausente ou ground para o térreo). memberConnectionIds são Connection.ids que o andar tem, na ordem em que o cliente as cruzou pelo feixe; 400 quando algum não é uma corda que o workspace tem, 409 se aquele id já existe |
POST …/ties/{id}/position | {x, y} | {ok}; a abraçadeira reposicionada, guardada como enviada; 404 nenhuma abraçadeira assim |
DELETE …/ties/{id} | {ok}; 404 nenhuma abraçadeira assim | |
POST /api/workspaces/{ws}/partituras | {partituraId, x, y, floorId?, mutationId?} | {ok}: estampa uma partitura centrada em x,y |
GET /api/agent-presets (leitura) lista {presets: [{id, name, agentType, icon, isManager, isDefault, command?, accountId?, accountName?}]} para a rota de criação de terminal. accountId e accountName (agentAccounts) são a conta de agente que o preset fixa, presentes só enquanto o comando dele inicia a CLI dessa conta; ausente é Default. Um corpo de criação sem nenhum dos campos de Novo Terminal cria o preset (o padrão, quando presetId está ausente); um corpo com qualquer um deles constrói sobre esse preset, ou sobre nenhum preset quando presetId está ausente, exatamente como a folha do host faz.
O host aplica a aparência do preset ao criar o terminal: cor, tema e fonte do terminal, e fonte e tamanho do chat. Campos explícitos no corpo de criação continuam vencendo os equivalentes do preset. Editar um preset muda as criações seguintes, nunca os terminais que já o usam. Presets e terminais também guardam startInChatMode, falso quando ausente; um terminal lembra separadamente da última face em que esteve como prefersChatMode, e só recorre a startInChatMode quando não tem face lembrada.
A lista de presets também carrega themes: [{id, name, isLight}], um defaultAppearance, e a appearance resolvida de cada preset mais uma color opcional. Um cliente esconde esses controles quando appearance e defaultAppearance estão ausentes, que é como um host mais antigo se apresenta.
Notas
| Rota | Auth | Descrição |
|---|---|---|
GET /api/workspaces/{ws}/nodes/{id}/note | leitura | NoteContent |
PUT /api/workspaces/{ws}/nodes/{id}/note | escrita | {text, ifRevision?} → NoteContent; 409 conteúdo travado, 412 revisão divergente (o corpo é o NoteContent atual) |
PUT /api/workspaces/{ws}/nodes/{id}/note/lock | escrita | {locked} → {ok}; define a trava de conteúdo da nota, a mesma contra a qual uma gravação responde 409, e a mesma que o Travar Conteúdo do host ativa. Idempotente: é o estado desejado, não um toggle. Não é o POST …/nodes/{id}/lock, que é a trava do próprio nó do canvas, uma propriedade diferente. O novo estado não é transmitido como uma mutação; ele viaja no isContentLocked do card da nota no próximo feed. 404 quando o nó não é uma nota que este host possui |
GET /api/workspaces/{ws}/nodes/{id}/file | leitura | os bytes crus de um nó de arquivo; honra Range (206, 416) |
GET /api/workspaces/{ws}/nodes/{id}/file/preview | leitura | um nó de arquivo de imagem, maior lado de 1100px, com um ETag; 304 em If-None-Match; 404 quando o arquivo não é uma imagem que o host consegue decodificar (canvasFiles). PNG (image/png) quando a imagem tem pixels de fato transparentes, JPEG (image/jpeg) do contrário: JPEG não tem canal alfa, então um PNG transparente servido como JPEG chega achatado sobre uma cor que o codificador escolheu. Leia o Content-Type; não presuma JPEG |
NoteContent: nodeId, fileName, displayName, text, color, customColor?, revision, lastModifiedAt, isExternal, isContentLocked. revision são os 16 primeiros caracteres hexadecimais do SHA-256 sobre o texto UTF-8; devolva-o como ifRevision para tornar a gravação condicional, de modo que uma nota editada no host nesse meio-tempo nunca seja sobrescrita às cegas.
Terminais
| Rota | Auth | Descrição |
|---|---|---|
WS /api/terminals/{id}/stream?token=… | leitura (entrada exige Controle total) | o stream do terminal, abaixo |
POST /api/terminals/{id}/prompt | escrita | {text} ou {segments: [{text} | {attachmentId}]}; 409 não está rodando |
POST /api/terminals/{id}/attachments?filename=… | escrita | corpo cru com o seu Content-Type → {attachmentId, fileName, mimeType, expiresAt} (15 min, ≤ 8 MiB, ≤ 64 arquivos) |
DELETE /api/terminals/{id}/attachments/{attachmentId} | escrita | {ok} |
GET /api/terminals/{id}/mentions | leitura | {mentions: [Mention]} para um seletor de @ |
GET /api/terminals/{id}/files?query=&scope=&limit= | owner | {query, root, entries: [{relativePath, name, isDirectory}], isTruncated}: o próprio índice de arquivos do compositor do host para o terminal, com raiz no diretório de trabalho dele, em qualquer ambiente em que rode, filtrado como o próprio seletor do host filtra dentro de scope (uma pasta relativa ao diretório de trabalho, vazia para o topo; uma consulta vazia lista os filhos daquela pasta); limit no máximo 200; 404 quando o terminal não tem diretório para ler |
POST /api/terminals/{id}/approve / reject | escrita | responde a um prompt S/n pendente; 409 quando não há nenhum |
POST /api/terminals/{id}/seen | escrita | marca o terminal como visto, limpando a atenção dele nas duas superfícies |
POST /api/terminals/{id}/restart | escrita | o botão de recarregar do host; acorda um terminal descarregado |
POST /api/terminals/{id}/unload | escrita | o Descarregar do host (nodeUnload): a sessão desmontada até virar seu card dormente, com o scrollback preservado, até um restart |
POST /api/terminals/{id}/focus | escrita | o host vai até o terminal (terminalFocus): o app vem para a frente, o workspace e o andar do terminal ficam ativos, a câmera vai até o nó, e o terminal recebe o foco |
POST /api/terminals/{id}/kill | escrita | encerra o processo |
GET / PUT /api/terminals/{id}/settings | leitura / escrita | {command, workingDirectory, roleId?, roles: [{id, name, icon, color}], maestroMode, icon?, color?, monitorActivity?, appearance?, themes?, accountId?, accounts?, accountNotice?, accountApplies?, accountRestart?}; o PUT aceita {command, roleId?, maestroMode, icon?, color?, monitorActivity?, appearance?, accountId?, restartForAccount?} como substituição completa (exceto accountId, veja Contas de agente) e ecoa as configurações aplicadas. O comando vale no próximo lançamento ou recarga; o resto na hora; uma responsabilidade alterada reinicia um terminal em execução. 400 responsabilidade desconhecida, cor inválida, ou um accountId em que o terminal não pode rodar |
GET /api/terminals/{id}/agent-accounts?command= | leitura | AccountList (agentAccounts): as contas em que o terminal poderia rodar command onde ele roda, para uma folha de edição cujo comando está sendo editado; sem command, o que está salvo |
Mention: kind, id, name, tokenName?, icon?, color?, platform?, floorId?, floorName, isOnActiveFloor, depth, section, sectionOrder, order. kind é agent, floor, note, portal, maestro, ou uma ação: createNote, createPortal, createDevicePortal, e para um Maestro createFloor, notifications, routines, workspaces; tolere outros. Um cliente só devolve uma ação quando o catálogo a listou.
Appearance, carregada igualmente pelas configurações de terminal, pelos snapshots de chat e pelos corpos de criação: {theme, customThemeId?, darkThemeId?, terminalFontSize?, chatFont, chatTextSize, startInChatMode}. theme é system, dark, light ou custom; um id customizado precisa ser um que o host tenha (400 caso contrário), e darkThemeId emparelha automaticamente um tema claro com um escuro. terminalFontSize é uma sobreposição do lado do cliente, de 8 a 24 pontos, e omiti-la restaura o dimensionamento automático. chatFont é system, monospaced, rounded ou serif; chatTextSize é system, small, medium, large, extraLarge ou extraExtraLarge. Os campos de tipografia são do próprio cliente: eles nunca mudam as fontes do host. themes, numa resposta de configurações, lista o que customThemeId pode nomear, como [{id, name, isLight}].
Note
Enviar um prompt ou uma entrada a partir de um dispositivo limpa a atenção do terminal, exatamente como clicar dentro dele no host faz. Se uma resposta depois levanta a atenção de novo é decisão do monitor de atividade do host, nas duas superfícies.
O stream do terminal
Ao conectar, o servidor envia {"type":"geometry","cols":N,"rows":N} seguido de um frame binário com a tela atual (ANSI). Depois disso, todo frame binário é saída crua do PTY. geometry pode chegar de novo a qualquer momento e é sempre seguido de uma tela nova; resete seu emulador quando isso acontecer.
Do cliente para o servidor, como frames de texto JSON:
{"type": "ping"}
{"type": "inputBytes", "data": "base64"}
{"type": "input", "data": "texto utf-8"}
{"type": "resize", "cols": 100, "rows": 40}
Um frame binário vindo do cliente é entrada crua em bytes. Frames do cliente precisam ser mascarados (RFC 6455); um frame maior que 1 MiB, ou um sem máscara, fecha o socket. O servidor responde {"type":"pong"} e ignora tipos desconhecidos.
resize reajusta o PTY do host à sua grade, limitado a 20…250 colunas por 5…120 linhas; o próprio nó do host reduz a fonte para continuar mostrando a grade inteira, e devolve a grade a si mesmo vinte segundos depois que o último espectador sai. Dispositivos somente leitura podem redimensionar; a entrada deles é descartada.
Você precisa de um emulador de terminal para renderizar os bytes (xterm.js em um cliente web, por exemplo). Se você só quer saber o que um agente está fazendo, as linhas de preview do feed são texto puro e não precisam de nenhum.
Responsabilidades de agente
Capability roleManagement. As responsabilidades pertencem ao host, não a um workspace: a lista é toda responsabilidade que ele tem, e workspaceId marca as que estão restritas a um workspace (ausente significa global).
| Rota | Auth | Descrição |
|---|---|---|
GET /api/roles | leitura | {roles: [Role]} |
POST /api/roles | escrita | RoleWrite → Role como foi guardada |
PUT /api/roles/{id} | escrita | RoleWrite, substituição completa → Role; um prompt alterado reinicia todo terminal em execução que tenha a responsabilidade |
DELETE /api/roles/{id} | escrita | {ok}; remove a responsabilidade de onde estiver atribuída |
Role: id, name, prompt, icon, color, workspaceId?, terminalsInUse. RoleWrite: {name, prompt, icon, color, workspaceId?}; 400 nome ou prompt em branco, cor inválida ou workspace desconhecido; 409 um nome que outra responsabilidade no mesmo escopo já tem.
Chat do terminal
Capability terminalChat. A segunda face de um terminal, não um agente separado nem uma sessão separada do provedor. Todo alvo é resolvido no térreo e em todos os andares sem ativar o workspace do host, as leituras e o socket continuam disponíveis com o processo parado, e os bloqueios de workspace, a revogação de pareamento e as escritas só para owner valem exatamente como nas rotas de terminal.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/terminals/{id}/chat | leitura | ChatSnapshot: a cauda ao vivo limitada e o estado atual |
WS /api/terminals/{id}/chat/stream?token=… | leitura | um frame de texto {"type":"chat","snapshot":ChatSnapshot} imediato, e depois um snapshot substituto sempre que o estado observado muda |
PUT /api/terminals/{id}/chat | escrita | {enabled, showsShellWindow?} → ChatSnapshot; entra na face de chat ou sai dela pelo coordenador do host, preservando o histórico. Com showsShellWindow presente a face fica como está e só essa configuração muda, então uma rota carrega as duas coisas |
PUT /api/terminals/{id}/chat/thread | escrita | {slot} → ChatSnapshot; seleciona um dos sete slots fixos (0…6); 400 para um slot inválido, 409 enquanto a troca está bloqueada |
DELETE /api/terminals/{id}/chat/thread | escrita | {slot} → {ok}; limpa aquele slot sem selecioná-lo nem tirar o lugar dele, com as mesmas validações e travas |
GET /api/terminals/{id}/chat/threads | leitura | ChatThreadPreviews (chatThreadPreviews): {threads: [{id, conversationId, entries: [ChatEntry]}]}, toda thread que não seja a selecionada e que tenha mensagens, pelo id do slot, com as 40 entradas mais novas. Para uma olhada antes de trocar, como o host mostra uma thread enquanto o ponto dela está sob o ponteiro: não seleciona nada, não marca nada como lido, e funciona enquanto a troca está bloqueada. Um slot que falta na lista está vazio. Anexos nela são obtidos pela rota de anexo, que alcança toda thread |
GET /api/terminals/{id}/chat/history | leitura | {conversationId, entries: [ChatEntry]}, a transcrição retida inteira, pedida em vez de empurrada |
POST /api/terminals/{id}/chat/history | leitura | {conversationId, beforeEntryId} → {ok}; pede ao host que leia a página anterior à entrada mais antiga que a cauda guarda, para quem chegou ao topo. Essas entradas chegam no próximo snapshot substituto pelo socket, nunca nesta resposta. 400 para um corpo ilegível, 409 quando a conversa já andou ou a âncora não está mais carregada |
POST /api/terminals/{id}/chat/read | escrita | marca as mensagens do chat como lidas; {ok} |
DELETE /api/terminals/{id}/chat/entries/{entryId} | escrita | exclui aquela entrada durável pelo coordenador do host; {ok}, e todo observador recebe a cauda alterada |
DELETE /api/terminals/{id}/chat/history | escrita | limpa a transcrição do Maestri e os anexos retidos, não a memória do provedor; {ok}; 409 enquanto um turno ou uma importação impede |
GET /api/terminals/{id}/chat/attachments/{attachmentId} | leitura | os bytes do anexo retido, endereçado por UUID dentro desta conversa; 404 quando não existe. Nunca um caminho de arquivo fornecido por quem chama |
DELETE /api/terminals/{id}/chat/attachments/{attachmentId} | escrita | remove o arquivo de toda entrada que o guarda e do armazenamento do host; {ok}; 409 enquanto uma limpeza desmonta o registro. Uma entrada que fica só com aquele arquivo vai junto |
ChatSnapshot: terminalId, conversationId, enabled, status, entries: [ChatEntry], unreadCount, canClear, talkingToAgentName?, answeringAgentName?, threads?, appearance?, turnInterrupted?, showsShellWindow?, hasOlderOnRecord?. status é idle, working, needsAttention, notRunning ou unloaded, e enabled diz se a face de chat está ligada. turnInterrupted diz que a pessoa parou o turno com Esc; ele viaja ao lado de status, que marca needsAttention, então um cliente mais antigo continua decodificando. showsShellWindow é a setinha do próprio terminal: se um turno abre a janela do shell embaixo da linha de legenda. Ausente significa um host que o antecede e sempre abria aquela janela. Os nomes entre agentes explicam por que o agente está ocupado, em vez de deixar um cliente deduzir isso da saída do terminal.
A cauda ao vivo é semeada com as 80 entradas mais novas quando a conversa carrega e depois cresce com a troca. Ela nunca é cortada pela frente enquanto a conversa está aberta, porque entradas sumindo acima de quem lê deslocam a thread embaixo da pessoa. Ela também cresce para trás, uma página de 60 por vez, quando um cliente pede pelo POST …/chat/history; a página é acrescentada no começo, então nada do que já foi renderizado se move. hasOlderOnRecord diz que o registro ainda guarda entradas mais antigas que a cauda: ofereça um botão de carregar mais só enquanto isso for verdadeiro, e trate um host que omite o campo como um que antecede a paginação e guarda a transcrição inteira atrás do GET …/chat/history. Estar fora da cauda não é ter sido excluído do histórico durável.
ChatEntry: id, role: user | agent, ts, kind: message | progress | final | divider, text, segments, authorName?, agentType?, clientMessageId?. text é o canônico, sem o envelope de entrega. Os segmentos do usuário preservam a ordem do compositor, cada um exatamente um entre {"text":"palavras"} e {"attachment":{"id":"UUID","isImage":true,"filename":"foto.png","contentType":"public.png"}}. Entradas de agente e divisores têm lista de segmentos vazia. Uma referência de anexo não guarda caminho do host nem token de staging que expira.
threads, quando presente, habilita as rotas de thread acima: {activeSlot, canSelect, items: [{id, color, hasMessages}]}. São sete ids de slot estáveis, 0…6, e o host fornece uma paleta fixa na ordem das configurações a partir do azul, com as chaves de cor blue, purple, pink, red, orange, yellow, green ou graphite; o cliente localiza os nomes. Essa ordem nunca muda: uma cor é a identidade de uma thread, tanto para a pessoa quanto para o agente. Trocar e limpar são recusados enquanto canSelect for falso, e o servidor reconfere essa trava a cada requisição. Um host sem threads fala só a interface original de conversa única, então um cliente não deve oferecer controles de thread nem chamar as rotas novas.
Histórico, anexos, marcas de leitura, uma limpeza comum e os prompts resolvem todos contra a thread selecionada. Uma troca muda o conversationId: substitua a transcrição e reinicie o scroll e o estado de envio dela em vez de fundir com a thread anterior. Trocar a partir de um cliente não ativa o workspace nem o andar do host.
O envio continua pelo POST …/prompt, com texto ou segmentos ordenados de anexos preparados. Enquanto o host está na face de chat, o coordenador dele registra o turno do usuário e enfileira a entrega, e estar trabalhando não impede outra mensagem composta. Um clientMessageId UUID opcional correlaciona um envio local com o id de entrada que o host atribui. É metadado efêmero de apresentação, não um token de idempotência: nunca repita automaticamente um envio ambíguo. A saída do terminal nunca é convertida em mensagens de chat; só a transcrição canônica do coordenador é sincronizada.
O socket de chat nunca redimensiona nem acorda um terminal, e nunca aceita entrada de terminal. Use ping/pong do WebSocket para liveness, e ignore valores de type desconhecidos e pong como o socket do feed pede. Reconectar sempre te entrega uma cauda substituta completa, incluindo remoções e status, então não funda snapshots como eventos de acréscimo, e nunca deixe um snapshot recebido por HTTP sobrescrever um mais novo já recebido pelo socket ordenado. Mantenha um stream por sessão de terminal, inclusive enquanto ela aparece em PiP; feche ao ir para segundo plano e reconecte ao voltar.
Um card de terminal no canvas pode carregar chatPreview: {conversationId, entries: [ChatEntry], isWorking, unreadCount} enquanto está mostrando o chat: no máximo quatro entradas recentes para uma miniatura, não um segundo armazenamento de histórico. Abrir o terminal usa o socket de chat e a rota de histórico.
Contas de agente
Capability agentAccounts. Uma conta de agente é uma identidade autenticada de uma CLI de agente, ao lado do próprio login da pessoa, que é a conta Default da CLI. Quais CLIs têm contas é o host que decide a partir do comando. Claude Code, Codex e Antigravity vêm embutidos, e qualquer outra CLI passa a ter contas quando a pessoa adiciona um arquivo de provedor para ela. Um cliente nunca casa comandos por conta própria; ele pergunta. Cada conta vive em um lugar, o host ou um ambiente remoto, e um terminal só pode rodar numa conta que vive onde ele roda: o host, ou o ambiente do workspace ou do terminal. Default nunca é guardada e não tem id: um accountId ausente ou null significa Default em todo lugar. Ids de conta são strings; ecoe-os literalmente.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/workspaces/{ws}/agent-accounts?command= | leitura | AccountList para um terminal novo no workspace rodando command, onde o workspace roda: o seletor da folha de Novo Terminal. 404 workspace desconhecido |
GET /api/terminals/{id}/agent-accounts?command= | leitura | AccountList para o terminal rodando command onde ele roda (o próprio ambiente, senão o do workspace): o seletor da folha de edição. 404 terminal desconhecido |
command é o comando como digitado; o host toma a primeira linha não vazia, aparada, como suas folhas fazem. AccountList é {accounts: [{id?, name}], notice?, accountApplies?}:
accountsé a lista do seletor do host: Default primeiro, semide nomeada com o rótulo da própria pessoa (senão a palavra do host para Default), depois as contas daquela CLI que vivem ali, poridename. Vazia quando o comando não inicia nenhuma CLI que tenha contas.noticeé a própria frase do host quando o lugar autentica a CLI por conta própria (um Docker Sandbox com o próprio segredo sbx): o que for escolhido, o login do lugar é o que roda. Um Docker Sandbox sempre tem uma para uma CLI cujas contas não podem rodar ali: as contas dela se recusam a iniciar nele.accountApplies, junto comnotice, étruequando o lugar autentica só alguns dos provedores da CLI (o OpenCode fala com muitos): a conta ainda vale para o resto, então a escolha continua oferecida e a frase nomeia os provedores que o lugar autentica. Ausente oufalse, o login do lugar substitui a conta.
Um cliente mostra um seletor de Conta só quando accounts tem mais de uma entrada, a regra do host: senão não há o que escolher. Com notice ele mostra a escolha desabilitada, com a frase embaixo, a menos que accountApplies seja true, quando a escolha continua habilitada sobre a frase. Uma seleção que não está na lista (o comando mudou de CLI desde que foi escolhida) lê como Default, a conta em que ela iniciaria. Pergunte de novo quando o comando mudar; faça debounce da digitação.
Configurações. GET …/settings carrega accountId (a conta em que o terminal inicia, resolvida: um vínculo que sobrou de outra CLI ou de outro lugar lê como Default, então ausente), accounts, accountNotice e accountApplies (os campos de AccountList do comando salvo) e, enquanto o terminal roda, accountRestart: {resumesSession, isKeptAlive}. No PUT, accountId é o único campo que não é uma substituição completa, porque tem três estados: ausente deixa a conta do terminal como está (um cliente de antes de agentAccounts nunca move um terminal ao salvar outra coisa), null é Default, uma string é aquela conta. Um cliente que mostra o seletor sempre a envia.
Um agente em execução fica na conta com que começou pela vida do processo. Quando a conta enviada difere daquela em que um terminal em execução iniciou, a folha de edição do host pergunta "Restart on {account}?" com Restart Now e At Next Launch; um cliente pergunta o mesmo antes de salvar, quando accountRestart está presente, e responde com restartForAccount:
true(Restart Now): o agente sai como ao encerrar, a linha de retomada é colhida, e ele inicia de novo na conta nova e retoma a sessão quandoresumesSession(a configuração "resume sessions" do host). A resposta vem primeiro; o reinício vem depois e o cartão o mostra no feed. Uma mudança de responsabilidade no mesmo salvamento já reinicia o terminal na conta nova, e nada reinicia duas vezes.- ausente ou
false(At Next Launch): a conta vale no próximo lançamento ou recarga do terminal. QuandoisKeptAlive(uma sessão mantida viva no tmux) esse próximo lançamento começa a sessão de novo, sem retomar, porque reanexar traria o agente de volta na conta antiga; diga isso.
Mover um terminal que não está rodando não pergunta nada; a conta vale no próximo lançamento.
Criação. POST …/workspaces/{ws}/terminals aceita accountId com os mesmos três estados. Ausente toma a conta do preset (presetId, ou o preset padrão para um corpo sem campos de rascunho), onde ela vive, e Default nos outros casos, como a folha do host semeia; null é Default; uma string é aquela conta. Não é um dos campos de rascunho: ela se sobrepõe ao que o resto do corpo produz. Uma folha de Novo Terminal semeia o seletor a partir do accountId do preset escolhido e envia a seleção efetiva sempre que mostra o seletor.
Validação. Um accountId enviado é conferido contra o comando que o terminal vai rodar (o do corpo, senão o salvo ou o do preset) e onde ele roda. Cada recusa é 400 com reason: "accountRefused" e uma message que diz qual, nomeando a conta e onde ela vive: um id que não é conta do host, um comando que não inicia nenhuma CLI com contas, uma conta de outra CLI, uma conta que vive em outro lugar que não onde o terminal roda. Mostre essa mensagem como o motivo de o salvamento ou a criação ter falhado; não é um problema de conexão, e tentar de novo repete. O host nunca cai para Default diante de um id enviado. Default (null) é sempre aceita.
Nota
Uma exportação de workspace (schema de transferência 2) e um backup versão 2 carregam chats/<UUID da conversa>/ para todo terminal do térreo e de todos os andares, threads inativas e anexos retidos incluídos. Uma importação remapeia os ids de conversa para armazenamento independente, e restaurar um backup antigo sem os arquivos de chat deixa o armazenamento de chat existente em paz.
Portais
| Rota | Auth | Descrição |
|---|---|---|
GET /api/portals/{id}/snapshot | leitura | image/jpeg com um ETag; 304 em If-None-Match; 404 enquanto o portal não tiver uma imagem ao vivo, inclusive sob o Descarregar do host. A página de um portal web, ou a tela de um portal de dispositivo |
POST /api/portals/{id}/reload | escrita | o recarregar do próprio host, que também serve para acordar: uma página descarregada é carregada de novo, o display de um dispositivo descarregado é reconectado, onde quer que o portal esteja |
POST /api/portals/{id}/back / forward | escrita | histórico, para um portal web |
POST /api/portals/{id}/navigate | escrita | {url} |
POST /api/portals/{id}/unload | escrita | o Descarregar do host (nodeUnload): uma página parada, o display de um dispositivo desconectado enquanto ele roda; isUnloaded fica no nó até um reload |
Snapshots são imagens, não um stream: o host renderiza a página ou o framebuffer atual a cada requisição. Faça polling com o ETag e uma imagem inalterada custa um 304 sem corpo.
Dispositivos
| Rota | Auth | Descrição |
|---|---|---|
GET /api/devices | leitura | {devices: [{id, name, lastSeenIP?, lastSeenAt?, createdAt, role?}]} |
DELETE /api/devices/{id} | escrita | revoga; um dispositivo pode revogar a si mesmo |
PUT /api/devices/{id}/role | escrita | {role: "owner"|"guest"}; 409 quando um dispositivo aponta para si mesmo com guest |
PUT /api/devices/{id}/name | leitura (a si mesmo) / escrita (aos outros) | {name}, com espaços aparados, de 1 a 80 caracteres; um dispositivo renomeia a si mesmo qualquer que seja seu papel |
Partituras
Capability partituras. A biblioteca de arranjos de canvas salvos do host, navegada e estampada a partir de um cliente; criar e editar continuam no host.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/partituras | leitura | {partituras: [Partitura]} |
GET /api/partituras/{id}/preview?appearance=light|dark | leitura | a miniatura do próprio host em JPEG, com um ETag |
POST /api/workspaces/{ws}/partituras | escrita | {partituraId, x, y, floorId?, mutationId?}: o layout centrado em x,y; o host preenche os terminais e adota as responsabilidades embutidas |
Partitura: id, name, summary, icon, color, workspaceId?, roles: [{id, name, icon, color}].
O formato 2 de partitura pode embutir as sete threads de chat e seus anexos, como o payload terminalChatArchives da área de transferência. Criar ou substituir uma oferece Incluir histórico de chat, desligado por padrão, e importar exige uma escolha própria; deixar um terminal de fora deixa o histórico dele de fora junto. Carimbar copia o histórico aceito para conversas novas em vez de ligar de volta ao terminal de origem. O formato 1 continua legível, e um app mais antigo rejeita o formato 2 em vez de descartar o histórico embutido em silêncio.
Árvores de arquivos
Capability fileTrees, somente para owner: um guest vê o nó no canvas mas é recusado em toda rota abaixo com 403. A pasta de um nó de árvore de arquivos, navegada e pesquisada a partir de um cliente, e o repositório dela acionado pelo próprio menu de git do host, onde quer que a árvore aponte: o próprio disco do host, um host SSH, um container, um sandbox.
Os caminhos dessas rotas são opacos: relativos à raiz, separados por /, "" para a raiz. Um cliente mostra name e ecoa path exatamente como o host entregou, e nunca junta, divide ou normaliza um. Um caminho que sairia da raiz (.., um caminho absoluto, ou no disco do host um symlink para fora da raiz) responde 400 ou 403.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/workspaces/{ws}/nodes/{id}/tree?path=&hidden= | owner | FileTreeListing da pasta em path; hidden (true/false) sobrepõe a própria configuração de arquivos ocultos do nó; 404 uma pasta que sumiu; 502 com as palavras do próprio ambiente quando não pôde ser lida |
GET /api/workspaces/{ws}/nodes/{id}/tree/file?path= | owner | os bytes do arquivo sob um Content-Type deduzido do nome (arquivos de código-fonte e configuração são text/plain); honra Range (206, 416); transmitido em stream no próprio disco do host, lido por inteiro sob um limite de 64 MB em outro ambiente (413 além disso) |
GET /api/workspaces/{ws}/nodes/{id}/tree/changes | owner | FileTreeChanges do repositório em que a raiz está; 404 quando não está em um |
GET /api/workspaces/{ws}/nodes/{id}/tree/diff?path=&scope= | owner | FileTreeDiff: o diff unificado do próprio git de um arquivo alterado, path relativo ao repositório como FileTreeChanges o lista, scope um de staged, unstaged, combined (padrão); 404 quando o arquivo não tem essa mudança |
GET /api/workspaces/{ws}/nodes/{id}/tree/search?query=&hidden= | owner | FileTreeSearch: arquivos e pastas em qualquer lugar sob a raiz cujo nome bate, através da própria busca do ambiente e ranqueados como a própria árvore do host os ranqueia, no máximo 200 (isTruncated além disso); os resultados são linhas FileTreeEntry sem tamanho nem data |
GET /api/workspaces/{ws}/directories?prefix= / ?path= | owner | prefix (fileTrees) responde DirectorySuggestions: pastas no ambiente do workspace cujo caminho começa com ele (~ pode começar), no máximo 30, para um campo que nomeia a pasta que uma nova árvore deve navegar. path em vez disso (workspaceManagement) responde uma DirectoryListing das pastas dentro dele, o mesmo formato que GET /api/directories devolve, mas resolvido no ambiente do workspace, onde os caminhos ficam relativos a ~ |
GET /api/workspaces/{ws}/nodes/{id}/tree/git | owner | FileTreeGitOverview: o repositório como o próprio menu de git do host o vê; 404 fora de um repositório |
POST /api/workspaces/{ws}/nodes/{id}/tree/git | owner | FileTreeGitRequest → FileTreeGitResult. Uma ação NOMEADA, nunca uma lista de argumentos: o host roda o mesmo git que seu próprio menu e folhas rodam, e nada além disso. ok: false com as palavras do próprio git quando ele recusou; 400 para uma solicitação que o host não transforma em uma invocação (uma mensagem ausente, um nome de branch que ele não aceita, um caminho que sai do repositório); 404 fora de um repositório. Naturalmente lento para pull, push, sync, fetch: um cliente espera |
GET /api/workspaces/{ws}/nodes/{id}/tree/pull-request/context?base= | owner | FileTreePullRequestContext (fileTreePullRequest): o que a folha do host mostra antes de um pull request abrir a partir do repositório da árvore; 404 fora de um repositório. Um repositório que é o próprio checkout de um andar, em qualquer andar do workspace, responde {floorId, bases: [], supportsDraft: false} e nada mais: um cliente abre o pull request daquele andar (Andares, abaixo) no lugar |
POST /api/workspaces/{ws}/nodes/{id}/tree/pull-request | owner | {base, title, body, draft?, onWebsite?} → {url?, openedOnWebsite} (fileTreePullRequest): faz push do branch do repositório e abre o pull request dele, como a folha do host faz para uma pasta; nada registra nem acompanha depois. Uma página no host do projeto é devolvida para o cliente abrir, nunca aberta no host do Maestri. 502 significa que o provedor recusou depois do push: só então um cliente pode oferecer onWebsite, que precisa do branch no host do projeto (409 caso contrário). 409 para trabalho não commitado, nada novo a propor, uma pasta fora de um branch ou no meio de um merge ou rebase, ou outra abertura da mesma pasta ainda em curso; 404 fora de um repositório; 503 com as palavras do host do projeto quando ele não pôde ser consultado |
FileTreeNode (em um nó do canvas do kind fileTree): rootPath, currentPath, environment?, showsHiddenFiles, pinnedPaths, preview?, branch?. rootPath é o caminho absoluto do host, para exibição; currentPath é para onde a árvore foi navegada por último, relativo à raiz; environment está ausente para o próprio disco do host, senão ssh, docker, sandbox ou customRuntime; pinnedPaths são relativos à raiz. preview são as primeiras linhas (no máximo 12) da pasta em que a árvore está, [{name, isDirectory, gitStatus?}], para um cartão desenhar a árvore em miniatura; ausente para uma árvore em outro ambiente. branch é o branch do repositório como a árvore o viu por último.
FileTreeListing: path, name, entries: [FileTreeEntry], isTruncated, git?. As entradas vêm com as pastas primeiro, na própria ordem do host para a árvore. FileTreeEntry: name, path, isDirectory, isSymbolicLink, size, modifiedAt?, gitStatus?; gitStatus é a letra do porcelain (M A D R C ? ! U), para uma pasta a pior entre seus filhos. git: branch?, changedCount, hasUpstream, ausente fora de um repositório.
FileTreeChanges: branch?, staged: [FileTreeChange], unstaged: [FileTreeChange]; um arquivo alterado nos dois lados aparece nos dois. FileTreeChange: path, name, directory, status, isStaged, additions?, deletions?. FileTreeDiff: path, scope, patch.
FileTreeGitOverview: branch?, hasUpstream, remotes, localBranches, remoteBranches, stashes: [{ref, message}], source?: {name, branches}. remoteBranches são branches de rastreamento remoto sem correspondente local, mostrados como origin/x; fazer checkout de um cria o branch local. source é o repositório do projeto a partir do qual o clone de um andar foi feito, com o nome da pasta e os branches locais dele. Ele só aparece para uma árvore em um andar isolado por git, e está ausente no térreo, num andar comum e num host que o antecede, então ofereça o Fetch do Repositório de Origem ali e em nenhum outro lugar.
FileTreeGitRequest: action mais o que ela precisa: commit (message, e paths para dar stage antes, ou stageAll), stage / unstage (paths), pull, push (com um upstream), publish (remote: o branch atual enviado com --set-upstream), sync (pull, depois push se o pull deu certo), fetch (--all), checkout (branch), newBranch (branch, criado a partir do atual), deleteBranch (branch, force?), merge (branch no atual, --no-edit), stashPush (message?, com arquivos não rastreados incluídos), stashApply / stashPop / stashDrop (stash: a ref), fetchFromSource (branch: um dos source.branches do overview, buscado do repositório de origem de um andar isolado por git para dentro do clone dele com o mesmo nome, como a própria folha do host faz; 400 sem um source, ou para um branch que a origem não tem). FileTreeGitResult: ok, output.
FileTreePullRequestContext: floorId?, branch?, bases, base?, isOnBase?, title?, body?, providerName?, account?, supportsDraft, problem?, commitCount?, files?. bases é para onde o pull request pode ir, na ordem do host: o branch que o HEAD do remoto de push nomeia, depois main ou master, depois os outros branches do remoto (como buscados ou enviados por último, sem rede), depois os que só o repositório tem; nunca o próprio branch. base é o que está em pré-visualização, o base pedido ou senão o primeiro de bases, ausente quando não há nenhum. isOnBase marca um repositório no branch para o qual os pull requests dele vão (main, cujo HEAD do remoto é main): problem então diz isso nas palavras do host, e um cliente mostra isso no lugar do formulário. providerName e account são o provedor e a conta que o abrem (nenhum: o site do host do projeto); problem é por que não pode abrir agora. commitCount e files: [{path, additions, deletions, isBinary}], o próprio formato da pré-visualização de aterrissagem, são o que o branch acrescenta à cópia remota de base (um branch local com esse nome quando o remoto não tem um), ausentes sem um base ou quando ele não está lá para comparar. Não há unpushedCommits nem agents: uma pasta não acompanha o pull request depois que ele abre.
Andares
Capability floorManagement, somente para owner. O Novo Andar, Aterrissar Andar, Excluir Andar, Renomear e o Descarregar de um andar do host, a partir de um cliente, através das mesmas operações que suas folhas e sua barra lateral de andares executam. Aterrissar e excluir exigem um andar de verdade: o térreo não tem id e não pode ser aterrissado nem excluído. Descarregar aceita o térreo também, como ground. Uma aterrissagem que precisa de uma pessoa primeiro é recusada com 409 nas próprias palavras do host, que nomeiam os arquivos: trabalho não commitado no andar, trabalho não commitado no térreo, ou um merge que entra em conflito. Um cliente mostra essas palavras e não tenta de novo por conta própria.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/workspaces/{ws}/branches | owner | {isGitRepository, current?, branches, canCopyIndependently?}: os branches locais do projeto e aquele em que o térreo está, para o seletor da folha de Novo Andar, lidos onde o projeto vive (o host, ou a máquina de SSH, Docker ou runtime personalizado do workspace); isGitRepository falso significa que nenhum isolamento é possível, como num sandbox Docker. canCopyIndependently diz que a folha de Novo Andar pode oferecer Cópia independente (o projeto no host, em APFS, com um repositório próprio); ausente em hosts mais antigos |
GET /api/workspaces/{ws}/pull-requests?query= | owner | {repository, pullRequests: [{number, title?, author?, branch?, isDraft?, isFork?, url?, floorName?}]}: os pull requests abertos do projeto para o lado de Pull Request da folha de Novo Andar, listados pelo provedor do host do projeto (pullRequestReview), os mais recentemente ativos primeiro e com um teto (50 para os provedores embutidos). Com query, os abertos que esse host encontra para essas palavras, no lugar do que o teto deixou de fora; vazio quando o provedor não sabe buscar. Um cliente filtra a lista enquanto a pessoa digita e pergunta com query quando a digitação pausa. repository é onde (github.com/acme/app); floorName o andar que já acompanha um, que não pode ser feito duas vezes; isFork um cujo branch o provedor diz estar num fork, que um andar pode ler mas não pode receber push. 409 com o próprio motivo do Maestri quando nada consegue listá-los (nenhum provedor com um comando list, um workspace apontando para o site do host do projeto, nenhum remote); 503 com as palavras do host do projeto quando ele não pôde ser consultado |
POST /api/workspaces/{ws}/floors | owner | {name, branch?, existingBranch?, gitIsolation?, copyGround?, independentCopy?, pullRequest?} → {ok, floorId, branch?, isGitIsolated, isPending?}: o Novo Andar do host, dividido como sua própria folha divide: o nome, o branch e as validações do clone respondem de uma vez, e um clone então copia atrás de um selo pendente (isPending), viajando no feed como pendingFloors: [{id, name, branch, completedItems, totalItems, error?}] até o andar pousar sob o mesmo floorId. Uma cópia que falha deixa o selo com error até DELETE …/floors/pending/{id} dispensá-lo (409 enquanto ainda copia, 404 desconhecido). Com gitIsolation (o padrão) o andar é um git worktree do projeto em branch, feito novo ou, com existingBranch, com checkout como está; um branch ausente é um slug do nome. O worktree é feito onde o projeto vive, no host ou na máquina de SSH, Docker ou runtime personalizado do workspace, e é semeado em copy-on-write a partir dos arquivos do térreo onde esse disco consegue clonar (APFS, btrfs, XFS). Um worktree não pode tomar o branch que o térreo tem em checkout: esse existingBranch é 409. Onde o projeto não pode hospedar um (não é um repositório git) o host faz um andar simples e responde isGitIsolated: false; num workspace sandbox Docker um branch explícito é 400. copyGround inicia o andar com o layout do térreo. independentCopy, onde canCopyIndependently permite, faz uma cópia APFS do projeto inteiro com um repositório próprio, em vez de um worktree. Um andar simples é feito antes da resposta. 409 para um nome ou branch já em uso. pullRequest (pullRequestReview) é um pull request aberto para revisar no andar: o número, #42 ou o link. O host pergunta ao host do projeto sobre ele e traz o branch para o projeto primeiro, depois faz o checkout (branch e existingBranch não se aplicam), e o andar o acompanha como pullRequest com isReview; um name vazio vira "#42" e o título. O andar fica com o próprio branch do pull request só quando o host do projeto confirma que ele está no repositório do projeto e que o commit é o do pull request; caso contrário, uma cópia dos commits (isReadOnly). 400 para um texto que não nomeia nenhum pull request do projeto, 404 para um que não está lá, 409 para um que não está aberto, já está num andar ou sendo feito em um, mudou enquanto era buscado, não pode ser buscado pelo provedor, ou cujo branch local seguiu o próprio caminho ou faz push para outro lugar; 503 quando o host do projeto ou o fetch não pôde ser feito |
GET /api/workspaces/{ws}/floors/{id}/landing?target= | owner | FloorLanding: o que a folha de aterrissagem mostra. branch? é o branch com checkout no clone do andar; groundBranch o próprio do térreo; branches os branches locais do projeto; target o pré-visualizado (target como pedido, senão o próprio do andar); isMerge se aterrissar em target faz merge em vez de só trazer o branch; commitCount e files: [{path, additions, deletions, isBinary}] contra target quando faz merge, senão contra groundBranch; conflicts os caminhos que um merge deixaria em conflito. 400 para um branch que o projeto não tem |
POST /api/workspaces/{ws}/floors/{id}/land | owner | {targetBranch?, deleteBranch?, keepFloor?} → {ok, branch, mergedInto?}. O branch do andar é trazido para o projeto; targetBranch diferente do próprio branch do andar faz merge nele, e deleteBranch então exclui o branch do andar (melhor esforço, como o host faz), exceto num andar worktree que keepFloor preserva, que continua trabalhando nesse branch. Um merge roda no checkout que tem targetBranch em checkout, o térreo ou outro andar, e é recusado ali como qualquer merge que sobrescreveria trabalho não commitado, ou com 409 quando esse checkout está no meio de um merge ou rebase próprio. O andar é então removido com seu branch preservado, a menos que keepFloor. 409 como acima, e também quando o branch pousou mas o clone do andar não pôde ser removido, caso em que o andar permanece e uma exclusão com force o remove; 404 nenhum andar assim; 400 um andar sem clone |
POST /api/workspaces/{ws}/floors/{id}/delete | owner | {deleteBranch, force?} → {ok}: o Excluir Andar da barra lateral, com hooks de teardown e tudo. Um clone que não pode ser removido é 409 com o motivo e o andar intocado; force: true remove o andar mesmo assim, como o próprio aviso do host oferece |
POST /api/workspaces/{ws}/floors/{id}/update | owner | {name?} → {ok}: os campos do andar, como a atualização de workspace aceita os de um workspace; um campo ausente permanece como está. name é o Renomear da barra lateral, sem espaços nas pontas; 400 vazio, 404 nenhum andar assim |
POST /api/workspaces/{ws}/floors/{id|ground}/unload | owner | {ok}: o Descarregar da barra lateral para um andar, o descarregar de workspace (POST …/{ws}/unload) restrito a ele. Seus terminais são parados e seus portais liberam seus recursos, e voltam na próxima visita; os outros andares continuam rodando. 404 nenhum andar assim |
Status do andar e resumos da barra lateral
floorStatus é aditivo a floorManagement; clientes condicionam Definir Status a essa capability e ao papel de owner, não a floorManagement sozinha. Nenhuma mudança de protocolo maior. Hosts mais antigos omitem esses campos opcionais e os clientes mantêm a visão de andares que já tinham; estados de status desconhecidos não podem impedir um workspace ou um feed de ser decodificado.
FloorStatus: {state, text, progress?, author?}. Os estados conhecidos são working, blocked, review, done. text é o relato curto, aparado pelo host e limitado a 120 Swift Characters (clusters de grafema estendidos); um texto mais longo vira os primeiros 119 caracteres mais …. progress é uma fração finita em 0…1, não uma porcentagem. author é o nome do terminal para relatos de agente; edições manuais de status o limpam. Clientes preservam strings de estado desconhecidas, mostram o texto do relato e um marcador neutro de espera, e oferecem só os quatro estados conhecidos ao editar.
FloorPullRequest: {state?, isDraft, number?, attention?, url?, title?, base?, review?, checks?, hasConflicts?, mergeMethods?, recordId?, host?, isReview?, isReadOnly?}, o pull request em cuja revisão o branch do andar está, quem quer que o tenha aberto: o host acompanha os que seus agentes abrem com a CLI do host do projeto e também os próprios. Os estados conhecidos são open, merged, closed; state está ausente enquanto o host não o leu do host do projeto (um aberto no navegador), e number enquanto não o conhece. O marcador do andar o segue, pela regra do host: merged mostra concluído e closed o anel de espera, por cima de qualquer relato; open mostra o relato, ou revisão quando não há um (em andamento para um rascunho); estados ausentes ou desconhecidos deixam o relato em paz. O host limpa o relato do andar quando o pull request fica merged ou closed, porque o relato era sobre um trabalho que o host do projeto encerrou; fora isso o relato nunca é reescrito, e um cliente continua editando-o como antes. attention é a única marca que o chip do host mostra num pull request aberto: conflicts, checksFailing, changesRequested, approved, checksRunning (tolere outros e não desenhe nada para eles). url é a página dele no host do projeto. isReview marca um andar feito para revisar o pull request de alguém (pullRequestReview): um cliente oferece o assess de Perguntar a um Agente e um Concluir Revisão que exclui o andar pela exclusão de sempre. isReadOnly marca um cujo andar tem uma cópia dos commits em vez do branch (um fork, ou um branch que o host do projeto não confirmou ser do projeto): o andar não pode fazer push para ele, então um cliente não oferece Push nem uma correção. Hosts mais antigos omitem o campo, e attention e url antes de pullRequestChecks.
| Rota | Auth | Descrição |
|---|---|---|
POST /api/workspaces/{ws}/floors/{id}/pull-request/check | owner | {ok: true} assim que o host do projeto respondeu; o resultado viaja no feed. O próprio Verificar Pull Request do host, pela mesma fila por andar das verificações periódicas. 404 para um workspace ou andar ausente, ou um andar sem pull request; 503 com as palavras do host do projeto quando ele não pôde ser consultado ou recusou (um limite de taxa entre elas) |
GET /api/workspaces/{ws}/floors/{id}/pull-request/context?base= | owner | {title, body, base, providerName?, account?, supportsDraft, unpushedCommits?, problem?, agents: [{id, name, isRunning}]}: o título e a descrição pré-preenchidos da folha, o provedor e a conta que o abrem (nenhum: o site do host do projeto), por que não pode abrir agora, e os agentes do andar para Perguntar a um Agente (pullRequestLanding) |
POST /api/workspaces/{ws}/floors/{id}/pull-request | owner | {base, title, body, draft?, onWebsite?} → {url?, openedOnWebsite}: faz push e abre como a folha do host faz. Uma página no host do projeto é devolvida para o cliente abrir, nunca aberta no host do Maestri. 502 significa que o provedor recusou depois do push: só então um cliente pode oferecer onWebsite, que precisa do branch no host do projeto (409 caso contrário). 409 para trabalho não commitado, nada novo a propor, um andar que já tem um, ou outra operação no pull request do andar ainda em curso |
DELETE /api/workspaces/{ws}/floors/{id}/pull-request?record= | owner | {ok}: Abrir um Novo Pull Request, esquecendo um fechado ou um aberto no site |
POST /api/workspaces/{ws}/floors/{id}/pull-request/merge | owner | {method, recordId?}, um de mergeMethods → {ok}. 409 para um rascunho, um método que o provedor não oferece agora, ou um pull request que não está aberto |
POST /api/workspaces/{ws}/floors/{id}/pull-request/remove | owner | {confirmExtraWork?, recordId?} → {ok, branch, mergedInto?}: o andar de um pull request com merge removido com o branch preservado, como uma aterrissagem responde. 412 quando o andar tem trabalho que o pull request com merge não tem, até ser confirmado; 409 para trabalho não commitado. Um branch diferente do do andar, sem mergedInto, é onde o trabalho foi guardado, e vale contar à pessoa |
POST /api/workspaces/{ws}/floors/{id}/pull-request/push | owner | {recordId?} → {ok}: os commits novos do andar enviados ao pull request dele |
POST /api/workspaces/{ws}/floors/{id}/pull-request/hand-off | owner | {terminalId, task, note?, recordId?} → {ok}: Perguntar a um Agente. task é review, checks ou conflicts, e precisa ser um problema que o pull request tem agora (409), ou assess num andar feito para revisá-lo (isReview, pullRequestReview): uma primeira revisão, relatada em vez de enviada por push. Num isReadOnly, só assess é aceito, porque uma correção não consegue chegar até ele; o host escreve o prompt da própria folha e o envia àquele agente, que precisa ser um do andar e estar rodando (404, 409) |
Toda requisição sobre um pull request existente pode nomear o registro que viu (recordId, ou record= no DELETE); o host a recusa com 409 quando o pull request do andar já é outro. Abrir, fazer merge, enviar e remover correm um de cada vez por andar, da folha do host ou de qualquer cliente; outro enquanto isso é 409.
Cada entrada de floors no feed, térreo incluído, ganha status, attentionCount, hasActivity e isDormant opcionais. Atenção conta terminais que precisam de atenção naquele andar; atividade significa que pelo menos um dos terminais está trabalhando; dormente significa que o andar tem terminais, mas nenhum rodando. Andares vazios não são dormentes. O branch que o térreo já tinha é o groundFloorBranch do workspace.
WorkspaceMeta, tanto em GET /api/workspaces quanto dentro de um feed, ganha floors: [FloorMeta], pendingFloors: [PendingFloor], groundStatus e groundFloorBranch opcionais. Os resumos usam o mesmo formato do feed, então uma barra lateral pode mostrar os andares de cada workspace sem buscar cada canvas. floors contém o térreo (id ausente ou nulo) primeiro, depois os andares de verdade do mais antigo ao mais novo; pendingFloors vai do mais antigo ao mais novo. Mostre criações pendentes do mais novo ao mais antigo, depois andares de verdade do mais novo ao mais antigo, e então o térreo. Na lista de workspaces, isActive reflete o andar do host; num feed com escopo, reflete o andar visto por aquele feed, como antes. A seleção explícita de andar no telefone continua local e tem precedência ao destacar linhas da barra lateral. Workspaces bloqueados omitem os quatro campos novos de workspace. Arrays ausentes significam indisponível, não uma lista de andares vazia e autoritativa.
| Rota | Auth | Descrição |
|---|---|---|
POST /api/workspaces/{ws}/floors/{id|ground}/status | owner | RemoteSetFloorStatusRequest ({status: FloorStatus} ou {status: null}) → {ok: true}. Substitui o relato inteiro ou o limpa. Não ativa o workspace nem o andar |
A chave status é obrigatória: {} é 400, nunca uma limpeza implícita. Texto vazio ou só com espaços, estados desconhecidos, corpos malformados e progresso fora de 0…1 são 400. O texto é normalizado como acima; um author enviado é ignorado porque a edição é manual. Um workspace ou andar de verdade ausente é 404, dispositivos convidados e workspaces bloqueados são 403, e um gerenciador de workspace que ainda não está pronto é 503. Limpar um andar de verdade ainda verifica que ele existe. O térreo só é endereçado pelo literal ground, nunca por um UUID inventado.
O host chama a mesma operação persistida setFloorStatus da própria folha e marca o feed como sujo; clientes inscritos recebem o snapshot completo coalescido de sempre, com os resumos atualizados. Clientes da lista de workspaces veem isso na próxima atualização. Edições de agente e do host também chegam pelo push periódico de feed que já existe. Um editor preserva o progresso enquanto o estado não muda, descarta-o quando o estado muda, e mantém a folha aberta numa escrita recusada.
Hooks de andar
Capability floorHooks, somente para owner: comandos de setup, run e teardown, lidos e substituídos por inteiro, e executados ou parados em um andar. Só o host os executa.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/workspaces/{ws}/hooks | owner | {hooks: {isEnabled, autoRunSetup, setup, run, teardown: [{id, command, isEnabled}]}, runs: [{commandId, floorId?, isRunning, exitCode?, finishedAt?}]}: os hooks de andar do workspace como o host os guarda, e toda execução que o host ainda lembra em seus andares |
PUT /api/workspaces/{ws}/hooks | owner | o formato hooks acima → {ok}: substituído por inteiro, como a folha do host os grava; os comandos mantêm seus ids |
POST /api/workspaces/{ws}/floors/{id}/hooks | owner | {action: run | stop | runSection, commandId?, section?} → {ok}: o botão de hooks do host naquele andar: um comando executado ou parado, ou os comandos habilitados de uma seção executados; 404 andar ou comando desconhecido |
Rotinas
Capability routines, somente para owner. Os prompts e lembretes agendados do host, através do mesmo agendador que sua própria janela usa. Global, como o host os guarda: uma rotina nomeia um workspace e, quando é um comando em vez de um lembrete, um terminal nele.
| Rota | Auth | Descrição |
|---|---|---|
GET /api/routines | owner | {routines: [Routine], history: [FireRecord], targets: [{id, name, icon, workspaceId, floorId?, floorName?}]}: toda rotina, o histórico de execuções do mais antigo ao mais novo, e todo terminal que uma rotina pode ter como alvo, por workspace e andar como o seletor do host os lista |
POST /api/routines | owner | Routine → {ok}: criada sob o id com que foi enviada. 400 nome vazio, um lembrete sem notas, 404 workspace desconhecido ou um terminal que não está nele, 409 o id já existe |
PUT /api/routines/{id} | owner | Routine → {ok}: substituída por inteiro, como a folha do host salva uma; o host mantém seus próprios contadores de disparo e os reinicia quando o horário muda. 400/404 como na criação |
DELETE /api/routines/{id} | owner | {ok}; 404 desconhecida |
POST /api/routines/{id}/action | owner | {action: run | enable | disable} → {ok}: o interruptor da lista, e o Executar Agora |
DELETE /api/routines/history | owner | {ok}: o histórico de execuções é limpo; as rotinas ficam intocadas |
Routine: id, name, workspaceId, terminalId?, prompt, preRunScript?, skipIfBusy, schedule, endRepeat, notifyOnFire, isEnabled, fireCount, lastFiredAt?, createdAt. schedule é {kind: once, at}, {kind: interval, seconds}, {kind: daily, hour, minute} ou {kind: weekly, weekdays: [1…7, domingo é 1], hour, minute}; daily e weekly são horários de relógio de parede no host. endRepeat é {kind: never}, {kind: afterCount, count} ou {kind: onDate, date}. createdAt é o início do agendamento para uma rotina que se repete. Uma rotina sem terminalId é um lembrete: ela notifica e não roda nenhum comando. fireCount e lastFiredAt são do host e são ignorados em uma escrita.
FireRecord: id, routineId, routineName, prompt, firedAt, outcome, terminalName?; outcome é sentToTerminal, terminalNotRunning, reminder, skippedByScript ou skippedWhileBusy.
Receitas
Cada uma destas é uma integração completa. HOST é o endereço, TOKEN o token de dispositivo obtido no pareamento, e --insecure está no lugar de fixar a chave.
Parear um script
# O código de seis dígitos da aba Pareamento, ou a senha da aba Manual
curl --insecure -X POST https://HOST:7434/pair \
-H 'Content-Type: application/json' \
-d '{"deviceName":"Ponte de atenção","code":"483920"}'
# → {"token":"…","deviceId":"…","deviceName":"Ponte de atenção","protocolVersion":1,"role":"owner"}
Guarde o token. Depois rebaixe o pareamento para Somente leitura na aba Dispositivos, se ele só precisa observar.
Saber quando um agente precisa de você, e ir até lá
Faça polling na lista de workspaces; attentionCount diz quantos terminais estão esperando. Quando não for zero, leia o feed para descobrir quais, e então mande a mesa até lá.
# 1. Tem algo esperando?
curl --insecure -H "Authorization: Bearer TOKEN" https://HOST:7434/api/workspaces
# → workspaces[].attentionCount, hasActivity, runningTerminalCount
# 2. Qual terminal (items[].terminal.needsAttention), e o que ele disse por último (preview)
curl --insecure -H "Authorization: Bearer TOKEN" https://HOST:7434/api/workspaces/WS/feed
# 3. Ao apertar um botão: leve o host até ele, como o clique na notificação faz
curl --insecure -X POST -H "Authorization: Bearer TOKEN" https://HOST:7434/api/terminals/TERMINAL_ID/focus
Os mesmos campos alimentam uma luz de status (hasActivity, attentionCount) ou um botão que responde a um prompt (approve / reject no terminal que um item pendingPrompt nomeia). Para evitar polling, mantenha o socket do feed aberto; um snapshot chega sempre que algo muda.
Enviar um prompt para um agente
curl --insecure -X POST -H "Authorization: Bearer TOKEN" \
-H 'Content-Type: application/json' \
https://HOST:7434/api/terminals/TERMINAL_ID/prompt \
-d '{"text":"Rode os testes e me diga o que falhou."}'
Para anexar um arquivo, prepare-o antes (POST …/attachments?filename=… com os bytes) e envie segments: [{"text": "…"}, {"attachmentId": "…"}]; o host entrega o anexo do mesmo jeito que o Compositor de Prompts dele faz.
Assistir a um terminal em um navegador
Abra wss://HOST:7434/api/terminals/TERMINAL_ID/stream?token=TOKEN, entregue cada frame binário ao xterm.js, resete o emulador a cada mensagem geometry, e envie {"type":"resize","cols":…,"rows":…} com a sua própria grade. Envie {"type":"input","data":"…"} para digitar.
Um cliente Python mínimo
import json, ssl, urllib.request
HOST, TOKEN = "https://192.168.1.20:7434", "…"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE # fixe a chave de segurança em qualquer coisa que você mantiver
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(HOST + path, data=data, method=method)
req.add_header("Authorization", f"Bearer {TOKEN}")
if data: req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, context=ctx) as r:
return json.load(r)
info = call("GET", "/api/info")
assert info["protocolVersion"] == 1
for ws in call("GET", "/api/workspaces")["workspaces"]:
if ws["attentionCount"]:
feed = call("GET", f"/api/workspaces/{ws['id']}/feed")
for item in feed["items"]:
t = item.get("terminal")
if t and t["needsAttention"]:
print(ws["name"], "→", t["name"], "|", " ".join(t["preview"][-2:]))
Versionamento
protocolVersionmarca uma mudança incompatível. Um cliente deve recusar um host com uma versão diferente.- Adições são sinalizadas por capabilities e por campos opcionais. Um cliente precisa tolerar campos desconhecidos, valores de enum desconhecidos, tipos de item de feed desconhecidos, tipos de mutação desconhecidos e tipos de mensagem de socket desconhecidos.
- As requisições são rígidas: valores de enum desconhecidos no corpo de uma requisição são recusados com
400.
Warning
Tudo que um dispositivo pareado pode fazer, um token vazado também pode, até ser revogado na aba Dispositivos ou expirar por 30 dias de inatividade. Mantenha tokens fora do controle de versão, prefira pareamentos Somente leitura para integrações que só observam, e lembre que o host notifica a cada novo pareamento, então um pareamento inesperado é visível.