mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
142 lines
7.6 KiB
Markdown
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
|