Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Como a stack se encaixa

Este capítulo explica a forma do sistema: qual peça faz o quê, por que as peças são separadas, e onde fica de fato a fronteira de segurança. Leia isto uma vez antes de qualquer outra coisa desta seção; todo capítulo posterior parte dele.

O problema que essa forma resolve

Um agente de IA lê e escreve arquivos, roda comandos e mantém memória de longa duração, tudo guiado por linguagem natural que ele não escreveu. Rode um agente para várias pessoas num processo só e basta uma injeção de prompt, um bug de path traversal ou uma ferramenta que vaza para que uma pessoa alcance as conversas, os arquivos e os segredos de outra.

Por isso a stack não compartilha um agente. Cada usuário ganha o próprio container, com o próprio diretório em disco, e a coisa que decide quem você é não é a mesma coisa que roda o seu agente.

Três camadas

A stack são três camadas, cada uma com exatamente um trabalho.

   your browser
        |
        v
+--------------------------------------------+
|  1. EDGE  --  mycelium gateway             |   the only thing exposed
|     authenticates, enforces RBAC,          |
|     injects a verified account profile     |
+--------------------------------------------+
        |  x-mycelium-service-name: alpha
        |  profile (accId, tenant, subscription)
        v
+--------------------------------------------+
|  2. ORCHESTRATION  --  crab-shell-proxy    |   holds the Docker socket
|     resolves (tenant, subscription,        |   runs as root
|     agent, user), starts that user's       |
|     container, proxies the turn            |
+--------------------------------------------+
        |  docker.sock            ^
        v                         |  HTTP / WebSocket on zombie_net
+--------------------------------------------+
|  3. AGENT  --  one harness container       |   non-root, one per user
|     per (tenant, subscription, agent,      |   own volume, own memory
|     user); ganglion or picoclaw            |
+--------------------------------------------+

Ao lado dessas três, mais dois serviços rodam mas não estão no caminho da requisição: chat-webapp, a UI de chat voltada ao membro, e harness-sphere, o watcher.

1. Mycelium, a borda

Mycelium é um gateway de API externo, desenvolvido separadamente deste projeto e construído ou baixado como parte da stack. É a única entrada para a API dos agentes: toda requisição que chega ao crab-shell-proxy passou por ele, e o proxy não confia em mais nada sobre quem está chamando. Ele verifica o token de quem chama, aplica o controle de acesso por papel e injeta um perfil de conta verificado na requisição antes de encaminhá-la.

O cliente de chat e a própria UI de administração do mycelium publicam portas próprias, porque são aplicações de navegador que uma pessoa abre. Isso não é uma segunda porta para os agentes — os dois chamam o gateway como qualquer outro cliente.

Essa última palavra é o ponto. Quem chama nunca diz ao proxy quem é; o mycelium diz ao proxy, no servidor, e a identidade flui de cima para baixo, a partir de uma fonte confiável, em vez de de baixo para cima, a partir do corpo da requisição. As rotas são protegidas por papel, então uma conta precisa ter o papel de convidado correspondente para sequer alcançar um agente.

Um tenant é uma organização no mycelium. Uma subscription é uma conta dentro de um tenant para a qual os membros são convidados. O par, mais o id de conta do próprio membro, é o que faz o agente de uma pessoa ser distinto do de outra.

O gateway do mycelium também expõe um endpoint JSON-RPC em POST /_adm/rpc que o chat webapp usa para operações de identidade e de participação. As requisições roteadas pelo crab-shell-proxy são a REST API do próprio proxy e são uma superfície separada.

2. crab-shell-proxy, o orquestrador

O proxy lê no nome de serviço injetado qual agente foi endereçado e no id de conta do perfil qual usuário está chamando, depois garante que o container daquele usuário está rodando — iniciando-o sob demanda, parando-o quando ocioso — e encaminha o turno.

Sua unidade de isolamento é uma chave de quatro partes: tenant, subscription, papel (a chave do agente, como alpha) e id de conta do usuário. Essa tupla é WorkspaceKey em internal/docker/manager.go, e ela nomeia tanto um diretório em disco quanto um container.

O nome do container é <prefix>-<role>-<hash>, onde o hash é um SHA-256 sobre os ids de tenant, subscription e usuário. A tupla completa carrega dois UUIDs e passaria do limite de 63 caracteres de um rótulo DNS, o que deixaria o container inacessível pelo próprio nome na rede Docker — então a identidade fica nos rótulos do container e em um marcador .crab-owner.json no diretório do usuário, não no nome.

3. O agente, atrás de um contrato de harness

A terceira camada não é um programa só. É um harness: um runtime de agente atrás de um contrato fixo, escolhido por agente. Dois são suportados — crab-ganglion-harness, que este projeto escreveu, e picoclaw, com o qual ele começou e que agora está sendo descontinuado. Qual deles um agente roda é declarado no config.yaml do proxy. Veja Harnesses para a escolha e suas consequências.

Quem detém o socket do Docker

Este é todo o argumento de segurança, então ele ganha uma seção própria.

O crab-shell-proxy detém o socket do Docker e roda como root. É o único componente que faz isso. O socket do Docker é o daemon do host: quem consegue escrever nele consegue iniciar, parar e executar comandos dentro de qualquer container, e daí chegar ao root do host. Isso faz do proxy a peça mais privilegiada da stack e seu plano de controle confiável.

Tudo que o proxy cria é o oposto. Os containers de agente rodam com um uid não-root (picoclawUser: "1000:1000" no config.yaml do proxy), ganham seus próprios namespaces de processo, rede e montagem, e ganham um bind do próprio diretório e de mais nada. Se o agente de um usuário for totalmente comprometido — levado por injeção de prompt a rodar código hostil — ele ainda assim não consegue ler os arquivos, a memória ou as conversas de outro usuário. Container diferente, diretório diferente, nenhuma superfície compartilhada. O isolamento é imposto pelo kernel, não por código de aplicação decidindo o que mostrar a quem.

O harness-sphere, o observador da stack, nunca recebe um socket do Docker, e isso é uma regra permanente e deliberada, não um descuido. Ele roda como root para percorrer a árvore de tenants criada pelo proxy, e três restrições mantêm isso estreito: seu bind /data é somente leitura, ele não publica portas e não recebe socket. Um segundo detentor de socket dobraria o raio de alcance do pior comprometimento da stack. Aquilo para que ele precisaria do socket — atribuir um container ao seu tenant — é atendido em vez disso por GET /v1/instances no proxy, atrás de um token próprio.

A stack é afinada para ser fácil de ler e de rodar localmente, não para ser endurecida. Antes de expô-la, isole o socket (um proxy de socket restrito, ou um host dedicado), termine o TLS na borda e troque os tokens e as chaves no .env.

Pelo que cada repositório é responsável

Este repositório é um nível de topo fino — os arquivos de compose, os perfis de deploy, a documentação e os overlays de build em fungi/ para o lado do mycelium — mais quatro submódulos git em crab/.

RepositórioResponsabilidade
crab/crab-shell-proxyO orquestrador. Detém o socket, é dono do layout em disco, serve a API HTTP. Go.
crab/crab-ganglion-harnessO runtime de agente do próprio projeto. Um binário Go estático em Alpine.
crab/crab-exoskeleton-webappO cliente de chat voltado ao membro. Next.js. Seu serviço no compose é chat-webapp, não o nome do repositório.
crab/harness-sphereO observador. Somente observabilidade, exclusivo desta stack. Rust.

Mycelium não é um submódulo. O diretório fungi/ guarda Dockerfiles que buscam o mycelium e sua UI de administração no upstream na hora de construir a imagem.

Cada submódulo tem seu próprio remote, seus próprios pull requests e seu próprio branch padrão, e a cadeia é mesclada de baixo para cima: um ponteiro aqui só pode nomear um commit alcançável a partir do branch padrão daquele submódulo, e uma verificação de CI garante isso.

Duas coisas que quem vai fazer o deploy deve saber agora

Os containers de agente não são iniciados pelo compose. O proxy os cria pela API do Docker, um por membro por agente, fora de qualquer projeto do compose. O docker ps os mostra como crabshell-<agent>-<hash>, e docker compose down não os remove.

Produção não tem imagem publicada do ganglion. O docker-compose.prod.yaml baixa imagens publicadas do mycelium, do proxy, do chat webapp e do harness-sphere, e não define CRAB_GANGLION_IMAGE; nenhum workflow neste repositório publica uma. O compose de desenvolvimento constrói a imagem localmente sob a tag zombie-crab/crab-ganglion:dev, que existe só na máquina que a construiu. Um deploy de produção que rode agentes ganglion precisa fornecer essa imagem por conta própria.

Para onde ir agora

Harnesses explica a camada de agente e como um deles é escolhido. Agentes, workspaces e projetos cobre o que o proxy realmente escreve em disco. Para os componentes como componentes, veja crab-shell-proxy e harness-sphere.