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

142 lines
7.6 KiB
Markdown

# 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
```mermaid
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