Introdução
Read this documentation in English
zombie-crab-project dá a cada usuário o seu próprio agente de IA, real e isolado, atrás de uma única porta de entrada autenticada. Este livro explica como executá-lo, como configurá-lo e como ele é montado.
O problema que ele resolve
Um assistente de IA auto-hospedado costuma ser construído em torno de uma ideia: um agente, um dono. Isso funciona bem no seu próprio laptop. Deixa de funcionar no momento em que uma segunda pessoa entra.
Um agente de IA lê e escreve arquivos, roda ferramentas, executa código e mantém memória de longa duração — tudo isso guiado por linguagem natural que ninguém verificou. Em um único processo compartilhado, uma prompt injection, um bug de path traversal ou uma ferramenta que vaza basta para um usuário ler as conversas, os arquivos e os segredos de outro. Separar usuários por uma chave em um mapa parece isolamento. Não é.
Este projeto responde a isso com uma fronteira que o kernel impõe, e não a aplicação. Cada usuário ganha o seu próprio container e o seu próprio volume, iniciado quando ele fala pela primeira vez e parado quando ele se cala. Se o agente de um usuário for completamente comprometido, ele ainda assim não alcança os dados de outro: outro container, outro volume, sem root, nenhuma superfície compartilhada.
Na frente disso fica uma única entrada autenticada. Um API gateway verifica quem está chamando e injeta um perfil de conta que o chamador não consegue forjar, de modo que a identidade desce de algo confiável em vez de subir do corpo da requisição. Os usuários são identificados por um id de conta estável, e não por um endereço de e-mail — troque o seu e-mail e o seu agente, com tudo que ele lembra, continua sendo seu.
Para quem é este livro
Se você quer executá-lo, o início rápido está a um clone, um .env e um comando compose de distância de um chat funcionando. Você precisa de um terminal e do Docker; não precisa saber Go, nem como nada disso foi construído.
Se você administra um deploy — convidando membros, decidindo qual modelo cada pessoa recebe, publicando skills e documentos compartilhados — os capítulos de administração foram escritos para você, e não supõem mais do que saber usar uma interface web.
Se você vai estendê-lo, os capítulos de componentes descrevem cada peça em separado, e o capítulo de desenvolvimento cobre o build e os gates pelos quais uma mudança tem de passar.
Se você está avaliando o projeto, leia esta página e depois Como a stack se encaixa. A versão curta: a stack tem três camadas, cada uma com exatamente um trabalho, e cada uma substituível sem tocar nas outras duas.
As palavras que este livro usa
Quatro termos aparecem em toda parte e vale a pena fixá-los antes de você encontrá-los em um comando.
Um harness é o programa que de fato é o agente: ele mantém a conversa, chama o modelo, roda as ferramentas. Este projeto escreve o seu próprio, o ganglion, e é ele que esta documentação ensina. Um harness mais antigo, o picoclaw, ainda é suportado e está de saída. Qual dos dois responde é declarado por agente.
Um agente é uma configuração com nome — um harness, um modelo, uma política de ciclo de vida, uma personalidade. alpha e beta são os dois que este repositório traz. Um agente não é um processo: cada membro que usa um agente ganha o seu próprio container executando-o.
Um tenant é uma organização, e uma subscription é uma conta dentro dele à qual os membros pertencem. Juntos, os dois decidem quem pode alcançar qual agente. Um membro alcança um agente quando tem um papel com o nome dele.
Um workspace é o diretório de um membro para um agente: as conversas dele, a memória dele, os arquivos dele. É o que o container recebe e que nada mais consegue ver.
Lendo este livro
Comece pelo início rápido. É o capítulo que todos os outros pressupõem. Ele leva um clone novo até uma conversa com o seu próprio agente em oito passos numerados, e cada um diz como saber que deu certo. Leia mesmo que você não pretenda segui-lo, porque os capítulos seguintes ficam mais fáceis de situar depois que você viu as peças subirem.
Daí em diante o livro está em grupos, e você pode escolher o que combina com o que você está fazendo.
Primeiros passos é o início rápido, depois Instalação para a versão longa — pré-requisitos detalhados, o que a primeira execução escreve em disco e como zerar tudo — e Configuração para cada arquivo e variável que você tocou no caminho.
Conceitos centrais explica o que a stack está fazendo. Como a stack se encaixa é a arquitetura e o raciocínio por trás do formato dela. Harnesses cobre os dois runtimes de agente e como um deles é escolhido. Agentes, workspaces e projetos cobre o layout em disco, skills e memória o que um agente sabe e lembra, e arquivos e entrega como um documento entra e como um resultado volta.
Uso é escrito para um membro, e não para um operador: o cliente de chat, os projetos e as tarefas agendadas.
Administração é o trabalho do dia seguinte: o guia de administração, criar um agente personalizado de ponta a ponta, e modelos e provedores.
Operação cobre os modos de deploy, o banco de dados e o seu único passo manual de migração, observabilidade e solução de problemas — que reúne as falhas que as pessoas realmente encontram, e vale uma passada de olho antes de você precisar dela.
Os componentes são um capítulo para cada um dos quatro programas do repositório: o orquestrador, o harness ganglion, o cliente de chat e o observador.
Desenvolvimento cobre trabalhar na stack e contribuir.
Cada capítulo é dono do seu assunto e aponta para os outros em vez de repeti-los. Se uma página parece parar antes de um tópico, o link naquele ponto é onde o tópico mora.
Uma palavra sobre o que isto não é
Esta stack foi ajustada para ser legível e fácil de rodar localmente, não para vir endurecida de fábrica. O orquestrador detém o socket do Docker e roda como root; é o componente mais privilegiado da stack e o primeiro que você isola antes de expor qualquer coisa. O tráfego entre o gateway e o que está atrás dele não é criptografado, porque se espera que o gateway seja a única coisa voltada para uma rede. Os segredos da configuração de exemplo são apenas marcadores, e dizem isso.
Nada disso está escondido neste livro. Onde um padrão é uma conveniência de desenvolvimento, o capítulo dono dele diz isso e diz o que fazer no lugar.
Início rápido
Este capítulo leva você de um clone novo até uma conversa com o seu próprio container de agente. Siga-o de cima para baixo. Todo comando aqui é executado a partir da raiz do checkout, e nada nele supõe que você já usou esta stack antes.
O caminho descrito é o mais curto que funciona: o arquivo compose de desenvolvimento, o agente alpha e um usuário — você. Tudo o mais que a stack sabe fazer é assunto de um capítulo posterior.
Antes de começar
Você precisa de quatro coisas:
- Docker, com o plugin Compose v2 (
docker compose, nãodocker-compose). O arquivo compose de desenvolvimento usadepends_on: condition: service_completed_successfully, que é um recurso do Compose v2. - Git, porque o produto é montado a partir de quatro submódulos e um clone sem eles não constrói nada.
- Uma chave de API de LLM. Os dois agentes que este repositório traz estão configurados para a DeepSeek (
provider: "deepseek",name: "deepseek-chat"emcrab/crab-shell-proxy/config.yaml), então uma chave da DeepSeek dá menos trabalho. Trocar o provedor é configuração, não mudança de código. - Acesso à internet durante o build, e paciência na primeira vez. Este arquivo compose constrói seis imagens a partir do código-fonte, incluindo o gateway do Mycelium a partir de um commit git upstream.
Os builds de imagem rodam com
network: hostporque um container do BuildKit recebe um único resolvedor DNS utilizável e nenhum fallback, e builds em paralelo perdem consultas. Isso já está escrito no arquivo compose; você não passa nenhuma flag para isso.
1. Clone o repositório com os submódulos dele
git clone --recurse-submodules \
https://github.com/LepistaBioinformatics/zombie-crab-project.git
cd zombie-crab-project
Como saber que deu certo: ls crab/crab-shell-proxy lista código-fonte Go. Se o diretório estiver vazio, você clonou sem os submódulos; rode git submodule update --init --recursive para corrigir sem clonar de novo.
2. Crie o seu .env
O repositório traz um exemplo comentado por modo de deploy. Copie o standalone, que é o padrão local:
cp deploy/standalone/.env.example .env
Agora abra o .env e substitua os valores de exemplo. Cinco deles importam para este passo a passo:
| Variável | Para que serve |
|---|---|
MYC_STANDALONE_BOOTSTRAP_SECRET | Libera a reivindicação única da conta Staff no passo 4. Vazia ou não definida, deixa os endpoints de bootstrap respondendo 404. |
MYC_PICOCLAW_ALPHA_TOKEN | O token bearer que o gateway injeta nas rotas do alpha e que o proxy confere. |
MYC_PICOCLAW_BETA_TOKEN | O mesmo, para o agente beta. Você não vai conversar com o beta, mas veja o aviso abaixo. |
PICOCLAW_ALPHA_API_KEY | A chave de LLM do próprio alpha. Essa é a que precisa ser de verdade. |
CHAT_WEBAPP_DB_PASSWORD | A senha do pequeno Postgres que guarda a sua lista de conversas. Qualquer valor, desde que seja o mesmo dos dois lados — o compose passa essa mesma string para o banco e para o aplicativo. |
Gere os segredos em vez de inventá-los; o arquivo de exemplo sugere openssl rand -hex 32 para cada um, e qualquer string longa e aleatória serve.
Os nomes com cara de picoclaw no
alphanão são erro de copiar e colar. Oalphajá rodou picoclaw e foi movido para o ganglion sobre os mesmos diretórios por usuário; oserviceNamee o token dele foram deixados de propósito como estavam, porque o gateway roteia pelo primeiro e injeta o segundo, então renomear qualquer um dos dois teria sido um agente novo para cada membro, em vez do mesmo agente em um runtime novo.
Os dois tokens de agente precisam estar definidos, mesmo que você use só um agente. O agente
betaroda o harness picoclaw, e para um agente picoclaw um token que resolve para nada é fatal: o proxy se recusa a subir em vez de tirar em silêncio o acesso de um membro. Oalpharoda o harness ganglion, onde a mesma omissão desativa apenas aquele agente. Veja harnesses para entender de onde vem essa diferença.
PICOCLAW_BETA_API_KEYpode ficar com osk-your-beta-keydo exemplo. A chave de um agente picoclaw é escrita na configuração do próprio usuário no momento do provisionamento, e uma chave errada aparece como erro de autenticação na primeira mensagem — ela não impede nada de subir.PICOCLAW_ALPHA_API_KEYé diferente: como oalphaé um agente ganglion, um valor vazio faz o proxy desativar oalphana subida, e as rotas dele passam a responder 404.
3. Suba a stack
docker compose up -d --build
Como saber que deu certo: docker compose ps lista os serviços. Dois deles têm mesmo de ter sumido:
picoclaw-image Exited (0)
ganglion-image Exited (0)
Esses dois são serviços só de build. Eles existem para que docker compose up produza as duas imagens de harness em vez de deixar isso como um passo manual que alguém esquece; cada um roda /bin/true e sai. Todo o resto deve estar running, e crab-shell-proxy e mycelium-gateway devem chegar a healthy.
Neste modo o proxy publica uma porta apenas de loopback, então você pode conferi-lo diretamente:
curl http://127.0.0.1:18080/healthz
Depois confirme que nenhum agente se desativou:
docker compose logs crab-shell-proxy | grep disabled
O silêncio é o bom resultado. Uma linha na forma agent "alpha" disabled: … — its routes will answer 404 nomeia a variável de ambiente que você ainda tem de definir, e você deve corrigir isso antes de seguir.
O build da própria imagem do ganglion roda
go vetego testantes de linkar o binário. Isso é de propósito — os testes são o teste de aceitação da imagem — então um teste falhando impede a stack de subir, em vez de entregar um harness quebrado.
4. Reivindique a conta Staff (uma vez por deploy)
O gateway começa sem conta nenhuma. A primeira é reivindicada por um fluxo web de uso único, liberado pelo segredo de bootstrap que você definiu no passo 2.
Abra http://localhost:8080/_adm/instance/bootstrap, envie o segredo de bootstrap e o seu endereço de e-mail. O deploy standalone não manda e-mail: o transporte de e-mail dele escreve no log. Leia de lá o código de seis dígitos:
docker compose logs mycelium-gateway | grep -i bootstrap
Conclua a reivindicação com esse código.
Como saber que deu certo: o fluxo devolve um token de Staff, e a URL de bootstrap para de responder — é um endpoint de uso único e vira 404 depois de reivindicado.
Use um endereço real que você reconheça depois. É a conta com a qual você vai entrar, e esse mesmo endereço tem de ser o que você convida no passo 6.
5. Crie um tenant e uma subscription
Um agente é alcançado dentro de um tenant (uma organização) por meio de uma subscription (uma conta sob esse tenant à qual os membros pertencem). A sua conta Staff pode criar as duas coisas, na própria interface de administração do Mycelium em http://localhost:8081: entre lá com a conta que você acabou de reivindicar e siga a cadeia Staff → tenant → subscription.
Essas telas pertencem ao Mycelium e são construídas a partir de fontes upstream, então este livro não as descreve clique a clique. O que importa aqui é o resultado: existe um tenant, e existe uma conta de subscription sob ele.
Como saber que deu certo: a subscription aparece na lista do tenant nessa mesma interface, e você consegue selecioná-la no passo 6.
6. Dê a si mesmo o papel alpha
Alcançar um agente é uma permissão, não um padrão. O gateway declara as rotas dele como protectedByRoles, com um papel nomeado a partir de cada agente, então uma conta precisa ter o papel de convidado alpha — em write, que é o que enviar uma mensagem exige — antes que qualquer outra coisa funcione. Enquanto não tiver, toda requisição é recusada com um erro de permissão.
Os papéis em si já existem: declará-los na configuração do gateway basta para o Mycelium criá-los na subida. Manual mesmo é a concessão. Faça isso pelo mesmo fluxo Staff → tenant → subscription → convite de convidado em http://localhost:8081, convidando o seu próprio endereço de e-mail para o papel alpha com acesso de escrita.
Como saber que deu certo: o convite aparece vinculado à sua conta naquela subscription. Quando uma subscription já tem membros, as mesmas concessões podem ser feitas pela própria área de administração do chat-webapp — veja o guia de administração.
7. Entre no cliente de chat
Abra http://localhost:3000. Este é o chat-webapp, o cliente voltado ao membro. Entre com o mesmo endereço de e-mail: não há senha, só um magic link, e neste modo o código dele vai para o log em vez de ser enviado por e-mail:
docker compose logs mycelium-gateway | tail -50
No primeiro acesso o aplicativo oferece criar a sua conta antes de deixar você entrar no chat; aceite, e ele a cria para você no gateway.
Como saber que deu certo: você cai na tela de chat em vez do formulário de entrada.
8. Envie a sua primeira mensagem
Sem nenhum workspace selecionado, a área de chat é um seletor: uma linha por tenant, uma caixa por subscription e um bloco para cada agente que você pode alcançar. Escolha alpha e envie uma mensagem.
Como saber que deu certo: o agente responde — e um container que não existia um instante atrás está rodando agora:
docker ps --filter name=crabshell
Você vai ver algo como crabshell-alpha-1a2b3c4d5e6f7890, rodando como um usuário sem root. O sufixo é um hash do seu tenant, da sua subscription e da sua conta: um nome de container é limitado a 63 caracteres, então a identidade mora nos labels do container, e não no nome dele.
Aquele container é seu. O agente de mais ninguém consegue ler os arquivos, a memória ou as conversas dele, porque é um container diferente com um volume diferente — que é justamente o ponto desta stack.
O
alphaestá configurado para escalar até zero. Cerca de meio minuto depois da sua última mensagem o container dele para, liberando a memória; a sua próxima mensagem o inicia de novo com tudo intacto. Um agente parado é normal, não uma falha.
Para onde ir agora
Se algum passo não coube na sua máquina — uma versão do Docker, um caminho, uma porta já em uso — Instalação é a versão longa dos passos 1 a 3, e explica o que a primeira execução escreve em disco e como apagar tudo. Para trocar o modelo, adicionar um agente ou entender o que você acabou de editar no .env, leia Configuração. Para entender o que de fato aconteceu quando você apertou enviar, leia Como a stack se encaixa.
Instalação
Esta é a versão longa dos três primeiros passos do início rápido. Leia quando aquela página não couber na sua máquina, quando você quiser saber o que a stack escreve no seu disco, ou quando precisar zerar um ambiente.
Pré-requisitos
Docker Engine com o plugin Compose v2. O comando é docker compose, com espaço. O arquivo compose de desenvolvimento espera por serviços que só fazem build com depends_on: condition: service_completed_successfully, que o script v1 docker-compose não entende. Se você também pretende rodar o overlay de produção, precisa do Compose 2.24 ou mais novo: docker-compose.prod.yaml usa a tag !reset para descartar o build: do arquivo base e a publicação de portas de desenvolvimento, e versões mais antigas não conseguem interpretá-la.
Git. O produto são quatro submódulos — o orquestrador, o harness, o cliente web e o watcher — e o arquivo compose faz o build a partir das árvores de trabalho deles.
Espaço suficiente para o build. Um primeiro up --build compila sete imagens a partir do código-fonte: o picoclaw com patch, o harness ganglion, o proxy, o cliente de chat, o watcher, o gateway Mycelium (construído a partir de um commit git upstream) e a interface de administração do Mycelium. Só o Postgres vem pronto. Espere que a primeira execução demore e que todas as seguintes sejam rápidas.
Portas livres no host. Na configuração padrão a stack publica 8080 para o gateway, 3000 para o cliente de chat, 8081 para a interface de administração do Mycelium e 127.0.0.1:18080 para a porta do proxy usada só em desenvolvimento. Os bancos de dados e os containers dos agentes não publicam nada: só são alcançáveis de dentro da rede da própria stack. Cada porta publicada é uma variável de ambiente (MYCELIUM_PORT, CHAT_WEBAPP_PORT, MYCELIUM_WEBAPP_PORT), então uma colisão é uma edição no .env, não um problema. Se você mudar MYCELIUM_WEBAPP_PORT, mude allowedOrigins na configuração do gateway junto — a interface de administração é uma aplicação que roda no navegador e chama o gateway diretamente, e uma divergência é um muro de CORS.
Uma chave de LLM. Os dois agentes que já vêm prontos estão configurados para o DeepSeek. O provedor é configuração, não código; veja modelos e provedores.
Clonando com submódulos
git clone --recurse-submodules \
https://github.com/LepistaBioinformatics/zombie-crab-project.git
Se você já clonou sem essa flag, os diretórios dos submódulos existem mas estão vazios. Preencha-os no lugar:
git submodule update --init --recursive
--recursive importa: os submódulos estão declarados no .gitmodules deste nível, mas este repositório é ele mesmo o meio de uma cadeia e vale manter o hábito.
O que há no repositório
docker-compose.yaml the whole stack, standalone/development default
docker-compose.prod.yaml production overlay (published images, Postgres)
docker-compose.observability.yaml opt-in metrics backend (collector, Prometheus, Grafana)
deploy/ per-mode configuration
standalone/ .env.example + the gateway config this mode mounts
prod/ the same pair for production
observability/ collector, Prometheus and Grafana configuration
picoclaw-glob/ the Dockerfile and patches for the picoclaw image
crab/ the crab side, one submodule per component
crab-shell-proxy/ the orchestrator (Go) — holds the Docker socket
crab-ganglion-harness/ this project's own agent runtime (Go)
crab-exoskeleton-webapp/ the chat client; its compose service is chat-webapp
harness-sphere/ the watcher; observability only, never a Docker socket
fungi/ the Mycelium side: gateway and admin UI Dockerfiles
docs/ task guides and this book
data/ everything the running stack writes (gitignored)
Dois nomes valem ser fixados na cabeça agora, porque diferem do diretório em que vivem. O repositório do cliente de chat é crab-exoskeleton-webapp, mas o serviço dele no compose — e portanto o nome em todo comando docker compose — é chat-webapp. E deploy/ guarda dois modos de deploy, standalone e prod; observability/ e picoclaw-glob/ são configuração para um overlay e para o build de uma imagem, não modos com os quais você pode subir a stack.
O que a primeira execução cria
Nada dentro de data/ está no git, e o diretório não precisa existir antes de você começar. O proxy cria o que precisa conforme avança, como root, porque é o componente que segura o socket do Docker.
data/templates/<agent>/— a semente por agente, clonada para o diretório de cada novo usuário. Para um agente picoclaw o proxy faz isso sozinho a partir de um template padrão compilado no próprio binário, então um checkout novo funciona sem nenhum passo de seeding. Um agente ganglion não recebe template nenhum: o template é um arquivo de configuração do picoclaw mais o arquivo de segurança dele, e escrever um para o ganglion deixaria dois arquivos que ninguém lê.data/tenants/<tenant>/subscriptions/<subscription>/agents/<agent>/users/<account>/— um workspace isolado por membro por agente. Esta é a árvore que é montada por bind nos containers dos agentes, e o único lugar onde vivem as conversas, a memória e os arquivos de um membro.data/user-secrets/edata/effective-secrets/— o cofre de segredos do próprio membro, e a visão mesclada dele com o que um administrador compartilhou no nível do tenant ou da subscription. A visão mesclada é o que é montado dentro de um container, somente leitura.data/effective-skills/,data/effective-persona/,data/managed-skills/— a mesma ideia para as skills e para os arquivos de identidade do agente. Veja skills e memória.data/restart/— marcadores de restart, deliberadamente fora da árvore do tenant para que um agente não consiga ler nem escrever o seu.data/model-registry.db— o registro de modelos do proxy, um único arquivo de banco de dados embutido ao lado dos diretórios.
Dois volumes nomeados do Docker também são criados, e eles não ficam dentro de data/: mycelium-data guarda o banco SQLite do próprio gateway, que é onde vivem contas, tenants e papéis, e chat-webapp-postgres-data guarda a lista de conversas que o cliente de chat mantém. O cliente de chat cria as próprias tabelas no primeiro uso, então não há passo de migração para ele.
O deploy de produção é diferente em exatamente um ponto doloroso: o backend Postgres do Mycelium não tem migrações embutidas, então o schema dele precisa ser aplicado à mão uma vez, depois da primeira subida. O SQLite aplica as suas sozinho. Veja o capítulo do banco de dados.
Rodando de outro lugar que não a raiz do projeto
O proxy entrega ao daemon do Docker um caminho no host como origem do bind-mount de todo container que ele cria, então esse caminho precisa ser um que o daemon consiga resolver — não um caminho dentro do container do proxy. O arquivo compose usa ${PWD}/data como padrão, o que é correto quando você roda docker compose a partir da raiz do projeto e errado nos outros casos. Se você roda de outro lugar, defina isso explicitamente no .env:
CRAB_HOST_DATA_ROOT=/absolute/path/to/zombie-crab-project/data
Voltando a um estado limpo
Para apagar todo agente por usuário e todos os templates e deixar a stack se reconstruir, pare a stack, remova os containers que o proxy criou fora do compose, apague o estado em disco e suba tudo de novo:
docker compose down
# os agentes não são serviços do compose, então o compose não os remove
docker rm -f $(docker ps -aq --filter 'name=crabshell') 2>/dev/null
# a árvore é escrita por processos que rodam como root, daí o sudo
sudo rm -rf data/templates data/tenants data/user-secrets data/effective-secrets \
data/effective-skills data/effective-persona data/managed-skills \
data/restart data/model-registry.db
docker compose up -d --build
--build não é opcional aqui. O template padrão a partir do qual o proxy refaz o bootstrap está embutido no binário dele, então uma imagem anterior a uma mudança de template restauraria o template antigo.
Depois entre na sua conta e mande uma mensagem: o proxy provisiona seu usuário de novo a partir do nada. Seu login sobrevive, porque contas, tenants e papéis estão no volume mycelium-data e não em data/, e sua lista de conversas também. Adicione -v ao docker compose down só se você quiser essas coisas fora também — aí você teria que reivindicar a conta Staff de novo desde o começo.
Para onde ir agora
Configuração cobre todo arquivo que você acabou de copiar ou editar e o que cada variável faz. Deploy cobre o overlay de produção e como os dois modos diferem. Se alguma coisa não subiu, solução de problemas reúne as falhas que as pessoas realmente encontram.
Configuração
Quatro arquivos decidem como esta stack se comporta. Este capítulo diz qual deles é dono do quê, percorre o catálogo de agentes em detalhe — porque é ali que você define um agente — e lista as variáveis de ambiente que vale conhecer.
As quatro superfícies
| Arquivo | É dono de | Como chega até a stack em execução |
|---|---|---|
.env na raiz do repositório | segredos, portas publicadas, referências de imagem | lido pelo compose |
crab/crab-shell-proxy/config.yaml | o catálogo de agentes: quais agentes existem, seu harness, modelo e ciclo de vida | copiado para a imagem do proxy no momento do build |
deploy/<mode>/config.*.toml | o gateway: quais rotas existem, qual papel protege cada uma, qual token é injetado | montado por bind no gateway |
docker-compose*.yaml | quais serviços rodam, o que eles montam, que ambiente recebem | o próprio comando compose |
A única coisa a internalizar: o catálogo de agentes é embutido na imagem do proxy, então adicionar ou remover um agente significa reconstruir o proxy. A configuração do gateway é montada, então uma mudança ali precisa apenas de um restart daquele serviço. Os dois têm que concordar — um agente que existe em um e não no outro é ou uma rota anunciada sem nada atrás, ou um agente que ninguém consegue alcançar.
O catálogo de agentes: config.yaml
Este é o arquivo que você edita para definir um agente. Todo o resto dele tem um padrão que funciona; o mapa agents: não.
Aqui está um agente completo, com as linhas que importam:
agents:
alpha:
serviceName: "alpha" # must match the gateway's service key
harness: "ganglion" # which runtime answers
token: { env: "MYC_PICOCLAW_ALPHA_TOKEN" } # read from the environment, never inline
template: "alpha" # <dataRoot>/templates/alpha
mode: "scale-to-zero" # or "continuous"
idleTimeout: 30s
model:
provider: "deepseek"
name: "deepseek-chat"
apiKeyEnv: "PICOCLAW_ALPHA_API_KEY" # the key lives in the environment
serviceName é o valor que o Mycelium injeta como x-mycelium-service-name quando encaminha uma requisição, e é a única coisa que diz ao proxy qual agente foi endereçado. O Mycelium pega o primeiro segmento de caminho da URL que chega como nome do serviço e o remove antes de encaminhar, então um membro que chama /alpha/v1/... alcança o agente cujo serviceName é alpha. Uma requisição que carrega um nome de serviço que nenhum agente reivindica é respondida com 404 e uma mensagem dizendo para você passar pelo gateway.
harness seleciona o runtime. Os dois valores aceitos são "ganglion" e "picoclaw"; qualquer outro faz o carregamento falhar com uma mensagem que nomeia os dois. Um agente que não declara harness nenhum recebe o ganglion, que é o que diz config.DefaultHarness. Declare o harness explicitamente em todo agente mesmo assim, como faz todo agente no catálogo deste repositório: um runtime não é algo que uma configuração deva escolher por omissão, e uma omissão aqui não degrada com elegância. Um agente ganglion sem imagem é desabilitado em vez de iniciado, então um agente que herda o padrão num host sem CRAB_GANGLION_IMAGE para de responder em vez de rodar outra coisa em silêncio. O log de boot diz isso, e nomeia as duas saídas. Veja harnesses para saber como os dois runtimes diferem.
token mostra o padrão que este arquivo usa para todo segredo: { env: "NAME" } lê o valor do ambiente do proxy no momento do carregamento, então nada confidencial é escrito aqui ou embutido na imagem. Uma string simples também é aceita, e é a escolha errada fora de um teste.
template nomeia um subdiretório de <dataRoot>/templates/. Para um agente picoclaw o proxy cria um template ausente a partir de um padrão compilado no binário. Um agente ganglion não tem template em disco: ele é configurado pelo ambiente e por arquivos escritos por usuário.
mode e idleTimeout são o ciclo de vida. scale-to-zero para o container depois da janela de ociosidade e o inicia de novo na próxima mensagem, com tudo preservado. continuous nunca o para, que é o que os conectores nativos do picoclaw precisam — eles discam para fora de dentro do container, então o proxy não enxerga essa atividade para manter o agente vivo. Para um agente ganglion não existe essa porta lateral e o modo é uma simples decisão de custo. idleTimeout precisa ser maior que zero quando o modo é scale-to-zero; nos outros casos é ignorado.
model fixa o provedor e o nome do modelo e, o que é crucial, nomeia a variável de ambiente que guarda a chave em vez da chave em si. Cada agente tem a sua, então dois agentes podem usar provedores diferentes e credenciais independentes. Um baseUrl opcional sobrescreve o endpoint que o proxy resolveria a partir do provedor — o que você quer quando roda um gateway ou um endpoint regional na frente do provedor.
O que acontece quando um agente ganglion não está totalmente configurado
Este é o comportamento com maior chance de surpreender você, e ele é deliberado.
Um agente ganglion se remove do catálogo, no boot, quando este ambiente não consegue rodá-lo: sem referência de imagem, sem token, ou com um valor vazio atrás do seu model.apiKeyEnv. Isso não derruba o proxy, e não é silencioso — o log de boot traz uma linha nomeando o agente e a configuração que falta, e as rotas do agente passam a responder 404:
agent "alpha" disabled: PICOCLAW_ALPHA_API_KEY is unset (the agent's model apiKeyEnv) — its routes will answer 404
O raciocínio é que um arquivo de configuração deve conseguir descrever vários deploys. Um agente ganglion que chega a um host sem chave para ele degrada para “esse agente não existe” em vez de “o proxy não vai subir”, o que derrubaria todos os outros agentes junto.
Um agente picoclaw se comporta de outro jeito de propósito: um token que ele não consegue resolver é fatal, porque descartá-lo em silêncio removeria o acesso de um membro sem nenhum sinal além de uma linha de log que ninguém lê até ficar trancado do lado de fora. A chave de modelo dele, em contraste, pode estar vazia — ela é escrita na configuração do próprio membro no momento do provisionamento e aparece como um erro de autenticação na primeira mensagem.
As configurações em volta do catálogo
O resto do config.yaml tem forma de máquina e em geral é fornecido pelo compose. Os valores que vale conhecer:
hostDataRooté o caminho absoluto no host da árvore de dados. O proxy o entrega ao daemon do Docker como origem do bind-mount dos containers que cria, então um caminho que só existe dentro do proxy não vai resolver. É isso queCRAB_HOST_DATA_ROOTsobrescreve.containerDataRooté onde essa mesma árvore é montada dentro do proxy. O padrão é/data.networké a rede Docker em que os containers criados entram. O arquivo compose fixa o nome real da rede emzombie_netpara que ele fique estável independentemente do nome do projeto compose.startupDeadline(padrão 35 segundos) limita uma partida a frio.turnIdleTimeout(padrão 120 segundos, definido como 600 no arquivo que vem pronto) limita quanto tempo o harness pode ficar em silêncio — não quanto tempo um turno pode levar. Um turno longo que usa ferramentas narra o tempo todo e o reinicia a cada frame.containerPrefix(padrãocrabshell) prefixa todo container que o proxy gerencia. O nome é<prefix>-<agent>-<hash>; o harness é registrado nas labels do container, não no nome dele.mediaMaxBytes(padrão 10 MiB) é a única coisa contra a qual um upload é checado.
Vários campos podem ser sobrescritos pelo ambiente para que o arquivo versionado continue portátil: CRAB_HOST_DATA_ROOT, CRAB_CONTAINER_DATA_ROOT, CRAB_NETWORK, CRAB_LISTEN, CRAB_PICOCLAW_IMAGE, CRAB_PICOCLAW_USER, CRAB_PICOCLAW_HOME, CRAB_GANGLION_IMAGE, CRAB_MCP_BASE_URL e GANGLION_OTLP_ENDPOINT.
A configuração do gateway
deploy/standalone/config.standalone.toml e deploy/prod/config.base.toml configuram o Mycelium. Por agente, o formato é um bloco de serviço nomeando o downstream, um bloco de segredo guardando o token bearer, e um bloco de caminho por rota:
[[alpha]]
host = "crab-shell-proxy:8080"
healthCheckPath = "/healthz"
[[alpha.secret]]
name = "alpha-authorization-header"
authorizationHeader = { headerName = "Authorization", prefix = "Bearer", token = { env = "MYC_PICOCLAW_ALPHA_TOKEN" } }
[[alpha.path]]
group = { protectedByRoles = [{ name = "alpha", permission = "write" }] }
path = "/v1/chat/completions"
secretName = "alpha-authorization-header"
methods = ["POST"]
Três coisas decorrem desse bloco. A chave de serviço (alpha) é ao mesmo tempo o primeiro segmento de caminho que quem chama usa e o serviceName que o proxy compara. O papel nomeado em protectedByRoles é criado automaticamente no boot a partir dessa declaração — mas conceder o papel a uma conta é uma ação humana, e até lá toda requisição é recusada. E o token é resolvido do ambiente a cada requisição, então o arquivo versionado não guarda segredo nenhum.
Adicionar uma rota significa adicionar um bloco de caminho. Uma rota que o gateway não conhece é recusada antes de o proxy sequer vê-la, com uma mensagem sobre o caminho não corresponder a nenhum serviço — o que parece um bug de roteamento e é na verdade um bloco faltando.
Variáveis de ambiente
A lista completa e comentada é deploy/standalone/.env.example; copie esse arquivo em vez de escrever um do zero. Estas são aquelas cujo comportamento não é óbvio pelo nome:
| Variável | Efeito |
|---|---|
MYC_PICOCLAW_<AGENT>_TOKEN | O bearer que o gateway injeta e o proxy verifica, por agente. |
PICOCLAW_<AGENT>_API_KEY | A chave de LLM do próprio agente, referenciada pelo nome a partir do config.yaml. |
MYC_STANDALONE_BOOTSTRAP_SECRET | Controla a reivindicação única da conta Staff. Vazio deixa esses endpoints respondendo 404. |
CRAB_WEBHOOK_SECRET | Autentica no proxy o webhook de conta criada do Mycelium. |
CRAB_MCP_TOKEN_SECRET | Vazio desabilita o grafo de memória — o endpoint não é registrado e nenhum bloco de servidor é escrito em workspace nenhum. Nada avisa você; as telas de memória simplesmente ficam vazias. |
CRAB_TELEMETRY_TOKEN | Vazio significa que a rota de inventário de workspaces não é registrada de jeito nenhum — 404, não 401. Nunca reutilize um token de agente aqui: um token de agente libera conversar como qualquer membro, e um componente de monitoramento não pode ter um. |
CRAB_GANGLION_IMAGE | A imagem do ganglion. Ela não tem padrão no proxy, de propósito; o arquivo compose de desenvolvimento fornece uma que ele mesmo constrói. |
CRAB_HOST_DATA_ROOT | O caminho no host da árvore de dados. Precisa ser absoluto e precisa ser um caminho que o daemon do Docker consiga enxergar. |
START_AT_SIGNIN | Defina como 1 e a landing page do cliente de chat nunca é servida: / vira a tela de login. |
COMPOSE_FILE | Faz de um overlay o padrão para todo comando compose. O overlay de observabilidade na prática exige isso — veja observabilidade. |
Duas dessas merecem ser repetidas como regra, porque compartilham uma: um segredo não definido significa que a funcionalidade está ausente, não desprotegida. Um deploy que esqueceu um valor não ganha superfície nova, em vez de ganhar um endpoint atrás de uma guarda adivinhável.
Um workspace ganglion não tem diretório
.secrets/. As credenciais chegam a esse harness como variáveis de ambiente no container, e é por isso que o proxy faz bind de cada workspace separadamente e de nada acima dele. Sob o picoclaw a visão mesclada de segredos é montada somente leitura emworkspace/.secrets. Veja agentes e workspaces.
Para onde ir agora
Criando um agente customizado percorre de ponta a ponta a edição do catálogo e o bloco correspondente no gateway. Modelos e provedores cobre cadeias de modelos e sobrescritas por membro. Deploy cobre o que muda em produção.
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ório | Responsabilidade |
|---|---|
crab/crab-shell-proxy | O orquestrador. Detém o socket, é dono do layout em disco, serve a API HTTP. Go. |
crab/crab-ganglion-harness | O runtime de agente do próprio projeto. Um binário Go estático em Alpine. |
crab/crab-exoskeleton-webapp | O cliente de chat voltado ao membro. Next.js. Seu serviço no compose é chat-webapp, não o nome do repositório. |
crab/harness-sphere | O 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.
Harnesses
Um harness é o programa que de fato é o agente: ele guarda a conversa, chama o modelo, roda ferramentas e escreve em disco o que aprendeu. Este capítulo explica o que é um harness nesta stack, por que existem dois, como um deles é escolhido por agente e o que acontece quando um harness não consegue atender algo que foi pedido à API.
O que é um harness aqui
O crab-shell-proxy não contém um agente. Ele resolve quem está chamando, garante que o container daquela pessoa está rodando e encaminha o turno para o que estiver dentro dele. O que está dentro é o harness.
O proxy conversa com um harness por um contrato fixo — iniciar o container, esperar que ele responda a um health check numa porta conhecida, enviar o turno, transmitir a resposta de volta — de modo que a camada de agente possa ser trocada sem tocar no gateway nem no orquestrador. Essa afirmação deixou de ser teórica quando uma segunda implementação apareceu.
Os dois harnesses
crab-ganglion-harness — o runtime do próprio projeto, escrito quando os limites do picoclaw passaram a custar mais do que economizavam. Um binário Go estático em Alpine, com quem se fala por HTTP nativo com server-sent events. Ele lê toda a sua configuração do ambiente mais um arquivo somente leitura, e é dono de exatamente um diretório. Sua única ferramenta de sistema de arquivos é um shell, e cada comando que ele roda é confinado ao workspace do turno pelo kernel (Landlock), então .. e /etc não são recusados por uma checagem de string — eles não existem, do ponto de vista do comando.
picoclaw — onde este projeto começou. Um container com quem se fala pelo WebSocket do Pico Protocol, configurado por um config.json e um .security.yml que o proxy escreve no diretório de cada usuário na hora do provisionamento. Ele não roda aqui como vem de fábrica: a imagem é um build com patch, porque o upstream compara seletores de despacho por igualdade exata de string e os agentes por projeto precisam de um curinga.
O picoclaw está sendo descontinuado, e o ganglion é o harness a usar. O próprio código-fonte do proxy diz isso: HarnessPicoclaw é documentado como “the harness being deprecated – still fully served, still the right value to declare for an agent that needs it, but no longer what an omitted key means.” Trabalho novo vai para o ganglion, e este livro ensina o ganglion.
Escolhendo um, por agente
A escolha é uma única chave em cada agente no config.yaml do proxy:
agents:
alpha:
serviceName: "alpha"
harness: "ganglion"
token: { env: "MYC_PICOCLAW_ALPHA_TOKEN" }
template: "alpha"
mode: "scale-to-zero"
idleTimeout: 30s
Dois valores aceitos, "ganglion" e "picoclaw". Qualquer outra coisa faz a carga da configuração falhar de imediato com uma mensagem que nomeia o agente — uma configuração desatualizada nomeando um runtime que o proxy não orquestra mais entregaria a um membro um container provisionado para outra coisa.
Declare explicitamente, em todo agente. Uma chave omitida até resolve, mas depender disso significa que uma mudança futura no padrão troca em silêncio qual programa responde aos seus membros.
O que uma chave omitida significa hoje
Um agente que não declara harness: recebe config.DefaultHarness, e DefaultHarness é o ganglion (internal/config/config.go). O valor vazio é substituído em applyDefaults, antes de a validação rodar, então nada depois da carga da configuração chega a ver uma string vazia.
A virada é recente e deliberada — o comentário da própria constante registra o raciocínio: os critérios de saída da spec do harness foram atendidos, todo recurso que o gate um dia reservou ao picoclaw agora é atendido pelo ganglion, e a documentação ensina este harness, então um padrão que discordasse da documentação custaria uma tarde a alguém.
A consequência que um operador precisa saber: um agente que não declarava nada era antes um agente picoclaw e agora é um ganglion, então ele precisa de
CRAB_GANGLION_IMAGEdefinido. Uma imagem faltando não derruba o proxy — veja abaixo — mas tira aquele agente de serviço.
Um agente ganglion que não consegue rodar
Um agente ganglion é verificado na carga por duas coisas sem as quais ele não funciona: uma referência de imagem e uma chave de API resolvida quando o agente declara um apiKeyEnv. Se faltar qualquer uma, só aquele agente é desativado — suas rotas respondem 404 e o log de inicialização nomeia exatamente a variável a definir. Não é um erro fatal, porque um único agente de teste mal configurado já pôs o proxy em um ciclo de quedas e levou junto os agentes que funcionavam. A imagem nunca recebe como padrão uma tag móvel, de propósito.
Os agentes picoclaw não passam por essa verificação: a chave de um agente picoclaw é escrita em um .security.yml por usuário na hora do provisionamento, e uma chave vazia aparece como erro de autenticação na primeira chamada ao modelo, que é do que todo deploy existente já depende.
O gate de recursos, com honestidade
Alguns recursos que a API expõe nasceram como construções do picoclaw — eram campos em um config.json do picoclaw — e um harness que não lê esse arquivo não pode atendê-los fingindo que lê. A regra está escrita em internal/httpapi/harness_gate.go e é curta: um recurso que um harness não consegue atender responde 501, nomeando o harness. Ele nunca tem sucesso em silêncio.
Essa regra existe por causa de uma falha real. Um terceiro harness anterior foi lançado com projetos e modelos pessoais não implementados, e ambos eram construções de configuração do picoclaw que ele nunca lia — então um projeto podia ser criado, guardado, listado e reportado como ativo sem mudar nada no agente que respondia. Disseram ao membro que tinha funcionado. Um 501 é pior de receber e muito melhor de depurar.
Quatro recursos são nomeados no gate:
| Recurso | Nome no gate |
|---|---|
| Projetos | projects |
| Seleção pessoal de modelo | personal model selection |
| O grafo de memória | the memory graph |
| Tarefas agendadas | scheduled tasks |
Os três primeiros estão listados em uma tabela chamada picoclawOnly. A tabela é deliberadamente uma allowlist do que funciona, não uma denylist do que não funciona: um terceiro harness adicionado depois é recusado por padrão e precisa ser declarado recurso por recurso, o que falha na direção segura.
Uma segunda tabela, alsoServedBy, registra os harnesses que desde então ganharam um desses recursos — e o ganglion está listado para os três. Projetos, seleção pessoal de modelo e o grafo de memória funcionam no ganglion hoje. Duas tabelas em vez de uma remoção, porque as duas afirmações continuam verdadeiras: o recurso ainda é, na origem, uma construção do picoclaw, e um harness que não o implementou continua sendo recusado.
Então a posição prática é esta: com os dois harnesses que esta stack entrega, requireHarnessFeature não recusa nada. O 501 é o que um terceiro harness receberia no primeiro dia, antes de alguém declarar o que ele sabe fazer. Esse é o estado que o gate foi feito para produzir, não uma lacuna.
scheduled tasks é declarado no gate e deliberadamente ausente de picoclawOnly. As rotas de leitura sob /v1/cron/* não precisam de um container rodando e nunca passam pelo gate: mostrar um agendamento inerte é melhor do que escondê-lo, e a resposta conta a verdade sobre quando uma tarefa dispara em vez de recusar.
Escritas de cron são só do ganglion
Criar, editar e apagar uma tarefa agendada pela API é um gate separado, escrito direto em cronWriteScope (internal/httpapi/cron_write.go) e não na tabela de recursos — e é o único lugar em que os dois harnesses realmente diferem hoje.
A verificação é agent.Harness != config.HarnessGanglion, e qualquer outra coisa recebe um 501 dizendo “creating scheduled tasks over this API is not available on the picoclaw harness (agent <key>): its agent creates them itself.”
A razão é a posse. No picoclaw o agendamento vive em timers dentro do container que este processo não enxerga; escrever o jobs.json dele de fora produziria um registro que o membro vê e um timer que nunca mudou. No ganglion o proxy é dono do agendamento, em um arquivo acima do bind do container, então ele pode tanto escrevê-lo quanto dispará-lo. Esse posicionamento também significa que um turno conduzido por texto não confiável não consegue agendar seus próprios turnos futuros.
No picoclaw, a superfície que ainda funciona é pedir ao agente na conversa. Veja Tarefas agendadas para a história do lado do membro.
Outras diferenças que vale conhecer
- Templates. Um agente picoclaw é semeado a partir de um diretório de template (o proxy embute um padrão e cria sozinho um que esteja faltando). Um agente ganglion é provisionado sem template nenhum, de propósito: o template é um
config.jsondo picoclaw mais um.security.yml, e semear um ali deixaria dois arquivos que nada lê. - Segredos. O picoclaw recebe credenciais como arquivos em
.secrets/. O ganglion as recebe como variáveis de ambiente, e um workspace ganglion não tem diretório.secrets/nenhum. - O que é montado. Os dois harnesses compartilham um mesmo layout em disco e diferem no que montam dentro do container. Essa diferença é estrutural e está coberta em Agentes, workspaces e projetos.
- Ciclo de vida. Cada agente é
scale-to-zerooucontinuous. Os conectores nativos do picoclaw discam para fora de dentro do container e passam ao largo do proxy, então um agente alcançado por esse caminho precisa sercontinuous. Um agente ganglion não tem essa porta lateral, então ali o modo é uma simples decisão de custo.
Uma ressalva de produção
Não existe imagem publicada do ganglion. O compose de desenvolvimento constrói uma sob zombie-crab/crab-ganglion:dev, uma tag que vive só na máquina que a construiu, e o docker-compose.prod.yaml não define CRAB_GANGLION_IMAGE. Se você fizer o deploy com o perfil de produção e rodar agentes ganglion, fornecer essa imagem é tarefa sua.
Para onde ir agora
Agentes, workspaces e projetos para o que cada harness vê em disco, Tarefas agendadas para a superfície de cron, e crab-ganglion-harness para o runtime em si.
Agentes, workspaces e projetos
Um agente é uma coisa para a pessoa que o usa e outra coisa em disco. Este capítulo cobre as duas, depois o layout de diretórios que o proxy escreve e, por fim, o único lugar em que os dois harnesses divergem de propósito.
O que é um agente para um membro
Quando você entra no cliente de chat, você não escolhe um modelo nem um container. Você escolhe um agente — alpha, beta, o que este deploy declarar — e ganha uma conversa com ele.
Sem nenhum workspace selecionado, a área de chat vira um seletor: uma linha por tenant, uma caixa por subscription dentro dela, e os agentes que você alcança como blocos mostrando suas permissões em cada um (um olho para leitura, um lápis para escrita). Clicar em um abre uma conversa nova.
Um agente é, portanto, um tipo de assistente, compartilhado no nome entre todos que o alcançam e privado na substância para cada um deles. Dois membros conversando com alpha estão falando com a mesma persona, a mesma configuração de modelo e as mesmas skills publicadas pelo administrador — em dois containers diferentes, com duas memórias diferentes, duas árvores de arquivos diferentes e nenhum caminho entre elas.
Seu agente é chaveado pelo seu id de conta, não pelo seu e-mail. E-mails mudam e são guardados só como um marcador legível para operadores; troque seu e-mail e o seu agente e o histórico dele continuam seus.
O que é um agente em disco
A unidade de isolamento do proxy é uma chave de quatro partes: tenant, subscription, papel (a chave do agente) e id de conta do usuário. Essa chave nomeia um diretório:
<data root>/tenants/<tenant>/subscriptions/<subscription>/agents/<agent>/users/<user>/
Cada componente é sanitizado antes de virar um segmento de caminho, então nada vindo de uma requisição consegue criar um separador ou um ... O diretório é criado preguiçosamente, no primeiro chat do membro — ou de antemão para uma subscription inteira, se o webhook opcional subscriptionAccount.created estiver registrado no mycelium.
Esse diretório é o que este livro chama de diretório do usuário. Ele pertence ao proxy. Parte dele é montada no container do agente; parte, de propósito, não é.
O layout
<user dir>/ the proxy's, NOT necessarily mounted
├── config.json harness configuration, proxy-written
├── .security.yml picoclaw only
├── .projects.json proxy-owned: which projects exist
├── .schedules.json proxy-owned: scheduled tasks (ganglion)
├── .crab-owner.json proxy-owned: who this workspace belongs to
├── .crab-model.json proxy-owned: this member's model choice
├── .crab-mode.json proxy-owned: this instance's lifecycle override
├── workspace/ THE MAIN AGENT
│ ├── AGENT.md SOUL.md HEARTBEAT.md USER.md
│ ├── memory/ public/ sessions/ ...
└── workspace-<project-id>/ ONE PER PROJECT, A SIBLING OF workspace/
├── AGENT.md SOUL.md HEARTBEAT.md USER.md
└── memory/ public/ sessions/ ...
Duas regras fazem valer a pena aprender este layout em vez de consultá-lo.
Todo harness organiza seu diretório por usuário da mesma maneira. Um caminho que significa uma coisa no picoclaw significa a mesma coisa em qualquer outro harness que esta stack criar. Isso não é uma gentileza com o picoclaw; é o que permite ao proxy ler e escrever esses diretórios com um só conjunto de funções em vez de um por harness. Um desvio por harness que esteja faltando não falha alto — ele lê um diretório que nunca existiu e relata que o membro não tem histórico, nem arquivos, nem tarefas agendadas.
Os dotfiles acima de workspace/ são do proxy, não do agente. Eles decidem quais identidades de projeto existem, qual modelo é usado, se o container pode ser mantido vivo e o que está agendado. Um agente capaz de editá-los poderia desviar para si as conversas de um colega, escolher o endpoint para onde as próprias chaves são enviadas, ou manter o próprio container rodando indefinidamente. Por isso eles ficam fora do alcance do agente — por dois mecanismos diferentes, dependendo do harness.
.crab-owner.json é um pequeno marcador JSON que registra a tupla completa do workspace e o e-mail do dono, para que um operador consiga descobrir a que humano um container pertence. Ele é necessário porque o nome do container não consegue carregar essa informação: a tupla completa são dois UUIDs, o que passa do limite de 63 caracteres de um rótulo DNS.
O workspace de um projeto é um irmão
Quando um membro cria um projeto, o proxy cria para ele uma segunda identidade de agente com um workspace próprio:
workspace-<project-id> correct
workspace/projects/<id> WRONG
workspace-<id> é irmão de workspace/, nunca filho dele. O nome não é uma preferência. O picoclaw deriva o workspace de um agente nomeado como <defaults.workspace>/../workspace-<id> quando a configuração do agente não define um, então a ferramenta dele resolve esse caminho independentemente do que o proxy escreve. O helper WorkspaceSegment do proxy devolve workspace para o projeto vazio e workspace-<id> caso contrário, e não faz desvio por harness.
Já fez. O ganglion manteve os projetos dentro do workspace principal por uma release, para poupar-se de um segundo bind, e o custo foi que uma função era na verdade duas fingindo ser uma — todo chamador que buscava as sessões de um projeto, seu diretório público ou sua mídia herdava a divisão, e um chamador que esquecia não falhava: ele lia um diretório que nunca existiu. O caminho filho legado sobrevive só para que o código de limpeza consiga achar uma subárvore escrita antes da migração. Nada pode criar um novo.
O workspace de cada projeto tem seus próprios arquivos de persona, sua própria memória, suas próprias sessões e seu próprio diretório public/. O histórico de um projeto não é alcançável pedindo o do workspace principal, e esse é o ponto: um projeto delimita as transcrições, a janela de contexto e os arquivos. Veja Trabalhando com projetos para o lado do membro.
O que os dois harnesses montam, e por que isso importa
O layout é compartilhado. O conjunto de binds não é, e essa diferença é estrutural, não acidental.
O picoclaw monta o diretório do usuário inteiro. Ele tem sua própria opção restrict_to_workspace, que mantém o agente dele dentro de workspace/, então o estado do próprio proxy pode ficar ao lado do workspace e ainda assim fora de alcance. O proxy monta ainda uma visão somente leitura de .secrets em cada workspace, um por projeto além do principal, mais a cascata de persona e a raiz de skills compartilhada.
O ganglion monta cada workspace separadamente — workspace/, e um bind por workspace-<id> — e nada acima deles.
A razão é que o ganglion não tem restrict_to_workspace, e não teria como ter um que fosse útil. Sua única ferramenta de sistema de arquivos é /bin/sh -c <string>: não há argumento de caminho para recusar, e qualquer denylist de .. ou /etc é derrotada por $(echo L2V0Yw== | base64 -d). O que o confina, em vez disso, é um domínio Landlock em volta de cada comando, cuja única hierarquia de leitura e escrita é o workspace do turno — mais a própria fronteira do container. Então o estado que pertence ao proxy precisa estar do outro lado da fronteira, e não apenas ao lado dela.
A primeira versão do caminho do ganglion usava um único bind largo, e o custo foi concreto: ele punha o bearer token do próprio container um nível acima do diretório de trabalho do shell, onde cat ../.crab-ganglion.json o lia.
A ilustração mais clara é .schedules.json. Ele não é um log — é uma instrução permanente ao proxy para acordar um container e rodar um turno, num timer, para sempre. Dentro de um bind do ganglion, um turno conduzido por texto não confiável poderia escrever uma. Para o picoclaw, guardar isso dentro do workspace é inofensivo, porque o agente que escreve é o mesmo processo que teria de agir sobre aquilo. Aqui o agente e o ator são processos diferentes.
Duas consequências vêm dos binds por workspace:
- O conjunto de binds muda quando o conjunto de projetos muda, e um conjunto de binds é fixado na criação do container. Então o proxy verifica se houve divergência toda vez que garante o container e o recria quando os projetos não batem mais — senão um projeto criado depois de o container subir ficaria invisível lá dentro, e o turno rodaria contra um diretório que o proxy nunca lê.
- As skills compartilhadas do administrador são montadas em todo workspace, não só no principal, porque a raiz do Landlock é o workspace do turno. Um índice de skills apontando para arquivos que um turno de projeto não consegue abrir é pior do que nenhum índice.
O que um workspace ganglion contém
Um workspace ganglion é semeado com memory/, public/, sessions/ e windows/. É o conjunto do picoclaw menos o que só o picoclaw tem, mais um próprio dele:
- sem
cron/— o proxy guarda os agendamentos do ganglion, acima do bind; - sem
.secrets/— as credenciais chegam a este harness como variáveis de ambiente, não como arquivos; windows/é próprio do ganglion, guardando em disco a janela de contexto de cada conversa. Não tem equivalente no picoclaw.
USER.md só é semeado se o workspace ainda não tiver um. É o único arquivo de persona que o agente escreve de volta, então sobrescrevê-lo toda vez que o container é garantido apagaria o que o agente aprendeu sobre o membro a cada recriação do container — o que, para um agente scale-to-zero, é rotina.
Dentro do container a raiz de dados é /data/.ganglion, e só os filhos workspace dela são montados. O arquivo de chave de credenciais e o registro de modelos ficam ao lado dessa raiz, somente leitura, fora de todo workspace: o workspace é a única hierarquia que um comando alcança, então uma lista de modelos gravável deixaria uma ferramenta conduzida por texto não confiável escolher o endpoint para onde vão as chaves do próprio deploy.
Para onde ir agora
Skills e memória cobre o que vive dentro de memory/ e skills/; Arquivos e entrega cobre public/. Trabalhando com projetos é a visão do membro sobre os workspaces irmãos descritos aqui.
Skills e memória
Duas coisas permitem que um agente seja mais do que um modelo com uma caixa de chat: skills, que são procedimentos que ele pode consultar, e memória, que é o que ele guarda entre uma conversa e outra. Este capítulo explica as duas, onde elas ficam no disco e qual cópia vence quando duas têm o mesmo nome.
O que é uma skill
Uma skill é um diretório com um arquivo chamado SKILL.md. O arquivo começa com um pequeno bloco de frontmatter que lhe dá nome e descreve quando usá-la:
---
name: quarterly-report
description: Build the quarterly sales report from the shared CSV exports. Use when the user asks for a quarterly report or for sales numbers by quarter.
---
# Quarterly report
1. Read the latest export from `workspace/.shared/subscription/`...
Só name e description são lidos. O formato é o do picoclaw, sem mudanças, então uma skill escrita para um harness carrega no outro — o ganglion diz isso no seu próprio pacote internal/skillfile, que existe justamente para manter uma única definição do formato para o código que lê skills e o código que as escreve.
O nome de uma skill precisa casar com ^[a-z0-9][a-z0-9._-]{0,63}$ quando um administrador envia uma, e shared-content é reservado — é o nome de uma skill que a própria plataforma entrega.
As duas raízes, e qual delas vence
Dentro de um workspace há dois lugares onde uma skill pode viver:
<workspace>/skills/<name>/SKILL.md yours, writable
<workspace>/shared-skills/<name>/SKILL.md the administrator's, read-only
skills/ pertence ao agente. É onde vai parar uma skill que o agente escreveu para si mesmo, e onde uma rodada de evolução aprovada pelo operador escreve seus rascunhos.
shared-skills/ é um bind mount somente leitura que o proxy cria a partir do que os administradores publicaram. Somente leitura no mount, que é uma garantia do kernel com a qual o agente não tem como discutir: o Landlock só restringe acesso, então nunca pode conceder uma escrita em um bind somente leitura.
Os dois caminhos acima são a cara de um workspace do ganglion, e são o que a skill
skill-creatorentregue diz ao agente. Os dois harnesses são servidos a partir do mesmo diretório mesclado no host, então uma única ação do administrador chega aos dois; o picoclaw o monta na sua própria raiz de skills em vez de dentro do workspace, porque é ali que o picoclaw procura.
Um nome presente nos dois resolve para a cópia do administrador, e a cópia sombreada do workspace fica registrada em log para que um operador veja qual venceu. A regra está declarada no carregador de skills do ganglion e de novo, em palavras simples, na skill skill-creator que o próprio agente lê. O contrário deixaria um agente sobrescrever a instrução de um administrador escrevendo um arquivo com o mesmo nome — uma escalada de privilégio disfarçada de regra de merge.
Há um segundo merge, separado, que acontece antes de tudo isso. Um administrador pode publicar skills em quatro escopos, e o proxy os achata no único diretório somente leitura que ele monta: tenant, depois tenant-para-este-agente, depois subscription, depois subscription-para-este-agente, com a fonte mais tardia vencendo por nome. Essa cascata decide o que
shared-skills/contém; a regra acima decide o que acontece quando o conteúdo dele colide com o do próprio agente.
Só o índice chega ao prompt
Esta é a parte que muda o jeito de escrever uma skill.
Todo turno recebe um índice das skills disponíveis — uma linha para cada, com nome, descrição e o caminho do corpo — e nada mais. Os corpos ficam no disco, e o agente lê o de que precisa com sua ferramenta de shell, que já alcança o workspace. No ganglion o índice é limitado a 8 KiB por padrão, cerca de dois mil tokens: o bastante para algumas dezenas de skills com uma frase cada, pequeno o bastante para que o índice nunca dispute espaço com a conversa.
Duas consequências se seguem, e elas são todo o ofício de escrever uma:
- A descrição é a skill. É a única parte que decide se o corpo chega a ser aberto. Escreva-a como a situação que ela atende, não como um título. “Use quando a pessoa pedir um arquivo exportado ou enviado” encontra seu momento; “Utilidades de arquivo” não.
- O corpo pode ser longo, e não custa nada até ser lido. Não comprima um procedimento em dicas para economizar espaço. Espaço não é o que você está economizando.
Colocar todos os corpos no prompt de cada turno gastaria a janela de contexto com instruções para um trabalho que este turno não está fazendo, que é como uma biblioteca de skills deixa de ser um ativo e vira um imposto.
O que vem na caixa
O proxy embute um pequeno conjunto de documentos gerenciados pelo operador no próprio binário e os monta em bind somente leitura em todo workspace que cria. O agente não consegue nem editá-los nem manter uma edição depois de um restart.
Três deles são skills:
| Skill | O que ela cobre | Onde ela é entregue |
|---|---|---|
shared-content | Onde ficam os arquivos e segredos publicados pelo administrador, a regra de nunca copiar um segredo para outro lugar, e onde escrever um arquivo que o membro possa baixar. | ambos os harnesses |
skill-creator | Como escrever, revisar ou avaliar um SKILL.md para este workspace. | ambos os harnesses |
ganglion-workspace | A única árvore gravável, o que o shell alcança e o que não alcança, e quais comandos a imagem Alpine realmente tem. | só o ganglion |
ganglion-workspace é a única entrada específica de um harness, e a restrição segue o mesmo princípio do gate 501 um nível acima: um documento que descreve o shell deste container, sua imagem e seu layout é a descrição da máquina errada para o outro harness, e um agente agindo sobre uma capacidade da qual lhe falaram em vez de uma que ele tem é exatamente a falha a evitar. Ele faz as vezes da skill de workspace que o picoclaw traz consigo, e que um agente do ganglion nunca recebe porque um agente do ganglion é provisionado sem template algum.
As duas skills entregues se sobrepõem um pouco e discordam em um ponto de propósito.
shared-contentdescreve um diretório.secrets/; um workspace do ganglion não tem nenhum, porque as credenciais chegam a esse harness como variáveis de ambiente. A skillganglion-workspacediz isso com suas próprias palavras e manda o agente ler o ambiente em vez disso. É uma divergência reconhecida, não um bug.
Memória
A memória de um agente nesta stack são arquivos em workspace/memory/, mais um grafo opcional. Eles não são intercambiáveis, e a diferença está em qual deles o membro consegue ver.
Os arquivos de memória
memory/MEMORY.md é o caderno do próprio agente — o que ele aprendeu, e aquilo em que ele escreve.
memory/MEMORY_CUSTOM.md é o arquivo do membro. Ele guarda as notas fixas dele para o agente: preferências, contexto, instruções que ele quer que sejam levadas em conta. O membro o edita direto pelo painel Workspace do cliente de chat, a qualquer momento, entre um turno e outro. O proxy escreve em nome dele, por um handle confinado pelo kernel, de modo que um componente de caminho trocado faz a syscall falhar em vez de redirecionar uma escrita feita pelo root. Um arquivo ausente é um documento vazio, não um erro.
Como o membro pode mudá-lo entre dois turnos quaisquer, a skill shared-content entregue manda o agente reler o arquivo atual sempre que ele for relevante, em vez de confiar no que lembra dele.
Outros três arquivos em memory/ são do operador, montados somente leitura:
CONTEXT_RECOVERY.mdavisa o agente de que seu contexto vivo pode ser zerado quando o container reinicia — redução de escala por ociosidade, uma mudança de configuração, um redeploy — e de que o histórico completo fica preservado para ele emworkspace/sessions/durable/<session-key>.jsonl, um arquivo append-only que só cresce. Se o agente parecer estar sem as mensagens anteriores, ele é instruído a reler esse arquivo em vez de adivinhar.FILE_DELIVERY.mdé a regra sobre para onde vai um arquivo produzido. Veja Arquivos e entrega.MEMORY_ROUTING.mddiz em qual memória escrever, e é montado apenas quando o grafo de memória está habilitado. Sem grafo o agente não tem ferramentamcp_memory_*alguma, e um documento mandando preferi-las estaria ativamente errado — pior do que silencioso.
Esses são arquivos de memória em vez de skills por um motivo que vale entender. Skills são carregadas por relevância: o agente precisa decidir ir procurar uma, então uma regra que tem de valer em todo turno só seria encontrada no momento em que fosse menos necessária. O diretório de memória é lido a cada turno.
O grafo de memória
Quando está habilitado, o agente também tem um grafo de conhecimento: entidades, observações sobre elas e relações nomeadas entre elas. É a memória que o membro pode navegar, buscar e auditar pelo cliente de chat, e é a única que registra de qual conversa um fato veio.
Ele é servido pelo próprio proxy. O proxy escreve um bloco de servidor MCP chamado memory no config.json do workspace, apontando para sua própria rota /v1/mcp com um token bearer que carrega o escopo do workspace mais um HMAC sobre ele — então o proxy não guarda token nenhum, e girar o segredo de assinatura revoga todos os tokens emitidos de uma vez. O ganglion tem um cliente MCP próprio e alcança o mesmo grafo que um container picoclaw no mesmo workspace alcançaria.
O grafo tem escopo de membro e abrange os projetos dele, em vez de ser um grafo separado por projeto.
As ferramentas que o agente recebe são todas prefixadas com mcp_memory_: mcp_memory_create_entities, mcp_memory_add_observations, mcp_memory_create_relations, mcp_memory_search_nodes, mcp_memory_semantic_search, mcp_memory_read_graph e mcp_memory_open_nodes. MEMORY_ROUTING.md dá ao agente três regras sobre usá-las, cada uma nascida de vê-lo errar: busque antes de criar, porque criar ignora um nome que já existe e duas grafias produzem duas entidades que nenhuma consulta reúne; crie também a relação, porque uma entidade sem relações é um ponto ao qual nada leva; e mantenha o entityType consistente, porque é por ele que o membro filtra a lista.
Habilitar isso é uma variável de ambiente, CRAB_MCP_TOKEN_SECRET, e deixá-la sem valor é suportado: a rota /v1/mcp não chega a ser registrada, nenhum bloco MCP é escrito em workspace algum, e todo o resto se comporta como antes. Isso é deliberado — um deploy que esqueceu o segredo tem de ficar sem memória, em vez de ganhar um endpoint sem autenticação alcançável por todo container da rede.
Fatos vão para o grafo; as anotações de trabalho do próprio agente vão para o
MEMORY.md. O documento de roteamento também proíbe o agente de alegar um salvamento que não fez, o que foi escrito depois de observá-lo acrescentando algo aoMEMORY.mde então dizendo ao membro que tinha escrito no grafo também.
Para onde ir agora
Arquivos e entrega para public/, o outro diretório visível ao membro em um workspace, e o Guia de administração para publicar skills e arquivos compartilhados em um escopo.
Arquivos e entrega
Arquivos viajam nas duas direções. Um membro anexa algo para o agente trabalhar; o agente produz algo que o membro tem de conseguir baixar. Os dois usam um diretório, e este capítulo é sobre esse diretório e as regras em volta dele.
Um diretório: public/
Todo workspace tem um diretório public/, e ele é o único diretório que a interface do membro lista. Essa única frase é todo o desenho. Um arquivo escrito em qualquer outro lugar do workspace existe, é perfeitamente legível pelo agente, e é invisível para a pessoa para quem o agente está trabalhando.
workspace/public/ what the member sees
workspace/public/attachments/ where the agent delivers
O workspace de um projeto tem seu próprio public/, irmão do principal, então um arquivo enviado para dentro de um projeto fica nesse projeto. Veja Agentes, workspaces e projetos.
Enviando um arquivo para um agente
No compositor do cliente de chat, anexe um arquivo. O cliente o envia para a rota de mídia do proxy, que:
- limita o corpo da requisição — o limite padrão é 10 MiB, configurável em
mediaMaxBytes, e um envio grande demais é recusado com413sem ser bufferizado por inteiro; - confere o tipo com uma lista de extensões permitidas, recusando qualquer outra coisa com
400; - reduz o nome do arquivo a um nome-base seguro — diretórios removidos, caracteres inseguros trocados por
_, pontos iniciais descartados — e recusa um que acabaria vazio ou que contivesse travessia de caminho; - escreve os bytes no diretório
public/do seu workspace e faz chown nele para que o agente não-root consiga ler o que o proxy root escreveu.
A resposta é o caminho relativo ao workspace, public/<name>, e é esse o caminho que o turno referencia, então o agente consegue abrir o arquivo exatamente pelo nome que lhe foi dito.
Dois comportamentos valem ser conhecidos porque são deliberados:
- Reenviar o mesmo nome sobrescreve. Um arquivo por nome, em vez de uma pilha crescente de
report(1).pdf. - Enviar para dentro de um projeto exige que o projeto viaje junto com o arquivo. A rota de upload é a única rota multipart da superfície de mídia, então o projeto chega como um campo de formulário em vez de um parâmetro de query. Mandar sem ele costumava deixar o arquivo no workspace principal, onde o agente do projeto nunca conseguiria abri-lo.
A escrita passa por um handle confinado pelo kernel no diretório public/, então um nome que resolve para um symlink apontando para fora da árvore faz a syscall falhar em vez de escrever onde quer que ele apontasse. Isso importa aqui mais do que importaria normalmente: o proxy roda como root, a árvore é gravável pelo agente dentro do container, e o conteúdo vem da rede.
Recebendo um arquivo de volta de um agente
Há dois caminhos pelos quais um arquivo chega a public/attachments/, e o membro não consegue distingui-los — o que é a intenção.
O próprio agente escreve o arquivo ali. Esse é o caminho comum no ganglion. O diretório de trabalho da ferramenta de shell é o workspace do turno, então public/attachments/report.pdf é um caminho relativo que cai onde o membro vai encontrar.
O harness entrega o arquivo por fora. O picoclaw responde a um pedido de “me manda o arquivo” com uma frase curta e empurra o arquivo ele mesmo pelo seu próprio canal de mídia. O proxy busca esses bytes — com um timeout de 60 segundos e um limite de 64 MiB — e os escreve em public/attachments/<name> sob a mesma sanitização de nome que um envio pelo navegador recebe. Copiar em vez de fazer proxy sob demanda é deliberado: o armazenamento de mídia do harness é um cache próprio com tempo de vida próprio, e um arquivo em public/ já é listável e baixável por tudo que serve um arquivo enviado pelo membro.
A lista de extensões permitidas que vale para os envios não vale para as entregas. Essa lista restringe o que um chamador de fora pode empurrar para dentro de um container; um arquivo entregue foi escrito pelo agente dentro do próprio workspace, então recusá-lo ali descartaria trabalho legítimo sem acrescentar nenhuma fronteira que o workspace já não tenha.
attachments/ é reservado
public/attachments/ é criado, nomeado e preenchido pelo proxy, então o membro não pode renomeá-lo, movê-lo, apagá-lo nem criar uma pasta própria com esse nome — a API recusa com um erro de “gerenciada pelo sistema”. Renomeá-lo desconectaria silenciosamente toda entrega futura.
Só o nível de cima é reservado. reports/attachments é uma pasta comum que um membro pode legitimamente querer; proibir a palavra em todo lugar seria uma regra sobre vocabulário, não sobre propriedade. E a recusa vive na API, não só na interface, porque esconder um botão não é uma permissão.
O que o membro vê
O arquivo aparece no painel Arquivos do workspace, na mesma lista dos arquivos que ele mesmo enviou, com clique para baixar. Pastas aninhadas são listadas, então um agente que organizou sua saída em subdiretórios é mostrado como ele organizou.
Na própria conversa, o proxy acrescenta um aviso curto à resposta:
📎 report.pdf — public/attachments/report.pdf
Esse aviso só existe no stream. Ele é injetado pela camada entre o agente e o membro e não faz parte da mensagem que é salva, então depois de recarregar a página ele some. O único registro durável de um arquivo entregue é o painel Arquivos — e o que o agente escreveu com as próprias palavras.
É por isso que o documento de memória FILE_DELIVERY.md entregue, lido a cada turno, manda o agente nomear o caminho na resposta:
Salvei o relatório em
public/attachments/relatorio-q2.pdf.
Esse exemplo é citado literalmente do documento que a plataforma entrega, escrito para um deploy cujos membros escrevem em português. O que importa nele é a instrução, não o idioma: diga no texto da resposta o caminho em que você escreveu.
e o proíbe de anunciar um arquivo que não escreveu de fato, ou um caminho que não usou de fato. A frase padrão do próprio picoclaw — “Requested output delivered via tool attachment.” — não nomeia nada, e uma resposta que diz só isso vira, depois de um reload, uma mensagem sobre um arquivo sem jeito nenhum de achá-lo.
O mesmo documento traça a linha para o agente em uma frase: tudo que o membro deve receber vai em public/attachments/; os arquivos de trabalho de que o agente precisa só para si vão em qualquer outro lugar. Na dúvida, entregue — um arquivo visível que ele ignora não custa nada, um invisível que ele queria custa a ele o pedido inteiro.
uploads/ é o nome legado
public/ se chamava uploads/. A renomeação aconteceu, os arquivos existentes foram junto, e nada mais deve ser escrito em uploads/.
O nome antigo sobrevive em três lugares, e em todos eles está tudo bem:
- Conversas antigas e referências antigas podem dizer
uploads/.... Ler um caminho desses ainda funciona onde o diretório ainda existe; o agente é explicitamente instruído a não criar uma pastauploads/para corresponder a um deles. - Uma migração única. O proxy roteia todo acesso ao diretório público do membro — uma listagem, um envio, uma entrega — por um único acessor que primeiro migra para o lugar um
uploads/anterior à renomeação. Enganchar o acessor em vez da etapa de provisionamento é o que faz isso alcançar workspaces criados antes da renomeação, já que esses nunca são reprovisionados. Se os dois diretórios existirem de algum jeito, eles são mesclados arquivo a arquivo e o arquivo mais novo vence uma colisão, sendo o mtime a única evidência disponível sobre qual cópia o membro quis guardar. - Comentários e identificadores no código do proxy ainda dizem “uploads dir” em alguns lugares. As constantes são o que vale:
PublicDirNameépubliceLegacyPublicDirNameéuploads, e a única coisa que deveria referenciar a segunda é a migração.
Um teste de regressão garante que toda skill e todo documento de memória entregues nomeiem
public/attachmentse que nenhum deles aponte uma escrita para o diretório legado. Ele existe porque um deles já apontou: a skillshared-contentmandava o agente escrever as entregas emuploads/attachmentsenquanto oFILE_DELIVERY.md, montado no mesmo workspace e lido a cada turno, dizia o contrário.
Para onde ir agora
O cliente de chat para o painel Arquivos em contexto, Skills e memória para os documentos citados aqui, e Solução de problemas quando um arquivo que o agente diz ter escrito não aparece.
O cliente de chat
Este capítulo é para quem usa um agente, não para quem opera um. Ele percorre o chat web — crab-exoskeleton-webapp, o serviço do compose chamado chat-webapp — da entrada na conta até a leitura de um arquivo que o agente escreveu, e dá nome a cada parte da tela para que o resto do livro possa se referir a elas.
Entrando na conta
Não há senhas. Você digita seu endereço de e-mail, o gateway te manda uma mensagem, e você conclui a entrada com um código de seis dígitos: a tela diz “Confira you@company.com para receber um link, abra-o e digite o código de 6 dígitos que aparecer.” Os dois passos vivem na URL em vez de na memória da página, então recarregar no formulário do código mantém você no formulário do código.
Depois disso o navegador guarda um cookie de sessão e nada mais — sem token, sem identidade, sem endereço do upstream. Toda requisição vai do navegador para a camada de servidor do próprio app, dali para o gateway, e só então para o orquestrador.
Se a tela de entrada disser que não consegue falar com o gateway, a stack por trás do app web não está respondendo. Isso é um problema de deploy, não de conta — veja Solução de problemas.
Escolhendo um workspace
Sua conta pode alcançar mais de um agente. Até você escolher um, o centro da tela é o seletor: uma grade intitulada “Escolha um workspace”, agrupada por tenant e por subscription, com um cartão por agente. Um tenant é a organização à qual você pertence; uma subscription é uma conta dentro desse tenant, e um agente pertence a uma delas. Um agente que você só pode ler é marcado com “acesso somente leitura”; o acesso de escrita é a norma e não é rotulado.
Clicar em um agente abre uma conversa nova com ele. Em qual agente você está fica guardado no fragmento da URL, então identificadores de workspace nunca chegam ao servidor.
O formato da tela
Uma vez escolhido um workspace há três colunas, e só a do meio está sempre presente.
- A barra lateral, à esquerda: o cabeçalho da marca, Nova conversa, a lista de lugares para onde você pode ir, sua lista de conversas e um rodapé de conta.
- O centro, que segura o que você está lendo — uma conversa, a tela inicial ou a tela de projetos — com uma trilha de navegação no topo.
- O painel, à direita, que abre ao lado da conversa em vez de por cima dela. Ele é irmão da coluna central no layout, então a transcrição se reacomoda no que sobra em vez de ficar coberta.
No celular a barra lateral e o painel são gavetas de altura total, e o único botão no canto superior esquerdo alterna a barra lateral.
A barra lateral, e a faixa em que ela se recolhe
A barra lateral pode ser arrastada para mais larga ou mais estreita pela sua borda direita, e recolhida com a seta circulada no cabeçalho. Tanto a largura quanto o estado recolhido são lembrados no seu navegador entre uma visita e outra.
Recolhida, ela vira uma faixa de 48 pixels de ícones em três grupos separados por fios finos, na mesma ordem em que a barra lateral aberta é lida: Nova conversa, depois os destinos, depois Conversas. Dois comportamentos valem ser conhecidos porque não são os de sempre:
- Passar o ponteiro sobre o ícone Conversas desliza a lista de conversas por cima da tela como uma prévia, e você pode mover o ponteiro para dentro dela e clicar em uma conversa. É a única entrada que faz isso; passar o ponteiro sobre qualquer outro ícone fecha a prévia.
- Passar o ponteiro sobre qualquer outro ícone mostra uma dica pequena de duas linhas — o nome, e uma linha dizendo o que ele abre — e escolhe qual painel o painel lateral mostraria, sem expandir a barra lateral. A seta circulada acima dos ícones é o único controle que a expande.
Os destinos
Seis linhas, e elas não são todas o mesmo tipo de coisa. Projetos substitui o que está no centro da tela; as outras cinco abrem o painel ao lado dele, e clicar na que já está aberta fecha o painel de novo.
| Linha | O que ela abre |
|---|---|
| Projetos | A tela de projetos — veja Trabalhando com projetos |
| Memória do workspace | Notas fixas que você escreve para o agente |
| Grafo de conhecimento | O que o agente aprendeu por conta própria |
| Tarefas agendadas | O que roda em horário programado, e seus resultados |
| Arquivos | Uploads e arquivos deste workspace |
| Segredos do agente | Chaves que o agente usa, e qual modelo responde |
Memória e grafo de conhecimento são duas coisas diferentes com dois nomes diferentes, e Skills e memória cuida das duas. Tarefas agendadas têm um capítulo próprio, Tarefas agendadas. Se o seu agente roda em um orquestrador mais antigo que os projetos, a linha Projetos não é renderizada, em vez de ser renderizada e morta.
A trilha de navegação
Uma barra no topo da coluna central diz onde você está: o workspace, a subscription, o projeto se você estiver em um, e a conversa. Cada segmento é um caminho de volta para cima — de uma conversa, “para cima” é a tela do próprio projeto. O chevron no fim abre as ações da conversa.
Conversas e histórico
A lista de conversas fica abaixo dos destinos. Ela tem duas visões, alternadas pelo controle Lista / Árvore: a lista conhecida por recência e uma árvore que desenha como o trabalho se desenrolou ao longo do tempo.
Acima da lista há um filtro. Ele aceita texto simples e aceita quatro prefixos que são sintaxe de busca e são iguais em todos os idiomas: tag:, alias:, text: e date:.
Cada linha carrega as ações que valem para uma conversa — renomeá-la, dar a ela um alias e etiquetas coloridas, ou excluí-la. O alias é como você chamou a conversa; o título é o que a primeira mensagem fez dela, e o alias vence sempre que existe um. A exclusão é descrita com todas as letras: o chat sai da sua lista e não dá para desfazer.
O título acima da lista muda conforme onde você está: “Conversas gerais” para as conversas que não pertencem a nenhum projeto, e “Conversas deste projeto” dentro de um.
Nova conversa não cria nada. Ele leva você à tela inicial — um compositor com as conversas do escopo listadas abaixo dele — e a conversa nasce da primeira mensagem que você envia.
O compositor
A caixa no rodapé diz “Escreva para o seu agente… (Shift+Enter para nova linha)”. Enter envia; Shift+Enter quebra a linha.
Três coisas podem acontecer enquanto você digita. Uma / no começo abre o menu de comandos de barra (/rename define o alias da conversa, /tag aplica uma etiqueta). Um @ abre um menu com os arquivos do seu workspace, para você apontar o agente para um deles pelo nome. Todo o resto é uma mensagem comum. Há também um editor markdown avançado atrás de um botão próprio, com prévia ao vivo e as ferramentas de formatação de sempre, para qualquer coisa longa demais para uma caixa de uma linha.
Acima do campo há um espaço de contexto. Ele mostra o que a próxima mensagem vai levar além do seu texto: uma mensagem que você escolheu responder, que viaja como citação; uma entidade do grafo de conhecimento; ou uma tarefa agendada, ou uma das execuções dela, escolhida no painel à direita.
Enquanto o agente responde, o botão de enviar vira Parar a geração. Ele não é decorativo — o turno é mesmo abortado lá em cima e retirado do transcript, e o que você tinha digitado volta para a caixa.
Anexar um arquivo
Três gestos, e todos produzem o mesmo anexo:
- o botão de clipe, que abre o seletor de arquivos do seu sistema operacional;
- colar um arquivo no compositor, que é como entra uma captura de tela (as imagens coladas são renomeadas na chegada, então uma segunda colagem não sobrescreve a primeira);
- arrastar arquivos de fora do navegador, o que mostra sobre a conversa o aviso “Solte para anexar a esta mensagem”.
Os arquivos anexados aparecem como quadrados acima do campo, mostrando a própria imagem quando é uma imagem, para você ver que escolheu a captura certa antes de enviar.
Arrastar arquivos para o painel Arquivos em vez de para a conversa é outro ato: aquele painel é arquivamento, não escrita, e o aviso diz “Solte para adicionar a este workspace”. O que acontece com um arquivo depois que ele chega — e como o agente devolve um para você — está em Arquivos e entrega.
O painel de prévia
Clicar num arquivo, seja no transcript ou no painel Arquivos, oferece sempre Baixar e Visualizar quando o formato é um dos que o app sabe desenhar. As imagens pulam o menu e abrem direto na prévia.
Alguns formatos ganham um leitor próprio: imagens, Markdown, HTML, PDF, código-fonte e as famílias de escritório .docx/.odt/.odp e .xlsx/.ods. Qualquer outra coisa que seja texto — inclusive um arquivo sem extensão nenhuma, como LICENSE, e um arquivo oculto como .gitignore — é mostrada como texto puro. O que fica só para download é o que realmente não dá para ler aqui: arquivos compactados, áudio e vídeo, executáveis, fontes, os binários de escritório anteriores a 2007 e qualquer arquivo cujos primeiros bytes se revelem binários. Até a leitura em texto puro é segura por construção — o texto é escapado dentro de um <pre>, e o orquestrador serve todo arquivo de mídia como um anexo que o seu navegador não vai renderizar, então os seus arquivos nunca viram uma página nesta origem.
O que o painel sabe fazer, uma vez que tem o arquivo:
- Markdown e HTML têm duas leituras, e o controle “Como ler este arquivo” alterna entre Renderizado e Código-fonte.
- Os PDFs são desenhados pelo próprio app, página a página, com anterior/próxima, zoom e ajuste à largura. Se o navegador não der conta, o painel avisa e oferece o download.
- As visões parciais assumem isso. Uma planilha mostra “Mostrando as primeiras n linhas”, uma apresentação diz que o texto dela foi extraído e que os slides estão no arquivo, e um arquivo grande demais para a prévia pede que você o baixe.
- Um arquivo que se revela não ser texto avisa isso assim que os bytes chegam, em vez de pintar uma tela de caracteres de substituição.
Os scripts dentro de uma prévia de HTML ficam desligados por padrão. Ligá-los abre um diálogo que declara as duas metades com honestidade: com os scripts ligados, a página pode mandar o que o documento contém para qualquer endereço da internet, mas a sua sessão, os seus cookies e o resto do app ficam fora do alcance dela. A permissão vale até você fechar o navegador e cobre todo arquivo HTML que você abrir nesse tempo.
Enquanto o agente trabalha
A faixa do assistente mostra o que o turno está fazendo — “Pensando…”, “Usando ferramenta”, uma sequência recolhida de passos de narração que diz quantos são, e o raciocínio do próprio modelo atrás de uma dobra. Se o fluxo é cortado, o app diz que a conexão caiu e que o agente continua trabalhando, que é uma mensagem diferente da que ele mostra quando o seu aparelho fica sem rede.
Sair de uma conversa no meio de um turno não interrompe nada. Uma barra no rodapé lista as conversas rodando em segundo plano, cada uma com o seu estado — trabalhando, reconectando, resposta pronta — e clicar numa delas leva você de volta.
Um aviso acima do painel central aparece quando mudou algo que o seu agente só vai perceber depois de reiniciar: um segredo que você salvou, um modelo ou uma skill compartilhada que um administrador mudou, ou um reinício que um administrador pediu. Ele nomeia o motivo e dá o botão a você, para que você escolha a hora e nenhum turno em andamento seja cortado.
O resto da moldura
O rodapé da barra lateral traz o seu endereço de e-mail, o seletor de idioma e Sair. Acima dele ficam o link para o console de administração — mostrado só se você puder chegar até ele — e Instalar app, porque o app web é instalável como PWA. No iPhone e no iPad o app explica o caminho Compartilhar → Adicionar à Tela de Início do Safari, porque o Safari não tem um botão de instalar próprio.
Para onde ir agora
Trabalhando com projetos explica o único destino que este capítulo deixou de lado de propósito, e Tarefas agendadas, o único painel cujo comportamento depende de qual harness o seu agente usa. Para o que o painel de memória e o grafo de conhecimento guardam de fato, leia Skills e memória; para envios e entregas, Arquivos e entrega.
Trabalhando com projetos
Um projeto divide um agente por assunto. Este capítulo é para o membro que percebeu que toda conversa com o seu agente compartilha os mesmos arquivos, as mesmas instruções e as mesmas anotações acumuladas, e quer um assunto mantido à parte do resto.
O que é um projeto
O seu agente tem um workspace: um diretório no host que guarda os arquivos dele, a memória dele e os transcripts de tudo que você já disse a ele. Um projeto é um segundo workspace ao lado do primeiro, do mesmo agente e do mesmo você, com uma cópia própria dos três.
No disco, o workspace de um projeto é workspace-<id>, irmão de workspace/ e nunca filho dele. Os dois harnesses o organizam do mesmo jeito, que é a razão inteira de o layout ter mudado para esta forma — Agentes, workspaces e projetos cobre o que há dentro.
O app enuncia a ideia em uma frase na tela de projetos: um projeto guarda arquivos, memória e instruções próprios, e herda o modelo, as skills e as credenciais deste agente.
Por que você criaria um
Porque “esta conversa é sobre a análise do ensaio de sementes, mantenha os arquivos e as instruções dela longe do meu outro trabalho” é, de outro modo, indizível. Tudo que você envia cai numa pilha só; toda anotação fixa que você escreve vale para tudo; e uma instrução que é certa para um assunto é ruído em toda outra conversa.
Um projeto responde às três de uma vez. O que ele não é: um jeito de dar acesso a outra pessoa. Um projeto pertence a uma tupla (tenant, subscription, agente, usuário), exatamente como o workspace ao lado do qual ele fica, e não há compartilhamento entre membros nem delegação entre projetos.
O que muda dentro de um projeto
| O workspace do próprio agente | Um projeto | |
|---|---|---|
| Arquivos | Compartilhados por toda conversa fora de um projeto | Do próprio projeto |
| Anotações de memória | Do próprio agente | Do próprio projeto |
| Instruções | A identidade do agente | A identidade mais o que você escreveu |
| Conversas | Listadas como “Conversas gerais” | Listadas dentro do projeto |
| Tarefas agendadas | Do próprio agente | Do próprio projeto |
| Grafo de conhecimento | Um grafo | O mesmo grafo |
| Modelo, skills, credenciais | Do agente | Herdados, não sobrescrevíveis |
A linha das tarefas agendadas tem uma ressalva de harness: só o ganglion realmente arquiva um agendamento dentro do projeto em que ele foi criado. O picoclaw mantém um único repositório de agendamentos por container, o que é uma limitação daquele harness e não uma decisão — veja Tarefas agendadas.
Duas outras linhas são as que as pessoas entendem errado.
O grafo de conhecimento não é por projeto. O painel de memória e o grafo de conhecimento são duas memórias diferentes com dois nomes diferentes. As anotações que você escreve no painel de memória pertencem ao projeto em que você está; o grafo que o agente constrói para si mesmo tem você como escopo e atravessa os seus projetos — um servidor de grafo só, alcançado com o projeto nomeado num cabeçalho, em vez de um grafo separado por projeto. Veja Skills e memória.
Os arquivos compartilhados por um administrador ficam no workspace principal. Arquivos e conteúdo que um operador publica para um tenant ou uma subscription descem só até o workspace do próprio agente; eles não são copiados para os workspaces de projeto. Já as skills compartilhadas, o modelo e as suas credenciais são herdados.
Um projeto também herda a identidade do seu agente, em vez de substituí-la. As instruções que você digita são somadas ao que o agente já sabe sobre si mesmo, e o app diz isso onde você as digita: “Diga o que é este projeto e como ele deve se comportar aqui.”
No harness ganglion a separação é imposta pelo kernel, não por convenção: um turno dentro de um projeto roda com o confinamento de sistema de arquivos enraizado no diretório daquele projeto, então a ferramenta de shell não alcança o workspace principal nem nenhum outro projeto. Foi isso que o layout de irmãos comprou — um projeto dentro do workspace principal nunca poderia ter passado de uma convenção.
Criando um
Abra Projetos na barra lateral. Diferente dos outros cinco destinos, este substitui o que está no centro da tela em vez de abrir um painel ao lado — um projeto é um lugar em que você entra, não um painel que você consulta.
Aperte Novo projeto e preencha dois campos:
- Nome — por exemplo, “Ensaio de sementes 2026”. O identificador é derivado do nome pelo orquestrador; você nunca digita um.
- Instruções — opcional, e a coisa em que vale gastar um minuto. O texto de exemplo é um bom modelo: “Sempre cite o protocolo do ensaio e responda começando pelo número da parcela.”
Salvar cria o workspace. A tela então conta a você a única consequência que, de outro modo, você descobriria como uma primeira resposta lenta: o agente reinicia na sua próxima mensagem. Um projeto muda o que o container do agente tem montado, então o container é reconstruído antes do próximo turno.
Cada projeto é um cartão nessa tela, mostrando o nome, as instruções e a data em que foi criado. O projeto em que você está agora é marcado como Atual, em vez de ser escondido ou movido para a frente — pedir para ver a lista não é a mesma coisa que sair de onde você está.
Trabalhando dentro de um
Entrar num projeto muda o escopo de quase tudo na tela. O caminho no topo o nomeia. A lista de conversas mostra “Conversas deste projeto” em vez de “Conversas gerais”. A tela inicial diz “Pergunte qualquer coisa neste projeto para começar” em vez de nomear o agente. Os painéis Arquivos, memória e Tarefas agendadas se dirigem todos ao workspace do projeto, não ao do agente.
Uma regra pega as pessoas de surpresa: uma conversa permanece no projeto em que começou. O seletor de projeto é oferecido quando você começa uma conversa e travado depois disso, porque o transcript de uma conversa vive fisicamente no workspace daquele projeto. O app diz isso com todas as letras — “Uma conversa permanece no projeto em que começou.”
Para mover trabalho entre projetos, passe o arquivo você mesmo pelo painel Arquivos. O agente não consegue copiá-lo de um para o outro; de dentro de um projeto, o outro não está lá.
Editando e excluindo
O lápis num cartão edita o nome e as instruções. Só o que você mudou é enviado, então renomear um projeto não apaga as instruções dele.
Excluir é a ação destrutiva, e a confirmação diz exatamente o que vai embora: “Os arquivos, a memória e todas as conversas dele são removidos. Não dá para desfazer.” Como criar, também reinicia o agente na sua próxima mensagem.
Excluir um projeto não exclui as tarefas agendadas que foram arquivadas nele. Elas continuam disparando, e o orquestrador de propósito volta a mostrá-las na lista de tarefas do próprio agente depois disso — uma tarefa que você não vê é uma tarefa que você não consegue parar. Exclua as tarefas de um projeto antes de excluir o projeto. Veja Tarefas agendadas.
Quais agentes têm projetos
Os dois harnesses que este livro cobre servem projetos: o picoclaw os constrói com a própria maquinaria de agentes e despacho, e o ganglion toma o projeto como um cabeçalho da requisição e trabalha a partir do workspace irmão correspondente. Veja Harnesses para o que é um harness e como um deles é escolhido.
O que não os tem é um orquestrador mais antigo que o recurso. Diante de um desses, o app web informa que o agente não suporta projetos, e tanto a linha da barra lateral quanto a tela são omitidas por completo, em vez de mostradas e mortas.
Para onde ir agora
Tarefas agendadas é a única superfície com escopo de projeto que tem uma restrição própria. Agentes, workspaces e projetos descreve como o workspace de um projeto aparece no disco, e Skills e memória explica as duas memórias que um projeto separa e não separa.
Tarefas agendadas
Uma tarefa agendada é uma mensagem que o seu agente manda para si mesmo num horário marcado. Este capítulo explica o que é uma delas, o que o painel Tarefas agendadas mostra a você e o fato incômodo sobre elas: ler tarefas funciona nos dois harnesses, mas criar, editar e excluir pela API é só do ganglion, e qualquer outro harness responde 501.
O que é uma tarefa agendada
Uma tarefa é um registro guardado: um nome, um agendamento e uma mensagem. Quando chega a hora, essa mensagem é repassada como um turno comum — o agente a lê, trabalha e escreve um transcript — e ninguém está esperando do outro lado. O resultado é guardado e lido depois no painel. Um disparo feito pelo agendador do próprio orquestrador, que é como o ganglion funciona, não é entregue a ninguém, e isso é proposital.
Existem três tipos de agendamento, e o painel nomeia cada um como o registro nomeia: uma expressão cron (Cron 0 7 * * 1), um intervalo que se repete (A cada 6h) ou um instante único (Uma vez em …). Uma tarefa também pode ser marcada para se remover depois de rodar, o que o painel mostra como “Se remove depois de executar”.
Isto é trabalho sem ninguém olhando. A mensagem é repassada a cada ocorrência, para sempre, sem ninguém lendo o resultado, e é por isso que o orquestrador a limita a 8 KiB e limita a trinta minutos um turno disparado.
O painel
Abra Tarefas agendadas na barra lateral e ele aparece ao lado da conversa, para você ler uma execução passada sem perder o chat em que está. O subtítulo dele declara o escopo com honestidade: “O que o agente executa em horários programados, e o que cada execução produziu. Somente leitura: peça ao agente para criar ou alterar uma tarefa.”
Cada tarefa mostra quando roda de novo, quando rodou pela última vez, se está desabilitada e para onde ela entrega, se o registro dela nomeia um destino. Abaixo dela ficam as execuções. Clicar numa delas abre o transcript daquela execução — as mensagens, as chamadas de ferramenta e os resultados delas — e Voltar às tarefas devolve você à lista.
Duas coisas sobre as execuções valem saber antes de você ler demais nelas:
- Só a execução mais recente de uma tarefa ativa registra um status. As execuções anteriores mostram quanto tempo levaram e quanto registraram, e nada mais. Não há marcas de sucesso ao lado delas porque o armazenamento não guarda um resultado por execução de onde desenhar uma.
- Uma execução pode sobreviver à tarefa dela. Uma tarefa que se remove depois de rodar deixa os transcripts para trás, então o painel agrupa esses sob “Tarefa removida” — a tarefa não está mais agendada, mas o trabalho que ela fez continua registrado.
O painel também oferece Ocultar concluídas, que dobra para fora as tarefas que já rodaram e não vão rodar de novo, uma atualização Buscar novas tarefas (o agente pode agendar algo entre duas visitas) e um controle Referenciar no chat em qualquer tarefa ou execução. Esse último joga a tarefa ou a execução no espaço de contexto do compositor, para você perguntar sobre ela na conversa ao lado sem copiar nada.
Quem pode criar uma, e onde
Esta é a parte que depende do harness do seu agente. Um harness é o programa dentro do container do agente que de fato roda o laço; os dois que este livro cobre são o picoclaw e o ganglion, e Harnesses explica como um deles é escolhido.
Ler funciona nos dois. Isso é proposital, e o próprio código do orquestrador diz por quê: esconder um agendamento que não vai disparar é estritamente pior do que mostrá-lo e dizer isso, porque uma tarefa que você não vê é uma tarefa que você não consegue parar. Então GET /v1/cron/tasks e GET /v1/cron/runs são atendidos seja qual for o harness, e não precisam de um container no ar.
Escrever é só do ganglion. POST, PATCH e DELETE em /v1/cron/tasks são recusados para qualquer outro harness com um 501, que o nomeia:
creating scheduled tasks over this API is not available on the
"picoclaw" harness (agent "beta"): its agent creates them itself
Isso não é um bug e não é um esboço. No picoclaw o agendamento vive em timers dentro do container, na memória do próprio agente, onde o orquestrador não os enxerga — então escrever o arquivo de jobs do picoclaw de fora produziria um registro que você lê e um timer que nunca mudou. Um 501 é pior de receber e muito melhor de depurar.
A razão de o ganglion poder ser escrito é a imagem espelhada. Ele não tem agendador nenhum; o orquestrador guarda o agendamento, num arquivo mantido acima do ponto de montagem do workspace do container, e dispara os jobs ele mesmo. Não há um segundo escritor para discordar. Manter esse arquivo fora do container também é uma propriedade de segurança: nada dentro do agente o alcança, então um turno conduzido por texto não confiável não consegue agendar os próprios turnos futuros.
O app web não escreve nem um nem outro. Seja como for que o seu agente esteja configurado, o painel hoje é somente leitura — a camada de servidor do app expõe só as duas rotas de leitura, e não há botão de criar em lugar nenhum do chat. Então, na prática:
| O seu agente roda | Como uma tarefa passa a existir |
|---|---|
| picoclaw | Você pede ao agente numa conversa; ele agenda o próprio job |
| o ganglion | Nada que um membro toque cria uma ainda |
Essa segunda linha é o estado honesto das coisas, e vale soletrar. Pedir a um agente ganglion que agende algo não vai funcionar: ele não alcança o armazenamento, e o guia do próprio workspace dele diz isso a ele — “Também não existe cron/: o seu trabalho agendado é guardado fora desta árvore, onde você não alcança.” O app web não tem controle de criar. E as rotas de escrita também não podem ser chamadas do navegador, porque o navegador guarda um cookie de sessão e nada mais: nenhum token, nenhum endereço de destino. As rotas estão construídas e o armazenamento está no lugar, mas a superfície que deixaria um membro chegar até elas não foi escrita.
A frase “peça ao agente” do painel descreve, portanto, o caso do picoclaw, e é o único pedaço da tela que ainda não distingue os dois harnesses.
O que a diferença custa a você de verdade
Se você só lê as suas tarefas, o harness quase não aparece. Onde ele aparece é em se o agendamento dispara ou não.
Um agendamento do picoclaw são timers no próprio processo, então um container parado não dispara nada. Num agente configurado para desligar quando fica ocioso, as tarefas são reais, aparecem listadas e são inertes — e o painel diz isso, em vez de deixar você supor que a tarefa rodou:
Estas tarefas não estão sendo executadas. Esta instância é desligada quando fica ociosa, e um agendamento só dispara enquanto ela está no ar. As tarefas abaixo continuam registradas — um administrador precisa mudar esta instância para contínua para que voltem a rodar.
Um agendamento do ganglion é guardado pelo orquestrador, que está sempre no ar e que liga o container para entregar o turno. Escalar a zero é exatamente o que aquele desenho tinha em mente, então o aviso nunca aparece para um agente ganglion.
Mais três propriedades do agendador do orquestrador valem saber, porque são escolhas e não acidentes:
- Um job é reivindicado antes de rodar, não registrado depois. Um orquestrador que morre no meio de um turno perde aquela execução, em vez de redisparar a cada arranque. Para trabalho sem ninguém olhando, um resumo diário perdido é uma lacuna; um redisparado é um agente fazendo trabalho de verdade duas vezes, sem ninguém vendo.
- Um agendamento atravessado por um cochilo dispara uma vez só. Repassar cada ocorrência perdida entregaria de uma vez um fim de semana de turnos de hora em hora.
- Dois turnos agendados nunca se sobrepõem num mesmo workspace. Um job que chega à hora enquanto outro seu está rodando fica pendente e é pego na passagem seguinte. As suas próprias mensagens não são afetadas em nenhuma das direções — uma execução agendada nem as bloqueia nem é bloqueada por elas.
Tarefas e projetos
As tarefas agendadas têm escopo como tudo mais num projeto: dentro de um projeto você vê as tarefas daquele projeto e nada mais, e fora de um você vê as tarefas que pertencem ao workspace do próprio agente. Agendamentos por projeto são algo que só o ganglion faz — no picoclaw os jobs de todos os projetos caem num repositório só, o que é uma limitação daquele harness e não uma decisão.
Uma consequência vale repetir do capítulo de projetos. Excluir um projeto derruba o roteamento dele, mas não os jobs agendados dele, então eles continuam disparando. Em vez de deixá-los ficar invisíveis, o orquestrador lista os jobs órfãos de um projeto excluído na lista de tarefas do próprio agente, onde você ainda consegue achá-los e pará-los.
Para onde ir agora
Harnesses explica a escolha em torno da qual este capítulo inteiro gira e por que uma chave harness: não declarada ainda significa picoclaw hoje. Trabalhando com projetos cobre o escopo, e o Guia de administração cobre o modo de instância que um administrador precisa mudar para fazer um agendamento do picoclaw disparar.
Guia de administração
Este capítulo é para quem cuida de um deploy no dia a dia: adicionar pessoas, dar material de trabalho aos agentes delas, decidir qual modelo elas recebem e consertar a configuração de um membro quando ela dá errado. Ele supõe que a stack já está instalada — veja instalação se não estiver.
Tenants, subscriptions e escopos
A identidade nesta stack vem do Mycelium, um gateway de API externo. Um tenant é uma organização; uma subscription é uma conta sob um tenant, e é o único nível em que existe uma lista de membros. Um tenant pode ter muitas subscriptions.
Tudo o que um administrador publica é publicado em um escopo, que é ou um tenant ou uma subscription. Um escopo de tenant alcança todo mundo abaixo dele; um escopo de subscription alcança apenas aquela subscription. Onde os dois têm algo a dizer, o mais estreito vence.
O que você pode administrar depende do seu tier, resolvido a partir do perfil Mycelium a cada requisição (CallerTier em crab/crab-shell-proxy/internal/authz/authz.go). A equipe da instância tem autoridade em todo lugar; um tenant-owner ou tenant-manager, sobre um tenant e todas as subscriptions abaixo dele; um subscriptions-manager, sobre exatamente uma subscription. A área administrativa lista apenas os escopos que você pode gerenciar, e o proxy confere de novo a cada escrita — a interface se estreitar sozinha é uma conveniência, não o portão.
O formato da área administrativa
A área administrativa fica no webapp de chat, em /admin. Ela tem dois itens de primeiro nível: Workspaces, que é tudo o que tem escopo, e Branding, que vale para a instância inteira (railItems em crab/crab-exoskeleton-webapp/app/admin/admin-nav.ts).
Em Workspaces você escolhe primeiro um agente, depois um escopo. Essa ordem é deliberada: os agentes vêm da configuração do proxy e existem antes de qualquer tenant, então pedir o escopo primeiro dava a entender que os agentes eram uma propriedade de uma subscription. Cada seção então age sobre esse par (agente, escopo).
As seções são Files, Secrets, Skills, Persona, Model, Config e Members, nessa ordem (SECTION_TABS em crab/crab-exoskeleton-webapp/app/admin/tabs.ts). Quais delas um dado agente oferece é decidido em agent-scope.ts, e hoje um agente ganglion e um agente picoclaw oferecem as sete. Há também um endereço legado “all agents” que guarda conteúdo compartilhado escrito antes de o conteúdo passar a ser por agente; ele oferece só as seções de conteúdo, porque escrever uma persona, atribuir um modelo e convidar alguém precisam de um agente real a que se prender.
Vale dizer o modelo mental uma vez: você edita material compartilhado em um escopo, o proxy mescla os escopos em uma visão efetiva no disco, e o container de cada membro monta essa visão somente leitura. Um membro nunca vê o workspace privado de outro.
Arquivos
Os arquivos enviados aqui chegam ao agente de cada membro dentro do escopo, como bind mounts somente leitura em workspace/.shared/<layer> dentro do container — um diretório por camada, chamados tenant, subscription, tenant-agent e subscription-agent (sharedFileBinds em crab/crab-shell-proxy/internal/docker/shared.go). O agente pode lê-los; não pode alterá-los.
Files é a única seção cujas escritas não precisam reiniciar o container. O mount é ao vivo, então um arquivo novo aparece imediatamente em todos os containers em execução, e o proxy deliberadamente não recria o container — fazer isso costumava truncar a conversa de um membro no meio do turno.
Secrets
Secrets são credenciais de que os agentes precisam sem que elas fiquem gravadas em uma imagem ou em um template. Quatro formatos são suportados (crab/crab-shell-proxy/internal/docker/secrets.go):
| Formato | Onde ele aparece |
|---|---|
dotenv | um arquivo .env com linhas NAME=value |
json | um objeto secrets.json |
file | um arquivo por secret, o conteúdo é o valor |
native | um slot nomeado na configuração do próprio harness |
Os três primeiros servem para uma skill que lê uma credencial de algum lugar convencional. O formato native preenche um slot que o próprio runtime conhece, e só dois formatos de slot são aceitos: web.<provider> para a chave de um provedor de busca, e model_list.<model>.api_keys para um modelo que o inventário já conhece (validateNativeSlot). Qualquer outra coisa é recusada.
Um secret web.<provider> é o que liga a busca na web de um agente. Os nomes de provedor que os dois harnesses aceitam não são idênticos, então leia modelos e provedores antes de registrar um.
Os secrets são somente escrita pela API. Você pode listar os nomes que existem; nada lê um valor de volta.
Skills
Uma skill é uma pasta que contém um SKILL.md — um front matter YAML com um name e uma description, depois um corpo em Markdown — mais os arquivos de apoio de que ela precisar. Skills e memória explica para que elas servem.
Publique uma em um escopo escrevendo o SKILL.md dela ali mesmo ou enviando um zip da pasta; o proxy aceita uma parte file ou uma parte body e recusa uma requisição que não traga nenhuma das duas. Você pode baixar de volta como zip qualquer skill publicada.
As skills se mesclam em quatro camadas, em precedência crescente: tenant, tenant+agente, subscription, subscription+agente (syncEffectiveSkills em crab/crab-shell-proxy/internal/docker/skills.go). Uma pasta de skill presente em mais de uma camada é tomada inteira da camada mais específica — as camadas não se mesclam arquivo a arquivo. O resultado mesclado é escrito em um diretório cujo inode é mantido estável, então uma edição chega aos containers em execução sem recriá-los.
Persona
Quatro arquivos definem a identidade de um agente, e o conjunto é fixo e fechado (PersonaFiles em crab/crab-shell-proxy/internal/docker/persona.go):
| Arquivo | O que é | Como é entregue |
|---|---|---|
AGENT.md | o que o agente faz e como ele se comporta | mount somente leitura |
SOUL.md | sua voz | mount somente leitura |
HEARTBEAT.md | sua lista de tarefas recorrentes | mount somente leitura |
USER.md | o que se sabe sobre o membro | apenas semeado |
O conjunto é fechado porque esses endpoints escrevem na raiz de um workspace: um nome de arquivo arbitrário aqui seria uma escrita de arquivo arbitrária alcançando todos os containers dentro do escopo.
Os três primeiros são resolvidos por workspace em ordem de precedência — subscription+agente, depois tenant+agente, depois o template do agente (resolvePersonaSources) — e montados somente leitura. Isso é precedência, não mescla: dois arquivos AGENT.md não podem ser combinados em uma identidade só. Um membro não pode editá-los, e uma edição feita dentro do container nunca sobrevive a um restart.
USER.md é a exceção, porque o agente escreve nele: é onde o agente acumula o que aprende sobre o membro. Defini-lo aqui determina de onde um workspace novo começa, e nunca sobrescreve um que já existe.
Cada linha diz se o arquivo está definido neste escopo ou herdado. O editor pré-carrega o que o agente de fato executa, resolvido cascata abaixo, para que você edite uma identidade real em vez de uma página em branco; salvar é o que a torna deste escopo. Limpá-la deixa o workspace voltar para o escopo mais amplo, ou para o template.
Model
Um administrador controla duas coisas separadas aqui: um inventário de modelos que o deploy pode servir, e uma cascata que decide para qual deles um dado workspace é resolvido.
O inventário é uma lista só para o proxy inteiro, guardada em um único arquivo — model-registry.db na raiz de dados do proxy. Um modelo é registrado uma vez, com as credenciais dele, e cada escopo aponta para esse registro em vez de guardar uma cópia. Um registro carrega um provedor, um nome de modelo usado como identificador em todo o resto, o identificador de fato enviado ao endpoint, uma URL base de API, uma chave de API somente escrita e a cadeia de fallback ordenada do próprio modelo.
A cadeia de fallback é uma propriedade do modelo, não de um escopo: ela nomeia outros modelos registrados para tentar depois deste. O editor oferece só modelos ativos como candidatos, porque o resolvedor pula um fallback não ativo de qualquer jeito. A ordem em que a lista do inventário é exibida é só apresentação e não tem efeito nenhum na resolução.
Um modelo tem um de três status. Active é oferecido normalmente. Disabled é reversível e exige que nada referencie o modelo — nenhum workspace, nenhum padrão de escopo, nenhuma cadeia de outro modelo — então o proxy recusa com a lista de quem o referencia quando algo o faz, e a mesma checagem impede apagar um modelo. Deprecated é para um modelo que as pessoas ainda estão usando: exige que você nomeie um substituto, e esse substituto só é seguido para workspaces que ainda não materializaram o modelo depreciado. Essa única condição é o que produz “membros novos ganham o sucessor, membros existentes ficam com o que têm” sem um segundo caminho de código.
A cascata
Seis níveis decidem qual modelo um workspace recebe. Leia de cima para baixo; cada um cobre menos gente que o de cima e o sobrepõe (Resolve em crab/crab-shell-proxy/internal/registry/resolve.go):
global the whole instance
agent every tenant running this agent
tenant one tenant
subscription one subscription
user (a pin) one person, set by an administrator
the member's own one person, registered by themselves
O nível mais estreito que nomeia um modelo vence, e é nele que um workspace recém-provisionado cai. Os níveis abaixo de um que foi limpo continuam definidos e assumem, e é por isso que o painel desenha a escada inteira em vez de um nível por vez: você vê o que a sua escrita sobrepõe e para onde limpá-la cairia. Um nível que você não tem autoridade para ler é desenhado como ilegível, e não como não definido — um nível de instância inteira que você não pode ver pode ainda estar cobrindo o seu escopo.
Você só pode escrever no nível em que o seu escopo está. Com uma subscription selecionada você pode editar o nível da subscription e pins individuais; com um tenant selecionado você pode editar o nível do tenant e mais nada (editableLevels em crab/crab-exoskeleton-webapp/lib/models.ts). Os níveis de agente e global são legíveis aqui e não graváveis aqui — o nível de agente é rotulado com o nome do agente selecionado, mas alcança todos os tenants que rodam aquele agente, o que, dentro de uma tela cuja barra diz “esta subscription”, soa como algo bem mais estreito do que é. O primeiro padrão global ou de agente tem de ser escrito por fora.
Um pin é uma pessoa só, e supera todo nível definido por administrador acima dele. Use-o para um indivíduo; para mover um grupo, defina o nível do escopo dele. Os pins vivem dentro de uma subscription, e uma pessoa só aparece depois que tem um workspace — isto é, depois do primeiro chat dela.
Um padrão de escopo que nomeia um modelo que não existe mais é pulado e a cascata segue para o próximo nível, porque um padrão desatualizado importado no boot recusaria todo workspace daquele tenant. Um pin que nomeia um modelo ausente não é pulado; é uma falha dura, porque foi definido de propósito.
Se nada resolve, o que acontece depende do harness. Um workspace picoclaw é recusado e nada é escrito, porque o picoclaw falha na inicialização quando o padrão dele nomeia um modelo ausente da sua lista de modelos, então um padrão silencioso produziria um container permanentemente sem boot. Um workspace ganglion cai para o modelo declarado na entrada de catálogo do próprio agente, e esse fallback deliberadamente não é registrado como uma atribuição.
Os modelos dos próprios membros
Um membro pode registrar modelos próprios, com as chaves de API dele, e um modelo pessoal supera todo nível acima dele, incluindo o pin de um administrador. A chave é dele, a escolha é dele — o controle do administrador aqui é uma trava de escopo, não um veto pessoa a pessoa.
Essa trava tem duas opções independentes, e cada uma pode ser ligada, desligada ou deixada herdando de um escopo mais amplo (ScopePolicy em crab/crab-shell-proxy/internal/registry/usermodels.go):
| Opção | Não definida em lugar nenhum significa |
|---|---|
| os membros podem usar modelos próprios | permitido |
| eles podem nomear um endpoint fora do catálogo | negado |
Os padrões são deliberadamente opostos. Os modelos pessoais são o ponto do recurso, então são permitidos a menos que alguém se oponha; um membro que não pode nomear um endpoint não pode apontar a instância para um, o que é uma classe inteira de risco em vez de um caso governado dela. A política tem uma cascata própria — subscription, tenant, agente, global — e o primeiro nível que define uma opção decide. Mudar uma política não reinicia nada; ela vale a partir da próxima vez que cada workspace for preparado.
Separadamente, a lista User models deixa você desabilitar um modelo de um membro. Ele começa habilitado: a opção é uma intervenção, não um portão a passar. Desabilitá-lo re-prepara todo workspace que o estava usando e levanta um aviso de restart em vez de forçar um. Os membros não têm uma opção dessas — o jeito deles de parar de usar um modelo é parar de selecioná-lo.
Um membro pode registrar no máximo dez modelos, precisa fornecer um endpoint e uma chave para cada um, e só pode escolher de uma lista de provedores mais estreita que a do inventário do administrador: a família compatível com OpenAI, com os provedores OAuth e os específicos de fornecedor deliberadamente ausentes.
O que acontece depois que um workspace tem um modelo — a cadeia de fallback na hora do turno, a cadeia de visão, a geração de imagens e quais ferramentas aparecem — está em modelos e provedores.
Config
Esta seção alcança a configuração de runtime da instância de um membro, ou de uma chave em uma subscription inteira.
Bulk trabalha em uma subscription por vez. Você nomeia um caminho pontilhado até um único valor, lê primeiro a distribuição atual — o que cada membro tem agora — e só então escreve. As instâncias cuja chave está ausente, bloqueada por um caminho conflitante, ou cuja configuração não pode ser lida, ficam de fora da escrita e são listadas para que você as conserte uma a uma.
As chaves que o proxy possui são recusadas, porque uma mudança ali não sobreviveria — o proxy as reescreve toda vez que prepara um workspace. ManagedConfigPaths em crab/crab-shell-proxy/internal/docker/instance_config.go é a lista: a lista de modelos, o provedor padrão, o nome do modelo e os fallbacks, o caminho do workspace, o gerenciador de contexto, a opção do canal pico e a entrada MCP do próprio grafo de memória do proxy.
Single instance abre a configuração de um membro como JSON formatado ou como uma árvore, valida e escreve de volta.
Como essa escrita chega a um agente ganglion é diferente do picoclaw. O config.json de um workspace picoclaw é semeado uma vez e depois editado no lugar, então uma edição simplesmente fica. A configuração de um workspace ganglion é renderizada inteira toda vez que o container é preparado, então uma edição escrita no arquivo sumiria no turno seguinte. Por isso o proxy guarda a edição em um overlay por instância ao lado do arquivo e a reaplica a cada renderização (crab/crab-shell-proxy/internal/docker/ganglion_overlay.go). Um overlay quebrado é registrado no log e pulado, em vez de tratado como fatal.
O overlay é também o único caminho pelo qual
agents.defaults.image_modelouagents.defaults.image_gen_modelpodem chegar a um agente ganglion. O renderizador do proxy nunca emite nenhuma das duas chaves, então nenhuma aparece no seletor de chaves do bulk — que é gerado a partir desse renderizador — e as duas têm de ser adicionadas à mão no editor de instância única. O editor compara o documento inteiro e registra cada folha alterada que não seja malformada nem de propriedade do proxy, e estas duas não são nem uma coisa nem outra. Veja modelos e provedores.
Uma mudança de configuração vale a partir do próximo start da instância. O controle de restart no menu decide como esse reinício acontece: now, o padrão; schedule, que reinicia o escopo na hora que você escolher; ou notice, que deixa o momento para o membro, que vê um aviso de restart pendente no chat (crab/crab-shell-proxy/internal/httpapi/restart_policy.go). A mudança em si sempre se propaga na hora — só o reinício do container é adiado.
Uma instância também pode ser fixada em um modo de ciclo de vida diferente do que o agente dela declara. A razão de isso existir são as tarefas agendadas que rodam a partir de timers dentro de um container: um membro que precisa de um relatório diário é motivo para manter um container rodando sem forçar o agente inteiro a isso.
Members
A seção Members lista as pessoas da subscription selecionada, com as instâncias e os arquivos guardados que cada uma tem. Um escopo de tenant não tem lista de membros, porque pertencer é um fato de nível de subscription.
Convidar é o maquinário de convidados do Mycelium, alcançado por JSON-RPC; o webapp não guarda estado de convite nenhum. Dois fatos fazem isso funcionar (crab/crab-exoskeleton-webapp/lib/invitations.ts):
- O nome de um papel de convidado é a chave do agente. O gateway declara
protectedByRoles = [{ name = "alpha", permission = "write" }]e o Mycelium cria esses papéis no boot, então “convidar alguém para o agentealpha” é literalmente “dar a essa pessoa o papel de convidado chamadoalpha”. - A permissão vive no papel, não na chamada de convite. Escolher leitura ou escrita é escolher qual id de papel enviar. Escrita é o que conversar exige; leitura é só visualizar.
O formulário não pergunta qual agente, e isso é uma correção, não um esquecimento: ele usa o agente que você selecionou no portão. Ele já teve um seletor de agente próprio, três campos abaixo, o que significava duas seleções de agente em uma tela só, e a que decidia o acesso era a invisível. Revogar se faz pela linha da própria pessoa na lista, não pelo formulário de convite.
Você pode ver os metadados dos arquivos privados de um membro — nome, tamanho, hora de modificação — e apagar um deles. Deliberadamente não há como abrir, baixar ou pré-visualizar daqui o arquivo privado de um membro, em nenhum tier. O painel não expõe nada disso e o proxy não tem endpoint nenhum para isso.
Branding
Branding vale para a instância inteira e não tem escopo de tenant, agente ou subscription. Ele define o nome da aplicação e deixa você enviar ou redefinir um logo claro, um logo escuro e um ícone da aplicação. Ele aparece só para quem pode editá-lo, porque um console que oferece algo que quem chamou não pode usar soa como uma tela quebrada, e não como uma resposta sobre a autoridade dessa pessoa. O proxy é o portão de verdade.
Para onde ir agora
Modelos e provedores é a outra metade da seção Model. Criando um agente customizado cobre adicionar um agente para estas seções administrarem. Resolução de problemas reúne as falhas que as pessoas de fato relatam.
Criando um agente personalizado
Este capítulo percorre a adição de um novo agente a um deploy, do primeiro diretório ao primeiro chat. Ele é escrito como um exemplo trabalhado: um agente chamado scribe, rodando o harness ganglion. Leia o guia do administrador primeiro, se você ainda não conheceu tenants, subscriptions e escopos.
O que é um agente
Um agente é uma personalidade com nome com quem os membros conversam. alpha e beta são os dois que vêm de fábrica. Um agente não é um container: cada membro que fala com o scribe ganha o próprio container, o próprio workspace e o próprio histórico, todos clonados do mesmo ponto de partida. Veja agentes e workspaces para esse modelo de isolamento.
Três coisas precisam existir antes que um membro consiga alcançar um novo agente.
- Uma entrada no catálogo de agentes do proxy, que nomeia o agente, o runtime que ele usa, o modo de ciclo de vida dele e o modelo padrão dele.
- Um diretório de template no disco, que fornece os arquivos de identidade do agente.
- Uma rota no gateway, porque o Mycelium é a porta de entrada e ele não encaminha uma requisição para um serviço de que nunca ouviu falar.
O resto deste capítulo são essas três coisas, nessa ordem, mais as variáveis de ambiente que as amarram.
Passo 1: o diretório de template
O proxy resolve o template de um agente em <data-root>/templates/<template>/ (TemplatesDir em crab/crab-shell-proxy/internal/config/config.go). No host, <data-root> é CRAB_HOST_DATA_ROOT; dentro do container do proxy a mesma árvore é montada em CRAB_CONTAINER_DATA_ROOT, que por padrão é /data.
Para um agente ganglion, a parte do template que importa é workspace/, porque esse diretório é a camada de baixo da cascata de persona:
data/templates/scribe/
└── workspace/
├── AGENT.md what the agent does and how it behaves
├── SOUL.md its voice
├── HEARTBEAT.md its recurring task list
└── USER.md what the agent starts out knowing about the member
Esses quatro nomes são o conjunto completo. PersonaFiles em crab/crab-shell-proxy/internal/docker/persona.go lista exatamente AGENT.md, SOUL.md, HEARTBEAT.md e USER.md, e os três primeiros são entregues como bind mounts somente leitura, enquanto o USER.md é semeado uma vez e depois deixado em paz — o agente escreve nele conforme aprende sobre o membro.
Um agente ganglion não tem
config.jsonnem.security.ymlno template dele. Esses são arquivos do picoclaw. Oganglion_config.godiz isso com todas as letras: a configuração do harness não é semeada a partir detemplates/<agent>/config.jsone nunca foi. Em vez disso, o proxy renderiza um arquivo de configuração para cada workspace, e as credenciais chegam como variáveis de ambiente. Não copie oconfig.jsonde um agente de fábrica para um template ganglion; nada vai lê-lo.
O workspace/skills/ e o workspace/memory/ do template são, do mesmo jeito, só do picoclaw. O seedWorkspace em crab/crab-shell-proxy/internal/docker/provision.go copia a allowlist config.WorkspaceSeed — USER.md, memory/ e skills/ — e ele está no caminho de criação do picoclaw; o createGanglion nunca o chama. Para dar skills a um agente ganglion, publique-as como skills compartilhadas pela área administrativa.
Um valor template: continua obrigatório para todo agente, seja qual for o harness: o validate no config.go recusa um agente que não declare nenhum. Para um agente ganglion ele aponta para o diretório que guarda aqueles arquivos de identidade.
Passo 2: a entrada no catálogo
O catálogo é o crab/crab-shell-proxy/config.yaml. O Dockerfile o copia para /etc/crab-shell-proxy/config.yaml e aponta CRAB_CONFIG para esse caminho, então o arquivo commitado é assado dentro da imagem do proxy. Um deploy que monte um arquivo próprio sobre esse caminho, ou aponte CRAB_CONFIG para outro lugar, pode editar o catálogo sem rebuild.
Adicione o agente sob agents::
agents:
scribe:
harness: "ganglion"
serviceName: "scribe"
token: { env: "MYC_PICOCLAW_SCRIBE_TOKEN" }
template: "scribe"
mode: "scale-to-zero"
idleTimeout: 30s
model:
provider: "deepseek"
name: "deepseek-chat"
apiKeyEnv: "SCRIBE_API_KEY"
serviceName precisa bater com o valor que o Mycelium injeta como x-mycelium-service-name, que é a chave de serviço do gateway, do passo 3. token é o bearer que o gateway apresenta; o proxy rejeita qualquer requisição cujo Authorization não bata com ele.
Declare harness: "ganglion" explicitamente. A chave é opcional e o DefaultHarness hoje é o ganglion (config.go), então omiti-la funcionaria — mas o mesmo arquivo argumenta contra confiar nisso: todo agente no config.yaml deste repositório escreve o harness dele por extenso, porque uma atualização de config não deveria mudar o runtime de um agente por omissão. O picoclaw continua plenamente atendido, e harness: "picoclaw" continua sendo o valor certo para um agente que precisa dele; veja harnesses para a diferença.
mode decide o ciclo de vida do container. scale-to-zero para o container depois de idleTimeout sem atividade; continuous o mantém rodando. idleTimeout precisa ser maior que zero quando o modo é scale-to-zero, e o validate recusa o agente caso contrário. Um agente ganglion é um bom candidato a scale-to-zero: ele escreve cada turno na transcrição antes de o modelo ser chamado e reconstrói a partir dessa transcrição uma janela de contexto que esteja faltando, então um container parado não perde nada.
O bloco model: é o piso do agente — o modelo em que um workspace roda quando o inventário de modelos não resolve nada para ele. Você também pode listar alternativas sob models:, que vira a allowlist selecionável; SelectableModels no config.go devolve o padrão seguido dessa lista, sem duplicatas de provedor e nome.
Passo 3: a imagem, e o que acontece sem ela
Um agente ganglion precisa de CRAB_GANGLION_IMAGE. Ela não tem padrão, de propósito: o comentário sobre GanglionImage no config.go registra que uma tag móvel uma vez deixou um deploy rodando por semanas um binário de três semanas atrás, porque a imagem do harness não é um serviço do compose e um redeploy nunca a puxa. Aponte-a para um digest ou uma tag por commit.
Quando ela não está definida, o agente não derruba o proxy. O ganglionUnprovisioned remove o agente do catálogo na carga, registra isso em DisabledAgents com o nome da configuração que falta, e as rotas do agente então respondem 404. O mesmo acontece quando o apiKeyEnv do agente resolve para um valor vazio, ou quando a variável de ambiente do token dele não está definida. Leia o log de boot do proxy se um agente novo parecer não existir: o motivo está lá, e ele nomeia a variável.
O
docker-compose.yamlusa por padrãozombie-crab/crab-ganglion:devparaCRAB_GANGLION_IMAGE, que é uma tag construída localmente. Odocker-compose.prod.yamlnão define imagem ganglion nenhuma e nenhum workflow em.github/workflows/publica uma, então um deploy de produção tem de construir e publicar a imagem do harness por conta própria antes que um agente ganglion possa iniciar.
Passo 4: a rota do gateway
O Mycelium roteia pelo primeiro segmento do caminho, e esse segmento é a chave de serviço literal. Copie o bloco [[alpha]] em deploy/standalone/config.standalone.toml — o bloco de serviço, o [[alpha.secret]] dele e cada [[alpha.path]] — e renomeie para scribe. Quem chama então alcança o agente em /scribe/.... Mantenha host, healthCheckPath, o conjunto completo de paths e os grupos protectedByRoles como estão; só o nome muda.
Faça isso em cada modo em que você faz deploy, porque são arquivos separados: deploy/standalone/config.standalone.toml e deploy/prod/config.base.toml.
As entradas protectedByRoles também declaram o papel de convidado. O nome de um papel de convidado é a chave do agente — o gateway declara protectedByRoles = [{ name = "alpha", permission = "write" }] e o Mycelium cria esses papéis no boot, que é o fato sobre o qual crab/crab-exoskeleton-webapp/lib/invitations.ts é construído. Então um agente novo traz o papel dele à existência, e convidar alguém para ele é, depois disso, um convite comum feito na seção Members da área administrativa.
Passo 5: o ambiente
Duas variáveis, no .env e nunca em um arquivo de configuração:
MYC_PICOCLAW_SCRIBE_TOKEN=<the same shared secret the gateway route uses>
SCRIBE_API_KEY=<the provider API key>
O token autentica o gateway junto ao proxy. A chave de API nunca chega a um arquivo que o agente possa ler: para um agente ganglion o proxy a passa como GANGLION_API_KEY, ou como uma variável GANGLION_MODEL_KEY_<NAME> por modelo quando o inventário governa o workspace. Modelos e provedores cobre essa nomenclatura.
Passo 6: reiniciar, e o primeiro chat
A configuração do gateway é montada a partir de deploy/<mode>/, então uma mudança de rota precisa de um restart. O catálogo de agentes é copiado para dentro da imagem do proxy, então uma mudança de catálogo precisa de um rebuild — a menos que o seu deploy monte um catálogo próprio, caso em que um restart basta. Localmente, a saída segura para tudo é reconstruir os dois:
docker compose up -d --build crab-shell-proxy mycelium-gateway
Na primeira vez que um membro com o papel scribe envia uma mensagem, o proxy cria o workspace dele: cria workspace/ com os subdiretórios memory/, public/, sessions/ e windows/, gera um token bearer por usuário para o container, semeia USER.md a partir da cascata de persona se alguma camada fornecer um, renderiza o arquivo de configuração do harness e inicia o container.
Os arquivos que o agente entrega de volta ao membro vão em public/attachments/. Veja arquivos e entrega.
Checklist
-
data/templates/scribe/workspace/{AGENT.md,SOUL.md,HEARTBEAT.md,USER.md} - Entrada de catálogo em
crab/crab-shell-proxy/config.yamlcomharness: "ganglion",serviceName,token,template,modeemodel -
CRAB_GANGLION_IMAGEapontando para uma referência imutável - Bloco de serviço
[[scribe]]na configuração do gateway de cada modo em que você faz deploy -
MYC_PICOCLAW_SCRIBE_TOKENe oapiKeyEnvdo modelo no.env - Proxy reconstruído ou seu catálogo remontado, gateway reiniciado
- Um membro convidado para o papel
scribepela seção Membros
Quando não funciona
As rotas do agente respondem 404. Ou o gateway não tem o serviço scribe — confira se você editou o arquivo que o modo em execução realmente monta — ou o proxy desabilitou o agente na carga. O log de boot diz qual dos dois.
As requisições são rejeitadas como não autorizadas. O MYC_PICOCLAW_SCRIBE_TOKEN no ambiente do proxy e o token = { env = ... } no bloco de segredo do gateway precisam resolver para o mesmo valor.
O membro conversa, mas o agente não tem personalidade. O GANGLION_SYSTEM_FILE aponta para workspace/AGENT.md dentro do container, e esse arquivo chega como um bind somente leitura vindo da cascata de persona. Se nenhuma camada da cascata fornece AGENT.md — nem a subscription, nem o tenant, nem o template — nenhum bind é emitido e o agente roda sem identidade. Confira se o workspace/AGENT.md do seu template existe.
Uma edição no template não chegou a um membro que já existe. AGENT.md, SOUL.md e HEARTBEAT.md são resolvidos de novo a cada ensure, então uma edição chega até eles. Mas o USER.md só é semeado quando o workspace não tem nenhum, de propósito: é o arquivo que o agente escreve de volta, e sobrescrevê-lo apagaria o que o agente aprendeu. Um novo valor para ele chega apenas aos workspaces novos.
Para onde ir agora
Modelos e provedores explica como o modelo da sua entrada de catálogo se relaciona com o inventário que um administrador gerencia, e quais ferramentas um agente ganha. O guia do administrador trata de convidar membros e de dar ao novo agente skills compartilhadas e arquivos compartilhados.
Modelos e provedores
Este capítulo explica como um turno acaba conversando com um modelo específico, o que acontece quando esse modelo falha, e por que dois membros do mesmo deploy podem ver ferramentas diferentes no mesmo agente. Ele foi escrito para o harness ganglion, que é o que este livro ensina; veja harnesses para o outro.
Dois sistemas estão envolvidos e ajuda mantê-los separados. O proxy decide quais modelos um workspace tem — isso é o inventário e a cascata descritos no guia do administrador. O harness decide, turno a turno, qual desses modelos de fato responde. Este capítulo é sobre a segunda metade, mais o encanamento que leva as credenciais de um lado ao outro.
Como o harness fica sabendo dos modelos
Toda vez que o proxy garante que o container de um membro está pronto, ele resolve o modelo daquele workspace a partir do inventário e escreve um arquivo de configuração no diretório do membro: .ganglion-config.json. O arquivo é montado no container somente leitura em /data/.ganglion/config.json, e o GANGLION_CONFIG_FILE aponta o harness para ele (crab/crab-shell-proxy/internal/docker/ganglion_config.go).
Ele é somente leitura, e fica acima do único diretório em que o container pode escrever, por um motivo específico: uma lista de modelos que o agente pudesse editar deixaria uma ferramenta guiada por texto não confiável escolher o endpoint para onde as próprias chaves de API são enviadas.
O formato do arquivo é o config.json do picoclaw, de propósito, para que uma única tela de administração gerencie os dois harnesses. O harness lê model_list, agents.defaults.model_name, agents.defaults.model_fallbacks, agents.defaults.image_model, agents.defaults.image_gen_model, tools.web.* e tools.mcp, e ignora tudo o mais que encontra. Um arquivo ausente não é um erro: o harness sintetiza uma lista de uma entrada só, chamada default, a partir de três variáveis de ambiente e se comporta como se comportava antes de o arquivo existir (LoadRegistry em crab/crab-ganglion-harness/internal/config/file.go).
As três cadeias
Um turno pede ao registro uma lista ordenada de modelos candidatos. Qual lista ele recebe depende do tipo de turno (Kind no mesmo arquivo):
| Tipo | Configurado por | Cai para a cadeia de texto? |
|---|---|---|
| texto | agents.defaults.model_name + model_fallbacks | ela é a cadeia de texto |
| visão | agents.defaults.image_model + image_model_fallbacks | sim |
| geração de imagem | agents.defaults.image_gen_model + image_gen_model_fallbacks | não |
A cadeia de texto
O Chain monta a lista de candidatos assim. Se o turno nomeou um modelo que o registro realmente conhece, esse modelo vem primeiro. Caso contrário, a lista começa com model_name seguido de model_fallbacks. Depois — para cada entrada que já está na lista — os fallbacks daquela entrada são acrescentados, um nível de profundidade. Entradas desabilitadas ou desconhecidas são descartadas, e duplicatas são unificadas.
Um turno que nomeia um modelo que o registro não tem é o caso comum, não uma anomalia: o proxy preenche o campo de modelo do turno com um valor de espaço reservado. Então o harness o resolve silenciosamente para a cadeia padrão em vez de reportar qualquer coisa ao membro.
A regra de um nível merece atenção. Os fallbacks de um fallback não são percorridos recursivamente, e a expansão itera sobre um instantâneo, então um ciclo na configuração não consegue travar um turno.
Um administrador não edita este arquivo para mudar a cadeia. O model_fallbacks é escrito pelo proxy a partir da cadeia declarada no registro de inventário do modelo que foi resolvido, então a cadeia é editada na seção Modelo, no guia do administrador.
A cadeia de visão
Um turno é um turno de visão quando qualquer mensagem na janela de contexto carrega um anexo (hasAttachments em crab/crab-ganglion-harness/internal/runtime/loop.go).
Se o image_model nomeia uma entrada, essa cadeia responde. Se não nomeia, o registro devolve a cadeia de texto no lugar. Isso não é gentileza: se um modelo enxerga ou não é uma propriedade do modelo, não de um slot, então um deploy cujo único modelo por acaso é multimodal não precisa de uma segunda entrada e não deve ser obrigado a escrever uma.
A cadeia de geração de imagem
Esta não cai para outra. Se o image_gen_model não nomeia nada, a cadeia fica vazia. O motivo está dito no código: um modelo de texto a quem se pede uma imagem devolve um texto descrevendo uma, o que é pior do que uma ferramenta ausente porque parece sucesso.
A consequência é a próxima seção.
O renderizador do proxy escreve
model_name,model_fallbacks,model_list,tools.webetools.mcp— e nãoimage_modelouimage_gen_model(ganglionConfigDocemcrab/crab-shell-proxy/internal/docker/ganglion_config.go). Para um agente ganglion, essas duas chaves só chegam a um workspace se forem adicionadas à mão no editor de instância única da seção Config, que as guarda no overlay por instância. Nenhuma das duas é uma chave de propriedade do proxy, então o overlay as mantém e as reaplica a cada renderização. Uma cadeia de visão ou de geração de imagem é, portanto, por membro neste harness, não algo que um escopo consiga definir.
O que um fallback significa na hora do turno
Uma cadeia não é um laço de repetição em volta do turno inteiro. O completeWithFallback em loop.go diz a regra: um candidato só é abandonado enquanto nada chegou ao membro. Assim que um único byte de conteúdo é emitido, o turno está comprometido com aquele modelo e a falha dele aparece — recomeçar com outro modelo emendaria duas vozes numa mesma resposta, e o membro já leu a primeira metade da primeira.
Quando a cadeia acaba, o erro que o membro vê é o do último provedor, não um resumo sintético. Um operador precisa do motivo pelo qual a última tentativa falhou, e “todos os 3 modelos falharam” enterraria esse motivo.
Há uma degradação em cima disso, e ela é um recurso, não gentileza. Se a cadeia inteira falha num turno que carrega uma imagem, a imagem é descartada e o turno é tentado de novo uma vez, só com texto, contando ao modelo o que aconteceu. O membro vê um aviso de progresso dizendo que a imagem não pôde ser lida e recebe uma resposta degradada que diz isso.
Isso existe porque a alternativa é permanente. A referência da mídia continua no histórico da conversa, então um harness que simplesmente falhasse falharia do mesmo jeito em todos os turnos seguintes daquela conversa. Esta stack passou exatamente por isso em produção.
Quais ferramentas do agente existem
Esta é a pergunta que os membros de fato fazem — “por que meu agente não consegue pesquisar na web”, “por que ele não consegue fazer uma imagem para mim” — e a resposta honesta é que o registro de ferramentas é condicional. Uma ferramenta cujos pré-requisitos não estão configurados simplesmente não está lá. Ela não fica presente e falhando, porque uma ferramenta de que o modelo sabe e que nunca pode responder é pior do que nenhuma ferramenta: ela gasta um turno descobrindo a ausência.
O tools() em crab/crab-ganglion-harness/cmd/crab-ganglion/main.go é a decisão inteira:
| Ferramenta | Presente quando |
|---|---|
| shell | sempre |
load_image | sempre |
set_reasoning_depth | sempre |
web_search, web_fetch | pelo menos um provedor de busca está habilitado e pronto |
generate_image | a cadeia de geração de imagem tem pelo menos uma entrada com chave de API |
| despacho de subagente | o fan-out de subagentes está habilitado com orçamentos diferentes de zero |
research | o despacho de subagente existe e um provedor de busca está configurado |
| ferramentas do grafo de memória | o proxy gerou um token MCP para o workspace |
O load_image é incondicional de propósito: uma imagem no workspace é algo que qualquer deploy pode ter. Se um modelo consegue ver o resultado é decidido pela cadeia de visão na hora da conclusão, não aqui.
O research precisa de um despachante e de busca, porque sem busca ele é um modelo a quem se pede que lembre — que é justamente a falha que ele existe para substituir.
O harness registra cada uma dessas decisões no boot. Se um membro relata uma capacidade faltando, as primeiras linhas de log do container dizem quais ferramentas foram habilitadas e quais não foram.
Por que a busca, em especial, é fácil de errar
Duas listas de nomes de provedores precisam concordar, e elas não coincidem por completo.
O proxy aceita um segredo compartilhado native no slot web.<provider> para estes: brave, tavily, kagi, gemini, perplexity, glm_search e baidu_search (webProviders em crab/crab-shell-proxy/internal/docker/secrets.go). Todos eles são escritos na configuração do ganglion como tools.web.<name>: {enabled: true}, com a chave passada em separado.
O harness implementa quatro: brave, tavily, searxng e duckduckgo, nessa ordem de preferência quando tools.web.provider não está definido (WebProviderNames em file.go). Qualquer outra coisa no arquivo é ignorada.
Então, dos provedores que um administrador pode registrar pela seção Segredos, só brave e tavily de fato dão a um agente ganglion uma ferramenta de busca. Registrar uma chave kagi ou perplexity produz uma configuração que parece correta e não rende ferramenta nenhuma.
A prontidão também varia por provedor (providers.go): brave e tavily precisam de chave, searxng precisa de uma base_url e de nenhuma chave, e duckduckgo só precisa estar habilitado. Um bloco de provedor sem enabled: true está desligado — declarar "brave": {} o deixa presente e desabilitado, o que combina com os próprios exemplos do picoclaw.
Chaves de provedor
Credenciais nunca viajam no arquivo de configuração. O proxy separa estrutura de segredos do jeito que o picoclaw faz: o arquivo carrega endpoints e nomes, o ambiente carrega chaves, uma variável por modelo.
Há três lugares de onde uma chave pode vir.
A chave do próprio agente. Cada agente em crab/crab-shell-proxy/config.yaml nomeia um apiKeyEnv, e o proxy lê essa variável do próprio ambiente. Para um agente ganglion isso vira GANGLION_API_KEY, ao lado de GANGLION_MODEL e GANGLION_BASE_URL. Esse trio é o piso: é com ele que um workspace roda quando o inventário não resolve nada para ele.
Se essa variável não está definida, um agente ganglion é desabilitado na carga. O proxy registra o motivo, nomeando a variável, e as rotas do agente respondem 404 em vez de o proxy se recusar a subir. Um agente picoclaw, de propósito, não está sujeito a isso — a chave dele é escrita num arquivo por usuário, e uma chave vazia aparece como erro de autenticação na primeira chamada ao modelo.
A chave de um modelo do inventário. Quando a cascata resolve um modelo do inventário, a chave dele é passada como GANGLION_MODEL_KEY_<NAME>, uma variável por modelo da cadeia. O nome vem de colocar o nome do modelo em maiúsculas e trocar tudo que está fora de A-Z0-9 por um sublinhado. Os dois lados calculam isso de forma independente — ganglionModelKeyEnv no proxy e KeyEnvVar no harness — e o contrato inteiro é os dois concordarem. Dois nomes de modelo que diferem só na pontuação colidem, e isso é aceito: a alternativa é uma codificação que ninguém consegue ler na saída do docker inspect, que é onde essas coisas são depuradas.
A chave de um provedor de busca. Mesmo esquema, prefixo diferente: GANGLION_WEB_KEY_<PROVIDER>, vinda do segredo compartilhado native no slot web.<provider>.
O harness resolve uma chave primeiro pelo ambiente e só depois pelo arquivo. O ambiente ganha porque é o caminho que o proxy usa, e porque uma chave que nunca entra num arquivo não pode ser lida por nada que seja apontado para o arquivo por engano.
Um modelo da cadeia cuja chave está vazia simplesmente não é oferecido à ferramenta de geração de imagem — o imagegen.New filtra candidatos por chave não vazia e não devolve ferramenta nenhuma quando nenhum sobrevive. Na cadeia de texto o efeito é outro: o candidato é tentado e falha na hora da chamada, e a cadeia segue em frente.
As chaves também podem ser guardadas criptografadas. Um valor que começa com
enc://é resolvido dentro do container a partir de dois fatores que chegam por caminhos diferentes — uma senha emGANGLION_KEY_PASSPHRASEe um arquivo de chave montado somente leitura a partir do host. Eles são de tipos diferentes de propósito: os dois como variável de ambiente significaria que um únicodocker inspectentrega o texto puro.
De onde vem o endpoint de um modelo
O ganglion recebe uma URL base e posta nela; ele não tem uma tabela interna mapeando nome de provedor para endereço. O picoclaw tinha uma, então um agente migrado do picoclaw chega nomeando um provedor, sem nomear endpoint, e costumava falhar no primeiro turno com unsupported protocol scheme "".
O proxy preenche a lacuna a partir de três fontes, nesta ordem (resolveGanglionEndpoints em crab/crab-shell-proxy/internal/docker/ganglion.go):
- O
api_basedo próprio registro de inventário, que é definitivo. Um modelo customizado é customizado justamente porque o endpoint dele não é o padrão do provedor dele. - O
baseUrldo agente, vindo doconfig.yaml. - O padrão do provedor, de um catálogo embutido com cerca de trinta pares provedor/modelo (
ProviderEndpoint,model-catalog.json).
As entradas de fallback também são preenchidas, não só a principal — uma cadeia cuja segunda entrada não tem endpoint é uma cadeia que funciona até o dia em que ela é necessária. Uma entrada principal sem endpoint em lugar nenhum é recusada onde um operador consegue ver.
Duas ressalvas vindas de provider_endpoint.go. A entrada de catálogo do Azure é um molde para preencher, não um endereço, então ela fica de fora da tabela de fallback. E os runtimes locais — ollama, lmstudio, vllm, github-copilot — têm entradas de localhost, o que dentro de um container quer dizer o container, não o host; usar um deles significa definir um baseUrl explícito.
Profundidade de raciocínio
Uma entrada de modelo pode declarar thinking_level, um entre off, low, medium, high, xhigh ou adaptive. Declarar a chave é a declaração de capacidade: um modelo sem thinking_level nunca recebe um campo de profundidade por caminho nenhum, porque o harness não tem como descobrir se um endpoint aceita um, e o operador tem.
Um valor não reconhecido é tratado como ausente e gera um aviso, em vez de ser lido como off. Essa distinção importa: um erro de digitação que silenciosamente quisesse dizer “pense menos” pareceria uma configuração funcionando até a hora da fatura.
Se um modelo rejeita um campo de raciocínio no meio da cadeia, o campo é removido e o mesmo modelo é consultado mais uma vez, em vez de a cadeia queimar seu orçamento por causa de um campo que ninguém pediu.
Para onde ir agora
O guia do administrador trata de registrar modelos e escolher quem recebe qual. Criando um agente customizado mostra onde o modelo padrão de um agente é declarado. Solução de problemas reúne os sintomas que esses mecanismos produzem.
Deploy
Este capítulo é para quem roda a stack numa máquina. Ele descreve os dois modos de deploy que existem, para que serve cada um e o comando exato que sobe cada um deles. Ele também diz com todas as letras onde a produção ainda não está pronta, para que você descubra isso aqui e não por um container que falhou.
O que é um modo
A stack é um conjunto de serviços do Docker Compose. Um modo é uma combinação de arquivos compose e um arquivo .env na raiz do repositório. Há um arquivo base que sempre participa, docker-compose.yaml, e overlays que mudam alguns dos serviços dele sem substituí-los.
Dois modos vêm como perfis, cada um com seu próprio diretório em deploy/ guardando o .env.example daquele modo e a configuração de gateway que ele monta:
| standalone (o padrão) | prod | |
|---|---|---|
| Comando | docker compose up -d | docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d |
| Mycelium | construído a partir do código em MYCELIUM_GIT_REF | imagem publicada em MYCELIUM_IMAGE_TAG |
| Armazenamento do Mycelium | SQLite no volume mycelium-data | um serviço mycelium-postgres dedicado |
| transporte stub: os magic links são escritos no log | SMTP real | |
| Configuração do gateway | deploy/standalone/config.standalone.toml | deploy/prod/config.base.toml |
Um terceiro arquivo, docker-compose.observability.yaml, não é um modo. É um overlay opcional que dá ao watcher um lugar para onde mandar suas métricas; ele é descrito em Observabilidade.
Os dois perfis fixam a mesma versão do Mycelium. O
deploy/standalone/.env.exampleconstrói o commit9298ecb44a69ced91cb2d7d3356a716aedca858b, descrito ali como o commit com a tag9.0.0-rc.13, e odeploy/prod/.env.examplebaixaMYCELIUM_IMAGE_TAG=9.0.0-rc.13. Mova os dois juntos. A configuração do gateway é vocabulário compartilhado entre os dois, e uma diferença de versão entre o que você testa e o que vai para o deploy é onde isso quebra.
Desenvolvimento num laptop: standalone
Copie o arquivo de ambiente do perfil para a raiz do repositório e edite-o:
cp deploy/standalone/.env.example .env
Os valores que você precisa definir antes do primeiro up são os bearer tokens de cada agente (MYC_PICOCLAW_ALPHA_TOKEN, MYC_PICOCLAW_BETA_TOKEN), a chave de LLM própria de cada agente (PICOCLAW_ALPHA_API_KEY, PICOCLAW_BETA_API_KEY) e MYC_STANDALONE_BOOTSTRAP_SECRET, que controla o registro único da conta Staff. O bearer token é o que o Mycelium injeta em uma requisição para aquele agente e o que o crab-shell-proxy valida; a chave de LLM é lida do ambiente do proxy e nunca é escrita em uma imagem ou em um arquivo versionado.
Depois:
docker compose up -d --build
O standalone constrói em vez de baixar. Tudo sob crab/ — o proxy, o chat webapp, o ganglion harness, o watcher — é construído a partir da sua árvore de trabalho, que é justamente o objetivo do modo: o que roda é o que você tem no checkout. O Mycelium é a exceção. fungi/mycelium/Dockerfile.standalone constrói o mycelium-api a partir do repositório git upstream na referência fixada em MYCELIUM_GIT_REF, sem fonte local, e fungi/mycelium-webapp faz o mesmo com a UI de administração. É também por isso que essa referência e o MYCELIUM_IMAGE_TAG do prod precisam avançar juntos.
deploy/standalone/.env.example deliberadamente não traz tags de imagem, porque CRAB_SHELL_PROXY_TAG e CHAT_WEBAPP_TAG não fazem nada em um modo que constrói.
Os dois serviços que só constroem
Dois serviços em docker-compose.yaml não são servidores. picoclaw-image e ganglion-image constroem uma imagem cada, rodam /bin/true e saem com zero; o crab-shell-proxy declara condition: service_completed_successfully para os dois, então as imagens existem com certeza antes que qualquer coisa possa criar um container a partir delas.
Eles existem porque os containers dos agentes são criados pelo crab-shell-proxy pelo socket do Docker, e não pelo Compose, então o Compose nunca os construiria nem os baixaria. A ausência deles é também o assunto de uma das falhas em Solução de problemas.
O build do
ganglion-imagerodago vetego testantes de linkar o binário. Um teste que falha, portanto, impede a stack de subir. Odocker-compose.yamldiz que essa é a intenção para um arquivo compose de desenvolvimento: os testes são o critério de aceitação da imagem.
Zerando tudo
Para apagar todos os agentes por usuário e todos os templates e deixar a stack se reconstruir sozinha:
docker compose down
docker rm -f $(docker ps -aq --filter 'name=crabshell') 2>/dev/null
sudo rm -rf data/templates data/tenants data/effective-secrets \
data/effective-skills data/user-secrets data/registered-models
docker compose up -d --build
A linha docker rm é necessária porque os containers por usuário foram criados pelo proxy, não pelo Compose, então o docker compose down não sabe deles. O sudo é necessário porque a árvore em disco sob data/ é escrita pelo proxy como root. O --build não é opcional: o template de fallback com o qual o proxy repovoa um data/ apagado está embutido no binário do proxy.
Contas e papéis não ficam em data/. Eles vivem em volumes nomeados — mycelium-data para o banco SQLite do próprio Mycelium e chat-webapp-postgres-data para a lista de conversas — então o seu login sobrevive à limpeza. Acrescentar -v ao docker compose down reseta esses também, e aí você teria de refazer o bootstrap da conta Staff. Veja Banco de dados e migrações.
Produção: o overlay prod
cp deploy/prod/.env.example .env
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d
O overlay muda quatro serviços. mycelium-gateway, crab-shell-proxy, chat-webapp e harness-sphere recebem cada um build: !reset null e um image: apontando para uma imagem publicada no GHCR. O !reset importa: um image: simples ao lado de um build: herdado ainda construiria a imagem localmente se ela estivesse faltando, e toda a postura deste modo é que esses quatro são baixados, nunca construídos.
!resetexige o Docker Compose v2.24 ou mais novo. Isso é um pré-requisito do modo prod, não um detalhe — um Compose mais antigo não vai entender o arquivo.
Outras duas coisas que o overlay faz passam fácil despercebidas e as duas são deliberadas:
- O
crab-shell-proxyrecebeports: !reset []. O arquivo base publica o proxy em127.0.0.1:18080para testes diretos, eportsconcatena entre as camadas de-fem vez de substituir, então sem o reset aquela porta continuaria ativa. Em produção o gateway precisa ser o único ponto de entrada, loopback incluído. - O
mycelium-webappainda é construído aqui. Ele é uma single-page app que roda no navegador e cuja URL de API é embutida no momento do build pelo argumento de buildVITE_MYCELIUM_API_URL, então ele não pode ser uma imagem pré-construída genérica.
Produção hoje não tem imagem publicada do ganglion
Esta é a única coisa sobre a qual vale ser explícito, porque um leitor que faz deploy em um servidor e configura um agente ganglion pode esbarrar nela.
O docker-compose.prod.yaml não define CRAB_GANGLION_IMAGE, e o deploy/prod/.env.example também não o menciona. Como o prod é um overlay sobre o arquivo base, o que de fato chega ao proxy é o padrão do arquivo base, zombie-crab/crab-ganglion:dev — uma tag que só existe na máquina que a construiu. O agente, portanto, não está desligado; ele está apontado para um nome que nenhum registry resolve.
Duas consequências vêm daí. Primeiro, o comando de prod herda sem mudanças o serviço ganglion-image, que só constrói, já que o overlay reseta apenas os quatro serviços acima. Um up -d de prod em um host que carrega as fontes dos submódulos vai construir o ganglion localmente, o que não é o que “imagens publicadas” leva você a esperar e o que exige a árvore de fontes e um build de Go naquele host. Segundo, em qualquer situação em que esse serviço não rode — um docker compose up -d crab-shell-proxy sozinho, um docker system prune que removeu a tag local, ou um deploy que apenas baixa imagens — o EnsureImage do proxy não acha nada localmente, cai para um pull no registry, e esse pull dá 404. Todo agente ganglion fica morto até alguém construir a imagem na mão.
Imagens imutáveis do ganglion existem, sim. O próprio
.github/workflows/release.ymldocrab/crab-ganglion-harnesspublicaghcr.io/lepistabioinformatics/crab-ganglion:sha-<short-sha>a cada push namain, deliberadamente sem:lateste sem nenhuma tag que seja reconstruída. O que falta é a ligação: nada no perfil de prod aponta para uma delas. O único workflow de release no.github/workflows/deste repositório é orelease-picoclaw-glob.yml, que publica a imagem picoclaw com patch.Até o perfil se atualizar, defina
CRAB_GANGLION_IMAGEno seu.envde produção com uma tagsha-específica ou um digest, e rodedocker pullnela antes de subir a stack. Ointernal/config/config.godocrab/crab-shell-proxypede uma referência imutável e explica por quê: uma tag móvel já deixou um host rodando um binário de três semanas atrás, silenciosamente, porque oEnsureImagenunca baixa o que já está presente.O pull não é faxina opcional. O serviço
ganglion-imageherdado pega oimage:do que você definir, mantendo o seubuild:context, então se essa referência não existir localmente oupvai construir a fonte do submódulo que está no checkout e marcar esses bytes com o nome publicado — que é exatamente a falha de bytes-errados-sob-um-nome-confiável que a referência imutável existe para evitar.
Note também que um agente que não declara nenhuma chave harness: é hoje um agente ganglion (DefaultHarness em crab/crab-shell-proxy/internal/config/config.go), então ele também precisa de CRAB_GANGLION_IMAGE. Declare o harness explicitamente em todo agente; a configuração que vem neste repositório faz isso, por esse motivo.
Antes de um deploy de produção
CRAB_HOST_DATA_ROOTprecisa ser um caminho absoluto do host. O crab-shell-proxy entrega esse caminho ao daemon Docker do host como origem do bind-mount dos containers que ele cria, então um caminho que só existe dentro do proxy não vai resolver.- Defina
noreplyEmailesupportEmailemdeploy/prod/config.base.tomlcom o mesmo endereço deMYC_SMTP_USERNAME. O Gmail rejeita umFromque não bate. A porta SMTP é fixa em 465 nesse arquivo em vez de ser uma variável de ambiente, porque o Mycelium a interpreta como número e um valor de ambiente é uma string. - Se você colocar um hostname na frente da stack, mude
domainUrleallowedOriginsno mesmo arquivo junto com o argumento de buildVITE_MYCELIUM_API_URLdomycelium-webapp. A UI de administração chama o gateway direto do navegador, então uma divergência entre os dois é um muro de CORS. - O catálogo de agentes é embutido na imagem do proxy nos dois modos (
crab/crab-shell-proxy/config.yaml), então adicionar ou remover um agente significa reconstruí-la. Montar o seu próprio arquivo sobre/etc/crab-shell-proxy/config.yamlé suportado, se você preferir. - Todo agente roteado precisa do seu bearer token definido. Deixar um vazio faz o gateway anunciar o agente e injetar um bearer vazio, o que falha na hora da requisição e não no boot.
Não existe perfil Dokploy
Existiu um terceiro perfil, docker-compose.dokploy.yaml com o seu próprio diretório deploy/dokploy/, e ele foi removido em vez de movido. O deploy para o qual ele foi escrito agora vive em um repositório separado, sobe apenas a metade CRAB da stack, e tinha virado a fonte da verdade de fato enquanto esta cópia era mantida na fé.
Se você está fazendo deploy no Dokploy, comece pelo prod, que é o perfil mais próximo, e acrescente os labels do Traefik e a rede externa de que a sua instalação precisa. git log -- docker-compose.dokploy.yaml deploy/dokploy/ ainda tem o original.
Para onde ir agora
Produção em Postgres precisa de um passo de schema, feito uma única vez, antes que alguém consiga entrar — é o Banco de dados e migrações. Com a stack no ar, Observabilidade cobre o watcher e o backend opcional de métricas, e Solução de problemas reúne as falhas com que as pessoas realmente esbarram.
Banco de dados e migrações
A maior parte do que esta stack persiste não está em banco de dados nenhum, e a maioria dos operadores nunca roda uma migração. Este capítulo diz quais serviços têm um banco, o que cada um guarda, e qual é o único passo de schema que existe, feito uma única vez — para que você descubra rápido se ele se aplica a você.
Qual serviço guarda o quê
O Mycelium, o gateway, é dono da identidade. Contas, tenants, subscriptions, papéis de convidado e os tokens de magic link por trás do login vivem todos aqui. Onde depende do modo de deploy. No standalone é SQLite: deploy/standalone/config.standalone.toml define [sqlite] path = "/data/mycelium.db", e docker-compose.yaml monta o volume nomeado mycelium-data em /data. No prod é Postgres: o overlay acrescenta um serviço mycelium-postgres sobre postgres:16-alpine, apoiado no volume mycelium-postgres-data, e aponta o gateway para ele por MYC_BASE_DATABASE_URL.
O chat-webapp é dono da lista de conversas. O chat-webapp-postgres, também postgres:16-alpine, no volume chat-webapp-postgres-data, guarda um conjunto pequeno de tabelas: conversations (id, e-mail do dono, agente, título, ids de workspace, arquivo de sessão, projeto), conversation_tags, e uma tabela branding de uma linha só para o nome do app e os logos. É deliberadamente um banco separado do banco do Mycelium, com um ciclo de vida diferente; o arquivo compose defende esse ponto na definição do serviço.
O crab-shell-proxy é dono de um pequeno armazenamento chave-valor. No boot ele abre model-registry.db sob a raiz de dados do seu container (crab/crab-shell-proxy/cmd/crab-shell-proxy/main.go). Isso é um arquivo bbolt, não SQL: o Open de internal/registry/registry.go cria cada bucket de que precisa se estiver faltando e carrega o seu próprio marcador schema_version com uma migração no boot. Ele não precisa de nada de você.
Tudo o que um agente produz são arquivos, não linhas de tabela. Transcrições, arquivos de memória, projetos, skills, anexos entregues e os .schedules.json/.projects.json que pertencem ao proxy vivem todos em disco sob a raiz de dados, na árvore de tenants descrita em Agentes, workspaces e projetos. É por isso que um reset é um rm -rf e não um DROP.
O
deploy/prod/config.base.tomltambém traz um bloco[redis], comhostname = "mycelium-redis"epassword = "unused-no-redis-container-in-this-stack". O formato de configuração exige que as chaves existam; nenhum container Redis roda nesta stack. Não saia procurando por ele.
Quando você precisa fazer alguma coisa
Três desses quatro não precisam de nenhuma ação.
O adaptador SQLite do Mycelium carrega migrações embutidas — o deploy/prod/config.base.toml diz isso onde explica que o de Postgres não carrega — então o standalone funciona a partir de um checkout limpo, sem nenhum passo de schema.
O chat-webapp cria o seu próprio schema de forma preguiçosa, em tempo de execução, na primeira consulta. O crab/crab-exoskeleton-webapp/lib/db.ts tem uma função ensureSchema() que emite comandos CREATE TABLE IF NOT EXISTS e ALTER TABLE ... ADD COLUMN IF NOT EXISTS uma vez por processo e memoiza a promise. Todo comando é idempotente, e colunas novas são adicionadas de forma aditiva, então as linhas que já existiam sobrevivem. Não há ferramenta de migração, nem diretório de migrações, nem comando para rodar.
O registro de modelos se inicializa sozinho, como visto acima.
Sobra exatamente um caso: o backend Postgres do Mycelium, em prod, uma vez, depois do primeiro up.
O passo de schema único, só em prod
O adaptador Postgres do Mycelium não tem migrações embutidas, diferente do de SQLite. O deploy/prod/config.base.toml registra isso logo acima do seu bloco [diesel], e o docker-compose.prod.yaml repete no cabeçalho do arquivo. Enquanto o schema não for aplicado, o gateway tem um banco que não consegue usar, então ninguém consegue entrar.
São dois passos, nesta ordem: o up.sql do upstream, e depois os scripts de migração que o up.sql não incorpora. Os dois vêm do repositório do mycelium na mesma release que este deploy fixa.
git clone --depth 1 --branch 9.0.0-rc.13 \
https://github.com/LepistaBioinformatics/mycelium.git /tmp/myc
cd /path/to/zombie-crab-project && set -a; . ./.env; set +a
Carregar o .env é o que coloca MYC_DB_USER, MYC_DB_NAME e MYC_DB_PASSWORD no seu shell; esses são os mesmos três nomes que o docker-compose.prod.yaml entrega ao serviço mycelium-postgres, então eles vão bater com o que você definiu em deploy/prod/.env.example.
Passo 1 — o schema base.
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml exec -T mycelium-postgres \
psql -U "$MYC_DB_USER" -d postgres \
-v db_name="$MYC_DB_NAME" -v db_user="$MYC_DB_USER" \
-v db_password="$MYC_DB_PASSWORD" -v db_role=service-role-mycelium \
< /tmp/myc/adapters/diesel_postgres/sql/up.sql
A conexão mira o banco de manutenção postgres porque um banco não pode ser criado de dentro de si mesmo. O README.md da raiz descreve o que o script faz em seguida — criar o banco da aplicação se ele estiver faltando, entrar nele, e criar os papéis e as tabelas — e registra que ele exige -v db_password. As flags -v são variáveis do psql que o script substitui dentro do seu próprio SQL.
O Compose já criou o banco e o papel de login quando você roda isso, porque o serviço
mycelium-postgresdeclaraPOSTGRES_DBePOSTGRES_USER. EntãoCREATE USER ... already existsé saída esperada aqui, não uma falha: opsqlimprime a mensagem e segue em frente.
Passo 2 — as migrações, em ordem de nome de arquivo.
for m in /tmp/myc/adapters/diesel_postgres/sql/migrations/*.sql; do
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml exec -T mycelium-postgres \
psql -U "$MYC_DB_USER" -d "$MYC_DB_NAME" < "$m"
done
O glob expande em ordem lexical, que é a ordem que esses scripts esperam. Note que o -d aqui é o banco da aplicação, não postgres.
Este passo não é opcional na 9.0.0-rc.13. As notas do próprio repositório — o deploy/prod/config.base.toml acima de [diesel], e o README.md da raiz — registram que o up.sql nessa tag traz kv_artifact e o índice de claim de message_queue, mas não traz instance_settings, resource_audit_log, nem as colunas tenant.encrypted_dek e kek_version de que a criptografia por envelope precisa. Essas só existem como scripts de migração.
Para conferir o resultado, conecte e olhe:
docker compose -f docker-compose.yaml -f docker-compose.prod.yaml exec mycelium-postgres \
psql -U "$MYC_DB_USER" -d "$MYC_DB_NAME"
\dt deve listar instance_settings e resource_audit_log; \d tenant deve mostrar encrypted_dek e kek_version.
Os dois comandos acima são os que o
README.mdda raiz dá, e o nome do serviço, os nomes de variávelMYC_DB_*e a cadeia de arquivos compose neles batem todos com este checkout. O que este repositório não consegue verificar por conta própria é tudo o que está dentro do clone do mycelium: os caminhosadapters/diesel_postgres/sql/up.sqleadapters/diesel_postgres/sql/migrations/, o valordb_role=service-role-mycelium, o que oup.sqlfaz quando roda, e quais tabelas oup.sqldaquela tag cria e não cria. Tudo isso vem do repositório upstream. Se um caminho mudou de lugar em uma release posterior, leia o clone em vez desta página.
Em um deploy com Postgres que não é conduzido pelo arquivo compose deste repositório, rode os mesmos dois passos com docker exec direto contra o container mycelium-postgres. Nada no SQL muda; só como você chega no psql.
O que um reset mexe e o que ele não mexe
O reset descrito em Deploy remove diretórios sob data/. Ele não toca nos volumes nomeados, então tanto as contas e os papéis do Mycelium quanto a lista de conversas do chat-webapp sobrevivem a ele, e o seu login continua funcionando. Isso é deliberado: apagar o estado dos agentes é uma ação rotineira de desenvolvimento, e perder a sua conta Staff toda vez tornaria isso bem menos rotineiro.
Acrescentar -v ao docker compose down remove os volumes também. No standalone isso apaga o mycelium.db, então você teria de refazer o bootstrap da conta Staff do zero. No prod isso apaga o diretório de dados do Postgres, então você teria de refazer também os dois passos de schema acima.
Para onde ir agora
Deploy cobre os modos a que estes bancos pertencem, e Solução de problemas cobre como um schema faltando ou um volume apagado realmente aparecem no cliente de chat.
Observabilidade
O harness-sphere é o watcher da stack. Este capítulo diz o que ele observa, o que ele mostra para você, como rodá-lo com e sem um backend de métricas, e as duas regras sobre ele que um operador não pode quebrar sem querer.
O que ele é, e o que ele não é
Antes deste serviço existir, a stack não emitia nada — não “pouco”, mas nada: nenhum endpoint de métricas, nenhum coletor, nenhum dashboard. A primeira pergunta que qualquer um faz durante um incidente, a máquina estava sob pressão quando aquilo ficou lento?, não tinha resposta.
O harness-sphere é um único binário Rust que transforma o que encontra em métricas OpenTelemetry padrão e as envia para o backend que você apontar. Ele é exclusivo desta stack, pela primeira linha do seu próprio README: coletores sem fonte aqui foram apagados em vez de deixados configuráveis.
Três coisas que ele deliberadamente nunca faz, e cada uma vale saber antes de você sair procurando o recurso:
- Ele não segura um socket do Docker. Veja a seção de regras abaixo.
- Ele não reporta custo de token. O picoclaw não escreve contagens de token em disco e não expõe endpoint de métricas; o único caminho que já existiu raspava um endpoint que esta stack não roda, e ele foi apagado. Isso não é trabalho adiado.
- Ele não lê conteúdo de transcrição. Corpos de mensagem são dados do membro. Ele conta mensagens, sessões e chamadas de ferramenta;
contentnem chega a ser desserializado.
O que ele observa
A stack é modelada como exatamente seis camadas: o host, o próprio watcher, e as quatro coisas que a máquina roda. Duas delas — Host e Watcher — são Critical, ou seja, o watcher sai com código diferente de zero em vez de rodar cego sem elas. Todo o resto é Optional: se estiver faltando ou se comportando mal, ele sai de lado em silêncio e nunca derruba o processo.
O host. Utilização de CPU, uso e utilização de memória, e swap. Esses são os números da máquina real, não os do container, e isso foi medido e não presumido — o Linux não coloca /proc em namespace, então o sysinfo dentro do container lê o host. Um container limitado a -m 512m ainda reportava os 33 GB do host. É exatamente por isso que o serviço não monta /proc nem /sys.
Ele mesmo. A sua própria CPU, memória residente e memória virtual. Um watcher cuja memória cresce sem limite é um watcher prestes a virar o incidente.
Três endpoints de serviço, por sonda TCP. O crab/harness-sphere/config.zombie-crab.toml lista os três pelo nome de serviço do compose, cada um marcado com a camada a que pertence: mycelium-gateway:8080 (gateway), crab-shell-proxy:8080 (proxy) e chat-webapp:3000 (webapp). Note o terceiro: o repositório se chama crab-exoskeleton-webapp, mas chat-webapp é o serviço do compose e portanto o único nome que resolve na zombie_net.
Um alvo que está fora do ar lê um 0 honesto em vez de sumir. É também por isso que o serviço não declara nenhum depends_on — colocar o watcher atrás da saúde daquilo que ele observa suprimiria justamente o sinal que ele existe para produzir.
Atividade de IA por membro, a partir da árvore de workspaces. Ele faz um glob de tenants/*/subscriptions/*/agents/*/users/* sob o mount somente-leitura /data e roda um coletor de sessão por tupla (tenant, subscription, agent, user), cada um carimbado com aquela tupla. A partir das transcrições JSONL em disco ele deriva contagens de mensagens por papel, contagens de conversas, contagens de chamadas de ferramenta, e execuções de tarefas agendadas contadas à parte. Quatro distorções são corrigidas, cada uma com um teste por trás: conversas de projeto vivem em um workspace-<id>/sessions irmão, sessions/durable/ espelha os arquivos vivos um a um e dobraria exatamente todos os números, cada execução de cron escreve o seu próprio arquivo de sessão, e uma transcrição que encolheu é relida do zero em vez de tratada como um delta negativo.
Os intervalos estão no mesmo arquivo de configuração: descoberta a cada 30 s, sessões a cada 60 s, material de aprendizado a cada 300 s, host a cada 10 s, ele mesmo a cada 30 s.
Estes são Gauges, não Counters. O coletor reporta o total absoluto que encontra em disco, derivado de novo a cada coleta, então passá-lo pelo
add()de um Counter contaria tudo em dobro a cada tick. A consequência a esperar é que um valor pode legitimamente cair quando transcrições são rotacionadas para fora.
O que ele ainda não consegue ver
O watcher lê a árvore de tenants em disco. Ele ainda não consome o inventário vivo GET /v1/instances do crab-shell-proxy, então ele consegue dizer que o workspace de um membro existe e quanta atividade há nele, mas não se o container daquele membro está rodando agora. Liveness por instância e CPU e memória por container dependem os dois desse inventário. O harness-sphere nunca calcula o hash do nome do container por conta própria, porque isso duplicaria uma preimagem que pertence ao proxy e divergiria em silêncio no dia em que o prefixo ou o hash mudar.
Como rodar
O watcher já está no arquivo compose base, então ele sobe junto com a stack. De fábrica o seu exportador é stdout: ele imprime os seus sinais no log do próprio container. Esse é um padrão deliberado — ele prova o pipeline inteiro sem precisar levantar backend nenhum.
docker compose logs -f harness-sphere
Para de fato olhar números ao longo do tempo, suba o overlay opcional, que acrescenta um OpenTelemetry Collector, Prometheus e Grafana:
docker compose -f docker-compose.yaml -f docker-compose.observability.yaml up -d
O Grafana fica então em http://localhost:3001 (GRAFANA_PORT), com acesso anônimo de admin e sem formulário de login — é um backend local em uma porta de loopback, e um prompt de login na frente dos seus próprios gráficos de CPU não tem modelo de ameaça por trás. Se algum dia você expuser isso para fora do localhost, aquelas três linhas GF_AUTH_* são a primeira coisa a remover. O Prometheus é publicado na 9090 para você rodar uma consulta PromQL crua quando um dashboard discordar do que você espera.
Dois dashboards são provisionados a partir de deploy/observability/grafana/dashboards/, junto com o datasource do Prometheus, então a stack já é útil no primeiro boot, sem nenhum clique: o zombie-crab — stack, organizado por camada, responde “a stack está saudável?”, e o zombie-crab — learning, organizado por membro, responde “onde está cada instância?”.
Consulte os nomes com underscore. A tradução de OTLP para Prometheus troca pontos por underscores e acrescenta um sufixo de unidade, então o que o watcher emite como
system.memory.usageé coletado comosystem_memory_usage_bytes. Declare os instrumentos com os nomes com ponto; consulte com os traduzidos.
Por que existe um collector no meio
O exportador OTLP do harness-sphere é construído com .with_tonic(), então ele fala OTLP sobre gRPC, enquanto o receptor OTLP do próprio Prometheus só fala HTTP. Os dois não podem ser ligados diretamente. O collector recebe gRPC na 4317 e reexpõe tudo em formato de exposição do Prometheus na 8889, que é o único alvo que o deploy/observability/prometheus.yml coleta.
O receptor HTTP dele na 4318 está ali por causa de um segundo produtor: o crab-ganglion-harness fala OTLP sobre HTTP com codificação JSON, o que ele consegue fazer só com a biblioteca padrão do Go, e o arquivo compose passa GANGLION_OTLP_ENDPOINT (padrão http://otel-collector:4318) pelo proxy para dentro de cada container de agente. Deixá-lo vazio desliga a exportação dentro do harness, em vez de fazê-lo logar uma requisição falha a cada turno.
O pipeline é só de métricas. O harness-sphere sobe com
sources=3 receivers=0— sem traces, sem logs — que é também por que a stack SigNoz vendorizada emcrab/harness-sphere/deploy/signoz/não é o que este overlay usa: seis serviços, incluindo ClickHouse e Zookeeper, dimensionados para um sinal que ainda não existe.
Dois ajustes que vão te morder
A cadeia de -f não é opcional em nenhum comando. O Compose aplica um overlay só quando você o nomeia. Rode docker compose up -d, ou restart, ou up -d harness-sphere sem os dois arquivos, e todo serviço que o overlay sobrescreve cai silenciosamente de volta para o arquivo base — o exportador volta para stdout e o bind-mount da configuração some. Nada avisa disso. Todo container continua saudável, o watcher continua coletando, os logs dele parecem movimentados, e o Grafana simplesmente fica vazio. Coloque isto no .env da raiz do repositório e um docker compose up -d pelado passa a estar correto:
COMPOSE_FILE=docker-compose.yaml:docker-compose.observability.yaml
Defina HARNESS_SPHERE_HOST_NAME com o nome da máquina real. O watcher lê o seu hostname para preencher o atributo de recurso host.name, que vira um label em toda série. O hostname de um container por padrão é o id dele, que muda a cada recriação — então todo redeploy criaria uma série temporal novinha e cada painel mostraria a mesma métrica uma vez por geração de container. O padrão do compose (zombie-crab-host) é estável em vez de exato, que é a metade importante: um label errado mas constante agrupa certo, um label certo mas mutável nunca agrupa.
As duas regras
Estas são as regras do .claude/CLAUDE.md, e elas estão declaradas aqui porque um operador que não entende por quê vai quebrar uma delas tentando fazer alguma coisa funcionar.
Ele nunca recebe um socket do Docker
O crab-shell-proxy já monta /var/run/docker.sock e roda como root. Ele é o plano de controle confiável da stack, e é o componente mais privilegiado que existe: um socket do Docker é start, stop e exec em qualquer container, e um caminho até root no host.
Um segundo serviço montando o socket dobraria o raio de destruição do pior comprometimento possível da stack, sem ganho nenhum — porque a única coisa para a qual o watcher precisaria de um socket, mapear um container em execução de volta para o seu tenant, é atendida pelo GET /v1/instances somente-leitura do proxy.
O harness-sphere de fato roda como root neste arquivo compose (user: "0:0"), o que sobrescreve o USER 10001:10001 que o Dockerfile dele define, e essa decisão é defendida no lugar em vez de presumida: o crab-shell-proxy cria a árvore de tenants como root:root 0700, e a barreira está no topo da árvore, então um watcher que não é root não consegue nem atravessar para dentro dela. As alternativas eram piores — um chmod 0755 deixaria todo caminho de workspace enumerável por qualquer uid local, e os nomes dos diretórios são UUIDs de conta.
O que mantém isso estreito são três restrições que sustentam o peso juntas, e uma mudança futura não pode afrouxá-las uma de cada vez:
- o bind de
/datacontinua:ro— ele pode ler a árvore, nunca escrever nela; - nenhum socket do Docker, jamais;
- nenhuma porta publicada, então não há superfície de entrada nenhuma. Todo coletor puxa.
CRAB_TELEMETRY_TOKEN não é um token de agente
O CRAB_TELEMETRY_TOKEN autoriza GET /v1/instances no crab-shell-proxy, e nada mais. Ele é uma credencial separada e somente-leitura de propósito, e o bearer token de um agente nunca pode ser usado no lugar dele.
A razão é o que um token de agente vale de fato aqui. O header de perfil do Mycelium é decodificado e nunca verificado pelo proxy (identity.SDKResolver.Resolve), então a checagem do bearer do agente é a única coisa que impede um chamador que alcançou o proxy diretamente na zombie_net de afirmar qualquer id de conta que quiser. Esse token libera conversar como qualquer membro de qualquer tenant. Entregá-lo a um componente de monitoramento deixaria esse componente mandar mensagens como outras pessoas. O handleInstances deliberadamente não passa pela resolução normal de agente pelo mesmo motivo, e compara o token em tempo constante.
Deixá-lo sem valor é suportado e é o padrão seguro. A rota então não é registrada de jeito nenhum — um 404, não um 401. Isso não é descuido: este endpoint revela toda a topologia de tenants, subscriptions e usuários do deploy, então um deploy que não optou por isso não ganha superfície nova. O watcher continua reportando o host, ele mesmo e as três sondas de serviço; ele só não consegue atribuir instâncias por tenant.
Gere um novo com openssl rand -hex 32, como dizem tanto o deploy/standalone/.env.example quanto o deploy/prod/.env.example.
Para onde ir agora
Solução de problemas cobre a falha do Grafana vazio e outras em forma de sintoma. harness-sphere descreve o componente em si, e Deploy cobre os modos de compose sobre os quais este overlay assenta.
Solução de problemas
Cada entrada abaixo é um sintoma que você consegue observar de fato, a causa por trás dele, e o que fazer. Todas são falhas para as quais esta stack tem evidência real, registrada nos arquivos compose, nos arquivos de configuração ou no código do próprio proxy. Se o seu problema não está aqui, o log de boot do crab-shell-proxy é quase sempre o primeiro lugar certo para olhar — ele nomeia variáveis faltando de propósito.
Um agente ganglion para de responder, e o proxy loga uma falha no pull da imagem
Sintoma. Conversar com um agente que roda o harness ganglion falha. O log do proxy carrega um erro de pull nomeando a imagem, tipicamente zombie-crab/crab-ganglion:dev. Os outros agentes não são afetados. Isso costuma começar logo depois de um docker system prune, ou em um servidor cujo deploy foi feito baixando imagens em vez de construindo-as.
Causa. O EnsureImage em crab/crab-shell-proxy/internal/docker/client.go tem um caminho rápido: ele pergunta ao daemon pela imagem localmente e, se ela estiver lá, retorna sem contatar registry nenhum. É isso que permite que uma tag construída localmente funcione. Ele não para por aí quando a imagem está faltando — ele cai para POST /images/create, um pull de registry de verdade, que dá 404 em uma tag que nenhum registry tem.
O CRAB_GANGLION_IMAGE padrão é zombie-crab/crab-ganglion:dev, um nome que só existe na máquina que o construiu. Um prune remove imagens não usadas, essa vai junto, e o agente fica morto até a tag voltar.
Correção. Reconstrua a tag. O serviço ganglion-image em docker-compose.yaml existe exatamente para isso: é um serviço que só constrói, que produz a imagem, roda /bin/true e sai, e o crab-shell-proxy espera a conclusão dele. O picoclaw-image faz o mesmo para o outro harness, então os dois são recuperados pelo mesmo comando:
docker compose up -d --build
Em um servidor, prefira a correção duradoura: defina CRAB_GANGLION_IMAGE no seu .env com uma referência imutável publicada — o workflow de release do crab/crab-ganglion-harness publica ghcr.io/lepistabioinformatics/crab-ganglion:sha-<short-sha> — e baixe-a. Veja Deploy para entender por que essa referência precisa ser imutável e não uma tag móvel.
Nada sobe: o gateway nunca fica saudável
Sintoma. O docker compose up -d retorna, mas o mycelium-gateway fica esperando e o chat-webapp nunca inicia. Nada está acessível.
Causa. O mycelium-gateway declara depends_on: crab-shell-proxy: condition: service_healthy, e o chat-webapp por sua vez depende de o gateway estar saudável, então um proxy que sai no boot leva a stack inteira junto.
O proxy sai fatalmente por um conjunto pequeno e específico de razões. O validate() dele rejeita uma configuração sem hostDataRoot, sem network, com um agente sem serviceName ou sem template, ou com um agente nomeando um harness que ele não orquestra. Separadamente, um agente picoclaw cujo bearer token não pode ser resolvido do ambiente é fatal — deliberadamente, porque descartar um em silêncio deixaria um membro trancado do lado de fora sem nenhum sinal no boot.
Correção.
docker compose logs crab-shell-proxy
A falha nomeia o que está faltando. Defina isso no .env e suba a stack de novo.
As rotas de um agente respondem 404 e todo o resto funciona
Sintoma. Um único agente se comporta como se não existisse — suas rotas respondem 404 — enquanto os outros agentes estão bem e o proxy está saudável.
Causa. Um agente ganglion se remove na carga em vez de derrubar o proxy, e esse é o comportamento projetado, não um defeito. Um mesmo arquivo de configuração pode descrever vários deploys, e um agente que chega a um host sem chave para ele degrada para “esse agente não existe” em vez de “o proxy não vai subir”, o que derrubaria junto todos os outros agentes. Há três motivos para isso acontecer: CRAB_GANGLION_IMAGE não está definida, a variável apiKeyEnv do modelo do agente não está definida, ou o bearer token dele não pode ser resolvido.
Nada é rebaixado em silêncio. O proxy imprime uma linha por agente desabilitado no boot: agent "<key>" disabled: <reason> — its routes will answer 404, em que o motivo nomeia a configuração, por exemplo que ganglionImage (ou CRAB_GANGLION_IMAGE) não está definida e não tem valor padrão de propósito.
Correção. Procure disabled no log de boot, defina a variável que ele nomeia e reinicie o proxy.
Um agente que não declara a chave
harness:é hoje um agente ganglion (DefaultHarnessemcrab/crab-shell-proxy/internal/config/config.go), então está sujeito às três verificações. Declare o harness explicitamente em todo agente — uma atualização de configuração não deveria trocar um runtime por omissão.
Criar uma tarefa agendada responde 501
Sintoma. O painel de Tarefas, ou uma chamada direta à API, recusa criar, alterar, desabilitar ou apagar uma tarefa agendada com um 501 Not Implemented e uma mensagem como:
creating scheduled tasks over this API is not available on the picoclaw
harness (agent beta): its agent creates them itself
Causa. crab/crab-shell-proxy/internal/httpapi/cron_write.go recusa toda rota de escrita de cron para um agente cujo harness não é o ganglion. No picoclaw o harness é dono do armazenamento de jobs e mantém a agenda viva em memória, então um botão no painel poderia discordar dos timers que estão de fato rodando. No ganglion o proxy é dono da agenda, acima do container, então ele consegue atender às escritas honestamente. As rotas de leitura são atendidas nos dois harnesses, e é por isso que você ainda vê as tarefas de um agente picoclaw e o histórico de execução delas.
Correção. Em um agente picoclaw, peça ao próprio agente no chat para criar, alterar ou remover a tarefa; ele é dono delas. Se você quer os controles de escrita do painel, use um agente no harness ganglion. Veja Tarefas agendadas.
O mecanismo geral é
requireHarnessFeatureeminternal/httpapi/harness_gate.go: um recurso que o harness não consegue atender responde 501 nomeando o harness, em vez de fingir sucesso. Essa regra existe porque um harness já retirado deixou, certa vez, projetos serem criados, guardados, listados e reportados como ativos sem mudar nada no agente que respondia. Como as tabelas desse arquivo estão hoje, ele não reserva nada de nenhum dos dois harnesses distribuídos, então a escrita de cron acima é o 501 que você vai realmente encontrar.
O gateway responde 400 "Request path does not match any service"
Sintoma. Uma requisição através do mycelium-gateway é rejeitada antes mesmo de chegar ao proxy, com exatamente esse texto.
Causa. O Mycelium roteia pelo primeiro segmento do caminho, comparado com uma chave de serviço na configuração TOML dele, e depois compara o resto do caminho com os blocos [[<agent>.path]] desse serviço. Um caminho sem bloco correspondente é recusado aqui. É o que acontece quando existe uma rota no proxy mas a configuração do gateway não foi estendida para permiti-la — as rotas de leitura /v1/cron/* são o exemplo de sempre, e todos os perfis em deploy/ já trazem um bloco por agente para elas.
Correção. Adicione o bloco [[<agent>.path]] correspondente à configuração de gateway que o seu modo monta (deploy/standalone/config.standalone.toml ou deploy/prod/config.base.toml) e recrie o gateway.
O mesmo texto de erro aparece por um motivo sem relação: a própria rota
/healthdo Mycelium só trataGETe rejeitaHEADcom essa mensagem. Se você a está sondando com algo que enviaHEAD—wget --spider, por exemplo — o 400 é sobre o método, não sobre roteamento. Os dois healthchecks emdocker-compose.yamlusam umGETsimples por isso.
O Grafana está vazio e todos os containers estão saudáveis
Sintoma. Os dashboards carregam, os painéis desenham, e não há dado nenhum. Nenhum container está com problema, nenhum log mostra erro, e o log do próprio harness-sphere parece movimentado.
Causa. A cadeia de -f foi omitida em algum comando. O Compose aplica um overlay só quando você o nomeia, então docker compose up -d, restart, ou mesmo up -d harness-sphere sem os dois arquivos devolve cada serviço que o overlay sobrescreve para o docker-compose.yaml. Para o harness-sphere isso significa que o exportador volta para stdout e o bind-mount da configuração dele some, então ele imprime as métricas nos próprios logs em vez de enviá-las. Nada, em lugar nenhum, avisa disso.
Correção. Suba nomeando os dois arquivos e depois elimine a armadilha colocando isto no .env da raiz do repositório; feito isso, um docker compose up -d puro passa a estar correto:
COMPOSE_FILE=docker-compose.yaml:docker-compose.observability.yaml
Veja Observabilidade.
O agente diz que salvou um arquivo, e o painel de Arquivos não o lista
Sintoma. O agente relata ter escrito um documento, e nada aparece no painel de Arquivos do membro. O arquivo foi mesmo escrito — ele está apenas em um lugar onde a interface não procura.
Causa. public/ dentro de um workspace é o único diretório que a interface do membro lista, e public/attachments/ é onde o agente é instruído a escrever as entregas. Um arquivo de memória gerenciado (FILE_DELIVERY.md) leva essa regra para dentro de todo workspace e é lido a cada turno, justamente porque um arquivo escrito fora de public/ é invisível para o membro, não importa como o deploy esteja configurado.
Correção. Peça ao agente para mover ou salvar de novo o arquivo em public/attachments/, dizendo o caminho. Não escreva em uploads/: esse é o nome antigo do diretório, mantido no código só para que uma migração única reconheça um workspace anterior à renomeação.
Uma surpresa relacionada: o aviso de clipe que o proxy acrescenta quando um arquivo é entregue existe só no stream e nunca é persistido. Depois de recarregar a página, o único registro de um arquivo entregue é o que o próprio modelo escreveu na resposta, e é por isso que a regra gerenciada também manda ele dizer o caminho em voz alta.
As abas Mapa e Entidades continuam vazias
Sintoma. O grafo de conhecimento nunca registra nada. Nenhum erro é mostrado e nada é registrado em log sobre isso.
Causa. CRAB_MCP_TOKEN_SECRET está vazia. Esse valor assina o bearer token que um agente criado apresenta de volta ao endpoint MCP do próprio proxy. Não definir é um estado suportado e deliberado: /v1/mcp não é registrado, nenhum bloco de servidor MCP é escrito em nenhum workspace, e todo o resto se comporta normalmente. Um deploy que esqueceu o segredo tem que ficar sem memória, e não com um endpoint sem autenticação alcançável por todo container da rede.
Correção. Gere um com openssl rand -hex 32, defina-o no .env e recrie o proxy. Os dois arquivos .env.example explicam isso.
O harness-sphere não consegue atribuir instâncias, ou /v1/instances responde 404
Sintoma. O observador reporta o host, a si mesmo e as três sondas de serviço, mas nada por tenant. Uma requisição direta a GET /v1/instances no proxy retorna 404.
Causa. CRAB_TELEMETRY_TOKEN está vazia, então a rota não é registrada de jeito nenhum — ausente, não 401. Esse endpoint revela toda a topologia de tenants, subscriptions e usuários do deploy, então um deploy que não optou por isso não ganha superfície nova.
Correção. Gere um segredo novo com openssl rand -hex 32 e defina-o. Não reaproveite o bearer token de um agente para isso: um token de agente dá acesso a conversar como qualquer membro de qualquer tenant, e o raciocínio está em Observabilidade.
rm -rf data/... falha com Permission denied
Sintoma. Apagar o estado em disco durante um reset falha.
Causa. O crab-shell-proxy cria a árvore do tenant como root, 0700, então os diretórios não são seus.
Correção. Use sudo nesse único comando, como faz a sequência de reset em Deploy. É também por isso que o harness-sphere roda como root com um bind somente leitura: nada mais conseguiria percorrer a árvore.
Nenhum e-mail de login chega no standalone
Sintoma. Você pede um magic link, ou o código de bootstrap do Staff, e nenhum e-mail chega.
Causa. O standalone não tem SMTP de verdade. Ele traz um transporte simulado que escreve a mensagem no log em vez de enviá-la — uma propriedade deliberada do modo.
Correção. Leia no log do gateway. Os links de login caem no mesmo lugar.
docker compose logs mycelium-gateway | grep -i bootstrap
Ninguém consegue entrar em um deploy de prod recém-criado
Sintoma. A stack de prod está no ar, o gateway está rodando, e a autenticação não funciona.
Causa. O adaptador Postgres do Mycelium não tem migrações embutidas, ao contrário do de SQLite, então um banco recém-criado não tem schema.
Correção. Rode a aplicação de schema única, em dois passos, descrita em Banco de dados e migrações. Os dois passos são necessários na versão que este repositório fixa; o segundo não é opcional.
Para onde ir agora
Se a falha é sobre como um deploy é montado, Deploy é o relato mais completo. Se é sobre o que o observador mostra ou deixa de mostrar, Observabilidade cobre essa superfície. Para o comportamento dos dois harnesses em si, veja Harnesses.
crab-shell-proxy
Esta página descreve o orquestrador como componente: do que ele é dono, do que ele deliberadamente não é, e como o código dele está organizado. Leia antes de abrir o repositório pela primeira vez.
O que é
crab-shell-proxy é um pequeno serviço em Go que fica entre o gateway Mycelium e os containers dos agentes. Ele lê para qual agente é a requisição e qual membro a fez, garante que o container daquele membro está rodando, e repassa a conversa para ele. O repositório é crab/crab-shell-proxy, o módulo Go dele é github.com/LepistaBioinformatics/crab-shell-proxy, e o serviço no compose tem o mesmo nome do diretório.
É o componente que segura o socket do Docker, e ele roda como root. O estágio de runtime do Dockerfile diz o porquê com todas as letras: ele precisa alcançar o socket (root:docker, modo 0660), ler arquivos de template pertencentes ao root, e escrever diretórios de dados por usuário. Todo o resto da stack está arranjado para não precisar desses privilégios, porque este componente já os tem. A nota de segurança do README diz sem rodeios: o proxy é o plano de controle confiável, e os agentes que ele cria são a parte sem root, em sandbox.
Um harness é o programa dentro do container de um agente que de fato conversa com o modelo e roda ferramentas. O proxy orquestra harnesses; ele não é um. Veja Harnesses.
Pelo que ele é responsável
Resolver identidade em container. O gateway verifica o token de quem chama e injeta um cabeçalho de perfil. O proxy tira o agente do nome de serviço injetado e o membro do accId do perfil, e garante que existe um container e um diretório para a tupla (tenant, subscription, agent, user) resultante. Usa-se o id da conta em vez do e-mail porque um e-mail pode mudar; o e-mail fica só como marcador legível em .crab-owner.json.
Ciclo de vida. Um agente é declarado como scale-to-zero, em que o container sobe do zero na primeira requisição do membro e é parado depois de uma janela de ociosidade, ou como continuous, em que ele nunca é parado automaticamente. Os dois são configurados por agente no config.yaml.
Escolher o harness. Cada agente declara qual runtime responde por ele:
agents:
alpha:
serviceName: "alpha"
harness: "ganglion"
template: "alpha"
mode: "scale-to-zero"
idleTimeout: 30s
internal/pico roda um turno contra o picoclaw pelo protocolo WebSocket dele; internal/ganglion roda um contra o ganglion por HTTP com SSE. Qual dos dois é usado é decidido por essa única chave. Um agente que não declara a chave harness: recebe o ganglion: o carregador de configuração em internal/config/config.go preenche um harness: vazio com DefaultHarness antes de validar qualquer coisa, e requireHarnessFeature em internal/httpapi/harness_gate.go lê a mesma constante. Ainda assim, declare a chave explicitamente em todo agente que você escrever — um agente ganglion sem imagem é desabilitado em vez de iniciado, então herdar o padrão em um host despreparado tira esse agente de serviço.
Dizer a verdade sobre o que um harness não consegue fazer. harness_gate.go mantém uma tabela de recursos que não são universais, e um recurso que o harness do agente não consegue atender responde 501 nomeando o harness, em vez de fingir sucesso. O arquivo registra o incidente que gerou a regra: um harness certa vez aceitou uma criação de projeto que ele não implementava, então um projeto podia ser criado, guardado e listado sem mudar nada no agente que respondia.
Tudo que é feito com um container. Provisionamento e posse de volumes, materialização de segredos, o registro de modelos, o servidor MCP do grafo de memória, as tarefas agendadas e a API de administração vivem todos aqui. A especificação do harness coloca isso como uma fronteira permanente: um harness dentro de um container não tem nada que iniciar, parar ou provisionar coisa alguma, nem a si mesmo.
A superfície HTTP. A parte voltada ao membro tem o formato da OpenAI — POST /v1/chat/completions, GET /v1/models, GET /v1/sessions/history — com GET /healthz para liveness e GET /doc/openapi.json para o documento OpenAPI embutido no binário. A administração fica em /v1/admin/..., e GET /v1/instances é um inventário somente leitura das instâncias em execução, que existe para que nada mais na stack precise perguntar ao próprio Docker.
Pelo que ele não é responsável
Ele não autentica ninguém. A identidade chega já verificada do gateway, e o trabalho do proxy é confiar nesse cabeçalho, não repetir a verificação.
Ele não roda o laço do agente. Decidir qual ferramenta chamar, quando parar e qual é a resposta pertence ao harness.
Ele não desenha nada. A interface voltada ao membro é o crab-exoskeleton-webapp, que chega ao proxy através do gateway e nunca fala direto com um container de agente.
Ele não coleta métricas sobre a stack. Isso é o harness-sphere, e a divisão sustenta peso: como o proxy já segura um socket do Docker, nada mais na stack ganha um.
Como é construído e testado
O build é o portão de testes. O estágio de build do Dockerfile roda go vet ./... e go test ./... antes de linkar o binário, então um teste que falha significa que nenhuma imagem é produzida e, portanto, nada é publicado. release-image.yml é o único workflow do repositório; ele constrói e publica essa imagem em um push para main ou em uma tag de versão. Não há um workflow separado de pull request, o que significa que as verificações que um contribuidor roda localmente são as mesmas que barram a imagem:
go vet ./... && go test ./...
Uma segunda suíte conversa com um daemon Docker de verdade e fica atrás de uma build tag, então ela não roda no comando acima e também não roda no build da imagem:
go test -tags integration ./internal/docker -run TestIntegration -v
config.yaml é embutido na imagem em /etc/crab-shell-proxy/config.yaml e guarda nomes de variáveis de ambiente, não valores, de modo que uma mesma imagem continua utilizável em vários deploys. Veja Configuração.
Como o código está organizado
cmd/crab-shell-proxy/main.go é o ponto de entrada; todo o resto está em internal/. Os pacotes que vale conhecer antes de começar a ler:
| Pacote | O que vive ali |
|---|---|
config | o catálogo de agentes, os padrões e a validação, e os helpers de caminho para o diretório de um membro |
httpapi | toda rota, incluindo o portão de recursos do harness |
docker | o cliente da API do Docker Engine escrito à mão e tudo que é feito com um container ou com o volume dele |
pico | rodar um turno contra o picoclaw |
ganglion | rodar um turno contra o crab-ganglion-harness |
registry | os modelos, a cascata deles, e quem pode usar qual |
history | ler as transcrições de volta do diretório de um membro |
memgraph, mcpserver, mcptoken | o grafo de memória e o endpoint MCP pelo qual os agentes chegam nele |
cron, projects, restart, authz, identity, turn | tarefas agendadas, projetos, controle de restart, autorização, o cabeçalho de perfil, e os tipos de turno compartilhados |
internal/docker é, com larga margem, o maior pacote, o que é um bom sinal de onde está o trabalho: a maior parte do que este serviço faz é manipulação cuidadosa de sistema de arquivos e de containers em nome de alguém cuja requisição ele já decidiu confiar.
Para onde ir agora
Harnesses explica os dois runtimes e como um deles é escolhido. Agentes, workspaces e projetos descreve o layout de diretórios que este serviço lê e escreve. Se você está prestes a mexer no código, Trabalhando na stack tem os comandos de build e de teste de cada repositório em um só lugar.
crab-ganglion-harness
O ganglion é o runtime de agente que este projeto escreveu para si mesmo, e o harness que este livro ensina. Esta página o descreve como componente: o que roda dentro do container de um agente, o que esse programa tem permissão de fazer, e como o repositório está organizado.
O que é
Um harness é o programa que roda dentro do container de agente de um membro. Ele sustenta a conversa, chama o modelo, roda as ferramentas que o modelo pedir, e escreve a transcrição em disco. O crab-shell-proxy inicia um container desses por membro e fala com ele; o que acontece lá dentro é assunto do harness.
crab-ganglion-harness é esse programa, escrito em Go. O go.mod dele declara o módulo, uma versão do Go, e mais nada: o harness não tem nenhuma dependência de terceiros, e a coisa toda compila em um único binário estático. Ele serve HTTP com Server-Sent Events nativamente, então o runner do proxy para ele não precisa traduzir protocolo — internal/ganglion/turn.go, no proxy, anota que é por isso que ele não tem análogo do palpite de 500 milissegundos de que o runner do picoclaw precisa para saber quando um turno terminou.
O outro harness é o picoclaw, um projeto de terceiros em que esta stack começou. Ele continua totalmente suportado e está de saída: um agente que não declara a chave
harness:recebe agora este aqui, não aquele. Harnesses cobre a comparação; escrevaharness: "ganglion"explicitamente em cada agente que você criar, em vez de se apoiar no padrão.
Pelo que ele é responsável
Servir um turno por requisição. A superfície HTTP são duas rotas e nada mais: POST /v1/chat/completions roda um turno, e GET /health responde {"status":"ok"} para a checagem de saúde do proxy. O turno termina quando o handler retorna.
Transmitir o trabalho enquanto ele acontece. Conforme o agente usa suas ferramentas, a narração e o raciocínio chegam por um canal de progresso. A resposta é diferente: um frame é acumulado e classificado quando está completo, e só então enviado de uma vez. O motivo está escrito na especificação, porque parece uma regressão para quem não o conhece. Se o texto de uma iteração é narração ou é a resposta final depende de como o frame termina, e isso não dá para saber antes de o stream acabar — o proxy mediu sete turnos em cento e doze entregando uma resposta inteira no mesmo frame que trazia uma chamada de ferramenta no fim. Emitir de forma otimista e corrigir depois faria a resposta se reescrever visivelmente na tela.
Limitar um turno. O único limite de um turno é quantas vezes ele pode voltar para pegar mais uma ferramenta. Um turno que chega ao teto para e avisa, em vez de falhar em silêncio. Dois lugares definem isso e o primeiro vence: agents.defaults.max_tool_iterations no config.json montado, que é a superfície que um administrador realmente tem, e GANGLION_MAX_ITERATIONS no container, cujo padrão é 12. O harness imprime no boot o número em vigor e de onde ele veio, porque 12 fica igualzinho se foi um operador que definiu, se foi o arquivo, ou se não foi ninguém.
Rodar ferramentas, confinado. A ferramenta que importa é a shell. Antes de rodar um comando, o binário se reexecuta em um modo sandbox, aplica um domínio Landlock, e só então executa o comando. O workspace é leitura e escrita; /usr, /bin e /sbin são leitura e execução; todo o resto, /proc incluído, é negado — e é isso que impede um comando de ler /proc/1/environ e, com ele, a chave de provedor do próprio harness. Se o kernel não conseguir fornecer um domínio Landlock, o harness se recusa a subir em vez de servir turnos sem confinamento.
Outras ferramentas só são registradas quando a configuração as suporta: load_image e set_reasoning_depth estão sempre presentes, web_search e web_fetch aparecem quando há um provedor de busca configurado, generate_image quando há um modelo de imagem, e subagents e research quando os sub-turnos estão habilitados. O que um operador montou por MCP entra por cima. Cada linha do log de boot diz quais delas foram ligadas.
Persistir a conversa. A transcrição é um JSONL só de acréscimo em workspace/sessions, e a janela de contexto é um artefato derivado, separado. Uma conversa que tem transcrição e não tem janela — o estado em que chega toda conversa migrada do picoclaw — tem a janela reconstruída a partir da transcrição, para que o membro não veja o histórico na tela enquanto o agente responde como se a conversa tivesse acabado de começar.
Manter o layout que a stack espera. Tudo vive sob o segmento workspace, porque é ali que o proxy olha de fora do container. Os arquivos de um projeto vão em workspace-<id>, irmão de workspace/, nunca filho dele; o harness migra no boot uma subárvore deixada no caminho antigo. Veja Agentes, workspaces e projetos.
Pelo que ele não é responsável
Ele não inicia, não para e não provisiona nada, nem a si mesmo. Ciclo de vida de container, volumes e a posse deles, materialização de segredos, identidade e autorização no mycelium, o registro de modelos, a API de administração, projetos e tarefas agendadas ficam todos no crab-shell-proxy. A especificação do harness lista isso como permanentemente fora de escopo.
Ele não decide quem pode aprovar uma ação sujeita a aprovação. O harness tem a porta Approver e a suspensão no meio do turno atrás dela, e o primeiro adaptador dela chama de volta o proxy, que é dono da identidade do membro — o harness só pergunta. A outra metade dessa ida e volta não está construída: não existe endpoint de aprovação no proxy, e com GANGLION_APPROVAL_ENDPOINT não definida o laço instala um aprovador que aceita tudo. Trate o fluxo de aprovação como uma costura que existe, não como um recurso que você pode ligar.
Ele não tem diretório .secrets/. As credenciais chegam a este harness como variáveis de ambiente. Um valor que não pode ficar em texto puro pode ser criptografado antes — crab-ganglion encrypt lê o texto puro pela stdin, nunca como argumento, e imprime o valor enc:// para você colar no ambiente do deploy.
Como é construído e testado
Este é o único dos quatro repositórios com um portão de pull request que roda a suíte de testes. .github/workflows/ci.yml roda, em cada pull request e em cada push para main:
gofmt -l . | tee /dev/stderr | (! read)
go vet ./...
go test -race ./...
go build ./...
-race é deliberado, não hábito: o escritor SSE tem dois escritores por construção, o turno e o heartbeat, e uma saída intercalada corromperia um frame que o cliente está no meio de interpretar.
Os mesmos testes rodam uma segunda vez dentro do Dockerfile, antes de o binário ser linkado, então a imagem não pode ser construída sem que eles passem. release.yml constrói com o cache de camadas desligado exatamente por isso — reaproveitar os testes de um cache faria uma execução verde deixar de ser evidência de que eles rodaram para estes bytes.
A imagem de runtime é Alpine, e não distroless, e o Dockerfile explica que isso foi descoberto na prática: distroless não tem /bin/sh, então a ferramenta shell nunca conseguiria funcionar. O container roda com uid 1000, que é o mesmo para o qual o proxy faz chown do volume de cada membro, e escuta na porta 18800.
Toda imagem publicada é endereçada pelo commit que a produziu. Não existe
:latestnem:main;release.ymlpublicaghcr.io/lepistabioinformatics/crab-ganglion:sha-<commit>e nada que venha a ser reconstruído. Essa é uma cicatriz própria desta stack: oEnsureImagedo proxy retorna cedo quando um nome resolve localmente, e a imagem do harness não é um serviço do compose, então uma tag reconstruída pode deixar um host rodando bytes antigos indefinidamente sem nada nos logs. Aconteceu uma vez, por três semanas.
A consequência para um deploy é que a referência da imagem é uma decisão que alguém toma. docker-compose.prod.yaml não define CRAB_GANGLION_IMAGE e não fornece padrão, então um deploy de produção tem que nomear um digest ou uma tag sha- explicitamente antes que um agente ganglion possa iniciar. O arquivo compose de desenvolvimento segue o outro caminho: um serviço ganglion-image, só de build, constrói o harness a partir do submódulo e o marca como zombie-crab/crab-ganglion:dev, e o proxy espera por ele. Como esse build roda a suíte de testes do próprio harness, um teste que falha impede a stack de desenvolvimento de subir. Veja Deploy.
Como o código está organizado
O repositório é hexagonal, e o formato é garantido por testes, não por convenção.
cmd/crab-ganglion/main.go a raiz de composição: o único lugar em que os adaptadores se encontram
internal/domain/ entidades, as portas, política pura — só biblioteca padrão
internal/runtime/ o laço do turno, compactação, sub-agentes
internal/config/ o ambiente e o config.json montado
internal/adapter/ um diretório por adaptador
internal/secret/ o resolvedor de enc://
internal/skillfile/ leitura de arquivos de skill
internal/domain/arch_test.go guarda duas regras. A primeira falha se o pacote de domínio importar qualquer coisa fora da biblioteca padrão, porque, no momento em que o domínio importa um cliente HTTP ou o SDK de um provedor, o laço deixa de ser testável sem eles. A segunda falha se um adaptador importar outro, e é isso que faz de um segundo provedor ou de um segundo ingresso um arquivo novo, e não uma refatoração.
Os adaptadores são agrupados pelo que eles acionam: httpsse é o caminho de entrada; provider/openai e provider/router são o caminho de saída para um modelo; store/jsonl e store/window são a transcrição e o contexto derivado; tool/* é um diretório por ferramenta; approver/proxy, telemetry/otlp, mcp, skills e evolution são o resto. Adicionar uma ferramenta é um arquivo mais uma linha de registro em main.go — não uma mudança no laço, na porta ou no registro.
A especificação de tudo isso vive no repositório do produto, em .specs/features/crab-ganglion-harness/, junto com as notas de design e os identificadores de requisito que os comentários do código citam.
Para onde ir agora
Harnesses compara os dois runtimes e explica o que o proxy faz quando um recurso não está disponível em um deles. Configuração cobre o ambiente que este container lê. Trabalhando na stack tem os comandos.
crab-exoskeleton-webapp
O webapp é a parte da stack que o membro realmente vê. Esta página o descreve como componente: o que ele é, a única regra de transporte que rege toda mudança nele, e onde vive o código dele.
O que é
crab-exoskeleton-webapp é uma aplicação Next.js 15 que usa o App Router. Ele é ao mesmo tempo a interface de usuário e um backend-for-frontend: as páginas em app/ desenham o chat e o console de administração, e os route handlers em app/api/ são uma camada no servidor que chama o upstream em nome do navegador.
O serviço dele no compose é
chat-webapp, não o nome do repositório. O camponamedopackage.jsondele também, e a imagem publicada emdocker-compose.prod.yamltambém. Se você está procurando este componente em um arquivo compose, emdocker compose logs, ou em uma listagem de containers, procure porchat-webapp.
O arranjo de backend-for-frontend é o ponto, não um detalhe. O navegador guarda um cookie de sessão e mais nada — nenhum token, nenhum id de conta, nenhuma URL de upstream. Toda requisição vai do navegador para o route handler, dele para o gateway Mycelium, e dele para o crab-shell-proxy, de modo que a mesma identidade verificada que protege o backend protege a interface sem que a interface tenha que reimplementar nada disso.
Pelo que ele é responsável
Sustentar a sessão. O login é um magic link. O route handler o completa e guarda a sessão do gateway em um cookie HTTP-only; middleware.ts protege /chat e /onboarding verificando que o cookie é interpretável e que o token dele ainda não passou da própria validade, redirecionando para /signin e limpando o cookie quando não passa. Essa verificação explicitamente não é validação — um token pode ser revogado no upstream enquanto a validade dele ainda está no futuro, então a resposta de verdade continua vindo da primeira chamada ao upstream, que limpa a sessão em um 401.
A experiência de chat. Respostas em streaming, histórico de conversas, busca, renomear e etiquetar, deep links da forma /chat/{agent}/{sessionId}, upload de arquivo para o workspace do membro, e visões de linha do tempo e de árvore da atividade passada. Veja O cliente de chat.
O console do operador. Tenants, subscriptions e membros, o registro de modelos por agente e as atribuições de modelo por usuário, skills compartilhadas e conteúdo compartilhado, segredos, e identidade visual. O gateway continua sendo quem impõe quem pode fazer o quê; estas telas são uma superfície sobre a API de administração do proxy, não um segundo sistema de autorização. Veja Guia de administração.
Um banco de dados próprio. Uma conexão Postgres, configurada por DATABASE_URL, carrega o índice de conversas e os metadados do lado da aplicação. No arquivo compose de desenvolvimento, isso é o serviço separado chat-webapp-postgres.
A regra de transporte: sempre JSON-RPC, nunca uma nova chamada REST
Esta é a única convenção que você precisa conhecer antes de escrever uma linha de código aqui.
O gateway do Mycelium expõe tanto uma API REST quanto um endpoint JSON-RPC 2.0 em POST /_adm/rpc. Eles não são intercambiáveis e REST é o padrão errado. Os endpoints REST beginners do gateway são exclusivos de provedor de identidade externo: para um usuário de magic link — que é todo usuário deste deploy — eles respondem 400 "Invalid provider". O dispatcher de RPC resolve o emissor interno, então ele é o único transporte que funciona para os membros desta stack. Isso foi estabelecido testando, e a evidência está guardada em .specs/features/onboarding/context.md neste repositório. A superfície de RPC também é mais ampla: operações inteiras, como convidar e desconvidar um convidado, não têm equivalente REST que esta stack consiga alcançar.
Na prática, isso significa chamar myceliumRpc() de lib/mycelium.ts, nunca adicionar um novo caminho fetchMycelium() a uma rota REST /_adm. Os parâmetros são camelCase, e o registro autoritativo dos nomes de método é ports/api/src/rpc/method_names.rs no código-fonte do mycelium — nunca adivinhe um, porque um nome inventado falha só em tempo de execução e a falha parece um problema de permissão.
A regra é imposta, não confiada. .github/workflows/mycelium-transport.yml passa um grep em app/ e lib/ a cada pull request que os toca e quebra o build quando um caminho /_adm é passado para fetchMycelium a partir de um arquivo fora da sua lista de permissões. A lista de permissões é onde vivem as exceções, cada uma com um motivo: o par de requisição e verificação do magic link antes da sessão, que não tem token para autenticar uma chamada RPC; o próprio lib/mycelium.ts, porque POST /_adm/rpc é o transporte RPC; e app/api/tenants/[id], que é anterior à checagem e contradiz a regra como escrita, colocado na lista para ficar visível em vez de silenciosamente tolerado. O comentário do próprio workflow é honesto sobre o seu limite: ele casa um caminho /_adm em uma linha, então uma chamada montada em várias linhas ou através de uma variável não é pega. É uma catraca contra o caminho fácil, não uma prova.
Mais uma fronteira que vale declarar: as requisições ao crab-shell-proxy — os caminhos /{agent}/v1/... e /alpha/v1/admin/... — são a API HTTP do próprio proxy e continuam REST. “Chame o mycelium por JSON-RPC” não é “converta o proxy para JSON-RPC”.
Pelo que ele não é responsável
Ele nunca fala com um container de agente. Não existe caminho desta aplicação até um harness que não passe pelo gateway e pelo proxy.
Ele não decide quem pode fazer o quê. A autorização é do gateway, e as telas a refletem em vez de implementá-la.
Ele não embute nada específico de deploy em tempo de build. MYCELIUM_INTERNAL_URL e DATABASE_URL são lidas no servidor a cada requisição, que é o que permite a uma única imagem publicada servir todo deploy. A UI de administração do Mycelium que vem ao lado dele, mycelium-webapp, é uma SPA puramente do lado do cliente cuja URL de API é compilada junto; as duas são diferentes nesse aspecto e o arquivo compose diz isso.
Como é construído e testado
O desenvolvimento é o loop comum do Next.js:
yarn install
yarn dev # http://localhost:3000
Dois workflows rodam no CI, e nenhum dos dois roda a suíte de testes. mycelium-transport.yml é o grep de transporte descrito acima, nos pull requests que tocam app/ ou lib/. release-image.yml constrói e publica a imagem em um push para main ou em uma tag de versão; o Dockerfile instala com yarn install --frozen-lockfile e depois roda yarn build, então um erro de build quebra a publicação e um teste falhando não.
yarn test roda a suíte do Vitest, que é substancial e cobre lib/, components/ e partes de app/. Ela está documentada no README do repositório e é a verificação para rodar antes de você abrir um pull request — só saiba que nada no CI vai rodá-la por você.
yarn lintnão funciona. O script énext lint, mas o ESLint não é uma dependência deste repositório: ele não aparece nem empackage.jsonnem emyarn.lock, e não está instalado. O script é um resquício. Não o coloque em um checklist de contribuição e não espere que um linter pegue nada aqui.
tsconfig.json define noEmit, então o TypeScript é um verificador de tipos em vez de uma etapa de build, e não há script que o invoque sozinho; os erros de tipo aparecem através de yarn build e do seu editor.
A imagem de produção é pequena porque next.config.ts define output: "standalone", que rastreia só as dependências realmente usadas em tempo de execução — o estágio final copia o servidor rastreado e os arquivos estáticos e não precisa nem de node_modules nem do yarn. O Dockerfile carrega um aviso longo contra reativar o corepack, que substituiu um yarn empacotado que funcionava por um shim que resolve a sua versão pela rede e transformou um build offline em um que precisava do registro npm.
Como o código está organizado
app/chat/ the chat experience
app/admin/ the operator console
app/api/ the backend-for-frontend route handlers
app/signin/ magic-link sign-in
components/ shared UI, plus the pre-auth landing page
lib/ everything that is not a component: mycelium.ts, session.ts,
the model, media, memory and admin helpers, i18n
middleware.ts the session guard on /chat and /onboarding
.specs/ specifications; start with .specs/project/PROJECT.md
Os testes ficam ao lado do código que cobrem, como *.test.ts e *.test.tsx. vitest.config.ts exclui node_modules, .next e .claude por glob em vez de por nome puro, porque um git worktree criado sob .claude/worktrees/ uma vez trouxe o seu próprio node_modules e os testes de outro branch para dentro da execução.
A estilização é Tailwind CSS v4 com class-variance-authority para variantes, em vez de strings className condicionais ou interpoladas.
Para onde ir agora
O cliente de chat cobre o uso da aplicação como membro, e Guia de administração cobre as telas do operador. Trabalhando na stack tem os comandos de build e teste de cada repositório, e Contribuindo tem as convenções que uma mudança precisa seguir.
harness-sphere
harness-sphere é o observador. Esta página o descreve como componente: o que ele observa, as três coisas que ele nunca vai fazer, e como o seu código está organizado.
O que é
O HarnessSphere é um único binário Rust autocontido que observa uma stack — a máquina em que roda, ele mesmo, e os serviços que o zombie-crab executa — e transforma o que encontra em métricas OpenTelemetry padrão, enviadas para o backend que você apontar. O binário se chama harnesssphere, com três s, e o repositório é crab/harness-sphere.
Ele existe porque antes dele não havia nada. Não “não havia o bastante”: a stack não emitia métricas, não tinha coletor nem dashboard, então a primeira pergunta que qualquer um faz durante um incidente — a máquina estava sob pressão quando aquilo ficou lento — não tinha resposta nenhuma. Porque o observador fica no mesmo host e na mesma linha do tempo que todo o resto, ele consegue responder a única coisa que ferramentas separadas não conseguem: se um agente ficou lento por causa do agente, ou por causa da máquina embaixo dele.
Ele é exclusivo desta stack. Ele começou como um observador de propósito geral e agora é o componente de observabilidade do zombie-crab-project. Os coletores sem fonte aqui foram apagados em vez de deixados configuráveis, e não há flag que os traga de volta. A publicação no crates.io está desativada; os releases de binário continuam.
Pelo que ele é responsável
A stack é modelada como exatamente seis camadas: o host, o próprio observador, o gateway, o proxy, o webapp e os containers de agente. Deliberadamente não existe uma variante coringa — um sétimo tipo de coisa aparecendo deve quebrar o build em vez de se arquivar sozinho sob “outros”.
Duas camadas são Critical e o resto é Optional, e a distinção é todo o modelo de resiliência. Host e Watcher são Critical: se uma delas falha de forma persistente, além de um limite configurável para que um soluço isolado seja perdoado, o observador descarrega o que consegue e sai com código diferente de zero, em alto e bom som. Todo o resto degrada, recua e tenta de novo, e nunca derruba o processo. Um alvo que não está respondendo lê um zero honesto em vez de sumir, e é por isso que o observador não precisa de ordem de inicialização e sobrevive a subir antes das coisas que observa.
Tudo é pull. Não há caminho de recepção, porque nada nesta stack empurra telemetria para ele e um receptor sem emissor é código que só pode estar errado. As métricas do host e as dele mesmo vêm do sysinfo; a liveness vem de uma conexão TCP ativa a cada tick; a CPU e a memória de container vêm da leitura direta dos arquivos de kernel do cgroup v2; e a atividade por membro vem de percorrer a árvore de tenants com glob e ler os transcritos JSONL em disco de forma incremental.
Essa última é a parte interessante, e ela tem quatro correções dentro, cada uma com um teste. As conversas de um projeto vivem em um diretório irmão workspace-<id>, e deixar de considerá-lo derrubava 42% das conversas no workspace que foi medido. Um diretório sessions/durable/ espelha os arquivos vivos um a um, então contá-lo dobra exatamente todo número. Cada execução de tarefa agendada escreve o seu próprio arquivo de sessão, o que desvia uma contagem de conversas para uma contagem de conversas mais cada execução de cron desde o provisionamento. E um transcrito que encolheu é lido de novo do zero em vez de tratado como um delta negativo.
O conteúdo das mensagens nunca é lido para virar um sinal. Só contagens, nomes e tamanhos.
Pelo que ele não é responsável
Ele nunca recebe um socket do Docker, por design. O proxy já tem um e roda como root; um segundo serviço com socket dobraria o raio de destruição do pior comprometimento possível da stack. Um socket é start, stop e exec em qualquer container e um caminho até o root do host, e tudo para o que o observador precisaria de um — mapear um container ao seu tenant — é servido pelo inventário somente leitura GET /v1/instances do proxy.
Ele não reporta custo de token. O picoclaw não escreve contagens de token em disco e não expõe endpoint de métricas, e o único caminho que já existiu era raspar um endpoint que esta stack não roda. Esse raspador foi apagado. Isso não está adiado; acabou. A contabilidade de tokens é uma das duas capacidades que justificaram escrever o crab-ganglion-harness, e ela vai chegar por aquele lado, não por este.
Ele não lê o conteúdo dos transcritos. O corpo das mensagens é dado do membro.
Ele não muda nada. Todo coletor puxa, o serviço não publica portas, e não há superfície de entrada nenhuma.
O
telemetryTokendo proxy, fornecido comoCRAB_TELEMETRY_TOKEN, protege a rota de inventário e não é um token de agente — o token bearer de um agente nunca deve ser usado no lugar dele. Com ele não definido, a rota nem chega a ser registrada, respondendo 404 em vez de 401, e esse é o padrão seguro, não um descuido.
Como ele roda nesta stack
O arquivo compose de desenvolvimento constrói o serviço a partir do submódulo e o roda com um bind somente leitura da raiz de dados do proxy em /data, um hostname estável e nenhuma porta publicada. Três detalhes ali são discutidos em detalhe no arquivo compose e valem a leitura antes de você mudar qualquer um deles.
O serviço recebe user: "0:0" nesta stack, mesmo que o Dockerfile dele rode como uid 10001. O motivo é que o proxy cria a árvore de tenants como root:root modo 0700, e a barreira fica no topo da árvore em vez de nas folhas, então um observador não-root não consegue atravessá-la de jeito nenhum. Três restrições mantêm essa permissão estreita e se sustentam juntas: o bind de /data continua somente leitura, nunca há um socket do Docker, e não há portas publicadas.
O hostname é fixado porque host.name é um rótulo. O hostname de um container assume por padrão o próprio id, que muda a cada recriação, então todo redeploy abriria uma série temporal nova — cinco tinham se acumulado em uma única noite de iteração. Um rótulo errado-mas-constante agrupa corretamente; um certo-mas-mutável nunca agrupa.
Deliberadamente não há depends_on. Colocar o observador atrás da saúde das coisas que ele observa suprimiria exatamente o sinal que ele existe para produzir.
Para de fato olhar os números, o repositório do produto carrega um overlay opcional com um OpenTelemetry Collector, Prometheus e Grafana:
docker compose -f docker-compose.yaml -f docker-compose.observability.yaml up -d
Quando você escrever queries, lembre que os nomes neste livro são os nomes OTel emitidos. A tradução de OTLP para Prometheus troca pontos por underscores e acrescenta um sufixo de unidade, então o que o observador emite como
system.memory.usageé coletado comosystem_memory_usage_bytes.
Como é construído e testado
Rust, um workspace do Cargo, edition 2024. rust-toolchain.toml pede o stable com rustfmt e clippy.
cargo build --release
./target/release/harnesssphere config.example.toml # imprime os sinais no seu terminal
O exportador de stdout é o padrão e prova a pipeline inteira sem nenhum backend para subir. Para um backend de verdade, compile com o adaptador OTLP — otlp é a única feature que o binário declara:
cargo build --release --features otlp
Saiba o que o CI verifica e o que não verifica aqui. audit.yml roda cargo audit --deny warnings quando um manifesto ou lockfile muda, semanalmente por agendamento, e sob demanda. deepseek-pr-review.yml posta um comentário de revisão automatizado em cada pull request. release-image.yml constrói e publica a imagem. Nenhum deles roda cargo test ou cargo clippy, e o Dockerfile compila o binário sem rodar a suíte. Os testes existem — crates/runtime/tests/ e harnesssphere/tests/ — mas rodá-los é com você.
O perfil de release é ajustado para ficar pequeno, com opt-level = "z", otimização em tempo de link e stripping. O unwinding de panic é mantido de propósito, porque o modelo de resiliência depende de capturar um panic dentro de um coletor antes que ele escape.
Como o código está organizado
O repositório é hexagonal, e as regras de arquitetura do ganglion foram modeladas nesta divisão de crates.
crates/domain/ canonical signal model, ports, pure policies — no I/O, no OpenTelemetry
crates/runtime/ supervisor, scheduler, circuit breaker, batching drain
crates/collectors/ host and self (Critical); process, endpoint probe, session, container (Optional)
crates/export/ stdout by default, OTLP behind the `otlp` feature
harnesssphere/ the binary: config, wiring, run
O domínio não ter nenhuma dependência de OpenTelemetry não é arrumação. Isso mantém a lógica importante — o circuit breaker, a política de criticidade, o enriquecimento — testável em unidade sem rede, e mantém um SDK pré-1.0 que ainda está mudando fora do núcleo.
A configuração é um arquivo TOML passado como primeiro argumento, com alguns overrides por variável de ambiente: HARNESSSPHERE_EXPORTER, OTEL_EXPORTER_OTLP_ENDPOINT e RUST_LOG. Todo coletor Optional fica desligado até ser configurado, então uma execução nova mostra só Host e Self. A configuração desta stack é config.zombie-crab.toml, que sonda o gateway, o proxy e o webapp, e deliberadamente deixa vazias as chaves de container de valor único — apontá-las para uma instância arbitrária produziria uma métrica que descreve um tenant e parece descrever a stack.
Para onde ir agora
Observabilidade cobre o que os números significam e como lê-los. Deploy cobre rodar a stack com o overlay de observabilidade acoplado.
Trabalhando na stack
Esta página é para quem está prestes a mudar o código. Ela diz onde o código está, como conseguir um ambiente de desenvolvimento e — a parte que vale ler com atenção — exatamente o que cada repositório verifica, porque os quatro componentes não verificam as mesmas coisas e um deles documenta um comando que não consegue rodar.
Onde o código está
O repositório do produto é zombie-crab-project, e quatro dos seus diretórios são repositórios Git separados trazidos como submódulos:
| Caminho | Linguagem | O que é |
|---|---|---|
crab/crab-shell-proxy | Go | o orquestrador |
crab/crab-ganglion-harness | Go | o runtime do agente |
crab/crab-exoskeleton-webapp | TypeScript | a interface para o membro, serviço compose chat-webapp |
crab/harness-sphere | Rust | o observador |
O próprio repositório do produto guarda os arquivos compose, a configuração de deploy sob deploy/, os Dockerfiles do gateway Mycelium e da sua UI de administração sob fungi/, este livro sob docs/book/, e as especificações sob .specs/.
Como eles são submódulos, um git clone simples te dá quatro diretórios vazios. Clone a cadeia inteira de uma vez:
git clone --recurse-submodules https://github.com/LepistaBioinformatics/zombie-crab-project.git
cd zombie-crab-project
Se você já clonou sem eles, git submodule update --init --recursive os preenche.
Um ambiente de desenvolvimento
O arquivo compose de desenvolvimento constrói todo serviço a partir do código-fonte, então os únicos pré-requisitos para rodar a stack são o Docker com o plugin Compose e uma chave de API de LLM. Copie deploy/standalone/.env.example para .env na raiz do repositório, preencha os tokens bearer, as chaves de API por agente e o segredo de bootstrap, e suba a stack. Instalação percorre isso em ordem, e Configuração cobre o que cada variável faz.
Duas coisas sobre esse build valem saber antes de você começar.
A primeira é que alguns builds rodam testes. crab-shell-proxy e crab-ganglion-harness rodam go vet e a sua suíte de testes completa dentro do build do Docker, antes de o binário ser linkado, então um teste falhando impede a stack de subir em vez de produzir uma imagem quebrada em silêncio. Esse é o comportamento pretendido para um arquivo compose de desenvolvimento, não um acidente.
A segunda é que as duas imagens de harness são produzidas por serviços que só constroem — picoclaw-image e ganglion-image — que constroem uma tag e saem, e o proxy espera pelos dois. Eles existem porque essas imagens não são serviços próprios e nada as puxa: docker system prune remove uma imagem não usada, e sem um serviço compose para reconstruí-la, todo agente que a usa ficaria morto até alguém lembrar de rodar docker build na mão.
Para trabalhar em um componente sem reconstruir tudo, você pode desenvolvê-lo nativamente contra o resto da stack. O webapp é o caso mais fácil — yarn dev na porta 3000 contra um gateway rodando — porque ele lê MYCELIUM_INTERNAL_URL e DATABASE_URL a cada requisição.
Os gates, repositório por repositório
Estas listas vêm dos workflows de cada repositório, do seu Dockerfile e do seu
package.json. Onde um repositório não tem verificação automatizada, esta página diz isso em vez de sugerir um comando que nada impõe.
crab-ganglion-harness
O único repositório com um workflow de pull request que roda os testes. .github/workflows/ci.yml roda em cada pull request e em cada push para main:
gofmt -l . # o workflow falha se isto imprimir qualquer coisa
go vet ./...
go test -race ./...
go build ./...
Rode esses quatro antes de abrir um pull request e você terá reproduzido o CI exatamente. -race não é decoração: o escritor de SSE tem dois escritores por construção, o turno e o heartbeat, e uma escrita intercalada corromperia um frame que o cliente está no meio de interpretar.
release.yml constrói e publica a imagem em um push para main ou em uma tag, com o cache de camadas desativado para que os testes dentro do Dockerfile rodem de verdade para aqueles bytes.
crab-shell-proxy
Não há workflow de pull request nenhum. O único workflow do repositório, release-image.yml, constrói e publica a imagem em um push para main ou em uma tag de versão — e o gate vive dentro do Dockerfile, que roda go vet ./... e go test ./... antes da etapa de build. Um teste vermelho significa nenhuma imagem.
Localmente, esses dois comandos são a verificação inteira:
go vet ./... && go test ./...
Uma segunda suíte dirige um daemon Docker real e fica atrás de uma build tag, então ela não roda nem no comando acima nem no build da imagem. Rode-a quando você tiver mudado o cliente da Engine API ou qualquer coisa sobre a criação de containers:
go test -tags integration ./internal/docker -run TestIntegration -v
Ela cria e remove um container Alpine descartável, então precisa de um /var/run/docker.sock alcançável, mas de nenhuma chave de LLM.
crab-exoskeleton-webapp
Dois workflows, e nenhum roda os testes. mycelium-transport.yml passa um grep em app/ e lib/ nos pull requests que os tocam, quebrando o build quando uma nova chamada REST ao gateway mycelium aparece fora da sua lista de permissões — veja o capítulo do componente para saber o que é essa regra e por quê. release-image.yml constrói e publica a imagem em um push para main ou em uma tag, o que roda yarn install --frozen-lockfile e depois yarn build.
Então a verificação efetiva do CI sobre o código é: ele compila. A suíte de testes é real e extensa, está documentada no README do repositório, e rodá-la é com você:
yarn install
yarn test # Vitest
yarn build # a mesma coisa que o build da imagem roda
Não rode
yarn lint. O script énext lint, mas o ESLint não é uma dependência deste repositório — ele não está nem empackage.jsonnem emyarn.locke não está instalado, então o script não consegue fazer nada. É um resquício de um scaffold.
Também não há script de verificação de tipos. tsconfig.json define noEmit, então o TypeScript é um verificador em vez de uma etapa de build, e os erros de tipo chegam até você através de yarn build e do seu editor.
harness-sphere
Nenhum workflow roda cargo test, e nenhum roda cargo clippy. O que o CI roda é cargo audit --deny warnings quando um Cargo.toml ou um Cargo.lock muda, semanalmente por agendamento e sob dispatch manual; um comentário de revisão automatizado escrito por modelo em cada pull request; e o build da imagem, cujo Dockerfile compila o binário de release sem rodar a suíte.
Os testes existem, sob crates/runtime/tests/ e harnesssphere/tests/, e rust-toolchain.toml pede rustfmt e clippy junto com o Rust stable. Nada vai rodá-los por você, então rode você mesmo:
cargo build --release
cargo test
A feature otlp é a única que o binário declara; um build que vai enviar métricas para um collector quer cargo build --release --features otlp.
Trabalhando neste livro
O livro é um mdBook sob docs/book/, com a fonte em inglês em src/ e a tradução em português gerada a partir de po/pt-BR.po. O inglês é a fonte e o português é uma tradução: traduzir significa editar o catálogo, nunca escrever uma segunda árvore de arquivos Markdown que pode divergir da primeira.
cd docs/book
mdbook build # o livro em inglês
MDBOOK_BOOK__LANGUAGE=pt-BR mdbook build --dest-dir book/pt-BR
O build em português precisa do mdbook-gettext, que vem do mdbook-i18n-helpers. .github/workflows/deploy-docs.yml constrói os dois em um push para main que toca docs/book/** e os publica no GitHub Pages, com o livro em português aninhado dentro do em inglês para que um único artefato carregue os dois.
Dê ao build em português um destino próprio, como acima: book.toml define build-dir = "book", então um mdbook build simples em português sobrescreveria o livro em inglês no lugar. Note também que create-missing = false está definido, então um capítulo listado em SUMMARY.md sem arquivo em disco quebra o build em vez de ser criado vazio.
Para onde ir agora
Contribuindo cobre as convenções que uma mudança precisa seguir, incluindo a cadeia de submódulos e como uma mudança se move por ela. Solução de problemas cobre as falhas que as pessoas de fato encontram rodando a stack.
Contribuindo
Esta página reúne as convenções que uma mudança precisa seguir: em que idioma escrever, onde vivem as especificações, e como uma mudança se move por uma cadeia de cinco repositórios. A maioria dessas regras é imposta por um workflow em vez de confiada, e esta página diz quais.
Tudo aqui é escrito em inglês
Este repositório e todo repositório abaixo dele é escrito em inglês, sem exceção por tipo de artefato: comentários de código, mensagens de commit, títulos e corpos de pull request, issues e comentários de issue, .specs/, READMEs, documentação, ADRs, nomes de teste, mensagens de falha e changelogs.
Isso não é uma preferência de estilo. O repositório é público sob MIT OR Apache-2.0, tem remote próprio, pull requests próprios e leitores fora do time. Um comentário em outro idioma é uma barreira para quem chega.
A regra vale em toda profundidade da cadeia — crab/crab-shell-proxy, crab/crab-ganglion-harness, crab/crab-exoskeleton-webapp e crab/harness-sphere inclusos. O repositório privado de marketing que carrega este como submódulo escreve em português, e a fronteira é o diretório modules/ daquele repositório. Uma consequência que vale declarar: mover um documento através dessa fronteira significa traduzi-lo, não copiá-lo.
A regra governa artefatos, não conversa. Falar com o dono do projeto em outro idioma enquanto se escreve um comentário em inglês é correto, não inconsistente.
Onde vivem as especificações
O trabalho de especificação vive sob .specs/, nunca na raiz de um repositório. A convenção é a mesma em todo repositório que tem uma:
.specs/project/ PROJECT.md (vision), ROADMAP.md, STATE.md
.specs/features/<slug>/ spec.md, context.md, design.md, tasks.md, reports
Uma pasta por feature, nomeada com o slug da feature. Os artefatos de execução — notas de progresso, relatórios de tarefa, notas de implementação — vão na mesma pasta da feature a que pertencem. Não deixe arquivos de rascunho na raiz do repositório; se uma ferramenta colocar um lá, mova-o para a pasta da feature correspondente.
O .specs/features/ do repositório do produto é o maior deles e é onde o trabalho entre componentes é especificado. O crab-ganglion-harness não mantém um .specs/ próprio: a especificação dele vive no repositório do produto em .specs/features/crab-ganglion-harness/, e o README dele diz isso.
Uma especificação também é o lugar certo para uma decisão que de outro modo seria perdida. Vários dos comportamentos mais surpreendentes desta stack — por que a resposta chega inteira em vez de palavra por palavra, por que uma imagem de harness não carrega tag móvel — estão documentados como requisitos com um motivo declarado, e os comentários de código os citam por identificador.
A cadeia de submódulos
O zombie-crab-project carrega quatro submódulos, cada um um repositório separado com os seus próprios pull requests. Uma mudança que toca tanto um submódulo quanto o repositório do produto é portanto dois pull requests, ou três, ou cinco.
Um ponteiro só pode nomear um commit alcançável a partir do branch padrão daquele submódulo. Alcançável, não igual: apontar para um commit mais antigo em main é comum e permitido. Apontar para um commit que existe só em um branch de pull request não é — esse commit desaparece quando o branch desaparece, e este repositório fica descrevendo uma árvore que nada aponta.
Então a cadeia é mesclada de baixo para cima, um nível por vez. Mescle primeiro o pull request do submódulo, depois avance o ponteiro para o commit de merge:
git -C crab/<name> checkout main
git -C crab/<name> pull --ff-only
git add crab/<name>
git commit -m "chore(submodule): advance <name> to #<pr>"
Uma mudança pode ter dois filhos independentes travando um pai. O trabalho de harness-sphere-integration foi o primeiro: ele precisava de um pull request em harness-sphere e de um em crab-shell-proxy, nenhum bloqueando o outro, ambos mesclados antes de o pull request do pai poder passar. Irmãos não têm ordem entre si — só filhos e pais têm. Mescle-os na ordem em que a revisão terminar, depois avance os dois ponteiros.
Abrir o pull request do pai cedo é uma questão de julgamento, não uma violação. A checagem é o que segura, então “abra a cadeia inteira para eu revisar” é aceitável desde que o corpo do pull request diga quais ponteiros são cabeças de branch.
O que o CI impõe
As regras acima são impostas por workflows, porque a falha que elas evitam acontece no botão de merge e um arquivo de instruções não está lá. Uma cadeia de quatro pull requests foi uma vez mesclada de baixo para cima na mão e um deles entrou com um ponteiro numa cabeça de branch, o que exigiu um segundo pull request para corrigir. O arquivo de instruções que deveria evitar isso dizia a coisa certa e não foi lido por ninguém no momento que importava.
.github/workflows/submodule-pointers.yml roda em cada pull request que toca crab/** ou .gitmodules. Para cada submódulo ele lê o ponteiro na árvore, pergunta à API do GitHub qual é o branch padrão daquele repositório, e compara os dois. O pull request passa só quando a comparação diz identical — o ponteiro é a ponta do branch — ou behind, significando que o ponteiro é um ancestral da ponta, que é o caso comum de um atraso deliberado. Qualquer outra coisa falha, nomeando o ponteiro e dizendo para você mesclar o filho primeiro.
Dois detalhes valem saber. O workflow não clona nada; ele resolve tudo pela API, então um submódulo privado não precisaria de token próprio. E ele itera sobre .gitmodules, então cobre os quatro submódulos mesmo onde uma regra escrita ficou para trás e ainda fala em três. Se um arquivo de regra e um workflow um dia discordarem, o workflow é a verdade.
crab-exoskeleton-webapp/.github/workflows/mycelium-transport.yml quebra um pull request que adiciona uma chamada REST ao gateway mycelium fora da sua lista de permissões. A lista de permissões é a lista de exceções, com um motivo para cada entrada, e adicionar uma linha nela é um ato visível no pull request que precisa disso. Veja o capítulo do webapp.
Os Dockerfiles dos dois componentes em Go rodam go vet e a suíte de testes completa antes de linkar o binário, então um teste vermelho significa que nenhuma imagem é construída e nada é publicado. No crab-shell-proxy, que não tem workflow de pull request nenhum, esse build é o gate.
O que não é imposto importa tanto quanto. Nada roda a suíte de testes do webapp no CI, e nada roda cargo test ou cargo clippy para o harness-sphere. Rode isso localmente antes de pedir uma revisão; Trabalhando na stack lista os comandos exatos de cada repositório.
Uma mudança, de ponta a ponta
- Escreva ou atualize a especificação sob
.specs/features/<slug>/no repositório a que a mudança pertence. - Faça a mudança no submódulo, com os seus testes, e rode as verificações daquele repositório localmente.
- Abra o pull request do submódulo. O título, o corpo e os commits dele são em inglês.
- Quando ele for mesclado, atualize o ponteiro no repositório do produto para o commit de merge e diga qual pull request ele nomeia.
- Abra o pull request do repositório do produto. Se você o abriu antes para que a cadeia inteira pudesse ser revisada junto, diga no corpo quais ponteiros ainda são cabeças de branch.
Para onde ir agora
Trabalhando na stack tem os comandos de build e teste. Os capítulos de componente — crab-shell-proxy, crab-ganglion-harness, crab-exoskeleton-webapp e harness-sphere — descrevem o layout de código de cada repositório que você talvez esteja prestes a mudar.