feat(catalog): 055.W4.1 - scanner de segredos bloqueante (D20(1)) + pin/canal (D20(2)/D19) - #2
Conversation
… pin & plugin channel (D20(2)/D19)
Secret scanning, BLOCKING at publish (AC1):
- lib/secret-rules.mjs vendors a SUBSET of gitleaks' corpus (config/gitleaks.toml @ master,
222 rules upstream, latest v8.30.1, MIT (c) 2019 Zachary Rice), vendored 2026-08-09 —
14 rules / 14 classes, ids+regex+entropy+keywords verbatim, (?i) lifted to a JS flag.
VC-1: REUSE of the DETECTION CORPUS. npm dep rejected (npm install on the publish path);
gitleaks binary rejected as primary (AC1 needs the gate inside publish.mjs, and a missing
binary forces fail-open or a broken pipeline). generic-api-key deliberately NOT vendored
(its allowlist needs RE2 (?-i:), and running it without one turns a missed secret into a
false refusal) — recorded in DELIBERATELY_NOT_VENDORED.
- lib/secret-scanner.mjs scans the MANIFEST (a public catalog entry) and the ARTIFACT's real
bytes. Findings are redacted; skipped binary/oversized members are counted and listed, never
silently dropped.
- AC2: a planted fixture PER CLASS, each REFUSED through the real CLI as a subprocess, each
invalid in exactly ONE way (asserted), plus a positive control so the gate is provably not a
blanket refusal. Fixtures are assembled at RUNTIME from fragments so no credential shape is
committed (AC7) and the repo's own secret-shape grep stays intact.
- AC3: what the scan CANNOT see is a field of every report, printed on every run including a
clean one — (a) it inspects the published manifest/artifact, NOT the target of an MCP pointer
resolved at runtime ({command,args}, mcp.rs:68, typically `npx <pkg>`); (b) an obfuscated or
encoded secret escapes. Same posture as capabilities.limits (C2).
Pin + channel:
- lib/pin.mjs: `<plugin_id>@<version>` -> digest. resolvePin is a PURE function of (index, pin)
— no clock, env, file or network — which makes AC4's determinism and AC5's independence the
same fact. Ambiguity is REFUSED, never tie-broken (order-dependent bytes are not a pin).
- AC4 proved against the live 055.W3.1 mirror: HTTP 200, recomputed sha256 ==
9ec01ff45d2966fde7de79e46b31fa97a9485f28e2b625fdfe0af0aaa433561a. Offline regression pin in
test/pin.test.mjs so CI does not depend on the bucket.
- AC5 both directions + scripts/check-channel-separation.mjs in CI, with its OWN negative
fixture (plant a coupling -> guard FAILS -> remove -> green).
- AC6: PIN_COST travels WITH every resolution and is printed in every output mode — pinning
PREVENTS an already-installed artifact from being REPAIRED; 055.W5.1 (index freshness) is
what gives that back. VC-3 honoured: repair is not revocation, O5/055.W1.3 has not closed.
index/index.json stays at ZERO real entries (hard boundary, D24(a)). No push, no PR, no tag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…F2) + citação do AC3 corrigida (F1) + 7 achados QG @qa CONCERNS 70/100 (0 CRITICAL, 0 HIGH, 3 MEDIUM, 5 LOW, 1 INFO). Modo complete-findings: 9/9 dispostos. Suíte 211 -> 222, 0 falhas. F1 (MED) — a citação central do AC3 apontava para crates/aiox-cockpit/src/mcp.rs:68, arquivo INEXISTENTE, e viajava dentro da string impressa em TODO run. Corrigida para crates/aiox-core/src/mcp.rs:68 (verificada no repo do produto: a linha 68 é `let entry = json!({ "command": core_bin, "args": ["mcp"] });`), nos 3 sites do achado + 3 sites da mesma classe herdados da 055.W4.2 (citações sem crate). O teste que existia para proteger a alegação casava só /mcp\.rs:68/ — substring frouxa demais para ver o crate errado; agora fixa o caminho COMPLETO + doesNotMatch no crate errado. F2 (MED) — o QG EXECUTOU duas evasões de uma linha (byte NUL inicial -> [binary]; padding >5 MiB -> [too large]) que faziam um artefato COM CHAVE AWS DENTRO sair com exit 0. A cegueira era declarada em todo run, então não era o "gate cego ao defeito"; o que faltava era o REGISTRO DA ESCOLHA. Disposição: FAIL-CLOSED — membro ilegível é não-publicável. Decisão, custo nomeado (asset binário legítimo · lixo do macOS .DS_Store/AppleDouble · texto >5 MiB) e o motivo de NÃO haver --allow-unscannable (override é o caminho de desabilitação que a AC1 proíbe) escritos no decision site e em docs/SECRET-SCANNING.md §5.1. As duas evasões viraram fixtures permanentes pela CLI real, mais um controle provando que a recusa é por NÃO CONSEGUIR OLHAR e não por ter achado algo. Razão de fazer agora: com 0 entradas reais o custo de falso-positivo é literalmente zero — o único momento em que apertar a regra é grátis (mesmo argumento que fez a D24 valer antes de existir catálogo). F3 (MED) — publisher/README.md afirmava que este controle "is not built here" e omitia 2 gates bloqueantes já shipados da enumeração que o leitor trata como completa. Ambos adicionados, a frase reescrita (o que de fato falta é D20(3)/055.W4.3 e D20(5)/055.W5.1), + nota de manutenção. Varredura da classe nos dois repos: nenhuma outra ocorrência. F4 (LOW) — guard de canal isentava por PREFIXO DE LINHA e um acoplamento real atrás de /* passava. Agora apaga o CONTEÚDO dos comentários (offsets preservados) com rastreio de aspas, para que o // de uma URL não apague o resto da linha (a direção perigosa). 3 fixtures: evasão pega · menção legítima segue isenta · URL seguida de acoplamento. F5 (LOW) — duplicata de mesmo digest era desempatada por exact[0], deixando mirror_url e tiers dependentes de ordem; tiers é o eixo de entitlement. Qualquer duplicata recusa. F6 (LOW) — digest.algorithm sem default: assumir a função que verifica bytes é como um mismatch vira pass silencioso. F8 (LOW) — re-hash do artefato antes de qualquer escrita (detecção, não prevenção; fechar por snapshot mostraria caminho temporário nas mensagens de erro). Fronteira de teste DECLARADA no decision site: único check sem fixture, porque exigiria mutação entre duas leituras da mesma invocação e um teste por timing seria flaky. F9 (INFO) — ci.yml descrita como inserção pura, não "no fim do job". index/index.json segue com ZERO entradas reais. Sem push, sem PR, sem tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ão (F10/F11) + 2 regressões do ciclo 1 (F12/F13) QG round 2: CONCERNS 80/100 (+10) — 9/9 do ciclo 1 RESOLVED e verificados por MUTAÇÃO, nenhum reaberto; 4 achados novos (1 MEDIUM, 3 LOW). Suíte 222 -> 231, 0 falhas. F10 (MED) + F11 (LOW) — UM fix, causa raiz compartilhada. O QG EXECUTOU: um membro tar DUPLICADO e sombreado (mesmo path duas vezes, credencial no primeiro, limpo no segundo) publicava com exit 0, e a chave AWS continuava recuperável dos bytes publicados (`tar -xOzf`). Reproduzido aqui antes de escrever o fix. Por que isso valia mais que o buraco que substituiu: o residual do F2 era tolerável porque a ferramenta DIZIA EM VOZ ALTA que não tinha olhado; este era SILENCIOSO — o membro sombreado não aparecia em files_total, nem em skipped_*, nem em unscannable. Cegueira não-declarada é a espécie desqualificante. Causa raiz: o inventário vinha do SISTEMA DE ARQUIVOS EXTRAÍDO, então tudo que não sobrevive à extração como arquivo regular distinto era invisível POR CONSTRUÇÃO — duplicatas colapsam, e symlinks eram descartados antes de poderem ser contados (F11: um arquivo de 3 membros reportava "2/2 file(s) scanned"). Fix: a MEMBER TABLE (`tar -tzf` + `-tvzf`) passa a ser a verdade sobre o que o arquivo contém; a árvore extraída só fornece BYTES dos membros que a tabela diz serem arquivos comuns. Duplicata · não-regular (symlink/hardlink/FIFO/socket/ device) · path absoluto ou com `..` · listado mas ausente após extração => todos UNSCANNABLE, caindo no caminho fail-closed que o ciclo 1 já construiu e testou. Diretórios NÃO são recusados (senão todo pacote seria recusado — há controle p/ isso). files_total agora conta membros de conteúdo do arquivo: "N/M" significa o que o leitor assume. O que a member table AINDA não vê (diferencial de parser entre implementações de tar · arquivo aninhado · listagens desalinhadas => recusa total) está DECLARADO nos limites impressos em todo run — a lição do F2 aplicada ao próprio fix do F2. F12 (LOW, REGRESSÃO QUE MEU FIX DO F4 INTRODUZIU) — CORRIGIDO, não só documentado. `const re = /[/*]/;` levava o blanker a estado de bloco e escondia o acoplamento da linha seguinte (o código PRÉ-F4 pegava). Pior: o comentário do F4 AFIRMAVA que confusão com regex só podia SUPER-reportar, "nunca perder um acoplamento" — falso, e numa story cujo tema é alegação imprecisa sobre a força de um controle. Duas medidas independentes: literais de regex em posição de início são reconhecidos e consumidos como código; e comentário de bloco NÃO TERMINADO passa a ser tratado como falha de parse que devolve o texto CRU (super-reporta — a direção segura — e teria pego o F12 sozinha). Residual reescrito com precisão: um regex classificado como divisão E contendo `/* … */` BALANCEADO ainda pode esconder código. F13 (LOW, REGRESSÃO QUE MEU FIX DO F5 INTRODUZIU) — validador e resolver concordam. Índice com duplicata de mesmo digest passava no validate-index (0 violações) e era RECUSADO pelo resolvePin: verde na CI, irresolvível em uso. Mesma regra agora nos dois. Deliberadamente NÃO por import (pin.mjs fica leve de propósito): o anti-drift é um TESTE que roda os dois sobre o mesmo fixture e exige concordância nas duas direções. Regressão revalidada: AC2 14/14 (0 mismatches) · AC3 limites no caminho de SUCESSO com o crate corrigido · AC4 espelho vivo (HTTP 200, MATCH) · AC6 PIN_COST + 055.W5.1 · fixtures das evasões do F2 + controles · ci.yml INSERÇÃO PURA (0 passos preexistentes removidos/renomeados vs 5ec619c; 8 -> 10) · index/index.json com ZERO entradas reais. Sem push, sem PR, sem tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…OWLIST (F14): isenção exige evidência positiva QG round 3: CONCERNS 80/100; 4/4 do ciclo 2 RESOLVED por mutação; 1 MEDIUM novo (F14). O QG recomendou FECHAR; o founder decidiu o contrário. Justificativa no Dev Agent Record da story — em uma linha: bypass PROVADO POR EXECUÇÃO faz um controle chamado BLOQUEANTE não fazer o que o nome diz; o AC2 já exigia negativo POR CLASSE e "typeflag de arquivo regular com nome terminando em /" é uma classe; e, diferente dos GAPs cross-repo que o épico aceitou antes, este estava NO NOSSO CÓDIGO com causa raiz já diagnosticada. E o QG estava certo sobre o essencial: o problema era o MÉTODO, não a instância. Três ciclos fecharam a forma demonstrada e deixaram a não demonstrada porque o classificador era um DENYLIST. Este ciclo não consertou "barra final" — inverteu o classificador. ANTES (a6d7f8e), sobre um ustar forjado (checksum válido, typeflag '0' = ARQUIVO REGULAR, nome ./config/payload/, 39 bytes com chave AWS): tar -tvzf -> renderiza `d` (bsdtar imprime qualquer nome com / final como diretório) scan -> "2/2 file(s) scanned", "Findings: none", exit 0 tar -xOzf ./config/payload/ -> AWS_ACCESS_KEY_ID=AKIA... (MESMO tar, MESMA máquina) Três membros reportados como 2/2, nenhuma menção ao terceiro. NÃO é o resíduo do diferencial de parser: aquele fala de implementações DIFERENTES discordando; aqui uma só bastou — logo era defeito de classificação, não limite declarado. DEPOIS: scan -> "2/3 file(s) scanned" [directory-with-data] config/payload — renders as a directory but carries 39 bytes of data — a real directory carries none... REFUSED — 1 member(s) could NOT be scanned (fail-closed, D20(1)/AC1) exit 1 A INVERSÃO. Antes: "este membro está excluído da classificação?" -> isentava por SEMELHANÇA (`type !== "d" && !nome.endsWith("/")`), então qualquer coisa parecida com diretório herdava a isenção independentemente do que carregasse. Agora: "dá para IDENTIFICAR POSITIVAMENTE como uma de duas coisas conhecidas?" — DIRETÓRIO (tipo `d` E tamanho conhecido igual a 0) ou ARQUIVO COMUM (tipo regular, sem `/` final, path seguro, único, mapeável) — e TODO O RESTO, INCLUSIVE O NÃO RECONHECIDO, é unscannable e recusa. O tamanho é a evidência que faltava; tamanho NÃO determinável também não é passe (alegação não verificável de ser diretório é recusada). Diretórios reais seguem isentos — é a única coisa que a inversão não pode apertar, porque recusá-los recusaria TODO pacote feito com `tar -czf x.tgz -C dir .`: fecharia o achado e quebraria o produto. Tem teste de controle próprio + teste da PROPRIEDADE do allowlist. Fixture negativo obrigatório (AC2): o ustar forjado vira fixture permanente, e o teste assere PRIMEIRO que o arquivo é válido e que a credencial é mesmo recuperável — a sonda da segunda engine tinha produzido um arquivo DANIFICADO, ou seja, afirmação não demonstrada. Regressão revalidada (a inversão toca o classificador por onde tudo passa): controle positivo 4/4 exit 0 · pacote comum exit 0 · binário limpo bloqueia · evasões do round 1 recusam · F10 duplicata sombreada E a variante com prefixo ./ recusam · F11 symlink recusa · AC2 14/14 · AC3 no caminho de sucesso · AC4 espelho vivo (HTTP 200, MATCH) · AC6 PIN_COST · ci.yml NÃO tocada neste ciclo (8 -> 10, 0 removidos/renomeados) · índice com ZERO entradas reais. Suíte 231 -> 235. F15 e F16 (LOW) cardados no repo do produto — F15 medido explicitamente: NÃO vem de graça com a inversão (vive na normalização de path, não na classificação de tipo). Sem push, sem PR, sem tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…NÁRIO (F17), não pelo texto renderizado Exceção EXPLÍCITA e ÚNICA ao circuit breaker, autorizada pelo founder (opção A do QG). O F17: o ciclo 3 inverteu o classificador para allowlist — certo na FORMA (isenção exige evidência positiva) e errado na FONTE. Tipo e tamanho vinham de `tar -tvzf`, uma renderização humana cujas colunas são função de campos de header que o atacante escreve. Um `uname` forjado (`0 Aug 1`) fazia o regex de tamanho casar o `0` injetado antes do `39` real: o membro voltava a ser isento e a credencial saía com exit 0. Reproduzido ANTES: `2/2 file(s) scanned`, `Findings: none`, exit 0 — enquanto `tar -xOzf artifact.tar.gz ./config/payload/` imprimia a chave AWS. DEPOIS: `2/3 file(s) scanned`, `[directory-with-data] config/payload — carries 39 bytes`, exit 1. Fecha a CLASSE, não a instância: `typeflag` no offset 156 e `size` no offset 124 do header ustar de 512 bytes. Nenhum campo forjado move o offset de outro campo, então não existe a variante "mais uma coluna". Custo nomeado: é um leitor de tar mínimo — o que parsear CLI existia para evitar — e ele entende `prefix`, PAX `x`/`g` (overrides de path/size HONRADOS), nomes longos GNU `L`/`K` e base-256, senão recusaria pacote legítimo. Checksum verificado como detector de dessincronização, não como anti-forja. O `tar` é demotado de fonte da verdade a SEGUNDA OPINIÃO: diferencial membro a membro, com `hidden-member` / `phantom-member`. Isso revelou cegueira NÃO DECLARADA que os 4 ciclos carregavam — o `tar` do macOS esconde membros AppleDouble `._*` da própria listagem, e um `xattr` com credencial ship recuperável dos bytes publicados (medido). Agora enumerados e recusados, com `COPYFILE_DISABLE=1` na mensagem ao operador. Defeito de produção junto: arquivo que o `tar` não desempacota LANÇAVA exceção não-tratada em vez de recusar com nome — falhava fechado por acidente, não por projeto. Virou `unextractable-archive`. Fixtures passam a usar `COPYFILE_DISABLE=1` porque uma fixture deve ser um pacote BEM-FORMADO — e a recusa correspondente ficou pinada por teste próprio, com o gatilho REAL (`xattr`), não uma versão forjada dele (a forjada esconde da listagem mas não extrai; medido e descartado). Suíte 235 → 241, 0 falhas. Controles positivos e negativos re-executados em varredura completa: 23/23, zero regressão e zero over-fire. Sem push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fferential` no texto do limite
A reescrita do resíduo (i) em SCANNER_LIMITS descrevia o diferencial corretamente mas perdeu a
expressão LITERAL `parser differential`, que um teste existente asserta ("what the member table
cannot see is DECLARED in the limits printed on every run").
O texto volta a nomeá-lo, sem perder o que a reescrita acrescentou: o diferencial agora é
DETECTADO e recusado nos dois sentidos, e o que sobra é a comparação ser entre exatamente duas
análises.
Suíte 241/241 em 3 rodadas consecutivas; varredura de controles 24/24 (3 positivos + 21 negativos,
agora incluindo o AppleDouble oculto).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 48 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (21)
Comment |
…rtamento do bsdtar
A CI (ubuntu, GNU tar) reprovou 4 testes que passam em macOS (bsdtar). Reproduzido em
node:20-bookworm + GNU tar 1.34 antes de qualquer conserto.
MEDIDO PRIMEIRO — a IMPLEMENTAÇÃO não está furada. Nas duas plataformas o walk de headers enumera
igual (aligned, 3 membros, typeflag '-' e size 39 lidos dos offsets 156/124) e o arquivo é
RECUSADO. O que diverge é o estágio: bsdtar desempacota, o membro chega ao classificador e é
recusado como `directory-with-data`; GNU tar se recusa a desempacotar, e a recusa acontece um passo
antes, como `unextractable-archive`. Duas recusas corretas do mesmo arquivo.
O DEFEITO ESTAVA NOS TESTES, e é a mesma classe que o F17: o fix tirou a renderização do caminho de
confiança da IMPLEMENTAÇÃO, mas a PROVA continuou dependendo do tar do host.
1. `tar -xOzf <membro>` como prova de que a credencial ship: em GNU tar imprime NADA para nome
terminado em `/` e sai 0. Trocado por checagem HERMÉTICA sobre os bytes descomprimidos do
próprio arquivo — a alegação é "os bytes ship", não "este tar me entrega o membro".
2. `files_total === 3` via scanArtifact: em GNU tar é 1, porque a recusa vem do estágio anterior.
3. A recusa esperada era hardcoded como `directory-with-data`. Agora o teste asserta o INVARIANTE
(recusa + índice intocado) incondicionalmente e DERIVA o motivo de `tarCanUnpack()`, medido por
execução e nunca inferido do nome da plataforma.
4. O envenenamento da listagem é propriedade do RENDERIZADOR DO BSDTAR (uname/gname em colunas
separadas); GNU tar imprime `user/group` junto e o regex do ciclo 3 lê o 39 verdadeiro. O teste
detecta a condição e SKIPA ALTO com o motivo, em vez de falhar onde ela não pode existir.
TESTE NOVO, e é o que faz a CI valer: `F14/F17 — the CLASSIFICATION is proven without depending on
WHICH tar is installed`. Sem ele a CI ficaria verde provando apenas que o GNU tar não desempacota o
arquivo — um teste passando pelo motivo errado, exatamente a classe que esta story caça. Ele asserta
o classificador direto (tarMemberTable + classifyMembers).
Provado por MUTAÇÃO no Linux: com `parseSizeField` devolvendo 0 (o defeito do F17 reintroduzido) o
teste hermético fica VERMELHO. A CI faz enforcement do conserto, não de um efeito colateral.
241/241 em bsdtar 3.5.3 e em GNU tar 1.34, 2 rodadas cada, guards da CI verdes. `lib/` intocada —
nenhuma mudança de implementação. Sem push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st — CI Linux reprovava A PR #2 reprovou o check `validate` com 238/241: os tres vermelhos eram justamente as provas do F14 e do F17. Localmente (macOS, bsdtar 3.5.3/libarchive) davam 241/241. CAUSA: os fixtures CONSTROEM o tar em Node (correto — `ustarMember`/`ustarHeader`), mas VERIFICAVAM com `execFileSync("tar", ["-xOzf", …, "./config/payload/"])`, extraindo POR NOME um membro cujo nome termina em `/`. Extrair um arquivo REGULAR com forma de diretorio e exatamente o caso ambiguo que estes fixtures constroem de proposito, e bsdtar e GNU tar resolvem de formas diferentes. O teste que provava o conserto deixava de provar no runner. E a mesma licao do F17 aplicada a PROVA em vez da implementacao: nao delegar a classificacao a uma ferramenta cujo comportamento varia — ler os bytes. O proprio arquivo de fixtures ja dizia isso sobre o gzip ("done with node:zlib rather than by shelling out, so the fixture does not depend on a binary"); o principio existia e nao tinha sido aplicado a verificacao. FIX: `readArtifactMembers`/`artifactMemberNames`/`artifactMemberText` em test/helpers/secret-fixtures.mjs, espelhando os offsets do `ustarHeader` logo acima (name@0, size@124 octal, typeflag@156). 4 verificacoes trocadas. DUAS chamadas ao `tar` PERMANECEM de proposito, porque nelas o comportamento da ferramenta E o objeto do teste, nao um detalhe de leitura: - :1148 — que o `uname` forjado envenena a listagem renderizada (ja condicional; o proprio executor mediu GNU tar 1.34 e faz SKIP ruidoso onde o bypass nao se reproduz) - :1186 — que a listagem do tar NAO admite o membro AppleDouble. Um leitor de header cru ENXERGA esse membro (e esse o achado), entao troca-lo inverteria o que o teste prova. `artifactMemberText` concatena TODAS as ocorrencias do nome, nao a primeira: o fixture do F10 carrega o mesmo nome duas vezes (um membro sombreando o outro) e `tar -xOzf` despeja as duas em sequencia — devolver so a primeira procuraria a credencial na copia inocente. FEITO PELO COORDENADOR DA WAVE, nao pelo executor: o pane da story ficou sem consumir a inbox e o `wave kill` foi recusado (WRITE_FORBIDDEN, pane de outra conexao), com o merge-back das duas pontas bloqueado. Nenhuma logica de produto foi tocada — `lib/secret-scanner.mjs` tem zero diff. node --test test/*.test.mjs -> 241/241 passed, duas execucoes consecutivas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scanner de segredos BLOQUEANTE no publish e pin de versao com canal separado.
Quatro fix-cycles: o classificador de membro virou allowlist (F14) e depois passou
a ler o HEADER USTAR BINARIO (offsets 156/124, checksum, PAX, GNU longname,
base-256) em vez do texto renderizado pelo tar (F17) - a classe inteira de bypass
por campo forjado morre ai. 241/241 testes.