24. Perfis de execução: standard e isolated-pod
- Status: Accepted (2026-09-21, decisão de produto do dono na #1329 — R5 assinada no comentário de 2026-09-21; implementação pelo coordenador autônomo)
- Deciders: @michelbr84 (decisão de produto e de segurança) + coordenador
autônomo (desenho, implementação e revisão com
security-auditor) - Date: 2026-09-21 (America/New_York)
- Tags: seguranca, toolgate, whatsapp, mcp, config, deploy, fase-4
- Supersedes: none
- Superseded by: none
- Links:
- Issue: #1329 (raiz
$HOMEdo MCPfilesystemautoprovisionado contorna o jail da #1244) — decisão registrada no comentário de 2026-09-21 - #1327 / #1330 (a recusa
FerramentaMcpRegistradasaiu; pisosearchno canal), #1244 (jail das file tools), #1264 e #1288 (whitelist doToolGatefail-closed), #1225 (sandbox) - ADR 0019 — confinamento das tools
- ADR 0023 — canal
whatsapp_linked docs/security/threat-model.md§5.15 (esta decisão),docs/execution-profiles.mdCLAUDE.mdregras 4, 6, 8 e 14
- Issue: #1329 (raiz
Context and Problem Statement
O GarraIA tem hoje uma postura de segurança, pensada para instalação direta
em máquina compartilhada: ToolGate fail-closed por modo (search como piso do
WhatsApp pessoal), jail de filesystem (agent.file_roots ∪ working_dir,
#1244), gate de comando arriscado no bash, sandbox opcional (#1225). Essa
postura está certa para um laptop e para um servidor com outros inquilinos.
Ela está errada para o cenário que a #1329 expôs: o operador instala o Garra num pod/container descartável, exatamente para dar ao agente autonomia plena (ler e escrever arquivos, rodar shell, instalar pacotes, usar MCP e subagentes) sem risco para nada fora do pod. Nesse cenário:
- o piso
searchdo WhatsApp nega ofilesystem__write_fileque o operador quer usar; - o servidor MCP
filesystemautoprovisionado no primeiro boot aponta para$HOME— dentro de um pod isso é o pod inteiro (aceitável), numa máquina compartilhada é o contorno do jail da #1244 (inaceitável) — e a mesma linha de código não sabe em qual dos dois está; - não existe nenhum lugar para o operador declarar "este processo roda num pod isolado", então toda decisão de política tem de assumir o pior caso.
O dono decidiu (#1329, 2026-09-21): full power inside the isolated pod; no implicit access outside the pod. Este ADR fixa como isso vira código.
Decision Drivers
- Explícito, nunca inferido. Detecção de container (
/.dockerenv,/proc/1/cgroup) não é prova de isolamento: um container com o socket do Docker montado ou com--pid=hostparece igual a um pod descartável. O perfil só muda por escolha do operador, persistida e auditável. - Um conceito, não dois sistemas. Reusar
ToolGate/AgentMode,piso_somente_leitura,FileJail,provision_filesystem_if_missinge as superfíciesconfig check//api/diagnostics//api/settings/effective. - Remetente ≠ canal. "WhatsApp conectado" não torna todo remetente confiável; o perfil completo é por identidade declarada, em conversa 1:1.
- Fail-closed em toda dúvida: valor inválido, identidade desconhecida,
grupo, lock envenenado — tudo cai no piso
standardou na recusa. - Zero regressão em instalação existente: seção ausente = comportamento de
hoje;
mcp.jsonjá gravado nunca é reescrito.
Considered Options
- Perfil de execução explícito (
execution.profile) com dois valores e política derivada por superfície — escolhida. - Só mudar a raiz do MCP autoprovisionado (opção 1 original da #1329) e deixar
o operador subir o
default_modedo WhatsApp paracode— resolve o$HOMEmas continua sem um lugar para declarar o pod, ecodepara todo remetente admitido é mais largo do que a decisão pede. - Autodetectar container e liberar automaticamente — rejeitada pelo driver 1.
- Não provisionar o
filesystem(opção 3 original) — perde a conveniência que motivou o autoprovisionamento e não resolve o piso do canal.
Decision Outcome
Config e ativação
execution:
profile: standard # default; ou isolated-pod
pod_root: /workspace # opcional; so vale em isolated-pod
- Nova seção
executionemAppConfig(garraia-config), enumExecutionProfile { Standard (default), IsolatedPod }no molde deSandboxMode(#[serde(rename_all = "kebab-case")], valorisolated-pod). Seção ausente =standard= comportamento de hoje. - Variável de ambiente
GARRAIA_EXECUTION_PROFILE(standard|isolated-pod) vence o arquivo, resolvida uma vez noConfigLoader, com a origem registrada (default|file|env) para as superfícies de observação. Valor inválido (env ou arquivo) é erro de carga: o gateway não sobe e oconfig checkreportaError— fail-closed, nunca "cai em standard em silêncio". GARRAIA_EXECUTION_PROFILEentra emKNOWN_GARRAIA_ENV_VARS.- Nenhum código lê
/.dockerenv,/proc/*/cgroupnem variável de ambiente de runtime de container para decidir o perfil. Um teste varre o módulo do perfil e proíbe esses literais (molde:detect.rs::o_modulo_de_deteccao_nunca_executa_binario).
O que cada perfil significa
| Superfície | standard (hoje) | isolated-pod |
|---|---|---|
| Piso do WhatsApp para remetente dono em conversa 1:1 | channels.whatsapp_linked.default_mode (default search) | default_mode se explícito; default code (sem whitelist: filesystem, bash, MCP, subagentes) |
| Piso do WhatsApp para remetente admitido que não é dono, ou qualquer mensagem de grupo | default_mode (default search) | igual ao standard — poder total nunca é herdado por grupo nem por contato só pareado |
Remetente fora de allow e sem código de pareamento | recusado em silêncio | recusado em silêncio (inalterado) |
Raiz do MCP filesystem autoprovisionado | agent.file_roots se houver; senão <data_dir>/workspace | execution.pod_root se houver; senão <data_dir>/workspace |
Jail das file tools nativas, gate de comando arriscado do bash, sandbox | inalterados | inalterados (o pod é a fronteira; o gate de comando arriscado continua sendo a proteção contra rm -rf / dentro do pod e pode ser alargado por agent.bash_allowlist) |
| Boot | info! com o perfil e a origem | warn! único: poder total dentro do pod, fronteira = pod, como reverter |
$HOME nunca é raiz implícita em nenhum perfil. Dentro de um pod dedicado
a fronteira default é o workspace do Garra (<data_dir>/workspace); um caminho
pod-local mais amplo (/workspace, /) é escolha explícita em
execution.pod_root. A raiz efetiva é logada no provisionamento e aparece no
diagnóstico.
Dono no WhatsApp (whatsapp_linked)
- Nova chave
channels.whatsapp_linked.owners: [<identidade>]— mesma normalização deallow(normalizar_identidade: dígitos, ou JID@lid). Quem está emownersé admitido como se estivesse emallow. - Pareamento por código de 6 dígitos nunca confere o perfil completo: é credencial fraca (memória do processo, um código). Só identidade declarada na config é dono.
- Mensagem
from_mecontinua fora (deve_responder); "note to self" não é caminho suportado nesta fatia — documentado. - O perfil efetivo do turno é uma função pura
perfil_do_turno(perfil_de_execucao, settings, remetente, is_group) -> PerfilDoTurno { Completo | Padrao }, avaliada depois deadmitire antes de montar oExecContext. Ele alimentapiso_somente_leitura, que continua respeitando um/modeexplícito da sessão. - Cada turno loga
phone_last4+perfil(completo|padrao) + o modo do piso. Nunca JID, telefone,push_namenem texto (a varredurafonte_nao_loga_jid_cru_nem_material_de_sessaocontinua valendo). ownerspreenchido com perfilstandardgera Warning noconfig check("só tem efeito em isolated-pod"), nunca poder.
Observabilidade
/api/diagnostics: checkexecution.profile—Okemstandard; emisolated-podWarningcom origem, piso do dono, número de donos, raiz do MCP enext_step("confirme que este processo roda num pod isolado; para reverter:execution.profile = standard"). Checkmcp.filesystem_root— emstandard,Warningquando ofilesystempersistido aponta para fora do jail (o$HOMEgravado por instalações anteriores à v0.4.4), com o passo para corrigir;Okemisolated-pod./api/settings/effective: linha read-onlysecurity.execution_profile(valor + origem), no molde desecurity.sandbox_mode.garraia whatsapp status: linha com o perfil, o piso do dono e a contagem de donos.config check: perfil e origem no sumário;Errorem valor inválido;Warningparaownersfora deisolated-pod.
CLI
- A instrução pós-link e as demais instruções do
garraia whatsapppassam a usar o nome do executável em execução (garraiaougarra, derivado decurrent_exe(), fallbackgarraia) — nunca um binário que pode não existir.
Consequences
Boas: o operador de pod declara uma vez e ganha o agente com autonomia
plena, com aviso claro e diagnóstico que diz exatamente o que foi liberado; a
instalação padrão fica mais segura do que hoje (o filesystem nasce no
workspace, não em $HOME); nenhuma superfície nova de política — só o
ToolGate de sempre com outro piso.
Ruins / custo: mais uma chave para documentar em dois idiomas; instalação
standard anterior à v0.4.4 mantém o $HOME no mcp.json (nunca reescrito) e
só recebe um aviso no diagnóstico com o passo para corrigir; o dono precisa
listar a própria identidade em owners (sem isso, isolated-pod não muda
nada no WhatsApp — e o diagnóstico diz "0 donos").
Riscos e mitigações
- Operador liga
isolated-podfora de um pod. Mitigação: aviso no boot,Warningpermanente no diagnóstico, nome do perfil autoexplicativo, docs com a lista do que NÃO é isolado por padrão (socket Docker,--pid=host, mounts, segredos do host). - Identidade forjada. O remetente vem do JID que o Baileys autentica;
@lidsem número entra como JID cru; comparação byte a byte após normalização. Grupo nunca herda. - Comando destrutivo dentro do pod. O gate de comando arriscado do
bashcontinua ligado em ambos os perfis; sem canal de confirmação ele é fail-closed. Isso é deliberado: o perfil libera ferramentas, não desliga proteções.
Amendment 2026-09-21 — bash sem humano no laço (#1272)
A linha "Jail das file tools nativas, gate de comando arriscado do bash,
sandbox: inalterados" da tabela "O que cada perfil significa" valia para o
jail e o gate, mas não para a existência do bash: em standard o
garraia mcp-server e o gateway registravam um bash irrestrito, e o gate
só pega comando que parece perigoso (cat /etc/shadow e echo x > /fora
passavam). Decisão do dono, aplicada às superfícies sem humano no laço
(garraia mcp-server e o runtime do gateway):
standard | isolated-pod | |
|---|---|---|
bash registrado | só com agent.sandbox docker/podman válido e o binário presente; todo comando via wrap_command, sem fallback para o host | sim, no host do pod (ou no sandbox, se configurado) |
bash com sandbox desligado, ssh, elevated, allowlist sem bash, sem backend ou binário ausente | não registrado (warn! no boot, check tools.bash, system prompt diz que não há shell) | registrado no host do pod |
| Jail das file tools, denylist e tier arriscado | inalterados | inalterados |
- A decisão é a função pura
garraia_gateway::bootstrap::exposicao_do_bash(perfil + policy + disponibilidade do binário), com a mesma regra de "nunca inferir de container" deste ADR (teste varre o fonte). - Nenhuma lista negra textual de comando é a fronteira: a contenção é o
isolamento de processo. O container ganha
--cap-drop ALL,--pids-limit,--user <uid>:<gid>(--userns=keep-idno podman) e monta só o diretório de trabalho canônico. - Como o
bashsandboxado escreve no diretório montado, as tools de git (git_diff,code_review) rodam endurecidas (fsmonitor, textconv, filtros, bare implícito, hooks) efile_writerecusa caminho com.git. garraia chatfica como está: o humano no terminal confirma e é o principal.
Testes de regressão exigidos pela decisão
- instalação padrão segura (perfil
standard, pisosearch, MCP write negado pelo runtime com a fiação real); isolated-podexplícito por arquivo e por env (env vence; inválido = erro);- canal sobe com servidor MCP registrado, nos dois perfis;
- dono em
isolated-podinvocafilesystem__write_filecom a fiação real (ponte falsa + provider stub que pede a tool) e a tool executa; - remetente desconhecido não recebe resposta; admitido-não-dono e dono em
grupo caem no piso
standarde a tool é negada; - nenhuma liberação por detecção de container (varredura de fonte + teste de
resolução sem config/env =
standard); - o literal
FerramentaMcpRegistradanão existe mais no fonte do canal; - instrução pós-link =
garraia startquando o executável se chamagarraia(egarra startquandogarra; fallbackgarraia).
Cada teste positivo tem o gêmeo negativo (tirar o dono de owners, voltar o
perfil para standard, mandar do grupo) para que remover a autorização ou a
permissão efetiva quebre a suíte.