engine-kit 0.0.0-stage → 0.2.0-bootstrap.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.
- package/CONTRATO-MOTOR.md +194 -0
- package/LICENSE +21 -0
- package/README.md +305 -2
- package/consumers/mcp-gateway.mjs +192 -0
- package/consumers/neural-link.mjs +66 -0
- package/kit/app-release.mjs +94 -0
- package/kit/budget.mjs +112 -0
- package/kit/cli.mjs +48 -0
- package/kit/doctor.mjs +122 -0
- package/kit/ensure.mjs +82 -0
- package/kit/gen-key.mjs +52 -0
- package/kit/index.mjs +54 -0
- package/kit/keystore.mjs +29 -0
- package/kit/lifecycle.mjs +160 -0
- package/kit/provision.mjs +379 -0
- package/kit/publish-base.ps1 +163 -0
- package/kit/resolve.mjs +119 -0
- package/kit/sign.mjs +67 -0
- package/kit/signature.mjs +106 -0
- package/kit/trust.mjs +72 -0
- package/kit/update-manifest.mjs +109 -0
- package/kit/update.mjs +68 -0
- package/kit/verify-release.mjs +94 -0
- package/manifest.json +182 -0
- package/package.json +50 -4
|
@@ -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
|
-
#
|
|
1
|
+
# engine-registry
|
|
2
2
|
|
|
3
|
-
|
|
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
|