Skip to main content

Perfis de execucao: standard e isolated-pod

Decisao: ADR 0024 (#1329, aceita 2026-09-21). Threat model: security/threat-model.md §5.15. Regra em uma frase: poder total dentro do pod isolado; nenhum acesso implicito fora do pod.

O GarraIA tem uma unica postura de seguranca de fabrica, pensada para rodar direto numa maquina compartilhada — laptop, servidor com outros usuarios, Termux. Ela e o perfil standard. O perfil isolated-pod existe para o outro cenario: o operador instala o Garra num pod ou container descartavel, justamente para dar ao agente autonomia plena (ler e escrever arquivos, rodar shell, instalar pacotes, usar servidores MCP e subagentes) sem risco para nada fora do pod. Nesse caso o pod e a fronteira de seguranca, nao o Garra.

O perfil so muda por escolha explicita do operador. Nenhuma linha do codigo le /.dockerenv, /proc/*/cgroup nem variavel de runtime de container para decidir: um container com o socket do Docker montado ou com --pid=host e indistinguivel de um pod descartavel visto de dentro, e "parece isolado" nao e isolamento. Dois testes varrem o fonte e reprovam esses literais.

O que cada perfil significa​

Superficiestandard (default)isolated-pod
Piso do WhatsApp pessoal para remetente dono em conversa 1:1channels.whatsapp_linked.default_mode (default search)default_mode se explicito; senao code (sem whitelist: filesystem, bash, MCP, subagentes)
Piso do WhatsApp para remetente admitido que nao e dono, ou qualquer mensagem de grupodefault_mode (default search)igual ao standard — poder total nunca e herdado por grupo nem por contato so pareado
Remetente fora de allow/owners e sem codigo de pareamentorecusado em silenciorecusado em silencio (inalterado)
Raiz do MCP filesystem autoprovisionadoagent.file_roots se houver; senao <data_dir>/workspaceexecution.pod_root se houver; senao <data_dir>/workspace
Raiz das file tools nativas quando nada foi declarado (#1378, #1449)<data_dir>/workspace/<sessao> — um subdiretorio por sessao, nunca o paiidem — igual ao standard; execution.pod_root muda so a raiz do MCP filesystem
Jail das file tools nativas (agent.file_roots), gate de comando arriscado do bash, agent.sandboxinalteradosinalterados — o perfil libera ferramentas, nao desliga protecoes
Log de bootINFO com o perfil e a origemum WARN unico: o que foi liberado, o que o perfil NAO isola, como reverter

$HOME nunca e raiz implicita em nenhum perfil. Dentro de um pod dedicado a fronteira default e o workspace do Garra (<data_dir>/workspace, que por default e <config_dir>/data/workspace); um caminho pod-local mais amplo (/workspace, /) e escolha explicita em execution.pod_root.

Como ligar​

# config.yml
execution:
profile: isolated-pod # standard (default) | isolated-pod
pod_root: /workspace # opcional; so vale em isolated-pod

ou, sem tocar no arquivo — o caminho natural num manifest de pod:

GARRAIA_EXECUTION_PROFILE=isolated-pod garraia start

Regras de resolucao:

  • A env vence o arquivo. GARRAIA_EXECUTION_PROFILE e lida uma vez pelo ConfigLoader, e a origem fica registrada (default | file | env) para as superficies de observacao dizerem "isolated-pod (fonte: env)" em vez de so o valor.
  • Secao ausente = standard = o comportamento de hoje. Instalacao existente nao muda nada ao atualizar.
  • Valor invalido e erro de carga, no arquivo ou na env: o gateway nao sobe, e garra config check reporta Error em execution.profile (exit 2). Nunca "cai em standard em silencio" — um typo no manifest do pod tem de aparecer, nao ser mascarado.
  • A env nunca vai para o disco. garra config set, garra whatsapp link e outros caminhos load-modify-save rodam com a env presente; ela nao e promovida a execution.profile por um save. Tirar a env devolve o processo a standard no proximo start.
  • execution.pod_root precisa ser absoluto; relativo resolveria contra o diretorio de quem iniciou o processo (o config check avisa). Declarado em standard, e ignorado com Warning.

Para reverter: execution.profile: standard no config.yml (ou remova a secao) e remova a env GARRAIA_EXECUTION_PROFILE; reinicie o gateway.

O que "poder total dentro do pod" libera​

Para o dono do WhatsApp pessoal (ver abaixo) em conversa 1:1 — e so para ele, numa sessao que nao escolheu /mode — o piso passa de search para code. Todo o resto continua no piso de standard: remetente admitido que nao e dono, contato so pareado, qualquer mensagem de grupo, e os outros canais (o perfil nao muda o piso deles). No turno do dono: o ToolGate do modo code nao tem whitelist, entao passa file_read, file_write, bash, run_tests, toda ferramenta de servidor MCP registrado (filesystem__write_file inclusa) e subagentes. E o mesmo ToolGate de sempre, com outro piso — nenhuma superficie nova de politica.

O que continua ligado em isolated-pod, por desenho:

  • O jail das file tools nativas (file_read, file_write, list_dir): agent.file_roots ∪ working_dir da sessao, exatamente como em standard (#1244). Quando agent.file_roots esta vazio, a raiz default e <data_dir>/workspace/<sessao> (#1378, #1449) — o mesmo caminho que em standard, e nao o pod inteiro. Note o <sessao>: o workspace padrao nao e raiz do jail, e sim o pai de um subdiretorio por sessao, que entra como working_dir daquela chamada. Sem isso a raiz seria a mesma para todo canal, contato e principal, e o que um escreveu o turno de outro leria — disclosure cross-principal e injecao indireta persistente (#1449). Uma sessao com projeto declarado nao muda de lugar: o working_dir dela vence. execution.pod_root muda so a raiz do MCP filesystem: ele nao e herdado pelas file tools nativas, para que fechar a #1378 nao alargue de carona o alcance de arquivo num perfil que nao e o assunto dela. Se quiser que as tools nativas alcancem o pod inteiro, declare isso: agent.file_roots: ["/workspace"]. A unificacao das duas politicas e a #1383. O working_dir sintetizado por sessao nao fica so nas tres file tools: bash (sandboxed), git_diff, code_review e repo_search tambem passam a operar dentro dele. No bash sandboxed isso troca a recusa fail-closed de antes (mount vazio sem working_dir) por execucao de verdade num diretorio novo e vazio; nas outras tres o efeito e estritamente mais estreito que o CWD do processo do gateway que usavam antes.
  • O gate de comando arriscado do bash (rm -rf /, git reset --hard, …). Sem canal de confirmacao ele e fail-closed; alargue com agent.bash_allowlist se o pod for descartavel de verdade.
  • agent.sandbox, se configurado.
  • Um /mode explicito da sessao continua vencendo o piso, para cima ou para baixo.

Bash nas superficies sem humano (#1272)​

O garraia mcp-server (tool garra_agent) e o runtime do gateway nao tem humano para confirmar comando. Nelas o bash:

  • em standard, so existe dentro de um sandbox docker/podman valido (agent.sandbox.mode: all ou allowlist com bash, binario instalado). Sem isso ele nao e registrado — ssh nao conta, porque e execucao remota, nao isolamento;
  • em isolated-pod explicito, roda no host do pod (ou no sandbox, se houver um), com a denylist e o tier arriscado ligados. Se a policy exige sandbox para o bash (mode: all) e ele nao e utilizavel (sem backend, ssh, binario ausente), o bash fica de fora tambem no pod, e o aviso diz o motivo real.

A mesma regra vale para toda tool que executa codigo controlado pelo repositorio. Hoje isso e o run_tests do gateway: ele roda o scripts.test do package.json, o build.rs e o conftest.py, que o file_write consegue escrever. Em standard ele so e registrado quando o sandbox docker/podman o cobre (mode: all sem ele em elevated, ou allowlist com run_tests); sem isso fica de fora. Em isolated-pod explicito roda no host do pod.

O boot avisa uma vez quando o bash fica de fora, o /api/diagnostics mostra o check tools.bash com o passo (not_configured quando o motivo e o proprio default agent.sandbox.mode = off, que nao e defeito; warning quando ha sandbox configurado e inutilizavel — #1471), e o modelo e avisado no system prompt de que nao ha shell. garraia chat nao muda: la o humano confirma no terminal.

Na ponte MCP HTTP (o garra_agent da #1615), o workspace das file tools do agente e escopado por orquestrador dentro do mesmo <data_dir>/workspace dos perfis: cada orquestrador recebe <data_dir>/workspace/<hash>, derivado da identidade dele por digest, e nunca agent.file_roots (essas raizes sao do operador, e a ponte atende pedido de terceiro) — #1626 item 1. Em isolated-pod isso tem um limite conhecido: o pod e a fronteira e todos os orquestradores compartilham o filesystem dele, entao o working_dir por orquestrador isola o bash so no sandbox docker/podman, nao no host do pod; isolar bash por orquestrador no pod exigiria um pod por orquestrador (follow-up).

O que o perfil NAO isola​

O perfil e uma declaracao do operador; ele nao cria a fronteira. Tudo abaixo fica ao alcance do agente se o pod expuser:

  • filesystem do host montado como volume;
  • o socket do Docker/Podman (/var/run/docker.sock) — e o host inteiro;
  • container --privileged;
  • --pid=host (processos do host) e --network=host (servicos do host em loopback, metadata endpoints de nuvem);
  • mounts nao declarados, bind mounts de $HOME, tmpfs compartilhados;
  • segredos do host injetados por env (chaves de provider, tokens de deploy, credenciais de nuvem) — o agente pode le-los pelo bash.

Se qualquer item da lista vale para o seu container, ele nao e um pod isolado e isolated-pod e a escolha errada. O aviso de boot e o Warning permanente em /api/diagnostics existem para lembrar disso.

Dono no WhatsApp pessoal (channels.whatsapp_linked.owners)​

"WhatsApp conectado" nao torna todo remetente confiavel. O perfil completo e por identidade declarada, em conversa 1:1:

channels:
whatsapp_linked:
type: whatsapp_linked
enabled: true
owners: ["5511999998888"] # mesma normalizacao de `allow`: digitos, ou JID @lid
# default_mode: search # se explicito, vale para o dono tambem
  • Quem esta em owners e admitido como se estivesse em allow, e em isolated-pod recebe o piso code (ou o default_mode explicito).
  • Pareamento por codigo de 6 digitos nunca confere o perfil completo. E credencial fraca (memoria do processo, um codigo); so identidade declarada na config e dono.
  • Grupo nunca herda. O dono mandando de um grupo cai no piso standard (default_mode), como qualquer admitido.
  • Mensagem from_me continua fora (deve_responder): "note to self" nao e caminho suportado nesta fatia.
  • owners preenchido com perfil standard gera Warning no config check ("owners so tem efeito em isolated-pod"), nunca poder.
  • Sem owners, isolated-pod nao muda nada no WhatsApp — o diagnostico diz "0 donos".

Cada turno loga phone_last4 + perfil (completo | padrao) + o modo do piso. Nunca JID, telefone, push_name nem texto (a varredura de fonte fonte_nao_loga_jid_cru_nem_material_de_sessao continua valendo).

Raiz do MCP filesystem autoprovisionado​

No primeiro boot o gateway grava um mcp.json com o servidor filesystem (@modelcontextprotocol/server-filesystem). Ate a v0.4.3 a raiz era $HOME — dentro de um pod, o pod inteiro (aceitavel); numa maquina compartilhada, o contorno do jail da #1244 (inaceitavel), e a mesma linha de codigo nao sabia em qual dos dois estava. A partir da v0.4.4:

PerfilRaiz
standardagent.file_roots da config, ou <data_dir>/workspace se vazio. A env GARRAIA_FILE_ROOTS e o working_dir da sessao, que o jail nativo tambem soma, nao entram aqui
isolated-podexecution.pod_root, ou <data_dir>/workspace se ausente

A raiz efetiva e logada no provisionamento e aparece no check mcp.filesystem_root do /api/diagnostics. No primeiro boot so o <data_dir>/workspace default e criado; uma raiz declarada (agent.file_roots, execution.pod_root) tem de existir — se nao existe, o autoprovisionamento nao acontece (um warn! diz qual raiz faltou) e nada e criado em lugar dela, nunca um diretorio mais largo.

Sem Node.js, nao provisiona. O servidor roda npx; se o npx nao esta no PATH (a CLI num Linux limpo, ou o desktop num Windows sem Node global — o Node privado do instalador e so da ponte do WhatsApp e nao traz npx), o boot nao grava nada, nem cria o workspace, e loga o motivo. A linha mcp.servers do /api/diagnostics fica skipped, dizendo que falta o npx e como ligar o filesystem, e o garra doctor mcp e a tool garra_diagnostics do agente dao o mesmo motivo; as file tools nativas (file_read, file_write, list_dir) seguem disponiveis. Instalado o Node.js 20+, o proximo boot provisiona sozinho.

Instalacao anterior a v0.4.4. O mcp.json ja gravado nunca e reescrito (a unica porta do autoprovisionamento e "arquivo ausente"), entao o $HOME fica la. Em standard, o diagnostico avisa (mcp.filesystem_root = Warning: o filesystem persistido aponta para fora das raizes declaradas) com o passo para corrigir. Para corrigir: edite <config_dir>/mcp.json e troque o ultimo argumento do filesystem por um diretorio dentro das raizes declaradas (uma das agent.file_roots, ou <data_dir>/workspace); reinicie o gateway. Em isolated-pod o check e Ok — o pod e a fronteira.

GARRAIA_DISABLE_MCP_AUTOPROVISION=1 continua desligando o provisionamento por completo.

Onde o perfil aparece​

SuperficieO que mostra
Log de bootstandard: INFO execution profile = standard (fonte: …). isolated-pod: um WARN unico com origem, pod_root, o que foi liberado, a lista do que nao e isolado e como reverter.
garra config checkexecution profile : isolated-pod (source: env) no sumario; Error em valor invalido; Warning para execution.pod_root fora de isolated-pod ou relativo, e para channels.whatsapp_linked.owners fora de isolated-pod (so a contagem, nunca as identidades).
GET /api/diagnosticsCheck execution.profile: Ok em standard; em isolated-pod Warning com origem, piso do dono, numero de donos, raiz do MCP e next_step ("confirme que este processo roda num pod isolado; para reverter: execution.profile = standard"). Check mcp.filesystem_root (le o mcp.json e a secao mcp: do config.yml, que vence): Warning em standard quando o filesystem aponta para fora das raizes declaradas (agent.file_roots / <data_dir>/workspace); Ok em isolated-pod. Caminhos dentro do data dir aparecem como <data_dir>/…; os de fora nao saem (a rota e auth-free e o caso legado e o $HOME do host) — a linha aponta a posicao da raiz ofensora na lista do servidor (raiz #2), e o caminho esta no mcp.json/config.yml e no log de boot (#1465).
GET /api/diagnostics (cont.)Check files.workspace (#1378, #1449): o workspace efetivo das file tools nativas e de onde veio a decisao — Ok com (fonte: agent.file_roots / GARRAIA_FILE_ROOTS) quando o operador declarou, Ok com <data_dir>/workspace/<sessao> (fonte: workspace padrao — nada declarado em agent.file_roots; um subdiretorio por sessao) numa instalacao limpa, e Warning quando nada resolveu (a sessao sem working_dir nao le nem escreve). Caminhos dentro do data dir aparecem como <data_dir>/…, e o identificador da sessao nunca sai na rota (ela e auth-free, e o nome do subdiretorio deriva do session_id, que pode ser PII).
GET /api/settings/effectiveLinha read-only security.execution_profile (valor + origem), no molde de security.sandbox_mode.
Web Console (header)Badge fixo data-testid="execution-profile-badge" no header, alimentado pela linha security.execution_profile acima: mostra o perfil e a origem sem o operador abrir Diagnostics. standard sai no tom cyan de informacao; isolated-pod sai no tom de alerta, com o aviso do ADR 0024 no tooltip (poder total dentro do pod, o pod e a fronteira, como reverter). Sem resposta da rota o badge fica desconhecido — nunca some, para nao passar por standard.
garra whatsapp statusLinha com o perfil, o piso do dono e a contagem de donos.
Log por turno do WhatsAppphone_last4 + perfil (completo | padrao) + modo do piso.

Exemplo: pod RunPod / Docker descartavel​

Um pod com o Garra e nada mais, sem volume do host, sem socket do Docker:

# ~/.config/garraia/config.yml dentro do pod
execution:
profile: isolated-pod
pod_root: /workspace

agent:
file_roots: ["/workspace"] # as tools nativas tambem enxergam o pod
# bash_allowlist: [...] # so se o pod for descartavel de verdade

channels:
whatsapp_linked:
type: whatsapp_linked
enabled: true
owners: ["5511999998888"]

Ou, deixando o arquivo em paz e declarando pelo ambiente do container:

docker run --rm \
-e GARRAIA_EXECUTION_PROFILE=isolated-pod \
-e OPENROUTER_API_KEY=... \
-p 3888:3888 garraia

Confira depois de subir: o log de boot tem o WARN do perfil, garra config check mostra execution profile : isolated-pod (source: env) e curl -s localhost:3888/api/diagnostics traz execution.profile como Warning com o numero de donos esperado.

Troubleshooting​

SintomaCausa provavel / o que fazer
isolated-pod ativo, mas o WhatsApp continua em search(a) owners vazio — o perfil completo e so para identidade declarada; o diagnostico diz "0 donos". (b) A mensagem veio de grupo — grupo nunca herda. (c) default_mode explicito na config — em isolated-pod ele vale para o dono tambem; remova a chave para o default code. (d) A sessao escolheu /mode search — escolha explicita vence o piso.
Gateway nao sobeGARRAIA_EXECUTION_PROFILE fora de standard | isolated-pod (a env ignora caixa; o log diz perfil de execucao invalido "…"), ou execution.profile no arquivo fora dessas duas grafias exatas (o arquivo diferencia caixa; o log traz o erro do parser, unknown variant …). Corrija ou remova; e fail-closed de proposito. garra config check reporta os dois casos como Error em execution.profile (exit 2).
config check: channels.whatsapp_linked.owners lists N identities but the effective execution profile is standardowners sem isolated-pod nao confere poder nenhum. Ou ligue o perfil (se este processo roda num pod isolado), ou remova owners.
config check: execution.pod_root (…) is set but the effective profile is standardpod_root so vale em isolated-pod; a raiz do MCP filesystem segue agent.file_roots / <data_dir>/workspace.
config check: execution.pod_root (…) is not an absolute pathUse um caminho absoluto pod-local (/workspace).
/api/diagnostics: mcp.filesystem_root = Warning em standardmcp.json anterior a v0.4.4 com $HOME como raiz (ou uma entrada filesystem em mcp: do config.yml, que vence o mcp.json). Troque o ultimo argumento do filesystem por um diretorio dentro das raizes declaradas (agent.file_roots ou <data_dir>/workspace) e reinicie.
/api/diagnostics: execution.profile = Warning e eu nao queria isolated-podVeja a origem no proprio check: env → remova GARRAIA_EXECUTION_PROFILE do ambiente do processo (manifest do pod, unit do systemd); file → execution.profile: standard. Reinicie.
file_write do dono e negado mesmo em isolated-podNao e o ToolGate: e o jail das file tools nativas (agent.file_roots), que continua valendo. Declare a raiz do pod la tambem. Para o filesystem__write_file (MCP) a raiz e execution.pod_root.
file_read / list_dir recusam tudo numa sessao sem projetoAntes da #1378 esse era o estado default de toda sessao do WhatsApp recem-vinculada (working_dir = null + agent.file_roots vazio = nenhuma raiz). Hoje a raiz default e <data_dir>/workspace/<sessao>. Se ainda recusa, veja files.workspace em /api/diagnostics: Warning ali quer dizer que o workspace nao pode ser criado (permissao do data_dir).
Uma sessao nao ve o que outra escreveu no workspace padraoE o desenho (#1449), nao defeito: cada sessao tem o seu <data_dir>/workspace/<sessao>, e o pai nao e raiz de ninguem. Para um diretorio compartilhado de proposito, declare-o em agent.file_roots — ai a escolha e explicita do operador e vale para todas as sessoes. A fronteira e a SESSAO, nao a pessoa: numa conversa de grupo (WhatsApp, iMessage) todos os membros compartilham a mesma sessao e portanto o mesmo diretorio — isso nao e novo, e a mesma fronteira que a conversa ja tinha.
bash recusa rm -rf … no podGate de comando arriscado, fail-closed sem canal de confirmacao. Deliberado: o perfil libera ferramentas, nao desliga protecoes. agent.bash_allowlist alarga.
A instrucao diz garraia … e eu uso garraToda instrucao do WhatsApp (pos-link, whatsapp status, next_step do /api/diagnostics, log do gateway) nomeia o executavel em execucao (current_exe(); so garra ou garraia sao aceitos, qualquer outro nome cai em garraia). Os dois nomes sao o mesmo binario (garra e symlink/shim).

Veja tambem​