Saltar para o conteúdo

Multijogador — identidade, pertença e presença

O Control Center funciona para um único operador, mas o modelo subjacente é multiator desde o início. Humanos e agentes trocam mensagens nos mesmos espaços, editam os mesmos tickets e aparecem na mesma lista de participantes. Tornar isto seguro e compreensível resume-se a três decisões: quem conta como ator, o que condiciona o acesso e que estado é partilhado de forma durável e qual é apenas difundido em direto.

Principals: utilizadores e agentes são atores em pé de igualdade

Secção intitulada “Principals: utilizadores e agentes são atores em pé de igualdade”

Tudo o que regista “quem fez isto” — mensagens, tickets, revisões, planos, registos de execução, entradas de auditoria — resolve-se através de um Principal, uma união selada de UserPrincipal e AgentPrincipal com uma forma compacta no protocolo (user:<id> / agent:<id>). A atribuição nunca colapsa N humanos (ou N agentes) numa única conta sentinela, pelo que uma revisão deixada por um agente e uma revisão deixada por uma pessoa são distinguíveis onde quer que apareçam.

Como o principal é a unidade de identidade, é também a unidade de contenção:

  • Os limites de taxa são por principal, não por sessão. A proteção contra inundação nas chamadas remotas de ferramentas é orçamentada por utilizador, não por ligação: todos os dispositivos de um membro consomem a mesma janela deslizante partilhada (120 chamadas por minuto, das quais no máximo 30 podem alterar estado), pelo que abrir três dispositivos não triplica a quota de ninguém e um membro ruidoso não deixa os restantes sem quota.
  • Anular é por principal. O diário de ações de cada cliente regista apenas as mutações que o seu próprio principal realizou. Pode anular com ⌘Z a sua própria edição de ticket; não pode anular a de um colega — quando a vê, o canal durável já a resolveu (ver abaixo).

A pertença é a fronteira de acesso — não a chave de emparelhamento

Secção intitulada “A pertença é a fronteira de acesso — não a chave de emparelhamento”

Duas credenciais separadas respondem a duas perguntas separadas. A chave de emparelhamento (ver Controlo remoto e telemóvel) autentica um dispositivo no servidor. A pertença autoriza o utilizador desse dispositivo a agir dentro de um espaço de trabalho. Um dispositivo emparelhado cujo utilizador não tem pertença num espaço de trabalho não o consegue ler nem alterar.

A pertença é uma linha por (espaço de trabalho, utilizador), guardada no ficheiro de base de dados do próprio espaço de trabalho — pelo que “quem está neste espaço de trabalho?” é uma leitura corrente limitada ao âmbito, enquanto a pergunta inversa, “em que espaços de trabalho estou?”, atravessa necessariamente ficheiros e é um dos poucos fan-outs sancionados entre espaços de trabalho. É o que o seletor de espaços de trabalho executa antes de qualquer espaço ter sido escolhido.

Cada linha transporta um papel de uma hierarquia de cinco níveis, ordenada por precedência:

Papel Pode
owner Controlo total, incluindo a eliminação do espaço de trabalho; exatamente um por espaço de trabalho
admin Gerir membros, convites, definições e operações destrutivas
member Leitura e escrita do dia a dia: mensagens, tickets, execuções de agente
viewer Só de leitura em todo o espaço de trabalho; os globs de exclusão de segredos ocultam os caminhos correspondentes nas superfícies que contêm código
guest O mesmo nível mínimo de só de leitura do observador, no posto mais baixo da hierarquia

Todas as decisões de acesso resumem-se a “o papel de quem chama é, no mínimo, X”. O nível mínimo é derivado do tipo de operação, a menos que esta declare um explicitamente: uma leitura exige guest, uma mutação exige member, uma operação destrutiva exige admin. As ACLs por objeto, para além das concessões por repositório abaixo descritas, ficam deliberadamente fora do âmbito.

Os convites são a forma de conceder pertença, em Definições → Espaço de trabalho → Membros. Um admin emite um código de uso único (como ligação ou QR) que expira ao fim de sete dias por predefinição; o servidor guarda apenas o hash SHA-256 do código, e o próprio código é mostrado exatamente uma vez. Ao resgatá-lo, o utilizador é aprovisionado de imediato, a pertença é registada com o papel do convite (nunca owner — a propriedade é transferida explicitamente, nunca concedida por convite) e passa-se ao emparelhamento do dispositivo. As credenciais de dispositivo emitidas após o emparelhamento têm elas próprias um prazo de 30 dias, pelo que um emparelhamento não é uma chave permanente.

As concessões por repo ficam abaixo da pertença. Como o servidor mantém checkouts completos, a pertença a um espaço de trabalho, por si só, nunca pode conferir mais privilégios do que o host de código: todas as superfícies que contêm código — ficheiros, diffs, revisão de PR, pesquisa de código — verificam a concessão do membro nesse repo. As concessões são por (espaço de trabalho, utilizador, repo) e classificadas como none → read → review → write, em que write significa conduzir execuções de agente que escrevem no repo. A ausência de uma linha significa none; os proprietários e os admins detêm implicitamente write em todos os repos ligados e não têm linha alguma.

O Control Center divide o estado partilhado em dois canais com regras de durabilidade opostas, porque os dois tipos de estado falham de formas opostas.

A presença — quem está online, onde está, o que está a escrever, o que um agente está a fazer neste instante — não vale nada no momento em que fica desatualizada. Se fosse persistida, todas as reconexões teriam de reconciliar “a base de dados diz que o Sam está a editar este ticket” com a realidade. Por isso, a presença nunca é escrita na base de dados. Vive num hub de presença em memória no servidor: os clientes publicam com presence.update, os pares subscrevem com presence.watch e as entradas expiram automaticamente ao fim de três sinais de atividade perdidos (os sinais de atividade são enviados a cada 10 segundos; o hub faz uma varredura a cada 5). Não há nada para reconciliar — apenas para deixar caducar.

Duas propriedades mantêm o canal fiável:

  • A identidade vem da sessão, nunca dos argumentos do cliente. O servidor mapeia o dispositivo autenticado da ligação para um id de utilizador; um cliente só pode atualizar a sua própria presença, pelo que ninguém consegue falsificar o estado ou o locus de outro participante. Uma atualização offline explícita (desligação da ligação ou o comutador de invisibilidade) remove a entrada de imediato.
  • A lista de participantes é centralizada no servidor. Os pares nunca trocam presença diretamente, pelo que todos os participantes veem a mesma lista, independentemente da topologia. Os agentes não têm cliente, pelo que o servidor sintetiza as suas entradas a partir do estado das execuções em direto — a pensar, a executar, bloqueado num portão de aprovação, concluído — com uma única execução representativa a decidir o estado e o locus de um agente, para que execuções simultâneas não façam a lista oscilar.

O fan-out é escalonado por consumidor: o computador e a web usam um nível completo (no máximo uma emissão da lista a cada 100 ms), o telemóvel um nível de resumo (a cada 500 ms), para que as atualizações à cadência do cursor não derretam a PWA.

O locus de um participante — onde está neste momento, até ao nível do artefacto — é uma união selada: Space, File (repo + caminho + linha opcional), Pr, Ticket, PlanNode. Nem todos os locus têm um destino para onde saltar: não existe uma rota de visualização de ficheiros, pelo que um locus File é apresentado como etiqueta em vez de destino. O canal também transporta reivindicações leves (“o Sam está a editar este ticket”, “um agente detém este worktree”): visibilidade de conflitos, não bloqueios. Os tipos de locus desconhecidos são interpretados como null, pelo que um cliente antigo simplesmente não apresenta o que não compreende.

Os registos que têm de sobreviver — tickets, o documento de Notas partilhado do espaço, mensagens — seguem o caminho oposto: autoritativos, ordenados e persistidos. As edições simultâneas resolvem-se por última escrita vence (LWW) pela ordem de receção no servidor, nunca por relógios de cliente. A resolução é deliberadamente granular: as edições de tickets são por coluna, pelo que duas pessoas a editar campos diferentes do mesmo ticket são ambas aplicadas e nenhuma sobrepõe a outra (as transições de fluxo de trabalho mantêm, em vez disso, o bloqueio otimista); o documento de Notas do espaço é uma única coluna, pelo que o documento inteiro é LWW, com as reivindicações leves no canal de presença a tornar a edição simultânea visível em vez de bloqueada. Deliberadamente, não é um CRDT — o ónus da prova recai sobre o CRDT, e o LWW por ordem de receção mais reivindicações visíveis cobre os padrões reais de colaboração.

“Último a escrever” significa ordem de receção no servidor, nunca um relógio de cliente — um portátil com o relógio desalinhado não consegue ganhar uma edição que perdeu. Por trás do canal durável está o feed de sincronização: os disparadores do SQLite acrescentam cada mutação a um registo de alterações por espaço de trabalho dentro da transação de escrita, atribuindo um número de sequência monotónico por espaço de trabalho na mesma transação da própria mutação. Um serviço do servidor acompanha esse registo e transforma-o em frames delta versionados, pela ordem da sequência. Os clientes evoluem de instantâneos completos para deltas; uma lacuna na sequência faz esse armazenamento recuar para o modo de instantâneo em vez de aplicar frames incorretamente.

Seguir, orientar, assumir o controlo, devolver o controlo

Secção intitulada “Seguir, orientar, assumir o controlo, devolver o controlo”

O canal de presença não é apenas uma lista — comanda os verbos de colaboração:

  • Modo de seguimento. Clique num participante e o seu cliente acompanha o locus dele, navegando à medida que ele navega. O seguimento dura “até agir”: no momento em que navega pelos seus próprios meios, o seguimento desliga-se. É apresentado um banner dispensável quando outro participante está a destacar (apresentar) um espaço em que está.
  • Orientar. Uma mensagem para um agente em execução é colocada em fila como orientação; o agente volta a lê-la antes de retomar o turno seguinte, pelo que pode dar um empurrão a uma execução a meio sem a matar.
  • Assumir o controlo. Assumir o controlo de uma conversa pausa todas as execuções ativas no limite do turno seguinte em estado limpo (uma execução do Claude Code, que não tem um limite seguro, é interrompida) com um instantâneo do git por turno, pelo que a assunção é reversível. Um marcador durável bloqueia novos despachos para o espaço e sobrevive a um reinício do servidor — o servidor nunca retoma automaticamente para uma edição inacabada de uma pessoa. Um editor em direto por worktree é uma reivindicação leve: uma segunda assunção do controlo é visível e recusada, não silenciosamente fundida.
  • Devolver o controlo. Devolver o controlo publica no espaço um resumo estruturado do diff do worktree, coloca-o em fila como orientação para as execuções em pausa, retoma-as e limpa o marcador.

A orientação e a assunção do controlo são intervenções feitas a posteriori. O dial de autonomia é a definição permanente: por espaço, por agente, definida no cabeçalho do espaço, decide o que acontece quando um agente chega a uma ação sujeita a portão.

Nível Valor no protocolo Efeito numa chamada de ferramenta sujeita a portão
Somente propor proposeOnly Recusada de imediato, com um motivo que o agente deve transmitir — pode descrever a ação, mas não executá-la
Agir com aprovação actWithApproval O portão de aprovação que nega por omissão: é apresentada uma confirmação a um operador ligado e, sem aprovador, a ação é recusada
Agir livremente actFreely Pré-aprovada, exceto quando se aplica uma regra deny rígida das barreiras — uma regra de negação vence sempre
(não definido) null O mesmo que agir com aprovação: a predefinição do servidor

Há duas coisas que ficam fora do dial, em ambas as direções. O verbo de saída de um modo é fixo e nunca pode ser negado, diga o que disser o dial — o modo orchestrate chegou a ser estruturalmente incapaz de emitir o seu único entregável porque uma predefinição negava a classe de efeitos que esse verbo declara. E um deny rígido proveniente das barreiras de ação não pode ser sobreposto por actFreely.

Tudo isto é aditivo. O fluxo de trabalho de um utilizador individual não muda: com uma única pessoa e nenhum agente, o hub de presença não tem nada para difundir, pelo que o canal emite o seu instantâneo inicial e depois fica inativo, com tráfego zero; a lógica de LWW e de sincronização é o mesmo caminho de escritor único de sempre; e a chave de emparelhamento continua a ser a credencial de dispositivo que sempre foi. A pertença já é a fronteira de acesso — com uma única pessoa existe uma só linha de proprietário e nenhum elemento de interface da lista.