Pular para conteúdo

AUR-408 — Perfil de ferramenta de documentação offline (docs-tool-offline-v1)

Documento histórico do board. Pode descrever componentes removidos; para uso atual, consulte o guia do produto.

Este documento descreve a entrada docs-tool-offline-v1 publicada no registry canônico de profiles. O card publica somente documentos: nenhum engine é invocado, nenhum documento é gerado ou renderizado, nada sob docs/ é produzido pelo profile e o runner permanece fail-closed para esta chave até um card de implementação dependente habilitá-la explicitamente.

Artefatos

Artefato Caminho
Schema .board/schemas/docs-tool-offline-profile.schema.json
Profile .board/oci/profiles/docs-tool-offline-v1.json
Lock .board/locks/oci/docs-tool-offline-v1.lock.json
Registry .board/oci/profiles/registry.v1.json

O registry passa a publicar exatamente oito chaves, em ordem canônica crescente: bootstrap-readonly-v1, docs-tool-offline-v1, fake-provider-v1, go-git-offline-v1, go-unit-offline-v1, parser-worker-v1, registry-v1, sqlite-offline-v1. Nenhuma chave existente muda de digest, schema ou lock.

docs- ordena depois de bootstrap- e antes de fake-, então a nova chave ocupa o índice 1 e as seis chaves seguintes deslocam-se uma posição à direita. Nenhuma chave sai da lista, nenhuma aridade exata vira mínimo e nenhuma igualdade vira desigualdade: cada chave continua asserida no único índice que a ordem canônica lhe dá.

Imagem pinada

O lock fixa Go e Bash na imagem de bootstrap já presente localmente, por digest imutável:

aurum-bootstrap-go-bash@sha256:503ac356fa6bca4bad56fade87b5479ff371bd446ffb9c7db91f211323c7c73e

A imagem foi escolhida por prova, não por conveniência. Sob --pull=never --network=none --read-only --user=65534:65534 --cap-drop=ALL (mais --security-opt=no-new-privileges) os três probes locais respondem:

Imagem Bash Go Ferramenta de documentação Outros
aurum-bootstrap-go-bash@sha256:503ac356… 5.2.26 go1.21.13 pandoc, mkdocs, jekyll, hugo, asciidoctor, gomarkdoc, sphinx-build ausentes sem ruby, python3, node, cc, gcc, git; uma única interface, lo
golang@sha256:4746d264… (imagem de go-git-offline-v1) 5.2.15 go1.21.13 as mesmas sete ausentes tem git, python3, cc/gcc; uma única interface, lo
ghcr.io/actions/jekyll-build-pages@sha256:6791ebfd… presente ausente jekyll e ruby presentes sem Go

Isso é o que determina o plano, e não o contrário. A única imagem local que carrega uma ferramenta de site é a de Jekyll, e ela é rejeitada por duas razões independentes: não tem Go, portanto não satisfaz o command declarado; e o modelo de plugin do Jekyll é execução dinâmica de código Ruby em tempo de build, exatamente o que este profile nega. Não existe imagem local que combine Go com pandoc, mkdocs, hugo ou asciidoctor.

Portanto generator_cgo: false e CGO_ENABLED: "0" fixam um gerador e um renderer puro Go, e native_site_tool: "denied" recusa explicitamente a classe inteira de ferramentas nativas. A ausência dessas ferramentas na imagem pinada não é uma lacuna tolerada — é exatamente o que o profile nega. Um gerador ligado por cgo, ou um native_site_tool diferente de denied, é rejeitado com unsafe-plan. Nenhuma imagem foi construída, baixada ou fabricada para este card.

Somente aurum-bootstrap-go-bash@sha256:<64 hex> é aceito. Uma tag mutável (aurum-bootstrap-go-bash:1.21), um nome sem digest ou outro repositório é rejeitado com mutable-input antes de qualquer resolução de imagem.

Plano declarado

Limites e isolamento:

Chave Valor
network none
user 65534:65534
cap_drop / cap_add ALL / none
mounts / devices / sockets none / none / none
pull never
tmpfs rw,nosuid,nodev
checkout_readonly true
read_only_rootfs true
no_new_privileges true
privileged false
timeout_seconds 60
memory_mb 256
cpu_millis 1000
pids_limit 128
tmpfs_mb 128
stdout_limit_bytes / stderr_limit_bytes 65536 / 65536
max_input_files / max_input_bytes 10000 / 67108864
module_cache /go/pkg/mod (module_cache_read_only: true)

Capacidades da toolchain documental, todas verificadas pelo loader:

Chave Valor Efeito
generator digest-pinned o gerador só é admitido content-addressed
generator_cgo false gerador puro Go: nenhuma lib nativa, nenhum site tool
generator_root /tmp/aurum-docs-tool o gerador vive no tmpfs limitado
renderer digest-pinned o renderer só é admitido content-addressed
plugin_set digest-pinned o conjunto de plugins só é admitido content-addressed
native_site_tool denied pandoc, mkdocs, jekyll, hugo, asciidoctor negados
dynamic_plugin denied nenhum plugin é carregado ou resolvido em tempo de execução
remote_fetch denied nenhum tema, asset, plugin ou fonte é buscado remotamente
snippet_execution denied nenhum bloco de código do documento é executado
snippet_languages ["bash", "go", "json"] allowlist fechada de linguagens inertes
fixture_source local as fixtures são locais, nunca remotas
fixture_root /tmp/aurum-docs-fixtures as fixtures vivem no tmpfs limitado
output_mode ephemeral o output existe apenas durante a execução
output_root /tmp/aurum-docs-output o output vive no tmpfs limitado, nunca no checkout
host_output denied nenhuma escrita fora do output efêmero
host_filesystem denied nenhum caminho do host é lido ou escrito
subprocess denied nenhum processo externo é criado
max_documents 64 tamanho exato do corpus admissível
max_document_bytes 1048576 teto exato por documento (1 MiB)
max_output_bytes 33554432 teto exato de output (32 MiB) dentro do tmpfs de 128 MiB
render_timeout_seconds 5 wall-clock por renderização

generator, renderer e plugin_set pinam a exigência, não os bytes: o gerador, o renderer, os plugins, os temas e qualquer documento renderizado são não-objetivos deste card e pertencem ao card de implementação dependente. O que este card garante é que uma ferramenta sem digest nunca é resolvida.

snippet_languages é um allowlist, comparado elemento a elemento: acrescentar uma linguagem é mudança de plano e é rejeitado exatamente como habilitar execução. snippet_execution: "denied" é independente disso — nenhuma linguagem da allowlist é executada; elas apenas descrevem o que o renderer emite como texto inerte.

Os limites são constantes exatas, nunca mínimos: max_output_bytes: 0 e max_output_bytes: 34359738368 são ambos rejeitados, o primeiro por ser ilimitado e o segundo por não caber no tmpfs que teria de segurá-lo. O loader verifica ainda, de forma independente das constantes, que max_output_bytes <= tmpfs_mb * 1024 * 1024 e que max_documents * max_document_bytes <= max_input_bytes — um corpus ou um output que não cabe no próprio limite teria de transbordar para fora dele. Com os valores publicados, 64 * 1048576 = 67108864 é exatamente max_input_bytes.

generator_root, fixture_root e output_root são validados por expressões ancoradas nas duas pontas (^/tmp/aurum-docs-output$ e análogas), então /workspace/docs, /tmp/aurum-docs-output/../../etc, /tmp/aurum-docs-output/ (barra final) e /tmp/aurum-docs-output-host (irmão com o mesmo prefixo) são negados pelo mesmo motivo: nenhum deles é o diretório limitado. A camada de integração exige adicionalmente que os três roots sejam distintos entre si — um gerador que escreve na própria árvore de fixtures, ou um output que sombreia o gerador pinado, tornaria mutável em tempo de render aquilo que foi pinado por digest.

Comandos permitidos

["go", "test", "-mod=readonly", "./..."]

Qualquer outro comando, ou o mesmo comando com aridade diferente, invalida o plano. O ambiente é fechado sobre exatamente estas chaves:

Variável Valor
GOPROXY off
GOSUMDB off
GONOSUMDB *
GOTOOLCHAIN local
CGO_ENABLED 0
AURUM_DOCS_GENERATOR_ROOT /tmp/aurum-docs-tool
AURUM_DOCS_FIXTURE_ROOT /tmp/aurum-docs-fixtures
AURUM_DOCS_OUTPUT_ROOT /tmp/aurum-docs-output
HTTP_PROXY (vazio)
HTTPS_PROXY (vazio)
NO_PROXY *

As três variáveis AURUM_DOCS_* e os três roots declarados são duas grafias dos mesmos diretórios; o teste de integração recusa qualquer divergência entre elas, porque AURUM_DOCS_OUTPUT_ROOT é exatamente onde um arquivo renderizado apareceria se apontasse para fora do tmpfs limitado. Nenhuma credencial, valor de ambiente do host ou segredo entra no documento, e o loader recusa qualquer valor com forma de credencial mesmo em campo cujo valor seja pinado por constante.

Códigos de rejeição

O loader (ValidateRegistryAUR408 / ValidateProfileAUR408) é side-effect-free, não abre socket, não renderiza documento, não cria subprocesso e devolve exatamente um código:

Código Causa
registry-missing registry ausente ou não regular
schema-invalid registry, schema ou profile fora da forma canônica (inclui chave duplicada dentro de um objeto JSON e limite obrigatório ausente, como render_timeout_seconds)
schema-missing documento de schema ausente
schema-digest-mismatch schema_digest não reproduz os bytes do schema
profile-missing documento do profile ausente
profile-unregistered chave desconhecida, ou docs-tool-offline-v1 ausente do registry
duplicate-profile a mesma chave publicada duas vezes
order-invalid chaves fora da ordem canônica crescente
credential-present valor de ambiente com forma de credential real
digest-invalid digest vizinho fora do formato sha256:<64 hex>, ou gerador, renderer ou plugin set sem digest
unsafe-plan limite obrigatório relaxado ou excedido, tmpfs ilimitado, corpus maior que o limite de entrada, output fora do tmpfs, site tool nativo, plugin dinâmico, fetch remoto, execução de snippet, allowlist de snippet ampliada, fixture remota, escrita no host, filesystem do host, subprocesso, output persistente, gerador cgo, rede, mount, socket, device, root ou capability
lock-missing lock ausente
mutable-input imagem por tag mutável, repositório inesperado ou lock fora do schema
lock-digest-mismatch lock_digest não reproduz os bytes do lock

As constantes de segurança do schema são pinadas duas vezes: nos bytes do documento de schema e em profileSchemaConstants408 / environmentSchemaConstants408, dentro de tests/unit/AUR-408.go. Relaxar o schema e recomputar o schema_digest no registry move os dois arquivos de forma consistente e ainda assim falha, porque o mapa em Go não se move com eles.

Mutantes provados

O acceptance executa cada mutante contra bytes reais em disco, na cópia descartável, e exige o código declarado; o restore volta byte-idêntico.

Mutante Mudança Código
MUT-001 chave docs-tool-offline-v1 duplicada com bytes diferentes duplicate-profile
MUT-002 imagem do lock por tag mutável mutable-input
MUT-003 network de none para host unsafe-plan
mount habilitado mounts para bind unsafe-plan
socket habilitado sockets para unix unsafe-plan
device habilitado devices para /dev/fuse unsafe-plan
root user para 0:0 unsafe-plan
capability adicionada cap_add para SYS_ADMIN unsafe-plan
plugin dinâmico dynamic_plugin para allowed unsafe-plan
fetch remoto remote_fetch para allowed unsafe-plan
snippet executável snippet_execution para allowed unsafe-plan
allowlist ampliada snippet_languages ganha "sh" unsafe-plan
site tool nativo native_site_tool para pandoc unsafe-plan
gerador cgo generator_cgo para true unsafe-plan
output no host host_output para read-write unsafe-plan
filesystem do host host_filesystem para read-only unsafe-plan
subprocesso subprocess para allowed unsafe-plan
output persistente output_mode para persistent unsafe-plan
output fora do tmpfs output_root para /workspace/docs unsafe-plan
traversal do output output_root para /tmp/aurum-docs-output/../../etc unsafe-plan
barra final output_root para /tmp/aurum-docs-output/ unsafe-plan
irmão com mesmo prefixo output_root para /tmp/aurum-docs-output-host unsafe-plan
fixture fora do tmpfs fixture_root para /workspace/docs unsafe-plan
fixture remota fixture_source para remote unsafe-plan
gerador fora do tmpfs generator_root para /usr/local/bin unsafe-plan
output ilimitado max_output_bytes para 0 unsafe-plan
output gigante max_output_bytes para 34359738368 unsafe-plan
corpus gigante max_documents para 128 unsafe-plan
tmpfs ilimitado tmpfs_mb para 0 unsafe-plan
gerador sem digest generator para unpinned digest-invalid
renderer sem digest renderer para unpinned digest-invalid
plugin mutável plugin_set para latest digest-invalid
limite ausente render_timeout_seconds removido do documento schema-invalid
credential real HTTP_PROXY para uma chave com forma real (montada em runtime) credential-present

O vetor de credential nunca aparece como literal em arquivo versionado: ele é montado em runtime, dentro da cópia descartável do acceptance, para que o portão de credenciais de oci-run não veja forma de segredo em nenhuma entrada materializada nem em nenhuma saída.

Loaders vizinhos

Registrar uma oitava chave fecha a allowlist dos loaders já entregues, então este card os estende no mesmo commit:

  • tests/unit/AUR-402.go — allowlist de chaves do registry canônico.
  • tests/unit/AUR-403.go — allowlist, aridade (8) e ordem posicional.
  • tests/unit/AUR-404.go — conjunto de chaves registradas e switch do loader.
  • tests/unit/AUR-405.go — conjunto de chaves registradas e switch do loader.
  • tests/unit/AUR-406.go — conjunto de chaves registradas e switch do loader.
  • tests/unit/AUR-407.go — conjunto de chaves registradas e switch do loader.
  • tests/contracts/AUR-404.go — aridade exata do registry (7 para 8).

Cada extensão é aditiva: nenhuma rejeição anterior desaparece, nenhuma igualdade vira desigualdade, nenhuma aridade exata vira mínimo e nenhuma chave sai de allowlist. Nenhum desses loaders lê os documentos da toolchain documental: cada um checa apenas os caminhos declarados e a forma do digest, de modo que registrar a ferramenta de documentação não pode re-apontar silenciosamente o plano Go unit, o plano Git, o plano do fake provider, o plano do parser worker nem o plano do state store.

A aridade total do registry é asserida em tests/unit/AUR-408.go, que este card possui. tests/contracts/AUR-408.go não assere aridade: este card é dono da chave docs-tool-offline-v1, não do tamanho do registry, e a próxima carta de profile deve quebrar apenas onde sua posse estiver declarada.

Diagnóstico offline

# a chave está publicada e em ordem?
grep -n '"key"' .board/oci/profiles/registry.v1.json

# os digests do registry reproduzem os bytes?
sha256sum .board/schemas/docs-tool-offline-profile.schema.json \
          .board/locks/oci/docs-tool-offline-v1.lock.json
printf '%s' "$(sed -n 's/.*"image": "\(.*\)".*/\1/p' \
  .board/locks/oci/docs-tool-offline-v1.lock.json)" | sha256sum

# o runtime da imagem pinada ainda satisfaz o plano puro Go?
docker run --rm --pull=never --network=none --read-only \
  --user=65534:65534 --cap-drop=ALL --entrypoint= \
  aurum-bootstrap-go-bash@sha256:503ac356fa6bca4bad56fade87b5479ff371bd446ffb9c7db91f211323c7c73e \
  bash -c 'command -v go; command -v pandoc || echo pandoc-absent; ls /sys/class/net'