Proteger dados sensíveis e atualizar andamento do plano

- Caminho do compartimento pai e CIDR do SSH saem do código para
  .secrets/oci-settings.json (fora do Git), com override por DMB_*.
- Relatórios de sanidade deixam de gravar o hostname local.
- tests/test_sensitive.py falha se OCIDs, chaves, IPs públicos ou valores
  de .secrets aparecerem em arquivos versionados.
- tools/plan_progress.py gera o bloco de andamento por fase no plano.
- Plano: status reais das tarefas de F0, F3 e F6; README e docs/05 com
  GPU em sa-saopaulo-1, runtime CUDA do Rizzo e pipeline sintético.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Marcos Lohmann
2026-10-07 14:08:44 -03:00
parent ce544639ac
commit 171595f4c9
13 changed files with 193 additions and 29 deletions

View File

@@ -24,6 +24,12 @@ O resultado é um relatório técnico com uma recomendação. Todos os resultado
Cada candidato é exposto por um adaptador local com o mesmo contrato JEV (`POST /v1/systemone`). Assim, o *harness* não sabe qual modelo está respondendo. Cada candidato é exposto por um adaptador local com o mesmo contrato JEV (`POST /v1/systemone`). Assim, o *harness* não sabe qual modelo está respondendo.
## Andamento
O andamento por fase fica no topo de [docs/03-plano-de-tarefas.md](docs/03-plano-de-tarefas.md), regenerado com `python tools/plan_progress.py` a cada atualização do plano.
**Primeiro ciclo:** classificação de intenções (D1) no atendimento de telefonia móvel, com dados 100% sintéticos anotados por três modelos generativos. CPU em us-chicago-1 (sub-rede privada) e GPU A10 em sa-saopaulo-1.
## Documentação ## Documentação
| Documento | Conteúdo | | Documento | Conteúdo |
@@ -32,6 +38,7 @@ Cada candidato é exposto por um adaptador local com o mesmo contrato JEV (`POST
| [docs/02-desenho-experimental.md](docs/02-desenho-experimental.md) | Tarefas de decisão, formato dos dados, partições, métricas e regras de isolamento | | [docs/02-desenho-experimental.md](docs/02-desenho-experimental.md) | Tarefas de decisão, formato dos dados, partições, métricas e regras de isolamento |
| [docs/03-plano-de-tarefas.md](docs/03-plano-de-tarefas.md) | As 78 microtarefas organizadas em fases, com dependências e adaptações ao contexto de roteamento | | [docs/03-plano-de-tarefas.md](docs/03-plano-de-tarefas.md) | As 78 microtarefas organizadas em fases, com dependências e adaptações ao contexto de roteamento |
| [docs/04-questoes-em-aberto.md](docs/04-questoes-em-aberto.md) | Decisões de projeto (Q1–Q13) e os mecanismos automatizados que as aplicam | | [docs/04-questoes-em-aberto.md](docs/04-questoes-em-aberto.md) | Decisões de projeto (Q1–Q13) e os mecanismos automatizados que as aplicam |
| [docs/06-guia-de-anotacao.md](docs/06-guia-de-anotacao.md) | Guia de anotação, usado literalmente pelos anotadores |
| [docs/05-ambiente-de-execucao.md](docs/05-ambiente-de-execucao.md) | Onde e como os scripts rodam: jobs e notebooks do OCI Data Science (a máquina local é só IDE) | | [docs/05-ambiente-de-execucao.md](docs/05-ambiente-de-execucao.md) | Onde e como os scripts rodam: jobs e notebooks do OCI Data Science (a máquina local é só IDE) |
| [config/benchmark.yaml](config/benchmark.yaml) | Parâmetros e critérios pré-registrados, congelados na tarefa 62 | | [config/benchmark.yaml](config/benchmark.yaml) | Parâmetros e critérios pré-registrados, congelados na tarefa 62 |
@@ -46,12 +53,14 @@ infra/ds/ Jobs do Data Science: build dos ambientes, execução do ben
infra/vm/ remote.sh para a VM de trabalho (bloqueada; ver docs/05) infra/vm/ remote.sh para a VM de trabalho (bloqueada; ver docs/05)
env/ Aquisição de artefatos, manifesto, Dockerfile por candidato e dependências fixadas (11–16) env/ Aquisição de artefatos, manifesto, Dockerfile por candidato e dependências fixadas (11–16)
contracts/jev/ Contrato JEV jev-compat-v1: esquemas, mapeamento de tipos, fixtures de conformidade (36–41) contracts/jev/ Contrato JEV jev-compat-v1: esquemas, mapeamento de tipos, fixtures de conformidade (36–41)
data/taxonomy/ Taxonomia de intenções/agentes e definição das tarefas de decisão (20–23) data/taxonomy/ Taxonomia de intenções (intents-v1.yaml: 42 intenções, 13 domínios) (20–23)
datagen/ Pipeline sintético: geração, mascaramento/deduplicação, anotação por 3 LLMs, partições (24–36)
data/raw/ Enunciados coletados e anonimizados — não versionar dados sensíveis (24–25) data/raw/ Enunciados coletados e anonimizados — não versionar dados sensíveis (24–25)
data/annotated/ Anotações, adjudicação e referência (29–32) data/annotated/ Anotações, adjudicação e referência (29–32)
data/splits/ Partições dev / calibração / teste congeladas (33–35) data/splits/ Partições dev / calibração / teste congeladas (33–35)
adapters/<modelo>/ Adaptadores JEV locais por candidato (42–51) adapters/<modelo>/ Adaptadores JEV locais por candidato (42–51)
harness/ Execução, controle de seeds/lotes/truncamento, coleta de medições (52–66) harness/ Execução nos jobs (job.py), sanidade, servidor, auditoria do bucket (18–19, 41, 51–66)
tools/ Utilitários do repositório (andamento do plano)
analysis/ Métricas, cortes, análise de erros e gráficos (67–74) analysis/ Métricas, cortes, análise de erros e gráficos (67–74)
reports/ Relatório técnico e anexos de reprodução (75–76) reports/ Relatório técnico e anexos de reprodução (75–76)
``` ```

View File

@@ -19,13 +19,34 @@ flowchart LR
As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo. As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
<!-- PROGRESSO:INICIO -->
**Andamento em 2026-10-07:** 40 de 78 tarefas concluídas (51%), 8 em andamento.
| Fase | Concluídas | Em andamento | Pendentes |
|------|-----------|--------------|-----------|
| F0 — Infraestrutura OCI | 6 | 2 | 2 |
| F1 — Ambiente e artefatos | 9 | 0 | 0 |
| F2 — Tarefas de decisão e taxonomia | 4 | 0 | 0 |
| F3 — Dados e anotação | 5 | 6 | 1 |
| F4 — Contrato JEV e adaptadores | 16 | 0 | 0 |
| F5 — Controle de execução | 0 | 0 | 3 |
| F6 — Piloto | 0 | 0 | 4 |
| F7 — Desenvolvimento e calibração | 0 | 0 | 4 |
| F8 — Teste final | 0 | 0 | 4 |
| F9 — Métricas e análise | 0 | 0 | 8 |
| F10 — Relatório e encerramento | 0 | 0 | 4 |
| **Total** | **40** | **8** | **30** |
Legenda: `[x]` concluída · `[~]` em andamento ou parcial · `[ ]` pendente.
<!-- PROGRESSO:FIM -->
--- ---
## F0 — Infraestrutura OCI ## F0 — Infraestrutura OCI
- [x] **1.** Criar um compartment OCI dedicado ao benchmark. - [x] **1.** Criar um compartment OCI dedicado ao benchmark.
- ↳ `root/<compartimento-pai>/decision-models`. Recursos em `us-chicago-1`. OCIDs em `.secrets/artefatos-OCI.md` (fora do Git). - ↳ `<compartimento-pai>/decision-models` (caminho em `.secrets/oci-settings.json`). Recursos em `us-chicago-1`. OCIDs em `.secrets/artefatos-OCI.md` (fora do Git).
- [ ] **2.** Aplicar as tags `projeto=benchmark-decisoes-ptbr`, `ambiente=benchmark` e `centro_custo=<definir>` aos recursos do projeto. *(Q13)* - [~] **2.** Aplicar as tags `projeto=benchmark-decisoes-ptbr`, `ambiente=benchmark` e `centro_custo=<definir>` aos recursos do projeto. *(Q13)*
- ↳ Parcial: `projeto` e `ambiente` aplicadas como tags livres por `infra/oci/provision.py`. Falta `centro_custo`. - ↳ Parcial: `projeto` e `ambiente` aplicadas como tags livres por `infra/oci/provision.py`. Falta `centro_custo`.
- [ ] **3.** Criar um orçamento para o compartment. - [ ] **3.** Criar um orçamento para o compartment.
- ↳ **Em aberto por decisão (2026-10-07):** o uso está coberto por cota interna de engenharia. Não será criado no primeiro ciclo. - ↳ **Em aberto por decisão (2026-10-07):** o uso está coberto por cota interna de engenharia. Não será criado no primeiro ciclo.
@@ -33,7 +54,7 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
- ↳ Em aberto, porque depende da tarefa 3. - ↳ Em aberto, porque depende da tarefa 3.
- [x] **5.** Criar um projeto no OCI Data Science. - [x] **5.** Criar um projeto no OCI Data Science.
- ↳ `benchmark-decisoes-ptbr`. - ↳ `benchmark-decisoes-ptbr`.
- [ ] **6.** Criar buckets no Object Storage para `entrada`, `artefatos`, `resultados` e `logs-imutaveis`. - [~] **6.** Criar buckets no Object Storage para `entrada`, `artefatos`, `resultados` e `logs-imutaveis`.
- ↳ Criados com o prefixo `dmb-` (nomes de bucket são únicos no namespace), privados e com versionamento. - ↳ Criados com o prefixo `dmb-` (nomes de bucket são únicos no namespace), privados e com versionamento.
- ↳ Pendente: a regra de retenção (WORM) de `dmb-logs-imutaveis`, que depende do prazo de retenção (tarefa 77). - ↳ Pendente: a regra de retenção (WORM) de `dmb-logs-imutaveis`, que depende do prazo de retenção (tarefa 77).
- [x] **7.** Definir políticas IAM de menor privilégio para leitura dos dados, escrita dos resultados e execução dos jobs. - [x] **7.** Definir políticas IAM de menor privilégio para leitura dos dados, escrita dos resultados e execução dos jobs.
@@ -42,8 +63,9 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
- ↳ `dmb-jobs-menor-privilegio`: os jobs (`request.principal.type='datasciencejob'`) leem `dmb-entrada`/`dmb-artefatos`, escrevem em `dmb-resultados`/`dmb-logs-imutaveis`, usam logs e repos. Inclui a política de serviço do Data Science para a VCN. - ↳ `dmb-jobs-menor-privilegio`: os jobs (`request.principal.type='datasciencejob'`) leem `dmb-entrada`/`dmb-artefatos`, escrevem em `dmb-resultados`/`dmb-logs-imutaveis`, usam logs e repos. Inclui a política de serviço do Data Science para a VCN.
- [x] **8.** Definir uma VCN privada para os jobs, com acesso ao Object Storage por Service Gateway. - [x] **8.** Definir uma VCN privada para os jobs, com acesso ao Object Storage por Service Gateway.
- ↳ `dmb-vcn` 10.20.0.0/16, sub-rede privada `dmb-subnet-jobs`. A rota e a security list só permitem tráfego para serviços OCI pelo Service Gateway. - ↳ `dmb-vcn` 10.20.0.0/16, sub-rede privada `dmb-subnet-jobs`. A rota e a security list só permitem tráfego para serviços OCI pelo Service Gateway.
- [ ] **9.** Criar uma rota temporária de saída por NAT Gateway, só para a aquisição inicial de dependências e pesos. - [x] **9.** Criar uma rota temporária de saída por NAT Gateway, só para a aquisição inicial de dependências e pesos.
- ↳ Remover a rota logo após a tarefa 16 e passar pelo gate de egresso antes de qualquer execução. *(Q3)* - ↳ Remover a rota logo após a tarefa 16 e passar pelo gate de egresso antes de qualquer execução. *(Q3)*
- ↳ **Não foi necessário:** a aquisição (tarefas 13–16) rodou fora da VCN e enviou os artefatos direto ao bucket; os ambientes são montados por jobs de build com rede gerenciada. `infra/oci/nat.py` fica disponível.
- ↳ Criar e ativar **só quando necessário**, com `infra/oci/nat.py on`. Para desligar: `nat.py off`, que fecha a rota e bloqueia o NAT. Na tarefa 78: `nat.py off --delete`. - ↳ Criar e ativar **só quando necessário**, com `infra/oci/nat.py on`. Para desligar: `nat.py off`, que fecha a rota e bloqueia o NAT. Na tarefa 78: `nat.py off --delete`.
- [x] **10.** Validar que os jobs acessam os buckets sem internet pública. - [x] **10.** Validar que os jobs acessam os buckets sem internet pública.
- ↳ Validado em 2026-10-07 com jobs do Data Science em `dmb-subnet-jobs`. Os pesos do Rizzo (4,4 GB) desceram pela Service Gateway em 28 s, com SHA-256 conferido. - ↳ Validado em 2026-10-07 com jobs do Data Science em `dmb-subnet-jobs`. Os pesos do Rizzo (4,4 GB) desceram pela Service Gateway em 28 s, com SHA-256 conferido.
@@ -85,26 +107,29 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
## F3 — Dados e anotação ## F3 — Dados e anotação
- [ ] **24.** Coletar textos PT-BR representativos do domínio de destino. *(Q7, Q15)* - [x] **24.** Coletar textos PT-BR representativos do domínio de destino. *(Q7, Q15)*
- ↳ **100% sintéticos** (`datagen.generate`, na VM). - ↳ **100% sintéticos** (`datagen.generate`, na VM): 4.068 enunciados em 339 lotes (42 intenções × 7 perfis + 5 categorias fora de escopo × 9 lotes), geradores `llama-4-maverick` e `command-a` alternados.
- ↳ A unidade é a **conversa com vários turnos**, não o enunciado isolado. Cada turno guarda o estado (agente ativo e sessões suspensas). - ↳ A unidade é a **conversa com vários turnos**, não o enunciado isolado. Cada turno guarda o estado (agente ativo e sessões suspensas).
- [ ] **25.** Remover ou mascarar dados pessoais, segredos e identificadores que o benchmark não precisa. - [x] **25.** Remover ou mascarar dados pessoais, segredos e identificadores que o benchmark não precisa.
- [ ] **26.** Separar os textos por domínio, comprimento, grau de ambiguidade e classe esperada. - [~] **26.** Separar os textos por domínio, comprimento, grau de ambiguidade e classe esperada.
- ↳ Acrescentar a posição do turno e a presença de agente ativo ou de sessões suspensas. - ↳ Acrescentar a posição do turno e a presença de agente ativo ou de sessões suspensas.
- [ ] **27.** Criar exemplos adversariais com abreviações, erros de ortografia, regionalismos, textos curtos e várias intenções. - ↳ Primeiro ciclo (só D1): metadados `domain`, `length_bucket`, `ambiguity`, `profile`, `sentiment`, `kind` e `n_options` gravados por `datagen.build`.
- [x] **27.** Criar exemplos adversariais com abreviações, erros de ortografia, regionalismos, textos curtos e várias intenções.
- ↳ Acrescentar respostas curtas que dependem do contexto ("sim", "o segundo"), troca de assunto, retorno a um assunto anterior e pedidos fora de escopo. - ↳ Acrescentar respostas curtas que dependem do contexto ("sim", "o segundo"), troca de assunto, retorno a um assunto anterior e pedidos fora de escopo.
- [ ] **28.** Definir os critérios de inclusão e exclusão dos exemplos. - ↳ Perfis de geração: informal/abreviado, erros de digitação, regionalismo, emocional, longo, indireto/múltiplo; fora de escopo em 5 categorias (inclui desabafo emocional sem pedido).
- [x] **28.** Definir os critérios de inclusão e exclusão dos exemplos.
- ↳ Exclui: duplicatas exatas e aproximadas, comprimento fora de 2–700 caracteres, três rótulos distintos na anotação e falha de anotação. Resultado do `prepare`: 4.057 de 4.068 mantidos (9 duplicatas exatas, 2 aproximadas; 4 trechos mascarados).
- [x] **29.** Produzir o guia de anotação humana. - [x] **29.** Produzir o guia de anotação humana.
- ↳ [06-guia-de-anotacao.md](06-guia-de-anotacao.md), usado literalmente pelos anotadores. - ↳ [06-guia-de-anotacao.md](06-guia-de-anotacao.md), usado literalmente pelos anotadores.
- ↳ O guia cobre D1–D4 com exemplos-limite (por exemplo, quando um enunciado curto continua a tarefa do agente ativo e quando inicia um novo agente). - ↳ O guia cobre D1–D4 com exemplos-limite (por exemplo, quando um enunciado curto continua a tarefa do agente ativo e quando inicia um novo agente).
- [ ] **30.** Anotar cada exemplo com dois avaliadores independentes. - [~] **30.** Anotar cada exemplo com dois avaliadores independentes.
- ↳ Avaliadores = modelos generativos em rodízio (`openai.gpt-5.5`, `google.gemini-2.5-pro`, `xai.grok-4.3`). *(Q15)* - ↳ Avaliadores = modelos generativos em rodízio (`openai.gpt-5.5`, `google.gemini-2.5-pro`, `xai.grok-4.3`). *(Q15)*
- [ ] **31.** Resolver as discordâncias com a revisão de um terceiro avaliador. - [~] **31.** Resolver as discordâncias com a revisão de um terceiro avaliador.
- [ ] **32.** Registrar, para cada exemplo, a resposta de referência, a justificativa e a versão da taxonomia. - [~] **32.** Registrar, para cada exemplo, a resposta de referência, a justificativa e a versão da taxonomia.
- ↳ Reportar também a concordância entre anotadores. - ↳ Reportar também a concordância entre anotadores.
- [ ] **33.** Separar os dados em desenvolvimento, calibração e teste final por grupo de origem. *(Q12)* - [~] **33.** Separar os dados em desenvolvimento, calibração e teste final por grupo de origem. *(Q12)*
- ↳ O grupo é `group_id`, ou seja, a conversa. - ↳ O grupo é `group_id`, ou seja, a conversa.
- [ ] **34.** Impedir que textos derivados do mesmo caso apareçam em mais de uma partição. - [~] **34.** Impedir que textos derivados do mesmo caso apareçam em mais de uma partição.
- [ ] **35.** Congelar o conjunto de teste final antes de qualquer ajuste de parâmetros. - [ ] **35.** Congelar o conjunto de teste final antes de qualquer ajuste de parâmetros.
- ↳ Registrar o hash do arquivo congelado e guardar uma cópia em `logs-imutaveis`. - ↳ Registrar o hash do arquivo congelado e guardar uma cópia em `logs-imutaveis`.
@@ -143,9 +168,10 @@ As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
## F6 — Piloto ## F6 — Piloto
- [ ] **55.** Executar um piloto de 50 exemplos por candidato em CPU. *(Q11)* - [ ] **55.** Executar um piloto de 50 exemplos por candidato em CPU.
- ↳ Sanidade em CPU (us-chicago-1, sub-rede privada) aprovada nos 4 candidatos: Laya 125 ms, GLiNER 194 ms, Rizzo Flow 7,3 s, SemIf 16,4 s (latência mediana). *(Q11)*
- [ ] **56.** Executar um piloto de 50 exemplos por candidato em GPU. - [ ] **56.** Executar um piloto de 50 exemplos por candidato em GPU.
- ↳ GPU em **sa-saopaulo-1, A10.1**, uma por vez *(Q14)*. Sanidade em GPU já em andamento. - ↳ GPU em **sa-saopaulo-1, A10.1**, uma por vez *(Q14)*. Sanidade em GPU: Laya 21 ms, GLiNER 16 ms, SemIf 66 ms (latência mediana). Rizzo Flow exigiu compilar o llama.cpp com CUDA para a glibc da imagem dos jobs (`infra/ds/build_llama_cuda.sh`).
- [ ] **57.** Medir memória de GPU, memória RAM, tempo de carga, tempo de aquecimento e falhas do piloto. - [ ] **57.** Medir memória de GPU, memória RAM, tempo de carga, tempo de aquecimento e falhas do piloto.
- [ ] **58.** Ajustar apenas os parâmetros operacionais necessários para eliminar falhas de execução. - [ ] **58.** Ajustar apenas os parâmetros operacionais necessários para eliminar falhas de execução.

View File

@@ -31,6 +31,33 @@ PYTHONPATH=infra/oci .venv/bin/python infra/ds/jobs.py run --name <nome> --entry
--env DMB_CANDIDATE=laya DMB_MODE=sanity --wait --env DMB_CANDIDATE=laya DMB_MODE=sanity --wait
``` ```
## GPU (sa-saopaulo-1)
| Item | Valor |
|------|-------|
| Shape | `VM.GPU.A10.1`, **uma GPU por vez** (limite da região) — `infra/ds/gpu_queue.py` enfileira |
| Dados | `dmb-artefatos-gru` (réplica server-side com `infra/oci/replicate.py`), resultados em `dmb-resultados-gru` |
| Rede | Gerenciada (limite de VCNs da região esgotado). Isolamento no processo + autoteste `process_guard` (`DMB_ISOLATION=app`) |
| Rizzo Flow em CUDA | O binário oficial do llama.cpp exige GLIBC 2.38; a imagem dos jobs tem 2.34. `infra/ds/build_llama_cuda.sh` compila o mesmo commit (`161755f`) com CUDA 12.8, sysroot glibc 2.17 e libstdc++ estática (maior GLIBC exigida: 2.27), publicado em `runtimes/rizzo_flow/cuda-latest.json` |
```bash
PYTHONPATH=infra/oci:infra/ds .venv/bin/python infra/ds/gpu_queue.py sanity laya gliner semif rizzo_flow
```
## Dados sintéticos (na VM)
```bash
export DMB_OCI_AUTH=instance_principal DMB_COMPARTMENT_OCID=<ocid do compartment>
python -m datagen.generate --out /data/runs/datagen-v1 # OCI Generative AI (us-chicago-1)
python -m datagen.prepare --run /data/runs/datagen-v1
python -m datagen.annotate --run /data/runs/datagen-v1
python -m datagen.build --run /data/runs/datagen-v1 --freeze
```
## Configuração local sensível
Fora do Git, em `.secrets/`: credenciais OCI, chaves SSH, o catálogo de artefatos com OCIDs (`artefatos-OCI.md`) e `oci-settings.json` (caminho do compartimento pai e CIDR autorizado para SSH; variáveis `DMB_PARENT_COMPARTMENT` e `DMB_SSH_CIDR` têm precedência). `tests/test_sensitive.py` falha se OCIDs, chaves, IPs públicos ou valores de `.secrets/oci-settings.json` aparecerem em arquivos versionados.
## VM de trabalho ## VM de trabalho
| Item | Valor | | Item | Valor |

View File

@@ -9,7 +9,6 @@ Uso: python -m harness.sanity --candidate laya --models-root models --device cpu
""" """
import argparse import argparse
import json import json
import platform
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -76,7 +75,7 @@ def main():
sanity.append(row) sanity.append(row)
report = { report = {
"candidate": adapter.describe(), "device": args.device, "host": platform.node(), "candidate": adapter.describe(), "device": args.device,
"load_seconds": round(load_s, 2), **guard, "load_seconds": round(load_s, 2), **guard,
"conformance": conformance, "sanity": sanity, "conformance": conformance, "sanity": sanity,
"summary": { "summary": {

View File

@@ -13,16 +13,17 @@ from datetime import datetime, timezone
import oci import oci
from session import CRED_DIR, REGION, load_config from session import CRED_DIR, REGION, load_config, settings
PARENT_PATH = "root/<compartimento-pai>" # Caminho do compartimento pai e CIDR do SSH ficam fora do Git: .secrets/oci-settings.json
PARENT_PATH = settings()["parent_compartment_path"]
COMPARTMENT_NAME = "decision-models" COMPARTMENT_NAME = "decision-models"
TAGS = {"projeto": "benchmark-decisoes-ptbr", "ambiente": "benchmark"} # centro_custo pendente (Q13) TAGS = {"projeto": "benchmark-decisoes-ptbr", "ambiente": "benchmark"} # centro_custo pendente (Q13)
BUCKETS = ["entrada", "artefatos", "resultados", "logs-imutaveis"] BUCKETS = ["entrada", "artefatos", "resultados", "logs-imutaveis"]
VCN_CIDR = "10.20.0.0/16" VCN_CIDR = "10.20.0.0/16"
SUBNET_CIDR = "10.20.1.0/24" SUBNET_CIDR = "10.20.1.0/24"
DEV_SUBNET_CIDR = "10.20.2.0/24" DEV_SUBNET_CIDR = "10.20.2.0/24"
SSH_ALLOWED_CIDR = os.environ.get("DMB_SSH_CIDR", "203.0.113.1/32") SSH_ALLOWED_CIDR = settings()["ssh_allowed_cidr"]
# VM de trabalho criada pelo usuário no console (o lançamento via API é negado neste compartment). # VM de trabalho criada pelo usuário no console (o lançamento via API é negado neste compartment).
VM_NAME = "instance-20261007-1206" VM_NAME = "instance-20261007-1206"
VM_SSH_KEY = ".secrets/ssh/ssh-key-oci.key" VM_SSH_KEY = ".secrets/ssh/ssh-key-oci.key"

View File

@@ -1,5 +1,7 @@
"""Sessão OCI do benchmark a partir das credenciais em .secrets/oci-credentials/.""" """Sessão OCI do benchmark a partir das credenciais em .secrets/oci-credentials/."""
import configparser import configparser
import json
import os
from pathlib import Path from pathlib import Path
import oci import oci
@@ -9,6 +11,16 @@ CRED_DIR = ROOT / ".secrets" / "oci-credentials"
REGION = "us-chicago-1" REGION = "us-chicago-1"
def settings() -> dict:
"""Configuração local fora do Git (.secrets/oci-settings.json); variáveis DMB_* têm precedência."""
path = CRED_DIR.parent / "oci-settings.json"
data = json.loads(path.read_text()) if path.is_file() else {}
return {
"parent_compartment_path": os.environ.get("DMB_PARENT_COMPARTMENT", data.get("parent_compartment_path")),
"ssh_allowed_cidr": os.environ.get("DMB_SSH_CIDR", data.get("ssh_allowed_cidr")),
}
def load_config(region: str = REGION, profile: str = "DEFAULT") -> dict: def load_config(region: str = REGION, profile: str = "DEFAULT") -> dict:
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
parser.read(CRED_DIR / "profile.config") parser.read(CRED_DIR / "profile.config")

View File

@@ -9,7 +9,6 @@
"probabilities_source": "native" "probabilities_source": "native"
}, },
"device": "cpu", "device": "cpu",
"host": "host-local",
"load_seconds": 15.93, "load_seconds": 15.93,
"conformance": [ "conformance": [
{ {

View File

@@ -9,7 +9,6 @@
"probabilities_source": "native" "probabilities_source": "native"
}, },
"device": "cpu", "device": "cpu",
"host": "host-local",
"load_seconds": 8.4, "load_seconds": 8.4,
"conformance": [ "conformance": [
{ {

View File

@@ -9,7 +9,6 @@
"probabilities_source": "native" "probabilities_source": "native"
}, },
"device": "cpu", "device": "cpu",
"host": "host-local",
"load_seconds": 6.18, "load_seconds": 6.18,
"conformance": [ "conformance": [
{ {

View File

@@ -9,7 +9,6 @@
"probabilities_source": "native" "probabilities_source": "native"
}, },
"device": "cpu", "device": "cpu",
"host": "host-local",
"load_seconds": 6.55, "load_seconds": 6.55,
"conformance": [ "conformance": [
{ {

View File

@@ -12,7 +12,7 @@ def test_external_connection_is_blocked():
s = socket.socket() s = socket.socket()
try: try:
with pytest.raises(NetworkAccessBlocked): with pytest.raises(NetworkAccessBlocked):
s.connect(("93.184.216.34", 443)) s.connect(("203.0.113.10", 443)) # TEST-NET-3 (RFC 5737)
finally: finally:
s.close() s.close()

52
tests/test_sensitive.py Normal file
View File

@@ -0,0 +1,52 @@
"""Nada sensível nos arquivos versionados: OCIDs, chaves, IPs públicos, namespace ou dados de .secrets/."""
import ipaddress
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PATTERNS = [
re.compile(r"ocid1\.[a-z0-9]+\.oc1\."),
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"\bfingerprint\s*=\s*[0-9a-f:]{20,}"),
]
IPV4 = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
def tracked_files():
out = subprocess.run(["git", "-C", str(ROOT), "ls-files", "-co", "--exclude-standard"],
capture_output=True, text=True, check=True).stdout.split()
return [ROOT / f for f in out if (ROOT / f).is_file() and not f.startswith(".secrets/")]
def local_secrets() -> list[str]:
"""Valores de .secrets/oci-settings.json não podem aparecer em arquivos versionados."""
path = ROOT / ".secrets" / "oci-settings.json"
if not path.is_file():
return []
values = []
for v in json.loads(path.read_text()).values():
values += [v] + [part for part in str(v).replace("/", " ").split() if len(part) > 5 and part != "root"]
return values
def test_no_sensitive_content():
secrets = local_secrets()
problems = []
for f in tracked_files():
try:
text = f.read_text()
except UnicodeDecodeError:
continue
rel = f.relative_to(ROOT)
problems += [f"{rel}: {p.pattern}" for p in PATTERNS if p.search(text)]
problems += [f"{rel}: valor de .secrets/oci-settings.json" for s in secrets if s and s in text]
for ip in IPV4.findall(text):
try:
addr = ipaddress.ip_address(ip)
except ValueError:
continue
if addr.is_global:
problems.append(f"{rel}: IP público {ip}")
assert not problems, "\n".join(problems)

42
tools/plan_progress.py Normal file
View File

@@ -0,0 +1,42 @@
"""Atualiza o bloco de andamento de docs/03-plano-de-tarefas.md a partir das caixas de cada tarefa.
Legenda: [x] concluída · [~] em andamento/parcial · [ ] pendente.
Uso: python tools/plan_progress.py
"""
import re
from datetime import date
from pathlib import Path
PLAN = Path(__file__).resolve().parents[1] / "docs" / "03-plano-de-tarefas.md"
START, END = "<!-- PROGRESSO:INICIO -->", "<!-- PROGRESSO:FIM -->"
def main():
text = PLAN.read_text()
phases, current = [], None
for line in text.splitlines():
if line.startswith("## F"):
current = [line[3:].strip(), 0, 0, 0]
phases.append(current)
m = re.match(r"- \[(x| |~)\] \*\*\d+\.\*\*", line)
if m and current:
current[{"x": 1, "~": 2, " ": 3}[m.group(1)]] += 1
rows = ["| Fase | Concluídas | Em andamento | Pendentes |", "|------|-----------|--------------|-----------|"]
totals = [0, 0, 0]
for name, done, doing, todo in phases:
rows.append(f"| {name} | {done} | {doing} | {todo} |")
totals = [totals[0] + done, totals[1] + doing, totals[2] + todo]
rows.append(f"| **Total** | **{totals[0]}** | **{totals[1]}** | **{totals[2]}** |")
block = (f"{START}\n**Andamento em {date.today():%Y-%m-%d}:** {totals[0]} de {sum(totals)} tarefas concluídas "
f"({100 * totals[0] // sum(totals)}%), {totals[1]} em andamento.\n\n" + "\n".join(rows)
+ "\n\nLegenda: `[x]` concluída · `[~]` em andamento ou parcial · `[ ]` pendente.\n" + END)
if START in text:
text = re.sub(re.escape(START) + ".*?" + re.escape(END), block, text, flags=re.S)
else:
text = text.replace("\n---\n", f"\n{block}\n\n---\n", 1)
PLAN.write_text(text)
print(block)
if __name__ == "__main__":
main()