teqo-backup/docs/plans/ops52-media-guard-impl.md

7.6 KiB

Impl: OPS52-media-guard — Guard de intenção explícita para escrita em bucket de produção

Status: aprovado (gate humano 2026-08-18) Atualizado em: 2026-08-18 Issue: #37 (OPS52-media-guard) Intenção: body da Issue #37 (spec — sem plano de intenção separado; achado do /simplify da #10) Appetite restante: pequeno (chore P3 — guard fail-closed de ~10 linhas + docs)

Leitura da intenção

  • Outcome: o modo reconcile (padrão) do pnpm media:recover só escreve no bucket com intenção explícita — MEDIA_RECOVER_CONFIRM=1 — e recusa com mensagem clara sem ela; --dry-run e --verify seguem desprotegidos (não escrevem). O risco fechado: um worktree que receba credenciais S3_* de prod (cópia all-or-nothing do main) rodando reconcile contra rows locais (db:pull inclui media) escreveria covers no bucket de prod — hoje só o echo do alvo torna visível, não impede.
  • O que NÃO negociar: fail-closed (sem flag → sem escrita, em qualquer cenário de DB/alvo); dry-run/verify sem fricção nova; o runbook do homeserver continua sendo o contrato de execução; zero mudança de comportamento do reconcile além do novo gate.
  • O que reavaliar: a forma da confirmação — a Issue propõe "ex.: MEDIA_RECOVER_CONFIRM=1 ou prompt interativo". Prompt interativo é pior para este repo: o runbook roda via SSH no homeserver (potencialmente sem TTY), e o padrão de guard do repo é env flag (ALLOW_REMOTE_DB, isRemoteDbOverrideSet). Env flag vence (ver Opções).

Abordagem recomendada

flowchart LR
  A[pnpm media:recover] -->|parse args| B{mode == reconcile?}
  B -->|não dry-run/verify| C[segue desprotegido — zero escrita]
  B -->|sim| D{MEDIA_RECOVER_CONFIRM true/1?}
  D -->|não| E[die com o comando correto — sem S3 client, sem DB]
  D -->|sim| F[guards existentes + echo do alvo + reconcile]

Opções consideradas:

  • A — Env flag MEDIA_RECOVER_CONFIRM (truthy true|1) obrigatória no reconcile, com helper genérico isTruthyEnv em scripts/lib/cli.mjs e isRemoteDbOverrideSet delegando para ele (sem twin). Recomendada.
  • B — Prompt interativo (readline) antes do reconcile.
  • C — Flag CLI --confirm no parsing de args.

Recomendação: A — paridade total com o guard de DB existente (ALLOW_REMOTE_DB=true|1): funcionamento em runbook não-interativo (SSH, set -a; source teqo-1313.env), sem fricção para dry-run/verify, e o parse de args do script continua restrito aos modos mutuamente exclusivos (--dry-run/--verify).

Rejeitadas: B (trava/falha sem TTY — o runbook roda por SSH no homeserver; mais código; fora do padrão de guards do repo); C (mistura conceito de modo com flag de confirmação; env var é o padrão e já é usada no runbook — o comando set -a; source injeta as duas flags juntas).

Componentes / mudanças

  • isTruthyEnv (scripts/lib/cli.mjs, novo export puro): value === 'true' || value === '1' — exatamente a semântica atual de isRemoteDbOverrideSet (sem trim, sem lowercase: TRUE/yes continuam recusados — não relaxar guard existente na refatoração). isRemoteDbOverrideSet passa a delegar (isTruthyEnv(process.env[ALLOW_REMOTE_DB_FLAG])) — comportamento idêntico.
  • scripts/recover-media.mjs: constante MEDIA_RECOVER_CONFIRM; imediatamente após o parsing de modo (antes de assertLocalDatabase, resolveS3StorageEnv e de qualquer rede): if (mode === 'reconcile' && !isTruthyEnv(process.env[MEDIA_RECOVER_CONFIRM])) die(...) — mensagem pt-BR com o comando correto (MEDIA_RECOVER_CONFIRM=1 pnpm media:recover) e a alternativa --dry-run. Bloco de comentário do header (safety model + modes) atualizado para documentar o guard. O gate vale para reconcile com QUALQUER alvo de DB (local ou remoto) — o risco é a escrita no bucket, não a origem dos dados.
  • tests/unit/cliEnvFlags.unit.spec.ts (novo): isTruthyEnv (true/1 passam; TRUE, yes, 0, vazio, ausente recusam) e delegação de isRemoteDbOverrideSet preservando o comportamento (mesmo caso ALLOW_REMOTE_DB=TRUE recusado).
  • Docs: runbook do docs/plans/ops52-media-recuperar-arquivos-impl.md (passo 3 do runbook ganha MEDIA_RECOVER_CONFIRM=1 + nota do guard no cabeçalho do passo 2/3); nota curta no AGENTS.md (parágrafo da media — reconcile exige MEDIA_RECOVER_CONFIRM=1); entrada docs/changelog/2026-08-18-ops52-media-guard.md + pnpm changelog:build.
  • Migration: nenhuma. Access/Consent: intocados. UI: N/A.
  • package.json: sem mudança (a flag é env, não arg do script).

Fases verificáveis

  1. Helper + testes — isTruthyEnv em cli.mjs + refactor de isRemoteDbOverrideSet + tests/unit/cliEnvFlags.unit.spec.ts; rodar pnpm test:unit.
  2. Guard no script — recover-media.mjs (gate + header doc); conferir comportamento: pnpm media:recover sem flag → exit 1 com a mensagem (com S3_* ausente local o guard do confirm dispara antes do die de S3 — ordem verificada); --dry-run/--verify seguem sem exigência.
  3. Docs + changelog — runbook (passo 3 + nota), AGENTS.md, docs/changelog/2026-08-18-ops52-media-guard.md, pnpm changelog:build + pnpm changelog:check.
  4. Gates finais — pnpm gate:fast, pnpm format:check, pnpm exec knip, pnpm check:cycles, pnpm test (unit+int), pnpm build local.

Rabbit holes / Não escopo (engenharia)

  • Testar o reconcile ponta a ponta — impossível por contrato (sem S3_* e sem DB prod nesta máquina); a prova é o runbook do homeserver (padrão da OPS52 fase 3). O que é testável (semântica do flag, preservação do ALLOW_REMOTE_DB) é unit-testado.
  • Prompt interativo / TTY detection — rejeitado (Opção B).
  • Guard para --dry-run — não: não escreve; o Issue pede explicitamente mantê-los desprotegidos.
  • Renomear ALLOW_REMOTE_DB ou mexer em outros scripts — fora de escopo; o refactor de isRemoteDbOverrideSet é só delegação (uma ortografia do truthy-env, sem mudança de aceite).
  • Adiado com gatilho: banir re-escritas === '1'/=== 'true' de env-read no codebaseConventions — quando um 4º site aparecer ou no próximo refactor dos 3 existentes (ver docs/plans/ops60-seed-posts-guard-escrita-s3.md, seção Explicitamente fora).
  • Registrado no simplify (OPS60, #49): db:seed:posts escreve no bucket via adapter S3 sem flag de intenção — mesma classe de risco; Issue nova com depends: [37].

Riscos e mitigação

  • R1 — Refatorar isRemoteDbOverrideSet relaxa o guard de DB. Mitigação: isTruthyEnv com a semântica EXATA atual (sem trim/lowercase) e teste de comportamento que pin o caso TRUE recusado.
  • R2 — Runbook desatualizado manda rodar reconcile sem a flag e o guard recusa (fricção na próxima execução de prod). Mitigação: runbook editado no mesmo PR; mensagem do die exibe o comando correto.
  • R3 — Automação futura (CI/homeserver) esquece a flag. Mitigação: mensagem de erro autoexplicativa + dry-run disponível como pre-flight; o contrato é do runbook.

Aceite de engenharia

  • Aceite de produto da intenção coberto: reconcile recusa sem MEDIA_RECOVER_CONFIRM=1 (fail-closed); dry-run/verify desprotegidos
  • Invariantes AGENTS/engineering-standards: identificadores em inglês; guard reusado do dono (cli.mjs) sem twin; zero migration/access/Consent
  • Testes de domínio: isTruthyEnv + preservação de isRemoteDbOverrideSet (unit); gates da checklist do AGENTS rodados
  • Docs: runbook atualizado (comando real com a flag) + nota AGENTS.md + changelog