engine-kit 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,194 @@
1
+ # Contrato de um motor no `engine-registry`
2
+
3
+ O que o repositório de **cada motor** precisa fazer para entrar aqui — e ser instalado com
4
+ integridade e autenticidade pelo `engine-kit`. Este documento é o insumo das sessões de
5
+ alinhamento: uma sessão por motor, seguindo este checklist.
6
+
7
+ > **Regra da casa:** o `engine-registry` (público) é o **canônico de consumo** — é o único que o
8
+ > `manifest.json` aponta e o único cuja falha de publicação aborta o release. O repositório
9
+ > privado do motor guarda **fonte, CI e histórico**, e pode receber um espelho da release. Dois
10
+ > canônicos seriam duas verdades, e a divergência apareceria para o usuário como *"atualizei e
11
+ > voltou a versão velha"*.
12
+
13
+ ---
14
+
15
+ ## 1. Uma vez por motor: a chave
16
+
17
+ ```sh
18
+ node kit/gen-key.mjs <motor>
19
+ ```
20
+
21
+ - grava a privada em `~/.engine-signing/<motor>_ed25519_private.key` (**nunca** versionar, **nunca**
22
+ subir para CI);
23
+ - imprime a **pública** para colar no `manifest.json`, em `install.publicKey`;
24
+ - recusa sobrescrever uma chave existente: trocá-la invalidaria todas as releases já assinadas
25
+ daquele motor nos consumidores que já a pinaram.
26
+
27
+ **Uma chave por motor.** Um vazamento obriga a republicar aquele motor, não a casa inteira.
28
+
29
+ ## 2. O artefato
30
+
31
+ O motor builda o próprio artefato — o `publish-base.ps1` **não** builda nada. Um wheel Python, um
32
+ bundle esbuild e um `.tgz` multi-plataforma não têm nada em comum: "parametrizar" o build seria
33
+ reescrever cada build dentro do script.
34
+
35
+ Requisitos do artefato:
36
+
37
+ - **self-contained**: sobe numa máquina limpa, sem depender do consumidor (traz o próprio
38
+ `node_modules` trimado, ou o runtime que precisar);
39
+ - o conteúdo vai na **raiz** do `.tgz` (o kit renomeia o staging inteiro para `<home>/<extractTo>`);
40
+ - traz um `package.json` com o **mesmo `version`** que vai para o `manifest.json`.
41
+
42
+ > **Armadilha real:** o `provision` compara a versão do `package.json` **de dentro** do artefato
43
+ > com o pin do registry. Se o manifest disser `0.1.1` e o artefato disser `0.1.0`, o pin nunca é
44
+ > satisfeito e a máquina **reinstala o motor em toda chamada**. Bump de versão exige rebuild.
45
+
46
+ > **Armadilha do tar no Windows:** num terminal Git Bash, o GNU tar do Git vem antes no `PATH` e
47
+ > lê `C:\...` como host remoto (`Cannot connect to C: resolve failed`). Empacote com o tar nativo
48
+ > (`C:\Windows\System32\tar.exe`, bsdtar), chamando-o pelo caminho absoluto ou pondo `System32`
49
+ > antes no `PATH`. Na extração o kit já faz isso sozinho (`kit/provision.mjs`).
50
+
51
+ ## 3. Publicar
52
+
53
+ ```sh
54
+ pwsh kit/publish-base.ps1 -Engine <motor> -Version <x.y.z> -Asset <artefato.tgz> -DryRun
55
+ pwsh kit/publish-base.ps1 -Engine <motor> -Version <x.y.z> -Asset <artefato.tgz> `
56
+ -SourceRepo AllanSantos-DV/<repo-privado>
57
+ ```
58
+
59
+ O script faz, em ordem e com *fail-loud* em cada passo:
60
+
61
+ 1. **zera `GH_TOKEN`/`GITHUB_TOKEN`/`GIT_CONFIG_PARAMETERS`** e confere `gh api user` contra o dono
62
+ do registry — o app Copilot injeta o token de uma conta de trabalho em todo processo filho, e
63
+ sem isso o `gh` publica pela conta errada;
64
+ 2. assina cada artefato e **prova a assinatura na hora** (valida o legítimo, recusa o adulterado);
65
+ 3. cria a release no registry (recusa se a tag já existir — artefato assinado não se sobrescreve);
66
+ 4. espelha no repo privado, se `-SourceRepo` (falha aqui **avisa**, não aborta: não é o canônico);
67
+ 5. atualiza o `manifest.json` **parseado** (nunca regex em JSON);
68
+ 6. **prova o caminho real**: baixa a release publicada e valida `sha256` + assinatura como um
69
+ consumidor faria. *"Publiquei"* não é prova — asset faltando, `.sig` esquecido e tag errada só
70
+ aparecem quando alguém tenta instalar, em produção.
71
+
72
+ Depois: commitar e enviar o `manifest.json`. **Consumidores só enxergam a mudança após o push** —
73
+ até lá o registry publicado continua descrevendo a versão anterior.
74
+
75
+ ## 4. Onde o motor instala — e como o consumidor ACHA
76
+
77
+ Esta seção existe porque a ausência dela custou caro: um consumidor escreveu o caminho do
78
+ executável **de cabeça**, errou por um segmento, e o mesmo palpite foi copiado para três
79
+ conectores em duas linguagens. O motor ficava "não instalado" com o binário no disco, e o
80
+ consumidor entrava em laço de reinstalação que nunca resolvia.
81
+
82
+ **Layout de instalação (Windows, por-usuário):**
83
+
84
+ ```
85
+ %LOCALAPPDATA%\<motor>\ ← raiz (a ÚNICA parte que o consumidor pode assumir)
86
+ venv\Scripts\<motor>.exe ← o CLI ⚠ tem 'venv'
87
+ venv\Scripts\python.exe ← o interpretador do motor
88
+ logs\daemon.log
89
+ run\daemon-<hash>.pid
90
+ cli.json ← o PONTEIRO publicado pelo motor
91
+ ```
92
+
93
+ **Como o consumidor acha o CLI — nesta ordem:**
94
+
95
+ 1. variável de ambiente de override (`<MOTOR>_CLI`), se existir e apontar para arquivo real;
96
+ 2. **`<raiz>/cli.json`** — o motor grava ali o caminho do próprio executável, derivado do
97
+ `sys.executable` do venv dele. Não é palpite: é o motor declarando onde está;
98
+ 3. só então o layout acima, como plano B para o **primeiro contato** (antes de o motor ter
99
+ rodado alguma vez, o ponteiro ainda não existe);
100
+ 4. `PATH` por último — e sabendo que normalmente **não resolve**: ninguém ativa o venv do motor.
101
+ Procurar no `PATH` primeiro ainda arrisca pegar um binário homônimo de outro projeto.
102
+
103
+ **Regras para quem escreve um consumidor:**
104
+
105
+ - **Não escreva o caminho de memória.** A resposta está no código do motor
106
+ (`bootstrap.daemon_paths()`, `sdk/python/vox_lifecycle.py`) e no `cli.json`. Se você está
107
+ digitando `Scripts` sem olhar, você está adivinhando.
108
+ - **Não duplique a descoberta em N conectores.** Cada cópia é um lugar a mais para o palpite
109
+ errado sobrar quando o layout mudar. Um erro copiado três vezes leva três releases para sair.
110
+ - **Ausência do motor é ESTADO, não exceção**: devolva "não instalado" e deixe o bootstrap
111
+ decidir. Levantar aqui transforma primeiro-uso em erro.
112
+
113
+ **Regra para quem publica o motor:** o motor **declara** onde se instalou (escreve o
114
+ `cli.json` no arranque do daemon e em qualquer invocação do CLI, com escrita atômica). Publicar
115
+ um ponteiro para arquivo inexistente é pior que não publicar — o consumidor confiaria nele.
116
+
117
+ ## 5. Disciplina de release
118
+
119
+ Release não é ferramenta de depuração. Publicar para "ver se agora vai" gasta número de versão,
120
+ polui o histórico e obriga todo consumidor a decidir se atualiza — para descobrir o bug seguinte.
121
+
122
+ - **Reproduza o defeito localmente antes de versionar.** Se você não sabe qual linha falha, não
123
+ há o que publicar.
124
+ - **Valide pelo caminho REAL do consumidor**, não pela peça isolada. Testar `find_vox()` sozinho
125
+ não prova que o worker sobe; foi assim que três releases seguidas saíram sem resolver.
126
+ - **Uma release deve carregar a correção E o teste que a trava.** Sem o teste, o mesmo erro
127
+ volta em silêncio na próxima refatoração.
128
+ - Sinal de alerta: duas releases do mesmo motor no mesmo dia com poucos minutos de intervalo.
129
+ Isso não é entrega incremental, é depuração em produção.
130
+
131
+ ## 6. Checklist por motor
132
+
133
+ ```
134
+ [ ] chave gerada em ~/.engine-signing/<motor>_ed25519_private.key
135
+ [ ] entrada do motor existe no manifest.json (kind, home, install.entry, platforms)
136
+ [ ] build produz artefato self-contained, com package.json na versão certa
137
+ [ ] publish-base.ps1 rodado em -DryRun sem erro
138
+ [ ] release publicada no engine-registry com .tgz + .sig + .sha256
139
+ [ ] manifest.json com publicKey, signatureAlgorithm, signatureRequired:true, repo=engine-registry
140
+ [ ] node kit/verify-release.mjs <motor> → verde
141
+ [ ] node tests/e2e-install.mjs --local → instala e sobe num HOME isolado
142
+ [ ] o motor PUBLICA o ponteiro do CLI (<raiz>/cli.json) no arranque — nada de o consumidor adivinhar
143
+ [ ] nenhum consumidor tem caminho de instalação escrito à mão (grep por "Scripts" nos conectores)
144
+ [ ] o defeito foi reproduzido pelo caminho REAL do consumidor, não só na peça isolada
145
+ [ ] a correção vai junto com o teste que a trava
146
+ [ ] consumidores apontados para o engine-registry (o repo antigo deixou de ser consultado)
147
+ [ ] grep nos repos dos consumidores pela URL antiga retorna ZERO ← critério de remoção da ponte
148
+ [ ] só então: release antiga marcada como deprecated / removida
149
+ ```
150
+
151
+ ## 7. Estado da migração
152
+
153
+ | Motor | `kind` | Fonte (privado) | Release canônica | Assinado | Ponte no canal antigo |
154
+ |---|---|---|---|---|---|
155
+ | `mcp-gateway` | daemon | `mcp-bridge` (branch `feat/mcp-gateway`) | `engine-registry` ✅ | ✅ | — (nunca esteve lá) |
156
+ | `embed-house` | daemon | `embed-house` | `engine-registry` ✅ | ✅ | `copilot-marketplace` |
157
+ | `neural-link` | **cli** | `neural-link` | `engine-registry` ✅ | ✅ | `neural-link-runtime` (redirecionado) |
158
+ | `vox-engine` | daemon (`pipe`), **self-managed** | `vox-engine` | `engine-registry` ✅ | ✅ | `copilot-marketplace` |
159
+
160
+ Cada repo de motor tem o próprio `publish.ps1`, que **builda** o que é específico dele e **delega**
161
+ ao `kit/publish-base.ps1` a parte comum (identidade do `gh`, assinatura, release, manifest, prova).
162
+
163
+ ### Consumidores repontados
164
+
165
+ | Consumidor | Motor | O que mudou |
166
+ |---|---|---|
167
+ | `modo-auto` + espelho no marketplace | embed-house | `provision.mjs` → registry |
168
+ | `copilot-voice` | vox-engine | `tools/sdk-drift.ps1` baixa o canônico do registry — **gate verde, provado nesta máquina** |
169
+ | `Action`, `cerne` | vox-engine | `vox_sdk.py` vendorizado → registry |
170
+
171
+ ### O que ficou de propósito
172
+
173
+ - **`copilot-voice` / `voice-chat`: o SDK vendorizado NÃO foi tocado.** Essas cópias são
174
+ **byte-idênticas** ao canônico publicado (é o que o gate anti-drift verifica). Editá-las agora
175
+ criaria drift contra o canônico atual e **quebraria o publish desses consumidores** — por 404 do
176
+ gate, não por bug. Elas migram sozinhas na próxima release do vox, quando os flats publicados já
177
+ carregarem a URL nova. Enquanto isso, continuam achando a release pela ponte.
178
+ - **Cópias em worktrees de feature** (`hermes-agent-*`, `cerne-feat-*`): são branches de trabalho,
179
+ não distribuição. Migram no merge.
180
+ - **Escopo por projeto no dispatcher**: o campo `project` já é aceito, validado e propagado; o
181
+ **filtro** por escopo é o próximo passo. A ordem é essa de propósito — ligar o filtro antes de
182
+ alguém mandar o campo silenciaria todos os handlers de uma vez.
183
+ - **Skill Manager provisionando pelo kit**: hoje ele embute e faz o deploy do runtime. É extensão
184
+ em produção; a troca entra junto com o ciclo do escopo.
185
+
186
+ ### Sobre remover as releases do `copilot-marketplace`
187
+
188
+ **Não apague antes do critério bater.** O critério é binário:
189
+
190
+ > um `grep` nos repositórios dos consumidores pela URL antiga daquele motor retorna **zero**.
191
+
192
+ Hoje ainda retorna para `copilot-voice`/`voice-chat` (SDK vendorizado byte-idêntico ao canônico
193
+ publicado). Enquanto não zerar, a ponte fica — com aviso de depreciação no corpo da release.
194
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allan Santos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,306 @@
1
- # Temporary Holding Version
1
+ # engine-registry
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Vitrine de **MOTORES** compartilhados e, em uma seção separada, de aplicativos
4
+ distribuídos. O `engine-kit` continua lendo somente `manifest.engines`.
5
+
6
+ Um motor **não é um plugin**: não se instala pela vitrine de extensões, não tem `hooks` nem
7
+ `extensions`. Ele é **infraestrutura**: instala **uma vez por máquina** em `~/.<motor>` e serve
8
+ **N consumidores** (Copilot, Claude Code, Cursor, VS Code, scripts…), sem depender do host.
9
+
10
+ ```
11
+ extensões/plugins ──declaram──► engine-kit ──resolve──► engine-registry
12
+ (voice-chat, (esta lib) provision (manifest.json)
13
+ modo-auto, lifecycle │
14
+ mcp-bridge…) │ ▼
15
+ └──────► ~/.<motor> (1 por máquina)
16
+ ```
17
+
18
+ ## Por que existe
19
+
20
+ Antes, cada plugin carregava seu próprio `boot.mjs` com a lógica de "achar ou baixar o motor" —
21
+ o mesmo código copiado em cada consumidor. Um motor novo obrigava a repetir tudo. Aqui a lógica
22
+ vive num lugar só e o consumidor apenas **declara a dependência**.
23
+
24
+ ## Motores registrados
25
+
26
+ | Motor | Versão | Tipo | O que faz |
27
+ |---|---|---|---|
28
+ | `embed-house` | 1.0.5 | `daemon` (http) | Casa de embeddings (MiniLM-L6-v2, 384-dim): carrega o modelo uma vez e serve vetores a N consumidores. |
29
+ | `vox-engine` | 0.28.7 | `daemon` (pipe), **self-managed** | Motor de voz (STT/TTS) compartilhado, com auto-unload quando ocioso. Traz o próprio instalador e updater. |
30
+ | `mcp-gateway` | 0.15.0 | `daemon` (http) | Agregador MCP: conecta uma vez em cada servidor MCP e reexpõe tudo num endpoint único. |
31
+ | `neural-link` | 0.7.15 | `cli` | Dispatcher adaptativo de hooks: recebe o evento por stdin, pontua os handlers e devolve uma decisão composta. |
32
+ | `vxk` | 0.18.0 | `cli` | Explicações visuais animadas e narradas como servidor MCP (stdio): devolve UM HTML autocontido e offline. A narração vem do `vox-engine`. |
33
+ | `cerne` | 0.4.10 | `daemon`, **self-managed** | Leitura ativa de vídeo (captura, quadros, transcrição, tradução) exposta via MCP. Instalador Inno por usuário, Windows. |
34
+
35
+ **Todos** são publicados aqui, assinados com Ed25519 e verificados antes de instalar.
36
+
37
+ Um motor pode ser de dois tipos (`kind`):
38
+
39
+ - **`daemon`** — processo longo que anuncia `runtime.json` e responde a um `healthCheck`. O kit
40
+ sobe e espera o auto-anúncio.
41
+ - **`cli`** — executável invocado **por evento** (ex.: um dispatcher de hooks). Não há processo
42
+ para manter vivo: provisionar já é entregar. O `lifecycle` devolve o caminho do binário sem
43
+ spawn, sem health e sem runtime — forçar um daemon aqui seria inventar um ciclo de vida que o
44
+ motor não tem.
45
+
46
+ E pode declarar `status: "self-managed"`: o motor traz o **próprio instalador/updater** (o caso do
47
+ `vox-engine`, um app Python). A entrada aqui é o **descritor de registro** — onde está a release,
48
+ qual a chave, qual a versão — e o `provision` recusa com razão explícita em vez de tentar extrair
49
+ um instalador como se fosse um tarball.
50
+
51
+ ## Instalar um motor com um comando
52
+
53
+ Sem clonar o repositório, com Node 18 ou mais:
54
+
55
+ ```sh
56
+ npx engine-kit ensure vxk # instala ou atualiza; reusa se já estiver em dia
57
+ npx engine-kit ensure vxk --version 0.18.0 # pin de versão
58
+ ```
59
+
60
+ O `npx` baixa o kit do npm e o kit baixa o motor da release, confere o **sha256**, a **assinatura Ed25519**
61
+ e a **versão** do artefato; qualquer divergência aborta sem instalar nada. A chave pública do primeiro contato
62
+ vem do `manifest.json` que viaja **dentro do pacote** (publicado no npm por publicação confiável, com
63
+ proveniência): se o registry servir outra chave, é ABORT já na primeira instalação. Motor que o pacote não
64
+ conhece é recusado (`npx engine-kit@latest ensure <motor>` traz o kit novo). Motores `self-managed`
65
+ (`vox-engine`, `cerne`) seguem pelo próprio instalador.
66
+
67
+ Atualização automática, ligada por padrão: o kit consulta a última versão no npm e, se houver uma nova, roda o
68
+ mesmo comando nela (o `npx` sem versão reusaria a da cache); o motor instalado é atualizado para a versão do
69
+ registry, com o aviso `motor atualizado: <motor> vA → vB`. `ENGINE_KIT_NO_AUTO_UPDATE=1` desliga as duas trocas e
70
+ deixa só o aviso de versão nova disponível. `--version` explícito sempre vale.
71
+
72
+ Publicar o kit: tag `engine-kit-v<versão do package.json>` criada na `main`; o workflow `.github/workflows/publish.yml`
73
+ roda a suíte e publica por OIDC (sem token guardado). Conferir a proveniência: `npm audit signatures` num projeto
74
+ que dependa do `engine-kit`, ou o selo "Provenance" na página do pacote.
75
+
76
+ ## Conciliar a máquina
77
+
78
+ ```sh
79
+ node kit/doctor.mjs # diagnostica: o que reusar, atualizar ou baixar
80
+ node kit/doctor.mjs --fix # aplica
81
+ ```
82
+
83
+ Desatualizado → atualiza. Ausente → baixa. **Atualizado → reusa** — e o diagnóstico não baixa nada,
84
+ porque reusar é o caminho comum e precisa ser barato. Antes de trocar um daemon vivo, o doctor pede
85
+ o `shutdownPath` que o registry declara: no Windows, arquivo em uso não pode ser sobrescrito.
86
+
87
+ ## Como consumir
88
+
89
+ ```js
90
+ import { ensureEngine } from "engine-kit";
91
+
92
+ const r = await ensureEngine("embed-house", {
93
+ // O kit NUNCA adivinha a saúde do motor: quem conhece o protocolo é o consumidor.
94
+ healthCheck: async (rt) => {
95
+ const res = await fetch(`http://127.0.0.1:${rt.port}/health`, { signal: AbortSignal.timeout(2000) });
96
+ if (!res.ok) return null;
97
+ const h = await res.json();
98
+ return h.dim === 384 ? h : null; // handshake: dimensão/modelo compatíveis
99
+ },
100
+ });
101
+
102
+ if (!r.available) {
103
+ // Degrade SINALIZADO — nunca silencioso.
104
+ log(`embed-house indisponível: ${r.reason}`);
105
+ } else {
106
+ usar(r.runtime.port);
107
+ }
108
+ ```
109
+
110
+ Declare a dependência no manifesto do seu plugin:
111
+
112
+ ```jsonc
113
+ {
114
+ "name": "meu-plugin",
115
+ "engines": { "embed-house": ">=1.0.0" }
116
+ }
117
+ ```
118
+
119
+ ### Adaptadores prontos
120
+
121
+ Cada motor tem um adaptador em `consumers/` que já sabe o handshake dele — o consumidor não
122
+ precisa escrever `healthCheck` nem descobrir onde mora o token:
123
+
124
+ ```js
125
+ import { ensureGateway } from "engine-registry/consumers/mcp-gateway.mjs";
126
+
127
+ const gw = await ensureGateway({ log });
128
+ if (!gw.available) { log(`gateway indisponível: ${gw.reason}`); return; } // degrade SINALIZADO
129
+
130
+ // gw.url -> http://127.0.0.1:7337/mcp
131
+ // gw.token -> bearer (lido do cofre com ACL do dono)
132
+ // gw.backends -> ["files", "memory", ...]
133
+ ```
134
+
135
+ ## Garantias do kit
136
+
137
+ - **Integridade fail-closed** — o sidecar `.sha256` é obrigatório; ausente, malformado ou
138
+ divergente = **aborta sem instalar**.
139
+ - **Autenticidade fail-closed** — quando o registry declara `install.publicKey`, o artefato só é
140
+ extraído se a **assinatura Ed25519** conferir. Isso cobre o que o SHA256 **não** cobre: quem
141
+ troca o artefato no release **regenera o `.sha256` junto** e passaria pela integridade. A
142
+ assinatura amarra o artefato a quem detém a chave privada — que nunca sai da máquina do dono.
143
+ - **Um provisionamento por vez** — lock atômico (`wx`): consumidores concorrentes não brigam;
144
+ os demais esperam o vencedor.
145
+ - **Reúso por semver same-major `>=`** — dois consumidores com pins diferentes convergem no maior,
146
+ sem loop de download. Major diferente = incompatível (o protocolo é atado ao major).
147
+ - **Troca atômica** — download em `.part` → rename; extração em staging → swap. No Windows, o
148
+ gancho `onBeforeReplace` derruba o daemon antes (arquivo em uso não pode ser sobrescrito).
149
+ - **Nunca lança** — toda falha vira `{ ok:false, reason }` / `{ available:false, reason }`. Quem
150
+ decide como degradar é o consumidor.
151
+ - **Singleton do motor** — a corrida é resolvida **dentro** do motor (port-lock/bind); o kit só
152
+ sobe e espera o auto-anúncio.
153
+
154
+ ## Assinatura dos motores
155
+
156
+ Contrato **`ed25519-sha256-raw`** (o mesmo que o `vox-engine` já pratica em produção — a migração
157
+ dele não exige reassinar nada):
158
+
159
+ - assina-se o **SHA-256** do artefato (*hash-then-sign*), não o artefato direto;
160
+ - o `.sig` é a assinatura Ed25519 **crua: 64 bytes binários**, nunca base64;
161
+ - a chave **pública** (32 bytes hex) é **pinada** no `manifest.json`, em `install.publicKey`;
162
+ - a chave **privada** fica em `~/.engine-signing/<motor>_ed25519_private.key` e **nunca entra em
163
+ CI** — é isso que impede um CI comprometido de forjar um release.
164
+
165
+ **Uma chave por motor**, não uma chave do publisher: um vazamento obriga a republicar aquele
166
+ motor, não a casa inteira.
167
+
168
+ Política do `provision`, sem meio-termo silencioso:
169
+
170
+ | Situação no registry | O que acontece |
171
+ |---|---|
172
+ | `publicKey` declarada | assinatura **obrigatória**; inválida/ausente = ABORT |
173
+ | `signatureRequired: true` sem `publicKey` | manifesto **inconsistente** = ABORT |
174
+ | `signatureAlgorithm` desconhecido | ABORT (nunca aceite silencioso) |
175
+ | nem chave nem exigência (motor ainda não migrado) | instala, mas o log **sinaliza** que a autenticidade não foi verificada |
176
+
177
+ ### Raiz de confiança — por que a chave no manifest não bastava
178
+
179
+ A `publicKey` vinha do **mesmo `manifest.json`** que o kit baixa. Quem controla o repositório troca
180
+ o artefato, o `.sig` **e** a chave — e o fail-closed aprova os três, porque todos vêm da mesma
181
+ origem. É uma tranca com a chave pendurada na porta. Uma review corroborada apontou; está fechado
182
+ assim:
183
+
184
+ | Camada | Como funciona | Fecha o quê |
185
+ |---|---|---|
186
+ | **TOFU** (padrão, automático) | a chave do 1º contato é gravada em `~/.engine-kit/trust.json`; qualquer troca depois = **ABORT** | repo comprometido **depois** da adoção — o caso realista |
187
+ | **Pin do consumidor** | `ensureEngine(nome, { trustedKeys: { "<motor>": "<hex>" } })`; divergência = **ABORT** já no 1º contato | não depende do registry em momento nenhum (padrão do `vox-engine`) |
188
+ | **Chave no pacote** (`npx engine-kit ensure`) | o comando fixa a chave do `manifest.json` embarcado no pacote do npm | registry comprometido **antes** do 1º contato: trocar a chave exige publicar um engine-kit novo, e a publicação deixa proveniência pública |
189
+
190
+ Rotação legítima é **ato explícito**: apagar a entrada em `~/.engine-kit/trust.json`.
191
+
192
+ **O que isto NÃO cobre, dito na cara:** comprometimento **anterior** ao primeiro contato (o TOFU
193
+ grava a chave do atacante), downgrade/replay e expiração. Fechar isso exige transparência pública
194
+ (**Sigstore/cosign**, **TUF**, *npm provenance*) — decisão de produto do dono, registrada e não
195
+ esquecida. Para 4 motores de um autor só, TOFU + pin cobre o vetor realista a custo operacional
196
+ zero; log de transparência cobre o resto a um custo bem maior.
197
+
198
+ ## Estrutura
199
+
200
+ ```
201
+ manifest.json # o registry: descritor de cada motor (release, asset, entry, runtime, chave)
202
+ schema.json # contrato do manifest (JSON Schema)
203
+ kit/
204
+ index.mjs # ensureEngine = resolve → provision → lifecycle
205
+ resolve.mjs # descobre o motor no registry (cache 6h, degrada p/ cache obsoleto)
206
+ provision.mjs # baixa/atualiza: SHA256 + assinatura fail-closed, lock e swap atômico
207
+ lifecycle.mjs # garante o processo vivo (daemon) ou entrega o binário (cli)
208
+ cli.mjs # comando `engine-kit` (bin do pacote): `npx engine-kit ensure <motor>`
209
+ ensure.mjs # ensure: resolve → provision com a chave do manifest embarcado no pacote
210
+ update.mjs # o kit se atualiza (relança na última versão do npm); ENGINE_KIT_NO_AUTO_UPDATE=1 desliga
211
+ signature.mjs # contrato ed25519-sha256-raw — assinar e verificar na MESMA fonte
212
+ keystore.mjs # onde moram as chaves privadas (~/.engine-signing)
213
+ gen-key.mjs # gera o par de UM motor (com auto-prova)
214
+ sign.mjs # assina um artefato e prova a assinatura na hora
215
+ update-manifest.mjs # patch do manifest parseado (nunca regex em JSON); --app para apps
216
+ verify-release.mjs # baixa a release publicada e prova o caminho do consumidor; --app para apps
217
+ app-release.mjs # URLs e verificação de TODOS os assets de um app (manifest.apps)
218
+ publish-base.ps1 # publicação de motor: identidade do gh → assina → release → manifest → prova
219
+ consumers/
220
+ mcp-gateway.mjs # adaptador pronto: handshake + token + shutdown do agregador MCP
221
+ site/ # vitrine de produto dos motores (estática); processo em docs/PAGES.md
222
+ tools/ # pages-branch.mjs: publica site/ na gh-pages
223
+ tests/
224
+ smoke.mjs # contratos do kit: node tests/smoke.mjs
225
+ test-*.mjs # contratos hermeticos (assinatura, trust, tar, apps, cache, doctor, gateway)
226
+ e2e-*.mjs # instalacao real de um motor do zero: node tests/e2e-install.mjs
227
+ ```
228
+
229
+ ## Verificar
230
+
231
+ ```sh
232
+ node tests/smoke.mjs # contratos, offline
233
+ node tests/smoke.mjs --network # resolvendo do registry remoto
234
+ node tests/test-signature.mjs # contrato de assinatura (hermético)
235
+ node tests/test-signature.mjs --network # + interop com uma release REAL do vox-engine
236
+ node tests/test-apps.mjs # catálogo de apps (hermético)
237
+ node tests/e2e-install.mjs # baixa e sobe o mcp-gateway num HOME isolado (prova real)
238
+ node tests/e2e-install.mjs --local # idem, mas usando o manifest.json que você vai publicar
239
+ ```
240
+
241
+ ## Publicar um motor
242
+
243
+ Veja o passo a passo completo em [`CONTRATO-MOTOR.md`](./CONTRATO-MOTOR.md). Em resumo:
244
+
245
+ ```sh
246
+ node kit/gen-key.mjs <motor> # uma vez por motor: gera a chave e imprime a pública
247
+ # ... o motor builda o próprio artefato ...
248
+ pwsh kit/publish-base.ps1 -Engine <motor> -Version <x.y.z> -Asset <artefato.tgz> -DryRun
249
+ pwsh kit/publish-base.ps1 -Engine <motor> -Version <x.y.z> -Asset <artefato.tgz>
250
+ ```
251
+
252
+ O `publish-base.ps1` confere a identidade do `gh` (o app Copilot injeta o token de uma conta de
253
+ trabalho nos processos filhos e o `gh` publica pela conta errada), assina, publica no registry,
254
+ atualiza o `manifest.json` **parseado** e então **prova**: baixa a release publicada e valida
255
+ `sha256` + assinatura pelo caminho de um consumidor.
256
+
257
+ O artefato deve ser **self-contained** (traz o próprio `node_modules` trimado): o motor precisa
258
+ subir numa máquina limpa, sem depender do consumidor.
259
+
260
+ ## Aplicativos
261
+
262
+ `manifest.apps` é o catálogo de **aplicativos distribuídos**: produtos que o usuário baixa e
263
+ instala, com instalador, interface e atualização próprios. O `engine-kit` **não** os provisiona —
264
+ `resolve`/`provision` leem só `manifest.engines`. A entrada declara versão, tag e os assets por
265
+ plataforma, e a release mora aqui, no registry público, com download **sem login**.
266
+
267
+ | App | Versão | Plataformas | O que faz |
268
+ |---|---|---|---|
269
+ | — | — | — | Nenhum app registrado ainda. |
270
+
271
+ Um app só entra nesta tabela e em `manifest.apps` **depois** de a release pública existir e passar
272
+ em `node kit/verify-release.mjs --app <nome>` (download anônimo de todos os assets, `.sha256` e
273
+ `.sig`). Aplicativo integrador (como o WhatsApp Bridge, tag `whatsapp-bridge-vX.Y.Z`) vai em
274
+ `apps`; motor compartilhado com GUI e instalador próprios (como o Cerne) vai em `engines` com
275
+ `status: "self-managed"`.
276
+
277
+ ### Contrato da entrada
278
+
279
+ - `release.assets[].name` aceita `{version}`; `checksum` e `signature` aceitam `{asset}` e
280
+ `{version}`. Nome literal, sem placeholder, também vale. Prefira templates
281
+ (`whatsapp-bridge-setup-{version}-win32-x64.exe`, `{asset}.sha256`): uma versão nova muda só
282
+ `version`, sem reescrever os assets.
283
+ - `signature` ausente = `{asset}.sig`. Com `release.publicKey` declarada, **todo** asset precisa
284
+ de `.sig` válido; `signatureRequired: true` sem `publicKey` é recusado.
285
+ - O schema é estrito: campo desconhecido na entrada, na release ou num asset é recusado.
286
+ - Uma chave por produto, como nos motores: `node kit/gen-key.mjs <app>` (a privada fica em
287
+ `~/.engine-signing/<app>_ed25519_private.key` e nunca entra em CI).
288
+
289
+ ### Publicar um app
290
+
291
+ O `publish-base.ps1` é dos motores (um `.tgz`, notas que falam do `engine-kit`). Um app publica
292
+ com **o próprio script**, que builda, assina cada asset com `node kit/sign.mjs <asset> --engine
293
+ <app>` e cria a release `<app>-vX.Y.Z` neste repositório. Do kit, o app usa só a parte do
294
+ manifest e a prova:
295
+
296
+ ```sh
297
+ # 1ª vez: registre a entrada à mão em manifest.apps (o script ATUALIZA, não inventa descritor)
298
+ node kit/update-manifest.mjs --app <app> --version <x.y.z> --public-key <hex> \
299
+ [--asset "<template>=<plataforma>" ...]
300
+ node kit/verify-release.mjs --app <app> # baixa todos os assets sem login e confere
301
+ ```
302
+
303
+
304
+ ## Licença
305
+
306
+ MIT