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 Host precisa ser um IP literal, localhost, um nome .local ou um nome .ts.net; qualquer outra coisa responde 403. 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 GET podem 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.

CapabilityO que significa
feedSnapshotsGET …/feed e o socket do feed existem
terminalStreamingo socket do terminal existe
terminalInputByteso socket do terminal aceita inputBytes e frames binários
terminalInputTexto socket do terminal aceita input (texto UTF-8)
terminalResizeo socket do terminal honra resize (limitado a 20…250 colunas por 5…120 linhas)
promptSegmentsPOST …/prompt aceita segments
attachmentStaging, attachmentDiscardas rotas de anexo existem
mentionCatalogGET …/mentions existe
terminalThemescards de terminal carregam theme
nativeNotesrotas de nota endereçadas por nó, com revision / ifRevision
pairingCodespareamento por código de seis dígitos
canvasMirroringo snapshot do feed carrega um canvas
canvasWritestodas 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
noteStackWritesas rotas da trilha do fichário existem
noteStackPagespáginas de fichário podem ser adicionadas e removidas
noteStackFilingfichá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
deviceRolesos papéis são aplicados e /api/info informa o de quem chamou
drawingWritesdesenhos podem ser adicionados, movidos e apagados
workspaceActionsas rotas de fixar, renomear, acordar, descarregar e limpar atenção existem; os metadados do workspace carregam workingDirectory
workspaceManagemento 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
terminalDraftsPOST …/terminals aceita os campos da folha de Novo Terminal
roleManagementas rotas de responsabilidade de agente existem; as configurações do terminal carregam icon, color, monitorActivity
nodeGroupso snapshot carrega groups, os nós carregam groupId, e as rotas de grupo existem
partiturasa biblioteca de partituras do host pode ser listada, pré-visualizada e estampada em um andar
nodeBatchDeletePOST …/nodes/delete remove uma seleção em uma única requisição
canvasFilesPOST …/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
deviceRenamingPUT /api/devices/{id}/name renomeia este dispositivo, ou outro dispositivo se você for owner
presenceo socket do feed aceita presence; o host desenha um cursor para o dispositivo no seu canvas
textStylingPOST …/nodes/{id}/text aceita as opções da barra de texto junto com o texto
nodeUnloadterminais e portais podem ser descarregados; restart e reload os acordam
terminalFocusPOST …/terminals/{id}/focus existe: o host vai até o terminal como faz ao clicar na própria notificação
cameraControlo 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
nodeFocusPOST …/nodes/{id}/focus existe: o revelar do host para qualquer nó do canvas
terminalFileMentionsGET …/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
fileTreesas 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
cableTiesas 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})
routinesas 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
floorHooksos 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
floorManagemento 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

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": "…"}}, com code sendo um de invalidRequest, unauthorized, forbidden, notFound, conflict, preconditionFailed, payloadTooLarge, rateLimited, internalError, unavailable, unknown. O status HTTP carrega a mesma informação.
  • Ids de caminho que não são UUIDs respondem 400; caminhos que não casam com nada respondem 404.
  • 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

RotaAuthDescrição
GET /api/workspacesleitura{"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=…leituraum FeedSnapshot
GET /api/workspaces/{ws}/iconleituraa imagem personalizada do workspace em PNG, com um ETag; 304 em If-None-Match, 404 quando o ícone é um glifo
POST /api/workspaces/{ws}/activateescritatorna-o o workspace ativo do host (404 desconhecido, 409 bloqueado pela licença)
POST /api/workspaces/{ws}/floors/{floor|ground}/activateescritatroca também o andar ativo do host
POST /api/workspaces/{ws}/pinescrita{pinned}; idempotente
POST /api/workspaces/{ws}/renameescrita{name}; 400 vazio
POST /api/workspaces/{ws}/wakeescritainicia os terminais do workspace sem ativá-lo no host
POST /api/workspaces/{ws}/unloadescritapara todos os terminais do workspace
POST /api/workspaces/{ws}/attention/clearescritalimpa o badge de atenção do workspace
POST /api/workspacesescrita{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}/updateescrita{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 local com andares isolados por git é movido para um ambiente
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=ownerno 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=…leiturasnapshots 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: [ { id?: UUID, name, color?, isActive, isGitIsolated?, branch?, isCloneMissing? } ],
  items: [ FeedItem ],
  canvas: CanvasSnapshot,
  epoch: UUID
}

isGitIsolated e branch (floorManagement) estão presentes assim que um andar é criado: se ele roda em seu próprio clone copy-on-write, e o branch dado checkout lá. isCloneMissing é verdadeiro quando a pasta desse clone sumiu do disco.

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.

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/n
  • note: { 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?.

  • 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.
  • preview sã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. isRunning diz que existe um processo; isLive, que o host tem o terminal carregado.
  • isUnloaded é o Descarregar do host: o terminal colocado para dormir manualmente até que POST …/restart o 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: TerminalCard
  • note: NoteCard
  • text {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 tem
  • file {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}}. device presente 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, ipados ou android. runtime é o que o nó do host mostra agora: uma página é loading, ready, failed ou empty; um dispositivo é idle, booting, connecting, live ou unavailable, com a frase do próprio host em runtimeDetail. Só ready e live tê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?}: a plaquinha que representa um terminal em outro andar
  • noteStack {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), 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; as rotas de conectar e desconectar não aceitam essas pontas. 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 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.

RotaCorpoResposta
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?, 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
POST / DELETE /api/workspaces/{ws}/connections{fromNodeId, toNodeId, mutationId?}{ok}; 400 para um par que o canvas não liga, ou entre andares
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?}]} para a rota de criação de terminal. 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.

Notas

RotaAuthDescrição
GET /api/workspaces/{ws}/nodes/{id}/noteleituraNoteContent
PUT /api/workspaces/{ws}/nodes/{id}/noteescrita{text, ifRevision?}NoteContent; 409 conteúdo travado, 412 revisão divergente (o corpo é o NoteContent atual)
PUT /api/workspaces/{ws}/nodes/{id}/note/lockescrita{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}/fileleituraos bytes crus de um nó de arquivo; honra Range (206, 416)
GET /api/workspaces/{ws}/nodes/{id}/file/previewleituraum 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

RotaAuthDescrição
WS /api/terminals/{id}/stream?token=…leitura (entrada exige Controle total)o stream do terminal, abaixo
POST /api/terminals/{id}/promptescrita{text} ou {segments: [{text} | {attachmentId}]}; 409 não está rodando
POST /api/terminals/{id}/attachments?filename=…escritacorpo 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}/mentionsleitura{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 / rejectescritaresponde a um prompt S/n pendente; 409 quando não há nenhum
POST /api/terminals/{id}/seenescritamarca o terminal como visto, limpando a atenção dele nas duas superfícies
POST /api/terminals/{id}/restartescritao botão de recarregar do host; acorda um terminal descarregado
POST /api/terminals/{id}/unloadescritao Descarregar do host (nodeUnload): a sessão desmontada até virar seu card dormente, com o scrollback preservado, até um restart
POST /api/terminals/{id}/focusescritao 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}/killescritaencerra o processo
GET / PUT /api/terminals/{id}/settingsleitura / escrita{command, workingDirectory, roleId?, roles: [{id, name, icon, color}], maestroMode, icon?, color?, monitorActivity?}; o PUT aceita {command, roleId?, maestroMode, icon?, color?, monitorActivity?} como substituição completa; uma responsabilidade alterada reinicia um terminal em execução

Mention: kind, id, name, tokenName?, icon?, color?, platform?, floorId?, floorName, isOnActiveFloor, depth, section, sectionOrder, order.

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).

RotaAuthDescrição
GET /api/rolesleitura{roles: [Role]}
POST /api/rolesescritaRoleWriteRole como foi guardada
PUT /api/roles/{id}escritaRoleWrite, 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.

Portais

RotaAuthDescrição
GET /api/portals/{id}/snapshotleituraimage/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}/reloadescritao 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 / forwardescritahistórico, para um portal web
POST /api/portals/{id}/navigateescrita{url}
POST /api/portals/{id}/unloadescritao 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

RotaAuthDescrição
GET /api/devicesleitura{devices: [{id, name, lastSeenIP?, lastSeenAt?, createdAt, role?}]}
DELETE /api/devices/{id}escritarevoga; um dispositivo pode revogar a si mesmo
PUT /api/devices/{id}/roleescrita{role: "owner"|"guest"}; 409 quando um dispositivo aponta para si mesmo com guest
PUT /api/devices/{id}/nameleitura (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.

RotaAuthDescrição
GET /api/partiturasleitura{partituras: [Partitura]}
GET /api/partituras/{id}/preview?appearance=light|darkleituraa miniatura do próprio host em JPEG, com um ETag
POST /api/workspaces/{ws}/partiturasescrita{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}].

Á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.

RotaAuthDescrição
GET /api/workspaces/{ws}/nodes/{id}/tree?path=&hidden=ownerFileTreeListing 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=owneros 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/changesownerFileTreeChanges 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=ownerFileTreeDiff: 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=ownerFileTreeSearch: 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=ownerprefix (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/gitownerFileTreeGitOverview: 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/gitownerFileTreeGitRequestFileTreeGitResult. 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

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}]. remoteBranches são branches de rastreamento remoto sem correspondente local, mostrados como origin/x; fazer checkout de um cria o branch local.

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). FileTreeGitResult: ok, output.

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.

RotaAuthDescrição
GET /api/workspaces/{ws}/branchesowner{isGitRepository, current?, branches}: os branches locais do projeto e aquele em que o térreo está, para o seletor da folha de Novo Andar; isGitRepository falso significa que nenhum isolamento é possível
POST /api/workspaces/{ws}/floorsowner{name, branch?, existingBranch?, gitIsolation?, copyGround?}{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 clone copy-on-write do projeto em branch, feito novo ou, com existingBranch, com checkout como está; um branch ausente é um slug do nome. Onde o projeto não pode hospedar um clone (não é um repositório git, não está em APFS) o host faz um andar simples e responde isGitIsolated: false; em um workspace em outro ambiente um branch explícito é 400. copyGround inicia o andar com o layout do térreo. Um andar simples é feito antes da resposta. 409 para um nome ou branch já em uso
GET /api/workspaces/{ws}/floors/{id}/landing?target=ownerFloorLanding: 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}/landowner{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). 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}/deleteowner{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}/updateowner{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}/unloadowner{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

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.

RotaAuthDescrição
GET /api/workspaces/{ws}/hooksowner{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}/hooksownero 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}/hooksowner{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.

RotaAuthDescrição
GET /api/routinesowner{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/routinesownerRoutine{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}ownerRoutine{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}/actionowner{action: run | enable | disable}{ok}: o interruptor da lista, e o Executar Agora
DELETE /api/routines/historyowner{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

  • protocolVersion marca 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.