Heavy-Work Host Isolation — heavy / long / destabilizing work (test, build, compile, package, publish/CI, ISO, benchmark, simulation) runs on dedicated VMs, not the developer laptop

mandatory

Toda execução **pesada** — teste, build/compilação, packaging, publicação/release (pipeline de CI), ISO, benchmark, simulação ou qualquer cenário que possa travar/consumir excessivamente o host (CPU, RAM, swap, GPU, IO, kernel) — acontece em uma **VM dedicada** hospedada em `s.khost1`, **nunca diretamente na máquina do desenvolvedor** (laptop, workstation). As VMs são compartilhadas entre IAs e humanos — para evitar interferência cruzada, cada VM aceita **uma única sessão de execução por vez** (mecânica de lock per-VM, análoga ao `koder-lock` de componentes). VMs cobrem 3 famílias de targets (linux / windows / android) através de qualquer tecnologia de virtualização (LXC, Docker, QEMU/KVM, emulador Android, etc.). As VMs **incus-backed** (Linux) são criadas via `klease create --ttl` (`infra/net/lease`) e reclamadas por TTL pelo `koder-leased` — ver §R2; isto fecha o problema da VM órfã. QEMU/KVM (Windows/Android) e Docker seguem provisionamento direto até o backend kbox do lease cobri-los.

Renomeada de test-host-isolation em 2026-05-24 (o nome "test" era estreito demais; cobre todo trabalho pesado, incl. buildcompilação publicaçãoCI). Refs antigas migram via `projectskoder-stack#163`.

A máquina onde o desenvolvedor (humano + IA) escreve código não é a mesma máquina onde os testes rodam. Esta separação tem duas razões:

  1. Estabilidade da sessão. Builds e testes pesados (Koda self-host,

    ISOs, fuzzers, emuladores, stress, soak) regularmente travam o laptop — OOM-killer mata a IDE, swap explode, kernel panics, GPU freezing. Cada incidente custa 10–30 min de reset + perda de contexto de sessão de IA.

  1. Isolamento entre IAs. O monorepo Koder é editado por múltiplas

    sessões de IA simultâneas (Claude, Codex, Gemini, etc.). Quando duas sessões rodam o mesmo emulador / a mesma VM / o mesmo Wayland compositor headless ao mesmo tempo, os outputs colidem silenciosamente (eventos de input intercalados, telas misturadas, arquivos sobrescritos, ADB device 0 disputado). A mecânica de lock per-VM, descrita aqui, impede isso.

Rules

R1 — Toda execução pesada roda em VM remoto

Escopo (importante): "pesado" não é só teste. Cobre toda a família — teste, build/compilação, packaging, publicação/release (pipeline de CI), ISO, benchmark, simulação/emulação. O nome antigo desta policy (test-host-isolation) era estreito demais; o princípio sempre foi isolamento de host pra qualquer trabalho pesadolongoque desestabiliza a workstation.

Roda em VM hospedada em s.khost1 (qualquer tecnologia de virtualização, ver §R2) toda execução que case com pelo menos um dos critérios abaixo:

Critério Exemplo
make build / make build-safe / make kasm de Koda bootstrap stage1 ~7 min, ~2.7 GB RSS nasm + ~600 MB koder = swap pressure
Suíte de testes de qualquer componente em modo "full" /k-test <módulo>, cargo test, go test ./..., flutter test --coverage, pytest -x
Test gráfico em modo headless (Chrome headless, Wayland sintético, Xvfb) flutter test integration_test --device-id chrome, Playwright/Selenium headless
Test que sobe emulador Android qualquer flutter test --device-id emulator-*, /k-reinstall <slug> --device android em fixture
Stress / soak / fuzz / chaos /k-test-gen-{stress,soak,fuzz,chaos}, criterion benches, property-based tests com seeds grandes
Build de ISO / image / pacote pesado lb build do infra/linux/kodix, kpkg pack, .deb/.AppImage gigantes
Pipeline de release / CI (act_runner job) build Flutter multi-plataforma (flutter build linux/apk/web), packaging (fpmappimagetool), orquestração WinRM, checkout do monorepo, publicação HubFlow. O runner não roda no laptop — vive no pool em s.khost1 (infra-RFC-002 + infra/ci#001#002). Caso pioneer: `k-reinstall kterm` v1.32.3 2026-05-24 — checkout de 1,81 GB + builds saturaram o laptop
Qualquer execução que historicamente já travou esta máquina listada em meta/context/test-host-isolation/known-hangs.md (catálogo crescente)

Exceções permitidas no laptop, sem necessidade de VM:

  • Edição de código (LSP, formatters, linters)
  • Build incremental / single-file de componente leve (<10s wall, <500MB RSS)
  • Teste unitário isolado em código puro (sem subprocess pesado)
  • Smoke "hello world" pós-edição (round-trip <30s, <500MB RSS)
  • Inspeçãoscripting via grepfindgit loggh/incus list
  • Comandos que conversam com o VM remoto (ssh khost1 ..., incus exec ...) mas não rodam carga aqui

R2 — Catálogo de VMs de teste em s.khost1

Toda VM de teste é provisionada em s.khost1 (177.93.107.211, EVEO Vivver LAN, ver meta/context/infrastructure/servers.md §s.khost1).

Convenção de nomes: s.khost1.<env>-<target>[-<slug>] onde <env> é um dos códigos canônicos da policies/environments.kmd (dev / stg / prd). Test VMs vivem em dev env por definição — outros envs (stg/prd) só fazem sentido pra cenários explícitos de teste-contra-staging ou teste-de-fumaça em produção. Containers de produção da Stack (s.khost1.flow, s.khost1.id, s.khost1.aivoice, …) não levam env prefix porque já são prd por convenção pré-existente.

Target Tecnologia preferida Nomenclatura Notas
Linux x86_64 LXC via incus s.khost1.dev-linux-<slug> Ubuntu 24.04 ou Debian 13 base; share /srv/koder-monorepo via virtiofs / 9p / NFS
Linux ARM64 LXC via incus (com --type=virtual-machine se cross-arch) s.khost1.dev-linux-arm64-<slug> Pra Koder Kodix ARM, kasm ARM, paridade variantes
Windows 10/11 QEMU/KVM s.khost1.dev-win10 / s.khost1.dev-win11 WinRM 5985 + RDP 3389; sucessor do ~/temp/win10-vm local que demora >10min pra subir
Android Emulador AOSP (sistema-imagem system-images;android-NN;google_apis;x86_64) sob QEMU/KVM, ou container redroid (Docker) s.khost1.dev-android-<api> (ex.: dev-android-34) ADB exposto via incus proxy na porta 5555; um emulador por VM (evita ADB device 0 disputa)
Container leve (linux user-space only) Docker / Podman s.khost1.dev-docker-<image> Pra testes que não precisam de kernel customizado

Provisioning canônico — lease (LEASE-001/006). As VMs de teste incus-backed (Linux x86_64 / ARM64 — as duas primeiras linhas da tabela) nascem via klease create (CLI do infra/net/lease), não por incus launch direto:

klease create --image <perfil> --ttl 4h --label project=<módulo>
# ... trabalhar; em execuções longas renovar enquanto usa:
klease renew <id> --ttl 4h
# ao terminar (ou deixar o TTL expirar):
klease release <id>

O daemon koder-leased (host-level em s.khost1) renova o lease enquanto a sessão o renova e reapa a VM por TTL quando a sessão morre sem renovar — é o reclaim de VM órfã que originou o componente (a saturação load 36 por VMs ad-hoc nunca reclamadas). klease list / klease info <id> / klease why <id> inspecionam o estado; o reaper exporta koder_lease_* (scrape pelo infra/observe). Garantia no-false-reap atestada (soak ativo, gate no_false_reap, infra/net/lease/docs/gates/).

Escopo do reaper (Phase 0 = backend incus). O lease só gere os alvos provisionados por incus. Windows (QEMU/KVM), emulador Android (QEMUKVM ou redroidDocker) e Docker/Podman ficam fora do reaper por ora — seguem provisionamento direto + o lock per-VM de §R3/§R4. O lease os alcança quando o backend kbox (LEASE-003, Phase 2) e o microVM scale-to-zero (LEASE-004, Phase 3) shiparem.

incus launch direto vira exceção documentada (ex.: koder-leased indisponível) — nesse caso, limpar a VM manualmente ao terminar.

VM ≠ servidor de produção. Containers de produção (s.khost1.id, s.khost1.flow, s.khost1.hub, etc., listados em infrastructure/servers.md §Containers ativos) não são VMs de teste. Não rodar teste neles a menos que seja um teste de integração contra produção, declarado explicitamente.

R3 — Lock per-VM: uma sessão de IA por vez

Cada VM de teste aceita uma única sessão de execução por vez, independentemente de qual IA (Claude, Codex, Gemini, etc.) ou humano está pilotando. O mecanismo de lock é análogo ao lock per-componente (koder-lock / meta/context/notices/active/lock-<slug>.md) mas em um diretório paralelo dedicado a VMs.

Diretório de locks de VM: meta/context/notices/vm-active/ (separado do vm-active/ vs active/ para não confundir locks de componente com locks de VM — uma sessão pode ter os dois ao mesmo tempo: lock no componente sendo editado + lock no VM onde os testes rodam).

Arquivo de lock: meta/context/notices/vm-active/lock-<vm-id>.md

Frontmatter obrigatório:

---
vm: s.khost1.dev-linux-<slug>       # ID canônico da VM (env prefix canônico — ver policies/environments.kmd)
session: <ai-session-tag>           # ex.: claude-2026-05-19-rfc019, codex-2026-05-19-id
ai: claude | codex | gemini | <other> | human
started: <iso-8601-utc>             # ex.: 2026-05-19T22:30:00Z
reason: |
  <uma frase: que teste/operação está rodando + ticket referenciado>
heartbeat: <iso-8601-utc>           # opcional; updateado por sessões longas
ttl_minutes: 60                     # opcional; default 120 se omitido
---

O conteúdo do arquivo é livre — vale repetir comandos exatos que estão rodando, output snapshots, etc., para outras sessões inspecionarem.

R4 — Fluxo de aquisição / liberação de lock de VM

Antes de rodar qualquer carga numa VM listada em §R2:

  1. Check. Ler meta/context/notices/vm-active/lock-<vm-id>.md.
    • Não existe → pode prosseguir (ir pra §R4.2)
    • Existe + não é minha sessão → outra sessão de IA/humano está

      usando essa VM. Não invadir. Opções:

      • (a) Aguardar (se urgente: pollar a cada 30s o arquivo até sumir)
      • (b) Usar outra VM (provisionar uma nova se a alvo for genérica

        o suficiente — ex.: dev-linux-koda-1 vs dev-linux-koda-2)

      • (c) Sinalizar pro usuário e parar — explicitamente reportar

        qual sessão está ocupando, há quanto tempo, e que ticket

    • Existe + é minha sessão → seguir adiante (lock já meu)
    • Existe + sessão expirada (mais que ttl_minutes sem update

      do heartbeat, OU started há mais de 24h e nenhum heartbeat) → considerar lock órfão; arquivar (mover pra meta/context/notices/vm-active-archive/lock-<vm-id>-expired-<date>.md) com nota explicando, e prosseguir pra §R4.2

  1. Place. Escrever o arquivo de lock com o frontmatter de §R3.

    Commitar (pode ser commit independente ou junto com o trabalho subsequente — escolher o que reduzir churn).

  1. Use. Executar os testes/builds no VM.
    • Para execuções >10min, atualizar heartbeat em intervalos

      <ttl_minutes/3 para evitar staleness.

  1. Archive. Ao concluir, mover o arquivo pra

    meta/context/notices/vm-active-archive/lock-<vm-id>-<YYYY-MM-DD>.md. Não deletar — histórico fica como audit trail.

Implementação preferida: comando koder-lock vm <subcmd> análogo ao koder-lock de componentes. Tracking em dev/koder-tools — até shipar, o fluxo manual acima vale.

R5 — Testes gráficos em modo headless NÃO são exceção

Mesmo em modo headless, testes gráficos consomem recursos de GPU virtual / framebuffer / Wayland compositor / ADB / emulador. Roda em VM:

  • Flutter integration tests headless
  • Chrome headless + PlaywrightSeleniumPuppeteer
  • Xvfb / Wayland compositor sintético (Weston/labwc) em test runners
  • Android emulator (qualquer API level)
  • iOS simulator (futuro — quando shippar, mesmo princípio em s.khost1

    via UTM ou similar)

Para emuladores Android, a regra de §R3 (uma sessão por VM) é especialmente importante: ADB device 0 é serializado pelo emulador. Duas sessões emitindo adb shell ao mesmo emulador intercalam comandos.

R6 — Onde ficam os dados de teste

Os repositórios da Stack vivem em ~/dev/koder/ no laptop. Para os VMs lerem o código, há 3 estratégias por ordem de preferência:

  1. Bind mount via incus (LXC compartilha o diretório

    real-time): incus config device add <vm> monorepo disk source=/home/koder/dev/koder path=/home/koder/dev/koder readonly=true

  2. NFS export do laptop pro s.khost1 (read-only)
  3. Git fetch dentro do VM (clone https://flow.koder.dev/Koder/koder.git) — mais lento, mas elimina dependência de o laptop estar ligado

Para artefatos de build / outputs / logs, escrever em path local da VM (ex.: /tmp/, /srv/test-out/). Não escrever de volta no monorepo exceto via PR/commit consciente.

R7 — Conflito entre lock per-componente e lock per-VM

Os dois locks são ortogonais. Uma sessão típica de IA que edita engines/lang/koda e roda testes em dev-linux-koda mantém:

  • meta/context/notices/active/lock-engines-lang-koda.md (componente)
  • meta/context/notices/vm-active/lock-s.khost1.dev-linux-koda.md (VM)

Liberar na ordem reversa da aquisição (LIFO): VM primeiro, depois componente. Garante que se outra sessão pegou o componente entre liberação e fim, ela ainda vê os artefatos da build prévia consistentes.

R8 — Onde criar a primeira VM (bootstrap)

Caminho canônico hoje é o lease (§R2): klease create --ttl 4h --label project=<módulo>, com reclaim por TTL. O bloco incus launch abaixo é o que o klease automatiza por baixo (e o fallback de bootstrap quando o koder-leased está indisponível); o device do monorepo entra via perfil de imagem ou incus config device add na instância do lease.

Para Koda especificamente — o primeiro consumidor desta policy — provisionar s.khost1.dev-linux-koda:

# Em s.khost1, root:
incus launch images:debian/13 dev-linux-koda
incus config device add dev-linux-koda monorepo disk \
  source=/home/koder/dev/koder \
  path=/home/koder/dev/koder \
  readonly=false
incus exec dev-linux-koda -- apt update
incus exec dev-linux-koda -- apt install -y nasm gcc binutils make git
# Run from inside:
incus exec dev-linux-koda -- bash -c \
  'cd /home/koder/dev/koder/engines/lang/koda && make build-safe'

Detalhes finais (NFS vs bind, network, snapshots para reset rápido) ficam no runbook meta/context/runbooks/test-vm-provisioning.md que a primeira sessão que provisionar deve escrever.

R9 — Carve-out: hardware ausente no datacenter

A regra geral §R1 é "trabalho pesado roda em VM no DC, nunca no laptop." Excepcionalmente, quando o trabalho pesado depende de hardware que o DC (s.khost1 hoje) não tem e o laptop tem esse hardware, a execução no laptop é autorizada.

O princípio é "DC primeiro; laptop só se o DC fisicamente não pode." Não é frequência, duração, nem ergonomia — é capacidade. Se o DC adquirir o hardware no futuro, o trabalho retorna pra lá sem necessidade de re-ratificar este carve-out.

Critérios cumulativos (TODOS obrigatórios)

  1. O workload falha ou degrada inviavelmente quando rodado no

    hardware disponível no DC (e o motivo é uma capacidade ausente — não preguiça de provisionar, não otimização de custo, não conveniência).

  2. O laptop tem a capacidade exigida (driver carregado,

    versão suficiente, recursos físicos adequados).

  3. A combinação "DC-não-tem + laptop-tem" está *egistrada e

    atualizada*em registries/hardware-readiness.md — qualquer workload novo precisa de uma linha (ou de cobertura por uma linha existente) antes de rodar.

Procedimento

  1. Conferir que a row aplicável está em hardware-readiness.md

    §"Tickets cobertos pelo carve-out".

  2. Place lock per-component (`koder-lock place slug --reason ticket

    running carve-out`) — mesmo lock que cobriria a VM remota se o DC tivesse o hardware.

  3. Executar; capturar evidência (logs, gráficos, baselines, screenshots

    de chrome://gpu quando aplicável).

  4. Persistir resultados no destino canônico do workload

    (registries/perf-baseline.md, manifest, ticket, etc.).

  5. Arquivar o lock.

Reversibilidade

O DC adquiriu o hardware? Reroutear o workload pro DC na mesma janela em que hardware-readiness.md flipa a row pra active no DC. O carve-out deixa de aplicar automaticamente — não precisa editar esta policy nem o ticket.

Limites estruturais (carve-out NÃO autoriza)

  • Workloads em que o DC tem o hardware mas a IA preferiu o

    laptop ("é mais rápido", "tenho lock aqui", "evito tar-pipe") — isso continua violação direta de §R1.

  • Workloads em que o hardware do laptop não atende ao requisito

    do workload (capacidade insuficiente, driver errado, versão velha) — o critério (2) acima reprova; o workload espera procurement.

  • Substituir a auditoria: continua sendo koder-spec-audit test-host

    + walks de PR — workloads cobertos pelo carve-out aparecem com o registry-row referenciado no commit message.

Por que existe

A Stack hoje tem 1 host (s.khost1) com Tesla T4 (datacenter-class, sem display, sem GL consumer) e um laptop com RTX 4070 Mobile (consumer-class com GL real). Há workloads — benchmarks gráficos Chrome (JetStreamMotionMark), iteração visual de WaylandCEF render paths — que só rodam onde tem GL consumer. Esperar procurement de GPU consumer pro DC trava esses workloads indefinidamente sem justificativa estrutural: o hardware necessário já existe na rede; está só no lado errado.

O carve-out reconhece essa anomalia sem fingir que não existe e sem abrir a policy pra usos não-justificados (volta-redonda nos critérios 1+2+3). Quando o DC adquirir GPU consumer (procurement ratificado em hardware-readiness.md row gpu-consumer), o carve-out naturalmente esvazia.

R10 — Perf gates ruidosos: pinar CPU/NUMA, não confiar no host nu

Um benchmark/throughput-gate que roda numa VM compartilhada de s.khost1 (load alto, 6+ sessões) tem ratio confiável só se ambos os lados do A/B compartilharem os mesmos cores + memória NUMA-local — afinidade, não host nu (medido: baseline do self-compile da koda variou 17 % run-a-run sem pin). Use o modo --pin "<cpulist>" [--membind <node>] do gate (mecânica: systemd-run --scope -p AllowedCPUs=… -p AllowedMemoryNodes=… -p MemoryMax=…, auto-liberado na saída do scope) — afinidade pina nossos procs, não evicta vizinhos. Exclusividade real de core só via reserved-compute-pool do Kodix (infra/linux/kodix#069) consumido por kbox run --reserve-cpus (infra/net/box#151); até lá, --pin é o interim e o futuro --reserve será o modo exclusivo. Referência de implementação: engines/lang/koda/scripts/verify-gc-change.sh --pin (koda#808).

Tests (validação da policy)

ID Verificação
T1 Audit walk encontra commits recentes que rodaram make build / make kasm no laptop sem lock de VM ativo. PASS = zero ocorrências; FAIL = listar PRs/commits violadores
T2 Audit walk encontra arquivos em meta/context/notices/vm-active/ com started há > ttl_minutes e sem heartbeat — locks órfãos
T3 Dois locks distintos em vm-active/ pra mesma VM — PASS = nunca
T4 infrastructure/servers.md §Containers ativos lista as VMs declaradas em §R2 que já estão provisionadas — sincronia entre policy + registro
T5 meta/context/runbooks/test-vm-provisioning.md existe e cobre os 4 targets (linux x86, linux arm, windows, android)
T6 Toda execução pesada rodada no laptop dentro do carve-out §R9 referencia em commit-msg a row aplicável de registries/hardware-readiness.md + lock arquivado. PASS = zero execuções de carve-out sem a referência; FAIL = listar commits

Trigger phrase mapping

Padrões que indicam violação iminente da policy:

  • make build / make build-safe / make kasm rodando em ~/dev/koder/engines/lang/koda no laptop → REROUTE pra VM
  • flutter test --device-id chrome no laptop → REROUTE pra VM
  • emulator -avd ... no laptop → REROUTE pra VM
  • qemu-system-... no laptop pra Windows VM local → MIGRAR pra s.khost1.dev-win10
  • lb build (ISO Koder Kodix) no laptop → REROUTE pra VM ou s.khost1 direto

Phase-current note

Fase atual — aceleração: o catálogo de VMs (§R2) está em construção. A regra obriga que antes de rodar o teste, a IA provisione a VM se ainda não existir, escreva o runbook em meta/context/runbooks/test-vm-provisioning.md, e só então rode. O custo de provisionar a primeira VM é amortizado por todas as sessões subsequentes.

Histórico

  • 2026-05-19 — policy ratificada após sequência de incidentes

    (#770#773#774) onde builds de Koda travaram o laptop com OOM-killer + swap saturado + segfaults durante GC mark walker, custando 30+ minutos por incidente. Owner direcionou: "todos os testes da koda devem ser realizados em uma vm de testes que deve ser criada no s.khost1". Esta policy generaliza pra Stack-wide.