# Promessa de estabilidade da CLI

> Aplica-se a partir da **v0.42.3**. Escrita para o `0.x` — cada quebra listada
> abaixo tinha de esperar por um major. **A v1.0.0 é esse major**: fechou a
> única quebra pendente (os atalhos de topo), e esta lista passa a valer como a
> promessa de semver real de resto em diante — um breaking change deixa de
> caber num `1.x`.

Um motor sem contrato não se automatiza. Quem escreve um `Makefile`, um passo de
CI ou um script de deploy precisa de saber o que pode partir num upgrade — e a
resposta «é 0.x, tudo pode partir» é verdadeira e inútil: garante que ninguém
depende de nada, o que é o mesmo que ninguém adoptar.

Isto listava o que se compromete e o que não se compromete dentro do `0.x`,
que é o que falta a maior parte dos projectos nessa fase. Desde a v1.0.0 é
mais do que isso: é o contrato de semver do projecto.

## Estável — não quebra sem um major

**Os verbos de ciclo de vida de container**, com os nomes e a semântica que
Docker e Podman lhes dão:

```
container run  create  ps  stop  start  restart  kill  rm  exec  logs  attach  stats
                wait   inspect   port   rename   pause   unpause
image     pull  ls  remove (rm)  tag  save  load  push   build (delonix build)
raiz      run  create  ps  start  stop  restart  kill  rm  exec  logs  attach  stats
          pull  images  rmi  tag  save  load  push            (aliases dos de cima)
          wait                                        (polimórfico — ver abaixo)
```

Concretamente, garante-se:

* **O nome do comando e a ordem dos argumentos posicionais.**
* **As flags curtas e longas listadas acima e os seus significados** — em
  `run`: `-d`, `-p`, `-v`, `-e`, `--name`, `--rm`, `--net`, `--restart`,
  `--memory`, `--cpus`, `--entrypoint`, `-w`, `-u`, `--add-host`, `--wait`,
  `--health-*`; em `exec`: `-i`, `-t`, `-e`, `-w`, `-u`. (Esta lista andou
  achatada, sem dizer a qual verbo cada flag pertence — `-i`/`-t` nunca
  existiram em `run`, só em `exec`; um teste em `main.rs` verifica agora as
  duas listas contra a árvore `clap` real, em vez de confiar na leitura.)
* **Os códigos de saída** — ver a secção «Códigos de saída» abaixo.
* **A saída JSON de `inspect`** — campos podem ser ACRESCENTADOS, nunca removidos
  nem com o tipo mudado.
* **Os verbos de topo (`raiz`) são aliases dos de grupo** — `delonix run` É
  `delonix container run`: mesma struct de flags, mesmo handler, mesmos códigos
  de saída, por construção (não há segundo contrato a partir). Herdam a promessa
  acima; o que AINDA não se promete é paridade total de flags e comportamento
  com uma versão de referência de Docker/Podman — essa medição é trabalho em
  curso, fora deste conjunto.

> **Quebra de contrato na v1.0.0.** Até à v0.69.0 os atalhos de topo (`ps`,
> `run`, `exec`, `logs`, `rm`, `images`) estavam aqui, como reescrita de argv
> para `container <verbo>`/`image list`. Saíram — corte limpo, sem alias: a
> grafia antiga falha com `unrecognized subcommand`, nunca em silêncio, a
> mesma regra que a reorganização da v0.30.0 já seguia. Os grupos continuam:
> `delonix container ps`, `delonix container run`, `delonix container exec`,
> `delonix container logs`, `delonix container rm`, `delonix image list`
> (ver a nota da v2.0.0 abaixo — essa grafia mudou outra vez).
> **Os códigos de saída não mudam nesta versão** — ver a secção abaixo.

> **Reintrodução na v5.x.** Os atalhos de topo VOLTARAM — `delonix run`,
> `create`, `ps`, `start`, `stop`, `restart`, `rm`, `exec`, `logs`, `pull` — por uma razão que a
> nota da v1.0.0 não pesou: a familiaridade de quem vem de Docker/Podman vale
> mais do que a pureza de uma CLI sem sinónimos, e o objectivo declarado é
> trocar o executável e manter o fluxo. NÃO é a reescrita de argv de outrora (a
> grafia antiga, frágil, que esta mesma nota tinha razão em cortar): cada verbo
> de topo é agora um subcomando `clap` de pleno direito que carrega a MESMA
> struct de argumentos e despacha para o MESMO handler do grupo — não há segundo
> caminho de código a divergir, e o `--help` é o mesmo módulo a módulo. É por
> isso que entram na promessa de estabilidade acima, e não numa à parte: o que
> `delonix container run` promete, `delonix run` promete pela mesma linha. O
> conjunto reintroduzido na primeira passagem foi o dos fluxos mais frequentes.

> **Tabela completada na linha 0.1.0** (o renumber que sucede à baseline histórica v5.1.x). Os restantes verbos da tabela
> Docker/Podman entraram pelo mesmo mecanismo — subcomando `clap` de pleno
> direito, MESMA struct de argumentos, MESMO handler do grupo: os de container
> `kill`, `attach`, `stats`, e os de imagem `images` (alias de `image ls`),
> `rmi` (alias de `image rm`), `tag`, `save`, `load`, `push`. Ficam de fora só
> `login`/`logout` — não constam da tabela pedida, e a sua forma de grupo
> (`image login`/`logout`) chega.
>
> **Comandos genéricos: política de resolução, nunca adivinhação.** Três nomes
> da tabela colidem com significados que este motor já tinha ou que seriam
> ambíguos; resolvem-se por regra escrita, não por palpite:
>
> * **`wait` é um só verbo com duas formas, escolhidas pela GRAMÁTICA.** Um ou
>   mais operandos posicionais de container ⇒ a forma `docker wait` (bloqueia
>   até cada um sair e imprime o código de saída — o MESMO handler que
>   `container wait`). Zero operandos ⇒ a forma de manifesto (`delonix wait -f
>   <ficheiro>`, a que este motor já tinha). As duas gramáticas são disjuntas;
>   misturá-las — um operando de container junto de `-f`/`--timeout` — é
>   RECUSADO em voz alta, nunca resolvido em silêncio. A forma de container
>   herda a promessa do grupo `container`; a de manifesto continua classificada
>   com o resto da superfície de manifesto (NÃO estável).
> * **`inspect` e `diff` ficam COM TIPO (type-scoped).** Não há um `delonix
>   inspect <nome>` genérico que decida sozinho se o nome é um container ou uma
>   imagem: `container inspect` resolve só containers (JSON de `docker
>   inspect`), `image describe` só imagens, `container diff` só containers, e
>   `delonix diff`/`delonix wait -f` na raiz são do MANIFESTO. É a mesma regra
>   que mantém separadas operações com efeitos diferentes (remover um recurso vs
>   limpar os não usados): um resolvedor genérico que adivinhasse o Kind é
>   exactamente o que esta CLI recusa fazer.

> **Quebra de contrato na próxima major (ADR-0062).** `container run` de uma
> imagem que declara `USER` passa a correr como esse utilizador, como no Docker
> e no Podman; até à v4.5.0 corria como uid 0 dentro do container, declarasse a
> imagem o que declarasse. `--user 0` (ou `user: "0"` num manifesto) é a forma
> explícita de ficar root. Uma imagem sem `USER` não muda. E um container com
> utilizador não-root deixou de ser dono do seu rootfs e de re-apropriar bind
> mounts do host: escreve só onde a imagem lhe dá posse, num volume nomeado
> vazio, ou onde as permissões do host deixam.

> **Quebra de contrato na v2.0.0.** O B2 da reestruturação da CLI (v0.67.0)
> tinha renomeado `image ls`/`image rm` para `image list`/`image remove` — a
> única excepção de nomenclatura numa CLI onde as outras 15 folhas do tipo
> "listar" usam `ls` (`network ls`, `volume ls`, `vm ls`, `pod ls`, `secret
> ls`, `stack ls`, …), seguindo o padrão Docker/Podman que este mesmo
> documento já cita para os verbos de container. Revertido: `image list`
> volta a `image ls` (`image remove` fica). Corte limpo, sem alias — a
> grafia `list` falha com `unrecognized subcommand`, nunca em silêncio.

## Códigos de saída

> **Alteração de contrato na v0.49.0.** Até à v0.48.0 **toda** a falha do motor
> era `1` — «não existe» e «rebentou» eram o mesmo número. Continua a valer que
> `0` é sucesso e não-zero é falha, portanto um `if delonix …; then` não muda de
> comportamento; o que muda é para quem testa `[ $? -eq 1 ]` **à espera de uma
> falha específica**. Ver a nota de migração no fim desta secção.

> **Alteração de contrato na linha 0.1.0.** Com `--detailed-exitcode`, um
> `stack plan` cujo plano tem um `Conflict` — um recurso com o carimbo
> `delonix.io/stack` de outra stack — respondia `0`, porque um conflito não é uma
> alteração. Passa a responder `5` (`DX-5601`), o mesmo que o `stack apply`
> recusaria, e ganha ao `2`. Um gate escrito como «`0` passa, `2` é deriva, o
> resto é erro» fica certo sem mudança; quem só testava `[ $? -eq 0 ]` passa a ver
> vermelho exactamente onde o `apply` ia falhar. Sem a flag, o `plan` continua a
> responder `0`.

> **Alteração de contrato na linha 0.1.0 (2).** O `stack plan` (e o `plan`)
> ganhou `--prune`, e passou a planear com as flags do `apply`. Sem `--prune`, um
> recurso que esta stack possui e o manifesto já não declara continua listado
> (`-`), mas como **mantido**: `changed: false` no `-o json` e fora do
> `--detailed-exitcode`. Antes contava como alteração, e um gate de deriva sem
> poda respondia `2` para sempre, porque o `apply` que o acompanha nunca o remove.
> Com `--prune` responde como antes: a remoção é uma alteração (`2`). Quem fazia o
> gate dar `2` com recursos que saíram do manifesto passa a precisar de
> `--prune`. O `action` continua `delete`, e o `planDigest` não muda.

> **Alteração de contrato na linha 0.1.0 (3).** O `stack apply --dry-run` (e o
> `delonix apply --dry-run`, o mesmo caminho) só imprimia o manifesto com os
> defaults e respondia `0` sem ler a máquina — um plano que o `apply` recusa
> passava na pré-visualização. Continua a imprimir o mesmo manifesto no stdout,
> mas agora corre tudo o que o `apply` corre antes da primeira escrita e sai
> como ele sairia:
>
> * `5` (`DX-5601`) com um recurso que outra stack carimbou;
> * `5` (`DX-5390`) com um `--plan-digest` que já não é o plano;
> * `1` (`DX-1000`) com um Replace ou um Reboot sem `--replace` ou `--reboot`,
>   com uma gralha nessas flags, ou com um valor que não tem a forma
>   `<Kind>/<nome>`, nome ou `all`;
> * `77` (`DX-7000`) quando o root do estado não se deixa ler e preparar.
>
> Nenhum recurso é criado nem mudado e nenhuma revisão é gravada. Mas o root
> não fica intocado: os stores criam as suas directorias e a chave do keyring
> dos túneis, como o `stack validate` já fazia. Quem queria só o manifesto, sem
> a máquina, tem o `delonix manifest render`, que continua a responder `0`.

| código | significado |
|---|---|
| `0` | sucesso |
| `1` | falha sem classe própria (o default de sempre) |
| `2` | uso inválido (o do `clap`) — e, em `stack plan --detailed-exitcode`, «há alterações» |
| `3` | o recurso existe mas **não está a correr** |
| `4` | **não existe** esse recurso |
| `5` | **conflito** — o nome já está tomado; e, em `stack plan --detailed-exitcode` e `stack apply --dry-run`, um plano com um recurso que outra stack carimbou (`DX-5601`) |
| `69` | **capacidade que este host não tem** — uma ferramenta por instalar, um backend indisponível |
| `74` | **o sistema de ficheiros disse que não** — disco cheio, caminho impossível |
| `77` | **permissão negada** — o remédio é uma permissão, e depois repetir |
| `124` | **o prazo esgotou-se** — `stack wait --timeout`, e o que vier a ter prazo |

`3` e `4` não são números inventados: são os códigos de estado do LSB que o
`systemctl` ainda fala (`3` = o programa não está a correr, `4` = não há tal
unidade). `5` não tem convenção por trás — é o número livre seguinte, abaixo da
gama que a shell usa (`126`/`127`, e `128+N` para sinais).

**`74` e `77` saíram do balde do `1`, e não de nenhuma classe publicada.** Até
à v0.66.1 uma falha de I/O respondia `1`, que a linha de cima descreve como
«falha sem classe própria» — dar-lhe classe é o que esta tabela existe para
fazer, e um script com `*) exit 1` continua a cair no mesmo ramo. O `77` é
recortado do `74` pelo `kind` do erro, porque é assim que ele chega: medido
contra um state root sem bit de escrita, `volumes create` e `secret create`
vêm ambos como permissão negada. É a falha mais accionável que existe — corrige
a permissão e repete — e respondia o mesmo número que um disco cheio.

**`69` e `124` também não.** `69` é o `EX_UNAVAILABLE` do `sysexits.h`; `124` é
o que o `timeout(1)` devolve quando o prazo passa, e está portanto já nos dedos
de quem embrulha um comando num. Entraram porque tinham **produtores reais mal
classificados**, não para completar uma tabela: um `stack wait` que esgotava o
tempo respondia `1` — o mesmo número de um apply rebentado, no comando cuja
função inteira é ser lido por CI — e um `wg`/`virt-customize`/`ngrok` em falta
respondia `1` também, indistinguível de um erro de escrita numa flag. As duas
chamadas seguintes de um reconciliador são opostas: esperar mais, ou parar e
instalar alguma coisa.

```bash
delonix stack wait -f delonix-manifest.yaml --timeout 120
case $? in
  0)   ;;                     # tudo de pé
  124) exit 0 ;;              # ainda a subir — o pipeline seguinte volta a tentar
  69)  echo "falta uma ferramenta neste nó" >&2; exit 1 ;;
  *)   exit 1 ;;              # qualquer outra coisa: pára
esac
```

### O número do dicionário: `DX-CDNN`

O código de saída e o `DX_*` dizem a **classe**. O número `DX-CDNN` diz **qual falha**
(ADR-0043): o milhar é a classe, a centena o domínio, os dois últimos a falha, e `00` a
própria classe. A CLI imprime-o na linha de erro — `error[DX-4501] no such VM: dev` — e
`delonix explain DX-4501` diz o que significa e o que fazer (`--json` para scripts). O
dicionário completo está em `delonix explain codes` e na página
[Dicionário de códigos](codigos.html), gerada da mesma tabela.

**Um número nunca muda de significado nem é reutilizado**: pode acrescentar-se, e uma falha
que deixe de existir fica com o número retirado. A mensagem ao lado pode ser reescrita e é
traduzida; o número é o contrato. Enquanto um sítio do motor ainda não tem entrada própria,
responde com a entrada genérica da sua classe (`DX-4000`, `DX-1000`, …).

### A identidade textual: `DX_*`

O número serve quem lê `$?`. Quem lê **texto** — um cliente HTTP, um consumidor
de `-o json`, um pipeline de logs — tem o código `DX_*`, que é a mesma
classificação noutra grafia:

| `DX_*` | código | quando |
|---|---|---|
| `DX_NOT_FOUND` | `4` | não existe esse recurso |
| `DX_NOT_RUNNING` | `3` | existe, mas não está a correr |
| `DX_CONFLICT` | `5` | o nome já está tomado |
| `DX_UNAVAILABLE` | `69` | capacidade que este host não tem |
| `DX_TIMEOUT` | `124` | o prazo esgotou-se |
| `DX_INVALID_ARGUMENT` · `DX_REGISTRY` · `DX_SYSCALL_FAILED` · `DX_INVALID_STATE` · `DX_IO` | `1` | falhas sem número próprio |

**Estes nomes são contrato**: um código pode ser ACRESCENTADO; um existente
nunca muda de grafia nem de significado.

A relação é **assimétrica de propósito**. Um `DX_*` mapeia sempre para UM
número — se mapeasse para dois, o `$?` e o texto contradiziam-se para a mesma
falha. Mas o `1` carrega VÁRIOS códigos, porque é o balde «sem classe própria» e
o texto pode dar-se ao luxo de ser mais fino: cada NÚMERO é uma promessa que tem
de valer o resto do `0.x`, enquanto `DX_REGISTRY` ao lado de
`DX_INVALID_ARGUMENT` não custa nada. Há teste a exigir as duas metades desta
regra, incluindo que o balde continue a ser um balde.

Hoje o código sai na **API de gestão** (`delonix serve api`), como campo
acrescentado ao corpo de erro — `{"error": "...", "code": "DX_NOT_FOUND"}`. O
campo `error` não foi tocado: a regra do ADR-0005 (acrescentar sim, remover ou
mudar de tipo não) vale aqui como vale no `-o json`.

**O que continua sem código próprio, e é honesto dizê-lo:** *permissão negada* e
*falha temporária* foram considerados e ficaram de fora — não há hoje uma
variante de erro que os produza (as falhas de permissão chegam embrulhadas no
`errno` de uma syscall, e o retry que existe acontece dentro do motor e nunca
chega a quem chama). Publicar um número que nada constrói é um número que nunca
pode voltar.

O que isto resolve é concreto: um reconciliador — o `Makefile`, o passo de CI, o
ciclo em bash que conduz esta CLI — não conseguia separar «cria, porque falta»
de «pára, porque falhou» sem ler a MENSAGEM de erro. E a mensagem é traduzida
(`--l18n=pt`), portanto um script que faz `grep 'no such'` funciona na máquina
onde foi escrito e deixa de classificar num nó com outra locale.

```bash
delonix container inspect web >/dev/null 2>&1
case $? in
  0) ;;                       # está lá
  4) delonix container run -d --name web nginx ;;   # falta — cria
  3) delonix container start web ;;                 # existe, parado — arranca
  *) exit 1 ;;                # qualquer outra coisa: pára, não adivinhes
esac
```

**O código de um `run`/`exec` continua a ser o do WORKLOAD, não o do motor.**
`run` em primeiro plano devolve o código do processo do container, `exec` o do
comando, e `healthcheck` sai `1` quando não saudável — as três promessas de
sempre, inalteradas. A consequência a reter: um container que saia `4` sai `4`,
logo **o `$?` de um `run`/`exec` nunca se lê como uma das classes acima** — esse
número foi escolhido pelo workload, não pelo motor.

**O que fica de fora**, e é honesto dizê-lo: uma pré-condição do host por
satisfazer (uma sessão sem delegação de cgroup, por exemplo) não tem código
próprio — o motor avisa e continua, portanto não há falha para classificar. E
os grupos ainda não estáveis (ver a lista mais abaixo) podem responder `1` onde
um comando estável já responde `4`: o `workload describe` de um nome que não
existe é o caso conhecido.

### Migrar

* `if delonix …; then` / `|| exit 1` / `set -e` — **nada muda**.
* `[ $? -eq 1 ]` a testar «este comando falhou» — passa a ser `[ $? -ne 0 ]`.
* `[ $? -eq 1 ]` a testar «não existe» — passa a ser `[ $? -eq 4 ]`, que é o
  teste que antes não havia maneira de escrever.

## Estável em conteúdo, não em formato

**As tabelas de `ls`/`ps`.** As colunas podem mudar de largura, de ordem ou
ganhar irmãs — são feitas para humanos e medem-se pelo conteúdo real. Um script
que faça `awk '{print $3}'` sobre elas parte, e isso não conta como quebra de
contrato.

**Para automação há `-o json`**, e é ele que é estável: um array JSON por
recurso, campos podem ser ACRESCENTADOS mas não removidos nem com o tipo mudado
(ADR-0005). Verificado a funcionar (2026-09-25, v4.4.0) em `container ps`,
`image ls`, `volume ls`, `network ls`, `vm ls`, `secret ls`, `workload ls` e no
verbo genérico `get <plural>` (`get pods`, `get volumes`, …), que é o caminho de
listagem dos Kinds sem `ls` próprio — o `pod ls` já não existe, e o grupo
`storage` fundiu-se no `volume`. Também `inspect` e `-q`/`--quiet`. **Excepção
medida:** `backup ls` ainda não aceita `-o json`.

> Uma versão anterior deste documento dizia que `-o json` estava «por fazer e é a
> lacuna reconhecida aqui». **Estava errado** — existe desde a ADR-0005. É
> exactamente o erro que o `paridade-docker-podman.md` abre por corrigir: inferir
> ausência a partir de um sintoma, sem ir ver.

## O schema dos manifestos — estável, e é o que mais importa

> Esta secção **substitui** a linha que dizia que o schema «é aditivo na prática,
> mas não é uma promessa». Era o compromisso ao contrário: a CLI estava mais
> protegida que o formato declarativo, quando é o formato que as pessoas põem em
> git e revêem em PR. Um `delonix-manifest.yaml` só se versiona se se souber que
> não parte sozinho.

Para os Kinds com spec tipado — **`Container`, `Pod`, `Volume`, `Network`** —
garante-se, dentro do `0.x`:

* **Um campo nunca é removido, nem muda de tipo, nem muda de significado.**
* **Um campo novo é sempre opcional** e tem um default que preserva o
  comportamento anterior. Um manifesto escrito hoje continua a fazer o mesmo
  amanhã.
* **Um nome antigo que seja renomeado mantém-se aceite como alias** (é o que já
  acontece com `restart`→`restartPolicy`, `options`→`mountOptions`,
  `wg_ip`→`wgIp`).
* **`apiVersion: delonix.io/v1` só muda com um `v2`**, e um `v2` não sai sem o
  `v1` continuar a ser aceite. Desde a v0.64.0 cada Kind tem também o grupo do
  seu domínio (`compute.delonix.io/v1alpha1`, `networking.…`, …) — o
  `delonix api-resources` diz qual é o de cada um. As duas grafias são aceites;
  a de grupo é a canónica, e o Kind só aceita **o grupo dele** ou o legado.

A verdade não é este texto, é o schema: **`delonix manifest schema`** emite-o a
partir do próprio código (ADR-0007), e o mesmo ficheiro está publicado em
[`schema/v1/delonix.json`](schema/v1/delonix.json). Um teste do repositório
falha se o publicado deixar de ser o gerado, precisamente para esta página não
voltar a poder mentir.

O que o schema RECUSA, e vale saber porque é o que aparece sublinhado no editor:
um campo desconhecido (um typo), **um `kind` que o motor não conhece** — um typo
no nome, ou um Kind removido — e **um `apiVersion` que não é o grupo daquele
Kind**. Até à v0.65.x o `kind` era uma string livre e qualquer nome passava, por
isso um `kind: Contaner` validava limpo e um `Egress` já removido também; e o
`apiVersion` era fixo no legado, por isso a grafia de grupo — a canónica — era
sublinhada como se fosse erro. Um validador que discorda do motor é pior do que
nenhum, porque o visto verde é o que as pessoas seguem.

Aponta o editor e escreve manifestos com completação e validação:

```yaml
# yaml-language-server: $schema=https://ngolacloud.github.io/delonix/schema/v1/delonix.json
apiVersion: compute.delonix.io/v1alpha1
kind: Pod
metadata: { name: web }
spec:
  containers:
    - name: web
      image: nginx:1.27
```

Há também uma extensão de VS Code que traz isto já ligado, mais um template por
Kind: [`angolardevops/delonix-vscode`](https://github.com/angolardevops/delonix-vscode).
Aponta para o MESMO ficheiro publicado acima — buscado ao vivo, não embutido —
por isso o editor e o `stack apply` não podem discordar.

Para saber o que mudou entre duas versões, não há uma página escrita à mão —
haveria a segunda fonte de verdade que a ADR-0007 aboliu. Há um comando:

```bash
scripts/schema-diff.sh v0.46.0          # dessa tag até à árvore actual
scripts/schema-diff.sh v0.46.0 v0.47.0  # entre duas tags
```

Compara campo a campo (nome e tipo), não o JSON cru, e sai **1** quando há
diferenças — serve directamente como gate de CI. Um campo removido ou com o
tipo mudado é assinalado como **quebra de contrato**, que é o que esta secção
promete não acontecer.

**Todos os Kinds têm hoje schema gerado.** Esta secção dizia que `Vm`,
`Cluster`, `ShareVolume`, `Image`, `Secret`, `Ingress`, `FirewallPolicy`,
`HTTPRoute`, `Tunnel`, `Workload` e `Stack` não o tinham — deixou de ser
verdade: `delonix explain <Kind>` responde para cada Kind que
`delonix api-resources` lista (verificado a 2026-09-25, v4.4.0), com os nomes
canónicos (`VirtualMachine`, `KubernetesCluster`, `NetworkPolicy`, `Gateway`; os
nomes antigos `Vm`/`Cluster`/`FirewallPolicy`/`Tunnel` continuam a resolver como
alias). `ShareVolume` já não existe: é um `kind: Volume` com bloco `share:`. Se
um Kind futuro chegar sem schema, o `delonix explain` recusa-o a dizer que Kinds
o têm, em vez de o omitir.

> **Três Kinds deixaram de existir** nesta série, fundidos no que já faziam:
> `Egress` → `FirewallPolicy` com `direction: egress`; `Dependency` →
> `FirewallPolicy` (açúcar, reduzido no load); `Storage` → `kind: Volume` com um
> bloco `nfs:`/`cifs:`/`webdav:`. Os nomes antigos continuam a carregar, com
> aviso de depreciação — a regra do «corte limpo» aplica-se a comandos, e um
> manifesto em git merece um degrau em vez de um erro.

## O ficheiro de providers do nó — `config.delonix.io/v1`

O `providers.yaml` (ADR-0054) diz que providers o nó tem, como o motor chega a
cada um e qual serve um pedido que não nomeia nenhum. Desde o catálogo 1.1.0
(ADR-0059 F1) aceita também um appliance OPNsense (`type: opnsense`) e o bloco
`networkDefaults`, que diz que provider responde a cada papel de rede
(`segment`, `gateway`; `nat`, `ipam` e `dns` são recusados até existir quem os
sirva). Desde o ADR-0064 D6 aceita também `type: powerdns`: a credencial do
PRÓPRIO motor para um servidor PowerDNS (`url`, `controllers`, `auth.keyFile`,
`allowPlainHttp`), usada só para remover os registos de gateway que o nó
Proxmox escreve e nunca remove. O `defaultProvider` continua a ser só o de computação. Procura-se por
esta ordem, e **o primeiro ficheiro que existe ganha, sem fusão**:
`DELONIX_PROVIDERS_CONFIG`, `$XDG_CONFIG_HOME/delonix/providers.yaml` (ou
`~/.config/…`), `/etc/delonix/providers.yaml`.

É um formato publicado, com a mesma promessa que o schema dos manifestos dentro
do `0.x`: **uma chave nunca é removida nem muda de significado, uma chave nova é
sempre opcional, e `apiVersion: config.delonix.io/v1` só muda com um `v2`** que
não sai sem o `v1` continuar a ser lido.

A verdade é o schema, gerado dos tipos que o motor lê:
**`delonix provider config schema`**, publicado em
[`schema/v1/providers.json`](schema/v1/providers.json), com o mesmo teste a
falhar se o publicado deixar de ser o gerado. O schema recusa uma chave
desconhecida, outra versão, um tipo de provider que não existe e um segredo
escrito no próprio ficheiro (`tokenSecret`, `password`, `secret` — só por
referência: `tokenSecretFile`, `passwordFile`, `secretFile`, `secretRef`).

O schema diz se o ficheiro está **bem escrito**; o
**`delonix provider config validate`** diz se ele **serve**: um
`defaultProvider` ou um `networkDefaults` sem entrada no ficheiro, um ficheiro
de token ou de segredo legível por outros utilizadores, uma CA que não existe. Nenhum dos dois contacta um nó.

```yaml
# yaml-language-server: $schema=https://ngolacloud.github.io/delonix/schema/v1/providers.json
apiVersion: config.delonix.io/v1
defaultProvider: libvirt
providers:
  - type: libvirt
  - type: opnsense
    url: https://fw.example
    auth: { keyFile: /etc/delonix/opnsense.key, secretFile: /etc/delonix/opnsense.secret }
networkDefaults:
  gateway: opnsense
```

## NÃO estável — pode mudar em qualquer versão

* **`serve cri`, `serve api`, `serve node-api`, `serve docker-api`** — superfícies de protocolo
  em construção. O `docker-api` publica a sua cobertura em
  `delonix serve docker-api --matrix`, e é essa tabela que diz o que existe
  hoje, não esta promessa. A **API de gestão** (`serve api`) é local (socket
  unix, só o próprio uid) e não tem contrato publicado: não construas automação
  sobre ela — para isso existe a CLI, com `-o json`. As rotas dela estão
  **congeladas** (só correcções) enquanto o contrato local de nó que a substitui
  é decidido (ADR-0040 e ADR-0041, ambos *Proposed*); uma promessa para esse
  contrato só entra nesta página quando a primeira vaga dele fechar.
  As rotas que mudavam a rede **sem o registo do container** —
  `/v1/net/firewall/:ip`, `/v1/net/egress[/:bridge]`, `/v1/net/attach-extra…` e
  `DELETE /v1/net/attach/:id/:ip` — passaram a responder **`501` com
  `DX-6302`**: o reapply seguinte desfazia o que escreviam, e só cobriam o IP
  primário de um container com várias redes. O caminho é a CLI (`net ingress`,
  `net egress`, `network connect`/`disconnect`), que passa pelo registo.
* **O nome do executável `delonix` é deste motor.** O control plane deixa de
  produzir um binário com esse nome (ADR 0038 do control plane); um `delonix`
  instalado por este projecto é sempre a CLI descrita nesta página.
* **`compatibility`** — a mesma tabela do `serve docker-api --matrix`, mas
  como verbo de topo e com `-o json`. Ainda só cobre `docker`; `compose`/
  `cri`/`oci` são superfícies futuras, cada uma com o seu próprio "três
  estados" por construir. Sem promessa de campos até `compose`/`cri`/`oci`
  entrarem — o formato pode mudar de forma para os acomodar.
* **`provider`** — `ls`/`describe`/`matrix`: o que cada provider (libvirt,
  cloud-hypervisor, proxmox, linux, opnsense) declara contra o catálogo de
  capacidades (ADR-0050, 1.1.0 desde o ADR-0059), medido no host. Os tipos são
  `compute`, `network`, `storage`, `image` e `gateway`; um `gateway` responde às
  linhas de rede. Os NOMES das capacidades (`vm.snapshot.memory`,
  `net.namespace-isolation`, …) e os seis estados são o que um script lê, e
  seguem a versão do catálogo (`catalog_version` no JSON): uma entrada nova sobe
  o minor, renomear ou remover sobe o major. As colunas da tabela e a forma do
  JSON além desses campos podem ainda mudar.
* **`cluster`, `vm`, `pod`, `workload`, `net`, `systemcontainer`** —
  a superfície ainda está a assentar. (O *schema* de `kind: Pod` é estável, ver
  acima; o que não é estável é o grupo de comandos `delonix pod`.)
  > **`storage`/`sharevolume` deixaram de ser grupos** (#216, «fold
  > storage/sharevolume into volume») — esta secção continuou a nomeá-los
  > durante um tempo depois de já não existirem, o que é exactamente o erro
  > que a lista mais abaixo existe para impedir (um nome classificado que já
  > não é um grupo real). A superfície deles vive hoje em `delonix volume`.
* **`volume`, `network`, `secret`** — só a saída `-o json` e (para `Volume`/
  `Network`) o schema do manifesto têm promessa (ver as duas secções
  acima); o resto da superfície imperativa (`create`/`rm`/`inspect` e as suas
  flags) nunca foi revisto para uma promessa própria. `volume` em particular
  acabou de absorver `storage`/`sharevolume` (#216) — está literalmente a
  meio de assentar, o pior momento possível para prometer nada sobre ele.
* **`apply`, `plan`, `wait`, `stack`, `manifest`, `get`, `describe`,
  `delete`, `diff`, `drift`** — o ciclo de reconciliação nativo (IaC, v0.47.0+). O
  SCHEMA dos manifestos que estes comandos consomem já é estável (secção
  acima); os VERBOS e flags destes comandos em si (`--detailed-exitcode`,
  `--prune`, `--replace`, …) nunca tiveram revisão própria, e a Kind mais
  recente (`Service`) ainda está a ganhar wiring nalguns destes caminhos.
* **`migrate`** — `migrate assess` lê um ficheiro compose que já tens e diz o
  que este motor serve, recusa e não implementa. É um RELATÓRIO (não cria, não
  puxa, não corre), e a tabela por trás dele é a do `compatibility compose` —
  uma medição com data, que se move quando o motor ganha chaves.
* **`compose`** — suporte nativo a `docker-compose.yml`, ainda a ganhar
  chaves do Compose Spec (`profiles`, `configs`/`secrets`, multi-ficheiro —
  ver o `AGENTS.md` para a lista completa do que falta).
* **`system`, `dashboard`, `completion`, `init`, `man`, `config`,
  `explain`, `api-resources`, `version`** — utilitários/introspecção, sem
  promessa declarada em nenhuma versão.
* **`hosts`** — `hosts sync` publica no `/etc/hosts` os nomes de serviço dos
  containers expostos (ADR-0048); superfície nova, sem promessa ainda.
* **`mcp`** — o servidor Model Context Protocol (ADR-0025), superfície nova. O
  transporte `stdio` é o suportado (um processo filho do cliente de IA por
  sessão, nunca um daemon); um transporte HTTP local, se vier a existir, seria
  loopback-only com um token local, como o `serve api` já é local-only e sem
  contrato publicado — não construas automação sobre ele ainda.
* **`policy`** — `delonix policy unset` (M04, `docs/roadmap/
  13-improvements-traceability.md`), o único verbo imperativo de
  `kind: RuntimePolicy`. Superfície nova, sem promessa declarada — o
  `policy.json` que ele apaga já existia e já era lido por `container run`/
  `vm create`; o que é novo é a forma declarativa (`stack apply`) e este
  comando de remoção. A leitura passa por `delonix get runtimepolicies`/
  `delonix describe runtimepolicy <nome>`, os verbos genéricos, que herdam a
  promessa (ou falta dela) desses verbos e não uma própria deste grupo.
* **`backup`** — o grupo não estava declarado de nenhum dos lados, e essa
  omissão é ela própria um defeito: quem quisesse saber se podia depender de
  `delonix backup vm x` não tinha resposta. Fica NÃO estável enquanto os
  arquivos ganham verbos. O **formato do arquivo** é outra coisa e tem a sua
  própria guarda: o `backup.json` traz um número de versão e um leitor que não o
  conheça RECUSA em vez de adivinhar uma disposição que nunca viu.

  **Mudou na v0.67.0:** `delonix backup <kind> <nome>` passou a
  `delonix backup create <kind> <nome>`, e o `delonix restore` de raiz passou a
  `delonix backup restore <arquivo>` — sem o `<kind>` posicional, que o arquivo
  já regista (continua a poder afirmar-se com `--kind` para que uma discordância
  seja recusada). O agendamento saiu de flags do backup para
  `delonix backup schedule`, com a mesma semântica: instala o temporizador **e**
  tira o primeiro arquivo. Corte limpo, sem aliases — a forma antiga falha com
  «unrecognized subcommand», nunca em silêncio.

  **`delonix system backup` não foi tocado nessa data.** É outro âmbito — o
  state root inteiro de um nó — e não uma segunda porta para este grupo.

  **Mudou depois (Sprint 4 da reestruturação da CLI, 2026-09-12):** o próprio
  `system backup`/`system restore` foi renomeado para `system snapshot
  create`/`system snapshot restore` — mesmo vocabulário de `vm snapshot`/
  `volume snapshot` (captura num instante), só que à escala do nó. O ÂMBITO
  não mudou nem se fundiu com este grupo — só a PALAVRA partilhada («backup»)
  desapareceu, que era a fonte real da confusão que o parágrafo anterior já
  andava a explicar por escrito. Corte limpo: a forma antiga falha com
  «unrecognized subcommand».
* **`gitops`** (`reconcile`/`status`/`suspend`/`resume`, ADR-0021) — grupo novo
  a 2026-10-12, NÃO estável. Age sobre uma `kind: GitOpsSource` que já existe;
  a fonte declara-se com `stack apply`, como qualquer outro Kind. O registo e o
  estado de cada passagem em `$DELONIX_ROOT/gitops/` são estado interno (lê-se
  com `gitops status -o json`, nunca do disco).
* **`oci-runtime`** (`create`/`start`/`state`/`kill`/`delete`, ADR-0080) — a CLI
  compatível com o `runc`: o init do próprio motor por trás do contrato do runc,
  para poder ocupar o lugar do `runc`/`crun`. Superfície nova e EXPERIMENTAL — o
  contrato que ela imita é o do `runc`, não uma promessa própria deste motor, e
  os verbos/estados podem ainda mudar enquanto a conformidade assenta.
* **Tudo o que começa por `net netns`** — plumbing interno exposto por
  conveniência de depuração.
* **O formato dos ficheiros de estado** em `$DELONIX_ROOT`. Lê-se pelo `inspect`,
  nunca do disco.
* **`stack history`**, **`stack rollback`** e o conteúdo de `$DELONIX_ROOT/stacks/` (ADR-0019). É um
  registo do que foi aplicado, e o desenho promete explicitamente que **nada o
  lê para decidir o que existe**: apagar essa pasta não muda o que o `plan`, o
  `apply`, o `prune` ou o `destroy` fazem — só perde o histórico, e há gate na
  bateria E2E a exigi-lo. Automação que dependa da presença de uma revisão está
  a apostar contra essa promessa.

## Como uma quebra é feita, quando tem de acontecer

Precedente já cumprido pelo projecto: a reorganização da v0.30.0 (`netns` →
`net netns`, `cri` → `serve cri`, …) foi um **corte limpo, sem aliases** — a
forma antiga falha com «unrecognized subcommand», nunca em silêncio.

Isso mantém-se como regra: **falhar alto**. Um alias de compatibilidade que
muda de comportamento é pior que um erro.

E uma lição que custou esta sessão: essa quebra deixou um chamador INTERNO por
actualizar (o `delonix-cri` continuou a invocar `delonix netns attach`), o que
partiu a criação de pod rootless durante meses — ver
[cri-conformance.md](cri-conformance.md). Um corte limpo obriga a fazer o grep
por chamadores em TODO o workspace, não só na documentação.
