Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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_IMAGE definido. 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:

RecursoNome no gate
Projetosprojects
Seleção pessoal de modelopersonal model selection
O grafo de memóriathe memory graph
Tarefas agendadasscheduled 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.json do 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-zero ou continuous. 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 ser continuous. 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.